Thibault Mocellin

Thibault Mocellin

Développeur Full-Stack freelance basé à Annecy 🇫🇷

Créer une application web avec Docker, Parse et Next.js - Partie 4 : Création de l'application Next

Posted on May 29, 2017

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

docker parse-server parse-dashboard

Cliquez sur le bouton “Create a Class” et ajoutez la classe TodoList

docker parse-server parse-dashboard add class

Ensuite nous allons ajouter la colonne label. Cliquez sur “Edit” puis “Add a column”.

docker parse-server parse-dashboard add column

Sélectionnez le type et validez.

docker parse-server parse-dashboard

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. docker parse-server parse-dashboard

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)}
        />
      )
    }
  }
}
  1. On importe React,Parse et le router Next qui nous permettra de naviguer jusqu’à la seconde page.
  2. On importe les composants nécessaires
  3. 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.
  4. 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.