Создание одностраничного приложения с Vue.js и Flask. Часть 2 | OTUS

Создание одностраничного приложения с Vue.js и Flask. Часть 2

В предыдущей части статьи мы настроили Flask, Vue и Bootstrap, а также приступили к созданию базового CRUD-приложения. Пришло время закончить начатое.

POST-маршрут

Сервер

Теперь давайте выполним обновление существующего обработчика маршрута для обработки POST-запросов и добавления новых книг:

@app.route('/books', methods=['GET', 'POST'])
def all_books():
    response_object = {'status': 'success'}
    if request.method == 'POST':
        post_data = request.get_json()
        BOOKS.append({
            'title': post_data.get('title'),
            'author': post_data.get('author'),
            'read': post_data.get('read')
        })
        response_object['message'] = 'Book added!'
    else:
        response_object['books'] = BOOKS
    return jsonify(response_object)

Также нужно обновить импорты:

from flask import Flask, jsonify, request

После запуска сервера Flask, можно выполнить проверку POST-маршрута на новой вкладке веб-браузера:

$ curl -X POST http://localhost:5000/books -d \
  '{"title": "1Q84", "author": "Haruki Murakami", "read": "true"}' \
  -H 'Content-Type: application/json'

Должный увидеть следующее:

{
  "message": "Book added!",
  "status": "success"
}

Кроме того, по адресу http://localhost:5000/books должна появиться добавленная книга.

Клиент

Далее, для добавления новой книги надо внести следующий modal. Давайте начнём с HTML:

<b-modal ref="addBookModal"
         id="book-modal"
         title="Add a new book"
         hide-footer>
  <b-form @submit="onSubmit" @reset="onReset" class="w-100">
  <b-form-group id="form-title-group"
                label="Title:"
                label-for="form-title-input">
      <b-form-input id="form-title-input"
                    type="text"
                    v-model="addBookForm.title"
                    required
                    placeholder="Enter title">
      </b-form-input>
    </b-form-group>
    <b-form-group id="form-author-group"
                  label="Author:"
                  label-for="form-author-input">
        <b-form-input id="form-author-input"
                      type="text"
                      v-model="addBookForm.author"
                      required
                      placeholder="Enter author">
        </b-form-input>
      </b-form-group>
    <b-form-group id="form-read-group">
      <b-form-checkbox-group v-model="addBookForm.read" id="form-checks">
        <b-form-checkbox value="true">Read?</b-form-checkbox>
      </b-form-checkbox-group>
    </b-form-group>
    <b-button type="submit" variant="primary">Submit</b-button>
    <b-button type="reset" variant="danger">Reset</b-button>
  </b-form>
</b-modal>

Эту часть кода следует поместить перед закрывающим тегом . Обратите внимание, что v-model является директивой, используемой для привязки обратно к стейту входных значений. Если интересует Hide-Footer, вы можете посмотреть, что он делает, в официальной документации по Bootstrap Vue.

Теперь следует выполнить обновление раздела script:

<script>
import axios from 'axios';

export default {
  data() {
    return {
      books: [],
      addBookForm: {
        title: '',
        author: '',
        read: [],
      },
    };
  },
  methods: {
    getBooks() {
      const path = 'http://localhost:5000/books';
      axios.get(path)
        .then((res) => {
          this.books = res.data.books;
        })
        .catch((error) => {
          // eslint-отключение следующей строки
          console.error(error);
        });
    },
    addBook(payload) {
      const path = 'http://localhost:5000/books';
      axios.post(path, payload)
        .then(() => {
          this.getBooks();
        })
        .catch((error) => {
          // eslint-отключение следующей строки
          console.log(error);
          this.getBooks();
        });
    },
    initForm() {
      this.addBookForm.title = '';
      this.addBookForm.author = '';
      this.addBookForm.read = [];
    },
    onSubmit(evt) {
      evt.preventDefault();
      this.$refs.addBookModal.hide();
      let read = false;
      if (this.addBookForm.read[0]) read = true;
      const payload = {
        title: this.addBookForm.title,
        author: this.addBookForm.author,
        read, // сокращённое свойство
      };
      this.addBook(payload);
      this.initForm();
    },
    onReset(evt) {
      evt.preventDefault();
      this.$refs.addBookModal.hide();
      this.initForm();
    },
  },
  created() {
    this.getBooks();
  },
};
</script>

