Создание одностраничного приложения с 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)
Далее обновляем кнопку «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>
Давайте сделаем проверку и попробуем добавить книгу:
Компонент 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>
После обновления браузера увидим:
Далее нужно добавить в шаблон фактический компонент 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 динамическим и передать пользовательское сообщение, используйте в 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(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(); }); },
Проверяем, как всё теперь работает:
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 отображается при нажатии кнопки, как и в том, что введённые значения заполнены верно.
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); },
Когда юзер нажимает кнопку удаления, происходит вызов метода
Вот и всё!
Итак, в нашей статье мы рассмотрели основные настройки при написании CRUD-приложения посредством Vue и Flask. Исходный код смотрите здесь.
Источник — «Developing a Single Page App with Flask and Vue.js».