DEV Community

Estéban
Estéban

Posted on

Gestion des articles - Créer un blog avec Adonis

Bonjour,

Bienvenue dans ce tutoriel pour apprendre à utiliser le framework web Adonis ! Si tu souhaites en savoir plus sur Adonis en 1 coup œil, je t'invite à lire cette page.

Dans cette partie, on va voir ensemble comment créer un article, le lier à un utilisateur et afficher sur une page !

Rappel

Ce tutoriel est la cinquième et dernière partie d'une série de tutoriels qui ont pour objectif de te faire découvrir Adonis au travers la création d'un blog.

Pour lire la partie précédente, c'est par là Création et visualisation des articles - Créer un blog avec Adonis

Tu trouveras aussi sur GiHub l'ensemble du code source du projet !

Sommaire

Ce tutoriel est découpé en différente partie pour t'aider et pour éviter d'avoir des articles trop longs où l'on pourrait se perdre !

Nous allons donc voir ensemble :

Finalement, tu auras un blog fonctionnel !

Gestion des articles

Comme le chapitre précédent, nous allons faire ce chapitre sous la forme d'un exercice. Je vais t'indiquer les étapes à suivre, te donner des indications et des ressources qui peuvent t'aider pour aller au bout puis nous ferons la correction ensemble !

Pour rappel, nous souhaitons réaliser une route articles/create qui permet de créer un article. Une fois l'article enregistré, nous devons être redirigé sur la page de l'article. Aussi, nous souhaitons avoir un bouton sur les pages des articles qui permet d'éditer l'article. Une fois l'édition terminée, nous devons être redirigé vers l'article.

Ce qu'il faut faire

La liste suivante est l'ensemble des tâches ordonnées t'indiquant ce qu'il y a à faire pour arriver au bout de cette dernière partie :

  1. Mettre en place le middleware d'authentification
    • Il ne faut le placer que sur les routes qui ont besoin d'être protégées
    • Un peu d'aide ici et !
  2. Créer les différentes méthodes de gestions des articles pour le controller
    • Il ne faut pas oublier de valider les données entrantes sur le serveur
    • Il ne faut pas non plus oublier de gérer les erreurs
    • Un indice ici
  3. Créer les différentes vues pour la création et l'édition d'un article

Gestion des articles

Mise en place du middleware d'authentification

Le middleware pour la gestion de l'authentification est fourni par Adonis. Ainsi, il nous suffit de l'ajouter sur les routes que l'on souhaite protéger par l'authentification.

Pour cela, on se rend dans le fichier routes.ts du dossier start :

Route.resource('articles', 'ArticlesController').middleware({
  create: ['auth'],
  store: ['auth'],
  edit: ['auth'],
  update: ['auth'],
})
Enter fullscreen mode Exit fullscreen mode

La route create permet d'afficher le formulaire de création d'un article.

La route store permet de recevoir les informations de la route create pour stocker l'article dans la base de données.

La route edit permet d'afficher le formulaire de l'édition d'un article.

La route update permet de recevoir les informations de la route edit pour mettre à jour un article de la base de données.

Ainsi, il est impossible d'accéder à l'une de ses quatres routes si l'utilisateur n'est pas authentifié.

Création du validator pour les articles

Encore une fois, utilisons la commande :

node ace make:validator article
Enter fullscreen mode Exit fullscreen mode

Dans le validator, on va vouloir l'unicité des titres de nos articles comme on l'a indiqué dans notre migration. On va aussi spécifier la taille maximale du titre et du contenu. Ainsi, le validator ressemble à cela :

public schema = schema.create({
  title: schema.string({ trim: true }, [
    rules.maxLength(256),
    rules.unique({ table: 'articles', column: 'title' }),
  ]),
  content: schema.string({ trim: true }, [rules.maxLength(1024)]),
})
Enter fullscreen mode Exit fullscreen mode

Ensuite, on personnalise les messages erreurs et le tour est joué !

public messages = {
  'title.required': 'Le titre est requis',
  'title.string': 'Le titre doit être une chaîne de caractères',
  'title.maxLength': 'Le titre doit être moins de 256 caractères',
  'title.unique': 'Le titre existe déjà',
  'content.required': 'Le contenu est requis',
  'content.string': 'Le contenu doit être une chaîne de caractères',
  'content.maxLength': 'Le contenu doit être moins de 1024 caractères',
}
Enter fullscreen mode Exit fullscreen mode

Pour en savoir plus : unique, validator, messages

Création des méthodes du controller des articles

On va donc devoir créer une méthode pour chacune de ses routes. Il est simple de s'y repérer puisque le nom de la méthode est le nom de la route.

Pour create :

public async create({ view }: HttpContextContract) {
  return view.render('articles/create')
}
Enter fullscreen mode Exit fullscreen mode

Rien de sorcier ici, on crée la page contenant le formulaire de création. Nous allons créer la vue dans la suite.

Pour store, on commence par récupérer les informations dont on a besoin pour créer l'article :

const { user } = auth
const { title, content } = await request.validate(ArticleValidator)
Enter fullscreen mode Exit fullscreen mode