Посмотрим, что выполняется в вышеописанном коде: 1) addBookForm() привязывается ко входным данным формы посредством v-model — это называют 2-сторонней привязкой; 2) onSubmit() запускается, если пользователь успешно отправляет форму. При этом при отправке предотвращается обычное поведение веб-браузера (evt.preventDefault()), плюс закрывается modal (this.$Refs.addBookModal.hide()), осуществляется запуск метода addBook() и очищается форма (initForm()); 3) addBook() отправляет POST-запрос в /books, что необходимо для добавления новой книги; 4) последующие изменения можно посмотреть самостоятельно в Vue-документации.

Далее обновляем кнопку «Add Book» в темплейте, дабы при нажатии кнопки отображался modal:

<button type="button" class="btn btn-success btn-sm" v-b-modal.book-modal>Add Book</button>

После выполнения всех вышеописанных действий компонент должен выглядеть так:

<template>
  <div class="container">
    <div class="row">
      <div class="col-sm-10">
        <h1>Books</h1>
        <hr><br><br>
        <button type="button" class="btn btn-success btn-sm" v-b-modal.book-modal>Add Book</button>
        <br><br>
        <table class="table table-hover">
          <thead>
            <tr>
              <th scope="col">Title</th>
              <th scope="col">Author</th>
              <th scope="col">Read?</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="(book, index) in books" :key="index">
              <td>{{ book.title }}</td>
              <td>{{ book.author }}</td>
              <td>
                <span v-if="book.read">Yes</span>
                <span v-else>No</span>
              </td>
              <td>
                <button type="button" class="btn btn-warning btn-sm">Update</button>
                <button type="button" class="btn btn-danger btn-sm">Delete</button>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
    <b-modal ref="addBookModal"
             id="book-modal"
             title="Add a new book"
             hide-footer>
      <b-form @submit="onSubmit" @reset="onReset" class="w-100">
      <b-form-group id="form-title-group"
                    label="Title:"
                    label-for="form-title-input">
          <b-form-input id="form-title-input"
                        type="text"
                        v-model="addBookForm.title"
                        required
                        placeholder="Enter title">
          </b-form-input>
        </b-form-group>
        <b-form-group id="form-author-group"
                      label="Author:"
                      label-for="form-author-input">
            <b-form-input id="form-author-input"
                          type="text"
                          v-model="addBookForm.author"
                          required
                          placeholder="Enter author">
            </b-form-input>
          </b-form-group>
        <b-form-group id="form-read-group">
          <b-form-checkbox-group v-model="addBookForm.read" id="form-checks">
            <b-form-checkbox value="true">Read?</b-form-checkbox>
          </b-form-checkbox-group>
        </b-form-group>
        <b-button type="submit" variant="primary">Submit</b-button>
        <b-button type="reset" variant="danger">Reset</b-button>
      </b-form>
    </b-modal>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      books: [],
      addBookForm: {
        title: '',
        author: '',
        read: [],
      },
    };
  },
  methods: {
    getBooks() {
      const path = 'http://localhost:5000/books';
      axios.get(path)
        .then((res) => {
          this.books = res.data.books;
        })
        .catch((error) => {
          // eslint-отключение следующей строки
          console.error(error);
        });
    },
    addBook(payload) {
      const path = 'http://localhost:5000/books';
      axios.post(path, payload)
        .then(() => {
          this.getBooks();
        })
        .catch((error) => {
          // eslint-отключение следующей строки
          console.log(error);
          this.getBooks();
        });
    },
    initForm() {
      this.addBookForm.title = '';
      this.addBookForm.author = '';
      this.addBookForm.read = [];
    },
    onSubmit(evt) {
      evt.preventDefault();
      this.$refs.addBookModal.hide();
      let read = false;
      if (this.addBookForm.read[0]) read = true;
      const payload = {
        title: this.addBookForm.title,
        author: this.addBookForm.author,
        read, // сокращённое свойство
      };
      this.addBook(payload);
      this.initForm();
    },
    onReset(evt) {
      evt.preventDefault();
      this.$refs.addBookModal.hide();
      this.initForm();
    },
  },
  created() {
    this.getBooks();
  },
};
</script>

