· 9 min read
Utilisation de Intro.js avec un projet Symfony
A la recherche d'idées pour améliorer l'onboarding de vos utilisateurs, je vous propose de découvrir comment utiliser Intro.js avec un projet Symfony
Objectifs
Dans cet article nous allons voir ensemble comment mettre en place un onboarding avec Intro.js dans une application Symfony.
Nous ferons en sorte que cet onboarding ne soit visible que lors de la première connexion de l’utilisateur.
Initialisation d’un projet Symfony
- On initie un nouveau projet Symfony
symfony new introjs --webapp
cd introjs
npm install --force
npm run build
docker-compose up -d
symfony serve -d
- Création d’une entité
User
On prends toute les propositions par défaut
symfony console make:user
- Création d’un controller
HomeController
symfony console make:controller Home
- Création d’une page de registration
symfony console make:registration
Creating a registration form for App\Entity\User
Do you want to add a @UniqueEntity validation annotation on your User class to make sure duplicate accounts aren't created? (yes/no) [yes]:
> yes
Do you want to send an email to verify the user's email address after registration? (yes/no) [yes]:
> no
Do you want to automatically authenticate the user after registration? (yes/no) [yes]:
> no
What route should the user be redirected to after registration?:
[12] app_home
> 12
- Création d’une page de login
symfony console make:auth
What style of authentication do you want? [Empty authenticator]:
[0] Empty authenticator
[1] Login form authenticator
> 1
The class name of the authenticator to create (e.g. AppCustomAuthenticator):
> AppAuthenticator
Choose a name for the controller class (e.g. SecurityController) [SecurityController]:
> SecurityController
Do you want to generate a '/logout' URL? (yes/no) [yes]:
> yes
- On modifier le fichier
src/Security/AppAuthenticator.php
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('app_home'));
}
- On modifie le fichier
config/packages/security.yaml
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/home, roles: ROLE_USER }
- On mets en place Boostrap 5 (via CDN) en modifiant le fichier
templates/base.html.twig
<!doctype html>
<html lang="en">
<head>
<title>
{% block title %}Welcome!{% endblock %}
</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
- On génére et applique les migrations
symfony console make:migration
symfony console d:m:m
- Et enfin on modifie
templates/home/index.html.twig
On mets en place un modèle de mise en page bateau, histoire de …
{% extends 'base.html.twig' %}
{% block title %}Hello HomeController!
{% endblock %}
{% block body %}
<header>
<div class="collapse bg-dark" id="navbarHeader">
<div class="container">
<div class="row">
<div class="col-sm-8 col-md-7 py-4">
<h4 class="text-white">About</h4>
<p class="text-muted">Add some information about the album below, the author, or any other background context. Make it a few sentences long so folks can pick up some informative tidbits. Then, link them off to some social networking sites or contact information.</p>
</div>
</div>
</div>
</div>
<div class="navbar navbar-dark bg-dark shadow-sm">
<div class="container">
<a href="#" class="navbar-brand d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="me-2" viewbox="0 0 24 24"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
<strong>Album</strong>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarHeader" aria-controls="navbarHeader" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
</header>
<main>
<section class="py-5 text-center container">
<div class="row py-lg-5">
<div class="col-lg-6 col-md-8 mx-auto">
<h1 class="fw-light">Album example</h1>
<p class="lead text-muted">Something short and leading about the collection below—its contents, the creator, etc. Make it short and sweet, but not too short so folks don’t simply skip over it entirely.</p>
<p>
<a href="#" class="btn btn-primary my-2">Main call to action</a>
<a href="#" class="btn btn-secondary my-2">Secondary action</a>
</p>
</div>
</div>
</section>
<div class="album py-5 bg-light">
<div class="container">
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
<div class="col">
<div class="card shadow-sm">
<img src="https://picsum.photos/199" alt="">
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<img src="https://picsum.photos/200" alt="">
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<img src="https://picsum.photos/201" alt="">
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
{% endblock %}
Installation de Intro.js
Passons enfin à la mise en place de la librairie.
- On install la libraire Intro.js
npm install intro.js --save
- Créons un fichier
assets/intro.js
import 'intro.js/introjs.css';
import introJs from 'intro.js';
introJs().setOptions({
steps: [{
intro: "Hello world!"
}, {
element: document.querySelector('.test'),
intro: "Ceci est un test",
}]
}).start();
- Ajoutons notre fichier dans la configuration de Webpack Encore
webpack.config.js
.addEntry('intro', './assets/intro.js')
- Et utilisons cela dans notre fichier
templates/base.html.twig
<!doctype html>
<html lang="en">
<head>
<title>
{% block title %}Welcome!{% endblock %}
</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{{ encore_entry_link_tags('intro') }}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{{ encore_entry_script_tags('intro') }}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
- Puis modifions le fichier
templates/home/index.html.twig
pour tester le bon fonctionnement.
<section class="py-5 text-center container">
<div class="row py-lg-5">
<div class="col-lg-6 col-md-8 mx-auto">
<h1 class="fw-light">Album example</h1>
<p class="lead text-muted">Something short and leading about the collection below—its contents, the creator, etc. Make it short and sweet, but not too short so folks don’t simply skip over it entirely.</p>
<p>
<a href="#" class="btn btn-primary my-2 test">Main call to action</a>
<a href="#" class="btn btn-secondary my-2">Secondary action</a>
</p>
</div>
</div>
</section>
Créons notre Onboarding
- Adaptons notre fichier
assets/intro.js
import "intro.js/introjs.css";
import introJs from "intro.js";
introJs()
.setOptions({
steps: [
{
intro: "Bienvenue sur votre espace personnel, nous allons vous guider pour vous aider à prendre en main votre espace personnel.",
},
{
element: document.querySelector(".intro-step1"),
intro: "Ceci est le nom de votre espace personnel.",
},
{
element: document.querySelector(".intro-step2"),
intro: "Ceci est le bouton d'action principale de votre espace personnel.",
},
{
element: document.querySelector(".intro-step3"),
intro: "Ceci est le bouton d'action secondaire de votre espace personnel.",
},
{
element: document.querySelector(".intro-step4"),
intro: "Ceci est une carte de votre espace personnel.",
},
{
element: document.querySelector(".intro-step5"),
intro: "Ceci est le bouton View",
},
],
})
.start();
- Adpatons notre fichier
templates/home/index.html.twig
{% extends 'base.html.twig' %}
{% block title %}Hello HomeController!
{% endblock %}
{% block body %}
<header>
<div class="collapse bg-dark" id="navbarHeader">
<div class="container">
<div class="row">
<div class="col-sm-8 col-md-7 py-4">
<h4 class="text-white">About</h4>
<p class="text-muted">Add some information about the album below, the author, or any other background context. Make it a few sentences long so folks can pick up some informative tidbits. Then, link them off to some social networking sites or contact information.</p>
</div>
</div>
</div>
</div>
<div class="navbar navbar-dark bg-dark shadow-sm">
<div class="container">
<a href="#" class="navbar-brand d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="me-2" viewbox="0 0 24 24"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
<strong>Album</strong>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarHeader" aria-controls="navbarHeader" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
</header>
<main>
<section class="py-5 text-center container">
<div class="row py-lg-5">
<div class="col-lg-6 col-md-8 mx-auto">
<h1 class="fw-light intro-step1">Album example</h1>
<p class="lead text-muted">Something short and leading about the collection below—its contents, the creator, etc. Make it short and sweet, but not too short so folks don’t simply skip over it entirely.</p>
<p>
<a href="#" class="btn btn-primary my-2 intro-step2">Main call to action</a>
<a href="#" class="btn btn-secondary my-2 intro-step3">Secondary action</a>
</p>
</div>
</div>
</section>
<div class="album py-5 bg-light">
<div class="container">
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
<div class="col intro-step4">
<div class="card shadow-sm">
<img src="https://picsum.photos/199" alt="">
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary intro-step5">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<img src="https://picsum.photos/200" alt="">
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card shadow-sm">
<img src="https://picsum.photos/201" alt="">
<div class="card-body">
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
</div>
<small class="text-muted">9 mins</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
{% endblock %}
- Et vérifions le résultat :
Onboarding que lors de la première connexion
- Gérons la première connexion en ajoutant un champ à notre entité User :
symfony console make:entity User
Your entity already exists! So let's add some new fields!
New property name (press <return> to stop adding fields):
> isFisrtConnexion
Field type (enter ? to see all types) [boolean]:
> boolean
Can this field be null in the database (nullable) (yes/no) [no]:
> yes
symfony console make:migration
symfony console d:m:m
- Refactorons l’import du CSS et du JS d’intro.js en le supprimant de
templates/base.html.twig
et en l’ajoutant danstemplates/home/index.html.twig
:
De cette manière Intro.js ne sera chargé que lorsque cela sera nécessaire.
{% block body %}
...
{% block javascripts %}
{{ parent() }}
{% if app.user.isFirstConnexion %}
{{ encore_entry_script_tags('intro') }}
{% endif %}
{% endblock %}
{% block stylesheets %}
{{ parent() }}
{{ encore_entry_link_tags('intro') }}
{% endblock %}
...
{% endblock %}
- Et enfin gérons le champ
isFirstConnexion
lors de l’ouverture de session en adaptant le fichiersrc/Security/AppAuthenticator.php
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
$user = $token->getUser();
if (true === $user->getIsFirstConnexion()) {
$user->setIsFirstConnexion(false);
$this->em->persist($user);
$this->em->flush();
}
if (null === $user->getIsFirstConnexion()) {
$user->setIsFirstConnexion(true);
$this->em->persist($user);
$this->em->flush();
}
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
return new RedirectResponse($targetPath);
};
return new RedirectResponse($this->urlGenerator->generate('app_home'));
}
Code source
Share: