Cet article est le quatrième de la série qui a pour but de créer une application web qui repose sur docker. Dans cet article nous allons créer l’application Next.js. Cette application sera une simple todo list.
Voici la liste des données que contiendra l’application :
TodoList
- label (string)
- objectId (champ par défaut de parse)
- updatedAt (champ par défaut de parse)
- createdAt (champ par défaut de parse)
- ACL (champ par défaut de parse)
TodoItems
- label (string)
- isDone (booléen)
- todoList (Pointer)
- objectId (champ par défaut de parse)
- updatedAt (champ par défaut de parse)
- createdAt (champ par défaut de parse)
- ACL (champ par défaut de parse)
Initialisation des données
Pour commencer nous allons créer les données dans Parse à l’aide du dashboard.
docker-compose up
Ensuite rendez vous sur le dashboard vous devriez visualiser ceci
Cliquez sur le bouton “Create a Class” et ajoutez la classe TodoList
Ensuite nous allons ajouter la colonne label. Cliquez sur “Edit” puis “Add a column”.
Sélectionnez le type et validez.
Reproduisez la même chose pour la classe TodoItems en tenant compte du types des champs.
Faites attention à bien sélectionner la classe TodoList lors de l’ajout du champ de type pointer.
Initialisation de Next.js
Maintenant que la structure des données est en place passons à l’application. Commençons par initialiser le projet :
mkdir web-app
cd web-app
yarn add next react react-dom parse
Ensuite ajoutez le code suivant dans le fichier package.json :
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
Créez un dossier pages et ajoutez le fichier index.js avec le code suivant :
export default () => <h1>Hello Next.js !</h1>
Lancez la commande :
npm run dev
L’application va se lancer et sera accessible via http://localhost:3000.
Pour terminer nous allons créer un fichier config.js à la racine du dossier web-app. Plus tard nous aurons besoin de l’app-id et l’url de Parse Server nous allons donc stocker ces informations dans ce fichier.
export const PARSE_APP_ID = 'web-app-docker'
export const PARSE_SERVER_URL = 'http://localhost:1337/parse'
La configuration est maintenant terminée.
Création des composants et des pages
L’application sera composée de quatre composants :
- TodoList.js
- TodoItems.js
- AddItem.js
- Layout.js Ces composants seront utilisés dans deux pages différentes :
- index
- todolist
Les composants seront situés dans le dossier components.
Commençons par le layout :
Layout.js
import Head from 'next/head'
export default ({ children, title = 'Titre par défault du layout' }) => (
<div>
<Head>
<title>{title}</title>
<meta charSet="utf-8" />
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" />
</Head>
{children}
<style global jsx>{`
html {
font-family: arial;
}
body {
display: flex;
align-items: center;
justify-content: center;
}
`}</style>
</div>
)
Passons au composant AddItem qui sera utilisé pour l’ajout d’objet de type TodoList et TodoItems.
AddItem.js
export default ({ addItem, placeholder }) => {
let input
return (
<div className="add-itm-ctnr">
<input ref={el => (input = el)} placeholder={placeholder} type="text" />
<button className="btn" onClick={() => addItem(input.value)}>
Ajouter
</button>
<style global jsx>{`
.add-itm-ctnr {
display: flex;
justify-content: space-between;
min-width: 576px;
margin-bottom: 32px;
}
input[type='text'] {
display: block;
margin: 0;
width: 350px;
font-family: sans-serif;
font-size: 18px;
appearance: none;
box-shadow: none;
border-radius: none;
height: 35px;
padding: 8px;
}
input[type='text']:focus {
outline: none;
}
.btn {
background-color: #4ca6af;
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
outline: 0;
}
.btn:hover {
cursor: pointer;
background-color: #34777d;
}
`}</style>
</div>
)
}
Maintenant nous allons ajoutez le composant TodoList qui permettra de visualiser, sélectionner et supprimer les objets de type TodoList.
TodoList.js
export default ({ todolist, deleteTodolist, showTodoList }) => {
const items = todolist.map(item => (
<li key={item.id} onClick={() => showTodoList(item.id)}>
<span>{item.get('label')}</span>
<i
className="fa fa-trash ic-delete"
onClick={event => {
event.stopPropagation()
deleteTodolist(item.id)
}}
/>
</li>
))
return (
<div>
<ul className="todolist">{items}</ul>
<style global jsx>{`
.todolist {
list-style-type: none;
padding: 0px;
}
.todolist li {
width: 550px;
height: 50px;
display: flex;
align-items: center;
padding: 12px;
border: solid 1px #d0d0d0;
font-size: 22px;
font-weight: 500;
margin-bottom: 12px;
border-radius: 2px;
justify-content: space-between;
color: #333;
-moz-box-shadow: 4px 6px 8px 0px #c0c0c0;
-webkit-box-shadow: 4px 6px 8px 0px #c0c0c0;
-o-box-shadow: 4px 6px 8px 0px #c0c0c0;
box-shadow: 4px 6px 8px 0px #c0c0c0;
}
.todolist li:hover {
-moz-box-shadow: 2px 4px 8px 0px #c0c0c0;
-webkit-box-shadow: 2px 4px 8px 0px #c0c0c0;
-o-box-shadow: 2px 4px 8px 0px #c0c0c0;
box-shadow: 2px 4px 8px 0px #c0c0c0;
cursor: pointer;
}
.ic-delete {
color: #d0d0d0;
}
.ic-delete:hover {
color: red;
}
`}</style>
</div>
)
}
Et enfin le composant TodoItems qui permet de visualiser, éditer et supprimer les objets de type TodoItems.
TodoItems.js
export default ({ todoListItems, removeItem, checkItem }) => {
const items = todoListItems.map(item => {
const visibility = item.get('isDone') ? 'visible' : 'hidden'
return (
<li onClick={() => checkItem(item.id)} key={item.id}>
<span>
<i className="fa fa-check" style={{ visibility, color: 'green', marginRight: 16 }} />
{item.get('label')}
</span>
<i
className="fa fa-trash ic-delete"
onClick={event => {
event.stopPropagation()
removeItem(item.id)
}}
/>
</li>
)
})
return (
<div>
<ul className="items">{items}</ul>
<style global jsx>{`
.items {
list-style-type: none;
padding: 0px;
}
.items li {
width: 550px;
height: 35px;
display: flex;
align-items: center;
padding: 12px;
border: solid 1px #d0d0d0;
font-size: 16px;
justify-content: space-between;
color: #333;
}
.items li:hover {
cursor: pointer;
}
.ic-delete {
color: #d0d0d0;
}
.ic-delete:hover {
color: red;
}
`}</style>
</div>
)
}
Maintenant que les composants sont créés nous allons passer aux pages.
Remplacez le code de la page index.js par le suivant :
import React from 'react'
import Router from 'next/router'
import Parse from 'parse'
import Layout from '../components/Layout'
import AddItem from '../components/AddItem'
import TodoList from '../components/TodoList'
import { PARSE_APP_ID, PARSE_SERVER_URL } from '../config.js'
export default class extends React.Component {
constructor() {
super()
this.state = {
todoLists: [],
isFetching: true,
}
Parse.initialize(PARSE_APP_ID)
Parse.serverURL = PARSE_SERVER_URL
}
async componentDidMount() {}
async _showTodoList(id) {}
async _addTodoList(value) {}
async _deleteTodoList(id) {}
render() {
return (
<Layout title="Next / Parse Todolist sample">
<h1>Next JS TodoList</h1>
<AddItem addItem={value => this._addTodoList(value)} placeholder="Ajouter une nouvelle liste" />
{this.renderContent()}
</Layout>
)
}
renderContent() {
if (this.state.isFetching) {
return (
<div style={{ textAlign: 'center', color: '#4ca6af' }}>
<i className="fa fa-circle-o-notch fa-spin fa-2x" />
</div>
)
} else {
return (
<TodoList
todolist={this.state.todoLists}
showTodoList={id => this._showTodoList(id)}
deleteTodolist={id => this._deleteTodoList(id)}
/>
)
}
}
}
- On importe React,Parse et le router Next qui nous permettra de naviguer jusqu’à la seconde page.
- On importe les composants nécessaires
- On définit l’état initiale du composant et on initialise Parse avec la valeur de l’app-id et l’url du fichier config.js.
- On définit l’ensemble des méthode dans lesquels nous viendrons ajoutez le code qui permet d’interagir avec Parse.
La première chose que nous allons faire est récupérer la liste des objets de type TodoList. Ajoutez le code suivant dans la méthode componentDidMount :
var TodoList = Parse.Object.extend('TodoList')
var query = new Parse.Query(TodoList)
query.descending('createdAt')
try {
var data = await query.find()
this.setState({
isFetching: false,
todoLists: data,
})
} catch (error) {
alert(error.message)
this.setState({
isFetching: false,
})
}
Ici nous définissons une simple Query qui récupère l’ensemble des objets de type TodoList, une fois récupérés on met à jour le state avec la liste récupérée.
Ensuite nous ajoutons le code de la méthode _addTodoList
if (value.length > 0) {
var TodoList = Parse.Object.extend('TodoList')
var todoList = new TodoList()
todoList.set('label', value)
try {
var res = await todoList.save()
this.setState({
todoLists: [res, ...this.state.todoLists],
})
} catch (error) {
alert(error.message)
}
}
Puis celui de la méthode _deleteTodoList
const idx = this.state.todoLists.findIndex(obj => obj.id == id)
const currentTodoList = this.state.todoLists.find(obj => obj.id == id)
try {
await currentTodoList.destroy()
this.setState({
todoLists: this.state.todoLists.slice(0, idx).concat(this.state.todoLists.slice(idx + 1)),
})
} catch (error) {
alert(error.message)
}
Pour terminer avec la page index nous ajoutons le code pour la méthode _showTodoList
Router.push({
pathname: '/todolist',
query: { id: id },
})
Ici on utilise le router de Next on indique la page via pathname ainsi que l’id de la todolist via query.
Passons à la création de la page todolist. Créez un fichier todolist.js dans le dossier pages et ajoutez-y le code suivant :
import React from 'react'
import TodoItems from '../components/TodoItems'
import AddItem from '../components/AddItem'
import Layout from '../components/Layout'
import Parse from 'parse'
import { PARSE_APP_ID, PARSE_SERVER_URL } from '../config.js'
export default class extends React.Component {
constructor() {
super()
this.state = {
items: [],
isFetching: true,
todoList: {},
}
Parse.initialize(PARSE_APP_ID)
Parse.serverURL = PARSE_SERVER_URL
}
async componentDidMount() {
//Récupération de la todoList
var TodoList = Parse.Object.extend('TodoList')
var query = new Parse.Query(TodoList)
query.equalTo('objectId', this.props.url.query.id)
try {
var _todoList = await query.first()
if (_todoList) {
var TodoItems = Parse.Object.extend('TodoItems')
var query = new Parse.Query(TodoItems)
query.equalTo('todoList', _todoList)
var _items = await query.find()
this.setState({
isFetching: false,
todoList: _todoList,
items: _items,
})
} else {
alert('Id invalide')
}
} catch (error) {
alert(error.message)
}
}
async _addItem(value) {}
async _deleteItem(id) {}
async _checkItem(id) {}
render() {
return <Layout title="Next / Parse Todolist sample">{this.renderContent()}</Layout>
}
renderContent() {
if (this.state.isFetching) {
return (
<div style={{ textAlign: 'center', color: '#4ca6af' }}>
<i className="fa fa-circle-o-notch fa-spin fa-2x" />
</div>
)
} else {
return (
<div>
<h3>{this.state.todoList.get('label')}</h3>
<AddItem addItem={value => this._addItem(value)} placeholder="Ajouter une tâche" />
<TodoItems
todoListItems={this.state.items}
removeItem={id => this._deleteItem(id)}
checkItem={id => this._checkItem(id)}
/>
</div>
)
}
}
}
Ici le fonctionnement est sensiblement le même que pour la page index à la différence que l’on va d’abord essayer de récupérer l’objet de type TodoList correspondant à l’id passé en paramètre et ensuite si l’objet existe pour cet id alors on récupère tous les objets de type TodoItems rattachés à ce dernier.
Pour finir on remplace le code des méthodes _addItem, _deleteItem, _checkItem.
async _addItem(value){
if(value.length > 0){
var TodoItem = Parse.Object.extend("TodoItems");
var todoItem = new TodoItem();
todoItem.set("label", value);
todoItem.set("isDone", false);
todoItem.set("todoList",this.state.todoList);
try{
var res = await todoItem.save();
this.setState({
items:[ res, ...this.state.items ]
});
}catch(error){
alert(error.message);
}
}
}
async _deleteItem(id){
const idx = this.state.items.findIndex((obj)=>obj.id == id);
const currentItem = this.state.items.find((obj)=>obj.id == id);
try{
await currentItem.destroy();
this.setState({
items: this.state.items.slice(0,idx).concat(this.state.items.slice(idx+1))
});
}catch(error){
alert(error.message);
}
}
async _checkItem(id){
const idx = this.state.items.findIndex((obj)=>obj.id == id);
const updatedItems = [...this.state.items];
const isDone = updatedItems[idx].get("isDone");
updatedItems[idx].set("isDone", !isDone );
try{
await updatedItems[idx].save();
this.setState({
items:updatedItems,
});
}catch(error){
alert(error.message);
}
}
Il reste un dernier point à régler, pour le moment lorsqu’on supprime un objet TodoList les objets TodoListItems qui lui sont rattachés ne sont pas supprimés. Pour résoudre ce problème nous allons rajouter un trigger lors de la suppression d’un objet TodoList.
Rajoutez le code suivant dans le fichier main.js situé dans le dossier cloud de Parse :
Parse.Cloud.afterDelete('TodoList', function (request) {
query = new Parse.Query('TodoItems')
query.equalTo('todoList', request.object)
query.find({
success: function (todoItems) {
Parse.Object.destroyAll(todoItems, {
success: function () {},
error: function (error) {
console.error('Erreur lors de la suppression des todo item ' + error.code + ': ' + error.message)
},
})
},
error: function (error) {
console.error('Erreur lors de la recherche des todo item ' + error.code + ': ' + error.message)
},
})
})
Ici on défini un trigger qui se déclenche après de la suppression d’un objet TodoList. Lors du déclenchement on vient récupérer l’ensemble des objets TodoItems qui lui sont rattaché et on les supprime.
Nous en avons fini avec la création de l’application Next dans l’article suivant nous verrons comment rajouter l’application à notre configuration Docker.
Vous pouvez retrouver le code source ici.