Alors quel est l'intérêt de créer nous-même un popup de confirmation alors qu'il existe la fonction javascript confirm()
?
Premièrement, parceque la fenêtre de confirmation par défaut du navigateur, elle est ... comment dire ... Bah moche quoi !
Deuxième raison, plus problématique, c'est que certain navigateur propose de bloquer la fenêtre de confirmation lorsqu'un site ou une application en "abuse" :
Du coup une fois la case cochée, l'utilisateur ne sera plus sollicité par la fonction confirm()
et ça devient un problème lorsque l'on a développé une confirmation de suppression de ce type :
if( confirm("La suppression est définitive, êtes-vous sûr ?") ){
model.destroyRecord();
}
Impossible ici d'arriver jusqu'à la suppression.
Ensuite, une solution interne offre plus de flexibilité et permet d'implémenter une méthode respectant mieux la "philosophie" Ember.js.
Dernière raison, récemment, dans une application qui utilise des boutons avec un état "pending", j'ai eu besoin lors d'un clique sur un bouton, qu'une confirmation soit demandée et que l'état "pending" de mon bouton ne se termine qu'après la réalisation de l'action (après un clique sur "OK") ou au clique sur "annuler".
Il me fallait donc une méthode confirm()
qui retourne une promise, pour la retourner directement au button :
Ici j'ai volontairement ralenti la requête de suppression pour bien voir que l'état "pending" s'arrête à la fin de la requête de suppression vers l'API.
C'est pour ces raisons (il y en a sûrement d'autre) que nous allons voir comment implémenter simplement un mécanisme de confirmation interne.
Implémentation
Nous allons passer par plusieurs phases pour l'implémentation (simple composant, service etc...). L'idée est de montrer comment faire ou ne pas faire et pourquoi. En fin d'article une version finale dans un twiddle est disponible.
Composant
Commençons par créer le visuel de notre pop-up et le composant associé.
ember generate component confirmation-popup
<!-- templates/components/confirmation-popup.js -->
{{#if visible}}
<div class="my-dialog-cover">
<div class="my-dialog" tabindex="-1">
<div class="dialog-header">
<div class="dialog-title">
Confirmation demandée
</div>
</div>
<div class="dialog-body">
Veuillez confirmer cette action
</div>
<div class="dialog-footer">
<button class="btn btn__accept" {{action 'okAction'}}>OK</button>
<button class="btn btn__decline" {{action 'cancelAction'}}>Annuler</button>
</div>
</div>
</div>
{{/if}}
//components/confirmation-popup.js
import Ember from 'ember';
export default Ember.Component.extend({
visible: false,
actions: {
cancelAction: function(){
this.set('visible', false);
},
okAction: function(){
this.set('visible', false);
this.get('onConfirmAction')();
},
}
});
/* style/app.css */
.my-dialog-cover{
width: 100%;
height: 100%;
position: absolute;
top: 0px;
left: 0px;
background-color: rgba(0, 0, 0, 0.5);
z-index: 10;
}
.my-dialog{
width: 500px;
background-color: #FFF;
margin: 200px auto auto auto;
z-index: 20;
}
.my-dialog .dialog-header {
min-height: 15px;
padding: 10px;
font-weight: bold;
font-size: 18px;
border-bottom: 1px solid #CCC;
}
.my-dialog .dialog-body {
padding: 20px 25px;
}
.my-dialog .dialog-footer {
padding: 10px;
border-top: 1px solid #CCC;
text-align: right;
}
.btn{
min-width: 90px;
padding: 0 8px;
height: 34px;
cursor: pointer;
line-height: 34px;
text-align: center;
color: #fff;
border: none;
}
.btn__accept{
background-color: #4949ff;
}
.btn__decline{
background-color: #AAA;
}
Un peu de css pour rendre notre pop-up "joli"
Nous avons donc un composant simple, permettant de demander une confirmation à nos utilisateurs :
Voici comment l'utiliser dans votre application:
<button type="button" {{action (mut confirmVisible) true }}>Go...</button>
...
{{confirmation-popup visible=confirmVisible onConfirmAction=(action 'onConfirmAction') }}
Ici, lorsque notre variable "confirmVisible" est mise à true
le pop-up sera affiché et le clique sur "OK" déclenchera l'action "onConfirmAction".
Retrouver le code dans un twiddle (je vous déconseille d'utiliser cette version, voire version finale de l'article).
Le souci ?
C'est simple et ça marche bien ! Le souci ? En fait pour moi il y en a plusieurs :
Tout d'abord, il faut inclure le composant "confirmation-popup" dans tous les templates où nous souhaitons utiliser une confirmation.
Ensuite, cette implémentation nous oblige à déclarer et gérer un boolean "visible" à chaque utilisation de notre composant.
Utilisez un service !
L'étape suivante va être d'améliorer un peu notre pop-up. L'idée: déléguer la gestion du boolean et rendre accessible, via un simple appel de méthode, l'utilisation de notre pop-up partout dans l'application.
Afin d'accéder facilement à notre méthode confirm()
nous allons donc créer un service Ember.js, qui aura également la charge de gérer la visibilité de la fenêtre et l'appel à l'action de confirmation :
ember generate service confirmation
//service/confirmation.js
import Ember from 'ember';
export default Ember.Service.extend({
visible : false,
confirmAction : null,
confirm: function( action ){
this.set('visible', true);
this.set('confirmAction', action);
},
okAction: function(){
this.get('confirmAction')();
this.set('visible', false);
},
cancelAction: function(){
this.set('visible', false);
},
});
Ensuite on ajuste logiquement notre composant :
//components/confirmation-popup.js
import Ember from 'ember';
export default Ember.Component.extend({
confirmationService: Ember.inject.service('confirmation'),
});
<!-- templates/components/confirmation-popup.hbs -->
{{#if confirmationService.visible}}
<div class="my-dialog-cover">
<div class="my-dialog" tabindex="-1">
<div class="dialog-header">
<div class="dialog-title">
Confirmation demandée
</div>
</div>
<div class="dialog-body">
Veuillez confirmer cette action
</div>
<div class="dialog-footer">
<button class="btn btn__accept" {{action 'okAction' target=confirmationService}}>OK</button>
<button class="btn btn__decline" {{action 'cancelAction' target=confirmationService}}>Annuler</button>
</div>
</div>
</div>
{{/if}}
Ici en fait on supprime toutes les variables et méthodes pour n'utiliser que celle du service.
On va également adapter la façon d'insérer le composant dans nos templates, car nous voulons pouvoir l'utiliser partout sans avoir à l'insérer à chaque fois :
<!-- templates/application.hbs -->
{{confirmation-popup}}
On l'insère donc une fois pour toutes dans le template de notre application.
Et enfin, voici comment déclencher la confirmation et gérer une action en retour (ici dans un controller) :
import Ember from 'ember';
export default Ember.Controller.extend({
confirmationService: Ember.inject.service('confirmation'),
confirmations : [],
actions : {
demandeConfirm: function(){
this.get('confirmationService')
.confirm(this.actions.retourConfirmation.bind(this));
},
retourConfirmation: function(){
this.get('confirmations')
.pushObject("Vous avez confirmé !");
}
}
});
Le twiddle de cette nouvelle version (je vous déconseille également d'utiliser cette version, voir version finale de l'article).
Donc, qu'apportent nos améliorations ? La possibilité d'utiliser la méthode confirm()
simplement en injectant le service "confirmation" dans un controller ou un composant.
Plus besoin d'insérer le composant entier et gérer nous-mêmes l'état du pop-up.
Le soucis ?
"Oh c'est pas mal quand même" ... Certes, ça peut répondre à pas mal de besoin ! Mais ce n'est pas parfait :
- Déjà, nous n'avons pas la possibilité de gérer une action en cas d'annulation (même si c'est très facile de le rajouter).
- Ensuite le passage d'une action (une closure) directement dans la méthode
confirm()
n'est pas très élégant (et pas très "Ember way"). - Enfin, ça manque de possibilités de personnalisation à mon goût (Message, titre personnalisé, etc...).
Améliorations
Nous avons donc un moyen d'appeler notre fenêtre de confirmation de partout dans notre application sans avoir à implémenter autre chose qu'un appel à la méthode confirm()
de notre service.
Nous allons maintenant explorer quelques pistes d'amélioration simple.
Gestion du retour par promises
La première est de faire en sorte que notre service retourne une promise et d'utiliser les mécanismes classiques d'une promise (resolve/reject) pour déclencher (et éventuellement chainer) notre/nos action(s) de confirmation ou d'annulation.
L'autre avantage de cette méthode et que nous pourrions ainsi utiliser une confirmation utilisateur à l'intérieur d'une promise existante sans casser une implémentation asynchrone.
Donc voici comment implémenter la gestion d'une promise dans notre service :
//service/confirmation.js
import Ember from 'ember';
import RSVP from 'rsvp';
export default Ember.Service.extend({
visible : false,
deferred : null,
confirm: function(){
this.set('visible', true);
let deferred = RSVP.defer();
this.set('deferred',deferred);
deferred.promise.finally(() => {
this.set('visible', false);
});
return deferred.promise;
},
okAction: function(){
this.get('deferred').resolve();
},
cancelAction: function(){
this.get('deferred').reject();
},
});
La méthode confirm()
va donc retourner une promise, en ayant pris soin d'ajouter une "étape" finale permettant de masquer le pop-up.
Les actions "OK" et "cancel" vont ensuite se contenter de résoudre ou rejeter la promise.
Je vous invite à lire la page sur la fonction RSVP.defer()
dans la documentation Ember.js.
Et voici ce que devient notre appel à la méthode confirm()
:
demandeConfirm: function(){
let confirmPromise = this.get('confirmationService').confirm();
confirmPromise.then(() => {
this.get('confirmations')
.pushObject("Vous avez confirmé !");
});
}
Ici, nous aurions pû utiliser les méthodes .error()
ou .finally()
sur le retour de notre service. Nous aurions également pû facilement chainer des promises.
Voilà une amélioration qui me soulage ! Quand on voit l'appel à la méthode confirm()
ça ressemble plus à ce que nous avons l'habitude de faire dans une application Ember.js non ?
Autre piste d'amélioration et de personnalisation
Comme évoque un peu plus haut, nous pouvons aller un peu plus loin dans la personnalisation de notre mécanisme de confirmation, voici quelques idées :
Gestion du titre et du message personnalisé
Ajouter dans l'appel à la méthode confirm()
2 paramètres permettant de gérer le titre et le message :
//service/confirmation.js
...
titre : null,
message : null,
...
confirm: function(titre = "Confirmation", message = "Confirmer cette action ?"){
this.set('titre', titre);
this.set('message', message);
...
...
<!-- template/components/confirmation-popup.hbs -->
<div class="dialog-header">
<div class="dialog-title">
{{confirmationService.titre}}
</div>
</div>
<div class="dialog-body">
{{confirmationService.message}}
</div>
...
L'implémentation de la méthode alert
Oui après tout, confirm()
ou alert()
même combat!
Et puis, il nous suffit de rajouter un boolean pour savoir si le bouton "annuler" doit être affiché et ajouter une méthode alert()
à notre service.
Conclusion
D'autres améliorations ou fonctionnalités peuvent bien entendu être ajoutées, libre à vous ! Pour ma part je me suis contenté de celles évoquées dans cet article.
Retrouvez le code complet de cet article (la version "finale"), dans un twiddle ici : https://ember-twiddle.com/cb76f7932238c768947e0264d702892a
Nous avons donc implémenté une façon plutôt simple et qui, je pense, respecte la "Ember way" de gérer un mécanisme de confirmation interne à l'application.
On voit qu'il n'est pas forcément nécessaire d'utiliser un plugin pour avoir un mécanisme de pop-up de confirmation fonctionnel et robuste.
Si toutefois vous souhaitiez une solution plus complète ou "clé en main", il existe bien sûr de nombreux plugins Ember.js. Pour n'en citer qu'un, vous avez par exemple Ember Dialog.