Давайте сделаем проверку и попробуем добавить книгу:

add_new_book_1-20219-7617cb.gif

Компонент Alert

Теперь пришло время добавить компонент Alert. Он нужен, чтобы пользователь получал сообщения о добавлении новой книги. Итак, создадим новый компонент, добавив новый файл Alert.vue в каталог client/src/components:

<template>
  <p>It works!</p>
</template>

Потом следует импортировать его в разделе script компонента Books и зарегистрировать:

<script>
import axios from 'axios';
import Alert from './Alert';

...

export default {
  data() {
    return {
      books: [],
      addBookForm: {
        title: '',
        author: '',
        read: [],
      },
    };
  },
  components: {
    alert: Alert,
  },

  ...

};
</script>

И вот сейчас в разделе template мы можем ссылаться на новый компонент:

<template>
  <b-container>
    <b-row>
      <b-col col sm="10">
        <h1>Books</h1>
        <hr><br><br>
        <alert></alert>
        <button type="button" class="btn btn-success btn-sm" v-b-modal.book-modal>Add Book</button>

        ...

      </b-col>
    </b-row>
  </b-container>
</template>

После обновления браузера увидим:

alert_1540x908_1-20219-2194ee.jpg

Далее нужно добавить в шаблон фактический компонент b-alert:

<template>
  <div>
    <b-alert variant="success" show>{{ message }}</b-alert>
    <br>
  </div>
</template>

<script>
export default {
  props: ['message'],
};
</script>

Тут нужно обратить особое внимание на параметр props (находится в разделе script). Мы можем передавать сообщение из родительского компонента (Books) так:

<alert message="hi"></alert>

alert_2_1540x994_1-20219-1228f1.jpg

Если желаете сделать Alert динамическим и передать пользовательское сообщение, используйте в Books.vue выражение привязки (binding expression):

<alert :message="message"></alert>

Теперь добавим message в параметр data в Books.vue:

data() {
  return {
    books: [],
    addBookForm: {
      title: '',
      author: '',
      read: [],
    },
    message: '',
  };
},

Далее следует обновить сообщение в addBook:

addBook(payload) {
  const path = 'http://localhost:5000/books';
  axios.post(path, payload)
    .then(() => {
      this.getBooks();
      this.message = 'Book added!';
    })
    .catch((error) => {
      // eslint-отключение следующей строки
      console.log(error);
      this.getBooks();
    });
},

Чтобы alert отображался, только когда showMessage имеет значение true, добавим v-if.

<alert :message=message v-if="showMessage"></alert>

Также надо добавить showMessage в data:

data() {
  return {
    books: [],
    addBookForm: {
      title: '',
      author: '',
      read: [],
    },
    message: '',
    showMessage: false,
  };
},

Выполним очередное обновление addBook(), установив значение true в showMessage :

addBook(payload) {
  const path = 'http://localhost:5000/books';
  axios.post(path, payload)
    .then(() => {
      this.getBooks();
      this.message = 'Book added!';
      this.showMessage = true;
    })
    .catch((error) => {
      // eslint-отключение следующей строки
      console.log(error);
      this.getBooks();
    });
},

Проверяем, как всё теперь работает:

add_new_book_2_1-20219-79dcd6.gif

PUT-маршрут

Сервер