Il est possible de récupérer l'utilisateur grâce au middleware précédemment ajouté. En effet, le middleware vérifie l'identité de la personne et l'ajoute dans nos controllers. On récupère aussi les informations dont on a besoin, validées par le validator !

Ensuite, on crée un article :

const article = await Article.create({
    title,
    content,
    ownerId: user!.id,
  })
Enter fullscreen mode Exit fullscreen mode

Puis on charge le owner avant de rendre la page d'un unique article à l'utilisateur :

await article.load('owner')

return view.render('articles/show', {
  article,
})
Enter fullscreen mode Exit fullscreen mode

Pour finir, la méthode ressemble à cela :

public async store({ request, view, auth }: HttpContextContract) {
  const { user } = auth
  const { title, content } = await request.validate(ArticleValidator)

  const article = await Article.create({
    title,
    content,
    ownerId: user!.id,
  })

  await article.load('owner')

  return view.render('articles/show', {
    article,
  })
}
Enter fullscreen mode Exit fullscreen mode

Pour edit, nous allons aussi rendre une page d'édition de l'article. Cependant, avant cela, nous devons utiliser le paramètre présent dans l'url pour récupérer l'article et ajouter son contenu au formulaire afin que ce dernier soit prérempli ! Si le formulaire est vide, comment savoir ce qu'il faut mettre à jour ?

public async edit({ view, params }: HttpContextContract) {
  const { id } = params

  let article: Article
  try {
    article = await Article.findOrFail(id)
  } catch (error) {
    console.error(error)
    return view.render('errors/not-found')
  }

  return view.render('articles/edit', {
    article,
  })
}
Enter fullscreen mode Exit fullscreen mode

En cas d'erreur pour trouver l'article, et comme on a pu faire sur show, on renvoie l'utilisateur sur une page d'erreur ! Cela permet de gérer le cas où l'utilisateur rendre une mauvaise valeur directement dans l'url !

Update est la méthode la plus complexe mais qui ne contient que des éléments que l'on a pu déjà aborder. Dans un premier temps, on récupère l'article que l'on veut mettre à jour :

const { id } = params

let article: Article
try {
  article = await Article.findOrFail(id)
} catch (error) {
  console.error(error)
  return view.render('errors/not-found')
}
Enter fullscreen mode Exit fullscreen mode

Ensuite, on extrait des données du formulaire en les passant dans le validator :

const { title, content } = await request.validate(ArticleValidator)
Enter fullscreen mode Exit fullscreen mode

Puis on met à jour l'article et on le sauvegarde :

article.title = title
article.content = content

await article.save()
Enter fullscreen mode Exit fullscreen mode

Ensuite, on charge l'utilisateur dans l'article et on le redirige vers la page de visualisation de son article :

await article.load('owner')

return view.render('articles/show', {
  article,
})
Enter fullscreen mode Exit fullscreen mode

Finalement, la méthode complète ressemble à cela :

public async update({ request, view, params }: HttpContextContract) {
  const { id } = params

  let article: Article
  try {
    article = await Article.findOrFail(id)
  } catch (error) {
    console.error(error)
    return view.render('errors/not-found')
  }

  const { title, content } = await request.validate(ArticleValidator)

  article.title = title
  article.content = content

  await article.save()
  await article.load('owner')

  return view.render('articles/show', {
    article,
  })
}
Enter fullscreen mode Exit fullscreen mode

Maintenant que tous nos controllers sont faits, passons à nos 2 vues !

Création des vues pour la gestion des articles

Les pages create et edit vont être très similaires. Elles vont toutes les 2 contenir un formulaire, vide pour la première et avec un article pour la seconde.

Pour create :

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Créer un article</title>
</head>
<body>
  <form action="{{ route('ArticlesController.store') }}" method="post">
    <div>
      <label for="title">Titre de l'article</label>
      <input type="text" name="title" id="title" value="{{ flashMessages.get('title', '') }}">
      @if(flashMessages.has('errors.title'))
      <div>{{ flashMessages.get('errors.title') }}</div>
      @endif
    </div>
    <div>
      <label for="content">Contenu de l'article</label>
      <textarea name="content" id="content" cols="30" rows="10" value="{{ flashMessages.get('content', '') }}"></textarea>
      @if(flashMessages.has('errors.content'))
      <div>{{ flashMessages.get('errors.content') }}</div>
      @endif
    </div>
    <button type="submit">Créer</button>
  </form>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Comme pour le formulaire d'authentification, on remarque les if qui permettent de conditionner l'affichage d'un message d'erreur par le serveur.

Pour edit :

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Créer un article</title>
</head>
<body>
  <form action="{{ route('ArticlesController.update', {id: article.id}) }}?_method=put" method="post">
    <div>
      <label for="title">Titre de l'article</label>
      <input type="text" name="title" id="title" value="{{ flashMessages.get('title', article.title) }}">
      @if(flashMessages.has('errors.title'))
      <div>{{ flashMessages.get('errors.title') }}</div>
      @endif
    </div>
    <div>
      <label for="content">Contenu de l'article</label>
      <textarea name="content" id="content" cols="30" rows="10">{{ flashMessages.get('content', article.content) }}</textarea>
      @if(flashMessages.has('errors.content'))
      <div>{{ flashMessages.get('errors.content') }}</div>
      @endif
    </div>
    <button type="submit">Modifier</button>
  </form>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Il y a tout de même quelques subtilités dans ce formulaire. Tout d'avoir cela :

