Todo App Mit Vue Frontend Und Flask Backend
Ein Blick auf Vue.js
Einleitung
Es ist immer gut Neues zu lernen. Und um neue Technologien zu lernen ist es meiner Meinung nach am Besten etwas damit zu erstellen, nur darüber zu lesen oder Videos darüber ansehen reicht meistens nicht.
Ich wollte mich also mit Vue.js vertraut machen, einem progressiven Javascript Frontend Framework.
Als Backend verwende ich hier Flask, ein Python Microframework für Webentwicklung. Perfekt für APIs (Daten Schnittstellen).
Als CSS Framework verwende ich hier Bulma, was in etwa mit Bootstrap zu vergleichen ist.
Anforderungen
Was soll diese Todo Liste können:
- Ich möchte, dass Alles auf einer Seite ohne diese neu laden zu müssen funktionieren soll.
- Ich möchte bestehende Einträge vom Backend laden und anzeigen.
- Ich möchte neue Einträge machen können und diese ans Backend schicken.
- Ich möchte den Text der Einträge ändern können, nur mittels klick auf einen aktiven Eintrag.
- Ich möchte Änderungen ans Backend schicken.
- Ich möchte Einträge auf “erledigt” stellen können und alle erledigten Einträge nach unten reihen.
- Ich möchte Einträge löschen können.
- Ich möchte nicht, dass leere Einträge hinzugefügt werden können.
- Ich möchte nicht, dass erledigte Einträge geändert werden können.
Hier wird das Hinzufügen eines neuen Eintrags gezeigt.
Code
Ich habe den gesamten Code einsehbar unter:
- Frontend: https://gitlab.com/mrf-todo/mrf-todo-vue-frontend
- Backend: https://gitlab.com/mrf-todo/mrf-todo-flask-backend
Die Erstellung der Projekte
Abhängigkeiten
- python3
- nodejs
- yarn
Vue.js Frontend
# vue-cli installieren
yarn global add vue-cli
# Ich musste auch den yarn path in meinem ~/.zshrc exportieren, um vue-cli zu ermöglichen
export PATH="$PATH:$(yarn global bin)"
# Projektordner erstellen und darin wechseln
mkdir -p ~/workspace
cd ~/workspace
# Ein Vue.js Projekt mit der webpack Vorlage erstellen
vue init webpack mrf-todo-vue-frontend
#? Project name mrf-todo-vue-frontend
#? Project description A Vue.js project
#? Author Martin Rupert Fischer <martin@martinfischer.software>
#? Vue build standalone
#? Install vue-router? Yes
#? Use ESLint to lint your code? Yes
#? Pick an ESLint preset Airbnb
#? Set up unit tests No
#? Setup e2e tests with Nightwatch? No
#? Should we run `npm install` for you after the project has been created? (recommended) yarn
# vue-cli · Generated "mrf-todo-vue-frontend".
# Vue.js Projekt ausprobieren
cd mrf-todo-vue-frontend
yarn dev
# Abhängigkeiten installieren
yarn add axios
yarn add --dev bulma node-sass sass-loader
Hier wird das Ändern eines Eintrags gezeigt.
Flask Backend
# Projektordner erstellen und hinein wechseln
mkdir -p ~/workspace/mrf-todo-flask-backend
cd ~/workspace/mrf-todo-flask-backend
# Eine virtuelle Umgebung erstellen und diese aktivieren
python3 -m venv env
source env/bin/activate
# Abhängigkeiten installieren
pip install flask flask-cors
# Datei erstellen
touch app.py
Code
Backend
app.py
from flask import Flask, jsonify, request
from flask_cors import CORS
import uuid
TODOS = [
{
"id": uuid.uuid4().hex,
"text": "Flask Backend",
"done": True,
},
{
"id": uuid.uuid4().hex,
"text": "Vue Frontend",
"done": False,
},
{
"id": uuid.uuid4().hex,
"text": "Schwimmen gehen",
"done": False,
}
]
DEBUG = True
app = Flask(__name__)
app.config.from_object(__name__)
CORS(app)
@app.route("/todos", methods=["GET", "POST"])
def all_todos():
response_object = {"status": "success"}
if request.method == "POST":
post_data = request.get_json()
TODOS.insert(0, {
"id": uuid.uuid4().hex,
"text": post_data.get("text"),
"done": post_data.get("done"),
})
move_done()
response_object["message"] = "Todo hinzugefügt!"
else:
move_done()
response_object["todos"] = TODOS
return jsonify(response_object)
@app.route("/todos/toggle-done/<todo_id>", methods=["PUT"])
def toggle_todo(todo_id):
response_object = {"status": "success"}
if request.method == "PUT":
toggle_todo_done(todo_id)
move_done()
response_object["message"] = "Todo getoggelt!"
return jsonify(response_object)
def toggle_todo_done(todo_id):
for todo in TODOS:
if todo["id"] == todo_id:
if todo["done"]:
todo["done"] = False
else:
todo["done"] = True
return True
return False
def move_done():
for todo in TODOS:
if todo["done"]:
TODOS.remove(todo)
TODOS.append(todo)
@app.route("/todos/remove/<todo_id>", methods=["DELETE"])
def remove_todo(todo_id):
response_object = {"status": "success"}
if request.method == "DELETE":
remove_todo_help(todo_id)
response_object["message"] = "Todo gelöscht!"
return jsonify(response_object)
def remove_todo_help(todo_id):
for todo in TODOS:
if todo["id"] == todo_id:
TODOS.remove(todo)
return True
return False
@app.route("/todos/edit/<todo_id>", methods=["PUT"])
def edit_todo(todo_id):
response_object = {"status": "success"}
if request.method == "PUT":
post_data = request.get_json()
remove_todo_help(todo_id)
TODOS.insert(0, {
"id": uuid.uuid4().hex,
"text": post_data.get("text"),
"done": post_data.get("done"),
})
move_done()
response_object["message"] = "Todo updated!"
return jsonify(response_object)
if __name__ == "__main__":
app.run()
Hier wird auf erldigt gestellt, gereiht und das Löschen gezeigt.
Frontend
src/main.js
import Vue from 'vue';
import App from './App';
import router from './router';
require('./assets/sass/main.scss');
Vue.config.productionTip = false;
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>',
});
src/App.vue
<template>
<div id="app">
<router-view/>
</div>
</template>
<script>
export default {
name: 'App',
};
</script>
<style>
</style>
src/router/index.js
import Vue from 'vue';
import Router from 'vue-router';
import Todo from '@/components/Todo';
Vue.use(Router);
export default new Router({
routes: [
{
path: '/',
name: 'Todo',
component: Todo,
},
],
mode: 'history',
});
src/components/Todo.vue
<template>
<div class="container">
<h1 class="is-size-1">Todos</h1>
<hr>
<new></new>
<div class="card" v-for="(todo, index) in todos" :key="index">
<header class="card-header">
<div v-if="!todo.editing">
<p v-on:click="enableEditing(index)"
class="card-header-title"
v-bind:class="{ done: todo.done }">
{{ todo.text }}
</p>
<button type="button"
name="button"
class="button is-light"
v-on:click="toggleDone(todo.id)">
<span v-if="todo.done">⭕</span>
<span v-else>✅</span>
</button>
<button type="button"
name="button"
v-on:click="removeTodo(todo.id)"
class="button is-danger">✖
</button>
</div>
<div v-if="todo.editing">
<p class="card-header-title">
<input class="input"
type="text"
v-focus
v-model="todo.text"
/>
</p>
<button v-on:click="saveEdit(todo.id, index)" class="button is-success">Speichern
</button>
</div>
</header>
</div>
</div>
</template>
<script>
import axios from 'axios';
import New from './New';
const focus = {
inserted(el) {
el.focus();
},
};
export default {
components: {
new: New,
},
data() {
return {
todos: [],
};
},
directives: { focus },
methods: {
getTodos() {
const path = 'http://localhost:5000/todos';
axios.get(path)
.then((res) => {
this.todos = res.data.todos;
this.setEditing();
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
});
},
setEditing() {
for (let i = 0; i < this.todos.length; i += 1) {
this.$set(this.todos[i], 'editing', false);
}
},
enableEditing(index) {
for (let i = 0; i < this.todos.length; i += 1) {
if (!this.todos[index].done) {
this.todos[index].editing = true;
}
}
},
saveEdit(id, index) {
this.editTodo(id);
this.disableEditing(index);
},
toggleDone(id) {
const path = `http://localhost:5000/todos/toggle-done/${id}`;
axios.put(path)
.then(() => {
this.getTodos();
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
this.getTodos();
});
},
removeTodo(id) {
const path = `http://localhost:5000/todos/remove/${id}`;
axios.delete(path)
.then(() => {
this.getTodos();
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
this.getTodos();
});
},
editTodo(id) {
for (let i = 0; i < this.todos.length; i += 1) {
if (this.todos[i].id === id) {
const payload = {
text: this.todos[i].text,
done: this.todos[i].done,
};
const path = `http://localhost:5000/todos/edit/${id}`;
axios.put(path, payload)
.then(() => {
this.getTodos();
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
this.getTodos();
});
}
}
},
},
created() {
this.getTodos();
},
};
</script>
<style lang="scss" scoped>
.card {
margin: 1rem 0;
.done {
opacity: 0.2;
}
}
</style>
Hier wird das Hinzufügen eines erldigten Eintrags, das Abbrechen und die Verhinderung leerer Einträge gezeigt.
src/components/New.vue
<template>
<div class="add-todo">
<button
type="button"
name="button"
id="plus-button"
class="button is-primary"
v-on:click="show = !show; hide = true;">✚
</button>
<transition name="slide-fade">
<div v-if="!hide" v-bind:class="messageKind" class="notification">
<button class="delete" v-on:click="hide = !hide"></button>
{{ message }}
</div>
</transition>
<transition name="slide-fade">
<div v-if="show">
<div class="field is-grouped">
<div class="control">
<input class="input"
type="text"
placeholder="Text eingeben"
autofocus
v-model="addTodoForm.text"
required>
</div>
<div class="control">
<label class="checkbox">
<input type="checkbox" v-model="addTodoForm.done"> Erledigt
</label>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-link" v-on:click="submitNew">Hinzufügen</button>
</div>
<div class="control">
<button class="button is-dark" v-on:click="cancelNew">Abbrechen</button>
</div>
</div>
</div>
</transition>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
show: false,
hide: true,
addTodoForm: {
id: '',
text: '',
done: '',
},
message: '',
messageKind: '',
};
},
methods: {
addTodo(payload) {
if (payload.text) {
const path = 'http://localhost:5000/todos';
axios.post(path, payload)
.then(() => {
this.$parent.getTodos();
this.message = `✔ Todo hinzugefügt: ${payload.text}`;
this.messageKind = 'is-success';
this.hide = false;
})
.catch((error) => {
// eslint-disable-next-line
console.log(error);
this.$parent.getTodos();
});
} else {
this.message = '⚠ Todo leer, nicht hinzugefügt!';
this.messageKind = 'is-danger';
this.hide = false;
}
},
initForm() {
this.addTodoForm.id = '';
this.addTodoForm.text = '';
this.addTodoForm.done = false;
},
submitNew() {
this.show = !this.show;
const payload = {
text: this.addTodoForm.text,
done: this.addTodoForm.done,
};
this.addTodo(payload);
this.initForm();
},
cancelNew() {
this.show = !this.show;
},
},
created() {
this.initForm();
},
};
</script>
<style lang="scss" scoped>
#plus-button {
margin-bottom: 20px;
}
.slide-fade-enter-active {
transition: all .5s ease;
}
.slide-fade-leave-active {
transition: all .3s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to {
transform: translateX(10px);
opacity: 0;
}
</style>
src/assets/sass/main.scss
@import '~bulma/bulma';
Schlussbemerkung
Vue.js macht es sehr einfach Daten an Elemente zu binden und das Einfache manipulieren dieser zu ermöglichen.
Ich kann mir gut vorstellen komplexe Systeme mit Vue zu erstellen. Die Struktur mit Komponenten, die frei verbunden und kombiniert werden können ist sehr gut.
Diese Todo App ist natürlich nur zu Demonstrationszwecken geignet, weil jegliche Authentifizierung, Datenbankanbindung, Sicherheit etc. fehlt.