Для обновления следует использовать уникальный идентификатор. Подойдёт uuid из стандартной Python-библиотеки.

Выполняем обновление BOOKS в server/app.py:

BOOKS = [
    {
        'id': uuid.uuid4().hex,
        'title': 'On the Road',
        'author': 'Jack Kerouac',
        'read': True
    },
    {
        'id': uuid.uuid4().hex,
        'title': 'Harry Potter and the Philosopher\'s Stone',
        'author': 'J. K. Rowling',
        'read': False
    },
    {
        'id': uuid.uuid4().hex,
        'title': 'Green Eggs and Ham',
        'author': 'Dr. Seuss',
        'read': True
    }
]

Импортируем:

import uuid

Выполняем рефакторинг all_books, что необходимо для учёта уникального идентификатора во время добавления новой книги:

@app.route('/books', methods=['GET', 'POST'])
def all_books():
    response_object = {'status': 'success'}
    if request.method == 'POST':
        post_data = request.get_json()
        BOOKS.append({
            'id': uuid.uuid4().hex,
            'title': post_data.get('title'),
            'author': post_data.get('author'),
            'read': post_data.get('read')
        })
        response_object['message'] = 'Book added!'
    else:
        response_object['books'] = BOOKS
    return jsonify(response_object)

Теперь добавляем новый обработчик маршрута:

@app.route('/books/', methods=['PUT'])
def single_book(book_id):
    response_object = {'status': 'success'}
    if request.method == 'PUT':
        post_data = request.get_json()
        remove_book(book_id)
        BOOKS.append({
            'id': uuid.uuid4().hex,
            'title': post_data.get('title'),
            'author': post_data.get('author'),
            'read': post_data.get('read')
        })
        response_object['message'] = 'Book updated!'
    return jsonify(response_object)

И вспомогательную функцию:

def remove_book(book_id):
    for book in BOOKS:
        if book['id'] == book_id:
            BOOKS.remove(book)
            return True
    return False

Клиент

Действуем поэтапно: 1. Добавляем modal и формы. 2. Обрабатываем нажатия кнопки Update. 3. Подключаем AJAX-запрос. 4. Оповещаем пользователя (Alert). 5. Обрабатываем нажатия кнопки Cancel.

Приступим.

1. Добавляем modal и формы

В первую очередь, следует добавить новый modal к темплейту, выполнив это сразу после первого modal:

<b-modal ref="editBookModal"
         id="book-update-modal"
         title="Update"
         hide-footer>
  <b-form @submit="onSubmitUpdate" @reset="onResetUpdate" class="w-100">
  <b-form-group id="form-title-edit-group"
                label="Title:"
                label-for="form-title-edit-input">
      <b-form-input id="form-title-edit-input"
                    type="text"
                    v-model="editForm.title"
                    required
                    placeholder="Enter title">
      </b-form-input>
    </b-form-group>
    <b-form-group id="form-author-edit-group"
                  label="Author:"
                  label-for="form-author-edit-input">
        <b-form-input id="form-author-edit-input"
                      type="text"
                      v-model="editForm.author"
                      required
                      placeholder="Enter author">
        </b-form-input>
      </b-form-group>
    <b-form-group id="form-read-edit-group">
      <b-form-checkbox-group v-model="editForm.read" id="form-checks">
        <b-form-checkbox value="true">Read?</b-form-checkbox>
      </b-form-checkbox-group>
    </b-form-group>
    <b-button type="submit" variant="primary">Update</b-button>
    <b-button type="reset" variant="danger">Cancel</b-button>
  </b-form>
</b-modal>

Теперь надо добавить стейт формы в часть data в разделе script:

editForm: {
  id: '',
  title: '',
  author: '',
  read: [],
},

2. Обрабатываем нажатия кнопки Update

Выполняем обновление кнопки «Update» в таблице:

<button
        type="button"
        class="btn btn-warning btn-sm"
        v-b-modal.book-update-modal
        @click="editBook(book)">
    Update
</button>