{{ flashMessages.get('content', article.content) }}
Enter fullscreen mode Exit fullscreen mode

Cela permet d'afficher le contenu d'un message flash s'il existe, sinon, le contenu de l'article. Ainsi, lorsque la page est chargée, le message flash est vide donc le contenu de l'article va venir remplir le formulaire. Mais supposons que l'utilisateur modifie des informations mais que ces dernières soient rejetées par le validator, alors il retrouvera dans son formulaire les données qu'il avait saisi et non les données initiales de l'article !

Ensuite, pour mettre à jour un article, il est obligatoire d'utiliser les verbes PUT ou PATCH. Cependant, les navigateurs ne les prennent pas en compte. Ainsi, on utilise ce petit tricks pour que cela fonctionne :

<form action="{{ route('ArticlesController.update', {id: article.id}) }}?_method=put" method="post">
Enter fullscreen mode Exit fullscreen mode

La documentation explique très bien cela ! Et pour que cela soit fonctionnel, il faut se rendre dans le fichier app.ts présent dans config et passer la clé allowMethodSpoofing à true !

Et enfin, nous allons ajouter un lien de modification de l'article lorsque l'utilisateur est connecté dans la vue show.edge :

@if(auth.isLoggedIn)
  <a href="{{ route('ArticlesController.edit', {id: article.id}) }}">Editer l'article</a>
@endif
Enter fullscreen mode Exit fullscreen mode

Pour en savoir plus : create, save, load, spoofing, isLoggedIn

Bonus

La page index.edge dans le dossier resources/views ne nous sert pas. Alors on peut supprimer le fichier et dans nos routes, nous allons la rediriger vers nos articles comme cela :

Route.get('/', async ({ response }) => {
  return response.redirect('/articles')
})
Enter fullscreen mode Exit fullscreen mode

Si l'on se rend sur http://localhost:3333/, on est alors redirigé vers http://localhost:3333/articles !

Sur notre page index.edge dans le dossier resources/views/articles, il est possible de mettre un bouton de création d'un article uniquement lorsque l'utilisateur est connecté ! En effet, même si la route est protégée, on ne souhaite pas que tous nos lecteurs voit un bouton de création d'articles alors que l'action n'est pas possible pour eux. Ajoutons cela à notre fichier :

@if(auth.isLoggedIn)
  <a href="{{ route('ArticlesController.create') }}">Créer un article</a>
@endif
Enter fullscreen mode Exit fullscreen mode

Et voilà, le tour est joué. Le lien ne va apparait que lorsque l'utilisateur est connecté grâce à la condition !

Conclusion

Et voilà pour cette cinquième et dernière partie. On a vu plus en détail la création et la gestion de pages protégées par un middleware.

N'hésite pas à commenter si tu as des questions, si ça t'a plus ou même pour me faire des retours !

Et tu peux aussi me retrouver sur Twitter ou sur LinkedIn !

Notre blog est maintenant terminé ! J'espère que ça t'a plus. N'hésite pas à commenter pour me dire ce que tu as pensé de la série !

À bientôt pour de nouveaux tutoriels !

Possibles évolutions

Je n'allais quand même pas te laisser sans un peu de choses à faire. Voici une liste d'amélioration pour ce blog qui vont te permettre de progresser dans la maitrise du framework :

  • Mettre en place un draft mode.
    • Pour cela, on peut ajouter une propriété à nos articles nommée isPublished
    • Si cette propriété est à false, alors l'article ne sera pas disponible pour les utilisateurs non authentifiés. On utilisera alors un filtre si l'utilisateur n'est pas login. Un peu d'aide ici et !
    • Si la propriété est à true, alors l'article est visible.
    • Lors de l'édition et de la création, on peut avoir la possibilité de changer cela via une checkbox.
  • Mettre des images aux articles
    • Pour cela, tu peux utiliser cela !
  • Ajouter du style à ton blog avec un peu de CSS
  • On peut créer une page de profil pour l'utilisateur connecté et lui permettre de voir l'ensemble des articles qu'il a écrit !
    • Cela pourrait t'aider !
  • Il est aussi possible de se dire que l'on peut créer une page où les utilisateurs peuvent se connecter et créer des articles, comme sur medium. Mais il ne faudrait alors pas
  • Et pour les plus motivé, on peut même imaginer créer un blog
    • Les lecteurs entre un pseudo, un message et publie le commentaire
    • On peut même imaginer que les lecteurs doivent se créer un compte !

Et voici un bon nombre d'idées pour améliorer le blog et pour te montrer que tu peux dorénavant faire beaucoup de choses sur le web ! Et je suis certains que ton imagination a encore pleins d'idées !

Je peux aussi te conseil d'aller voir ce repo qui contient une application real-world et qui te montre comment faire et en plus, c'est par le créateur d'Adonis !

Bye ! 👋

Discussion (0)