Чтобы обновить значения в editForm, добавляем новый метод:

editBook(book) {
  this.editForm = book;
},

Чтобы обрабатывать отправку формы, тоже нужен метод:

onSubmitUpdate(evt) {
  evt.preventDefault();
  this.$refs.editBookModal.hide();
  let read = false;
  if (this.editForm.read[0]) read = true;
  const payload = {
    title: this.editForm.title,
    author: this.editForm.author,
    read,
  };
  this.updateBook(payload, this.editForm.id);
},

3. Подключаем AJAX-запрос

updateBook(payload, bookID) {
  const path = `http://localhost:5000/books/${bookID}`;
  axios.put(path, payload)
    .then(() => {
      this.getBooks();
    })
    .catch((error) => {
      // eslint-отключение следующей строки
      console.error(error);
      this.getBooks();
    });
},

4. Оповещаем пользователя (Alert)

Обновляем updateBook:

updateBook(payload, bookID) {
  const path = `http://localhost:5000/books/${bookID}`;
  axios.put(path, payload)
    .then(() => {
      this.getBooks();
      this.message = 'Book updated!';
      this.showMessage = true;
    })
    .catch((error) => {
      // eslint-отключение следующей строки
      console.error(error);
      this.getBooks();
    });
},

5. Обрабатываем нажатия кнопки Cancel

Добавляем метод:

onResetUpdate(evt) {
  evt.preventDefault();
  this.$refs.editBookModal.hide();
  this.initForm();
  this.getBooks();
},

Обновляем initForm():

initForm() {
  this.addBookForm.title = '';
  this.addBookForm.author = '';
  this.addBookForm.read = [];
  this.editForm.id = '';
  this.editForm.title = '';
  this.editForm.author = '';
  this.editForm.read = [];
},

Приложение следует в обязательном порядке протестировать и убедиться в том, что modal отображается при нажатии кнопки, как и в том, что введённые значения заполнены верно.

update_book_1-20219-1f5a1b.gif

DELETE-маршрут

Сервер

Выполним обновление обработчика маршрута:

@app.route('/books/', methods=['PUT', 'DELETE'])
def single_book(book_id):
    response_object = {'status': 'success'}
    if request.method == 'PUT':
        post_data = request.get_json()
        remove_book(book_id)
        BOOKS.append({
            'id': uuid.uuid4().hex,
            'title': post_data.get('title'),
            'author': post_data.get('author'),
            'read': post_data.get('read')
        })
        response_object['message'] = 'Book updated!'
    if request.method == 'DELETE':
        remove_book(book_id)
        response_object['message'] = 'Book removed!'
    return jsonify(response_object)

Клиент

Выполним обновление кнопки «Delete»:

<button
        type="button"
        class="btn btn-danger btn-sm"
        @click="onDeleteBook(book)">
    Delete
</button>

Выполним добавление методов для обработки нажатия кнопки, а потом удалим книгу:

removeBook(bookID) {
  const path = `http://localhost:5000/books/${bookID}`;
  axios.delete(path)
    .then(() => {
      this.getBooks();
      this.message = 'Book removed!';
      this.showMessage = true;
    })
    .catch((error) => {
      // eslint-отключение следующей строки
      console.error(error);
      this.getBooks();
    });
},
onDeleteBook(book) {
  this.removeBook(book.id);
},

Когда юзер нажимает кнопку удаления, происходит вызов метода onDeleteBook(), запускающего другой метод под названием removeBook(). Данный метод отправляет DELETE-запрос на сервер. Когда ответ приходит, происходит отображение Alert и запуск getBooks().

delete_book_1-20219-b795c5.gif

Вот и всё!

Итак, в нашей статье мы рассмотрели основные настройки при написании CRUD-приложения посредством Vue и Flask. Исходный код смотрите здесь.

Источник — «Developing a Single Page App with Flask and Vue.js».

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
0 комментариев
Для комментирования необходимо авторизоваться
Популярное
Сегодня тут пусто