Séparations des rôles du HTML / CSS / JavaScript et mise en application
Cet article fait office de conventions d'architecture d'un site web orienté composant pour la partie cliente, peu importe la technologie d'implémentation finale, de manière à ce que le code soit valide, performant et maintenable par des développeurs front-end, des développeurs back-end ainsi que des pousseurs de contenu. Ces techniques étant en constantes évolution, et les problématiques évoluant au fur et à mesure de mes itérations créatives, cet article est voué à se compléter et changer.
Présentation globale des rôles
HTML
Le HTML est le centre de tout. C'est lui qui doit présenter le contenu, qui est la clé de voûte de tout site web. Il doit être au service du contenu avant tout et non à celui du design (visuel). Le site web doit être valide et respecter les normes W3C en fonction de son DocType et être le plus cohérent à travers tout le site. Ce qui doit justifier son changement est un apport / une modification de contenu. Il est le lien entre le travail des développeurs front-end, des développeurs back-end ainsi que les pousseurs de contenu et doit bouger le moins possible quand il est question de changement de design.
Il y a 3 types de fragment HTML sur une page :
- Les gabarits de page (ou « layouts », ou « templates » ou encore « grids ») : ils représentent la structure globale d'une page qui doit accueillir des composants.
- Les composants (ou « modules », ou « views » ou encore « items template ») : ils représentent une partie auto-suffisante à elle-même, traitant du même type de contenu et l'organisant hiérarchiquement.
- Les contenus (ou « contents », ou « data ») : ils représentent les données à exposer au visiteur et pour lesquelles le site web existe. Elles sont sémantiquement parsemées de balise HTML et sont les zones qui proviennent généralement de fichier externe (HTML, Markdown, JSON, etc.) ou de base de donnée (SQL, NoSQL, etc.).
Exemple de gabarit, composants et contenus
CSS
Le CSS est ce qui s'occupe d'habiller graphiquement le site web. Il doit être totalement invisible pour les développeurs back-end et les pousseurs de contenu. Ils ne doivent jamais mettre les mains dans des fichiers CSS, Less, Stylus, etc. Cependant, les classes sur lesquels ils s’appuient demande à être documentées.
Il y a 2 types d'approche en CSS :
CSS-Driven, avec l'utilisation des sélecteurs CSS ciblant le HTML. Elles s'appliquent en premier lieu sur les composants mais également sur les gabarits. Elles ne doivent jamais être utilisées dans du contenu. Le CSS-Driven est décrit, par exemple, avec BEM. Une documentation des états ou variations possibles des gabarits et composants doit être fournie aux développeurs back-end.
HTML-Driven, avec l'utilisation de patrons de conception (ou en anglais « Class Pattern »). Ils s'appliquent en premier lieu sur les contenus (et possiblement sur les gabarits). Ils ne doivent jamais être utilisé dans des composants directement. Le HTML-Driven est décrit par exemple avec OOCSS (Bootstrap, Semantic-UI, etc.) et une documentation de l'utilisation des patrons de conception doit être fournie aux pousseurs de contenu pour les remplirs s'ils ne sont pas assistés d'un éditeur WYSIWYG (« What you see is what you get »).
Exemple CSS-Driven
Le composant user-profile a un fond vert. Le sous-composant avatar est à gauche. Le sous-composant information est à droite. Avec la variation as-popup le composant est une popup caché. Avec l'état is-opened le composant est visible.
Exemple HTML-Driven
Le patron img-responsive rend l'image adaptable à son conteneur. Le patron lead met en avant le texte. Le patron btn transforme le lien en bouton et le patron btn-default lui donne un skin par défaut.
JavaScript
Les JavaScript s'occupent, en partie, de rendre dynamique votre site web notamment en permettant les interactions entre l'utilisateur et celui-ci. Les JavaScript doivent, tout comme les feuilles de style être séparés du code HTML faisant office de gabarit. Ils se placent soit avant, soit à la fin du contenu de <body> en fonction de leur nature.
Les Avants DOM (ou Bootstraper), sont les fichiers qui se placent en haut des pages HTML. Ils n'interagissent pas avec le DOM mais évitent le phénomène de FOUC. Ils sont peu nombreux et doivent être le plus léger possible car ils bloquent l'affichage de la page. On aura, par exemple, Modernizr, RequireJS, etc. pour les bibliothèque ou Vue.js, Angular, React, etc. pour les frameworks utilisés en mode applications web monopage (injection du DOM côté client sans changement de page). Placer un fichier ici avec les propriétés defer ou async revient à les placer dans la partie Après DOM.
Les Après DOM (ou DOM Ready) :
- Les communs, ils sont chargés sur chaque page et sont :
- Le fichier principaux du site (ou Common, ou Main, ou App). Ils représentent l'unique point de démarrage du code qui va tourner dans toute votre page courante et également le code utilisé sur les patrons de conception.
- Les bibliothèques externes (ou Vendor), comme jQuery, Underscore, axios, etc. qui sont utilisées à travers toutes les pages (possiblement amené depuis des CDN).
- Les frameworks externes (ou MVVM), comme Vue.js, React, etc. qui sont utilisées à travers toutes les pages pour les architectures web isomorphiques (possiblement amené depuis des CDN).
- Les spécifiques, ils sont chargés uniquement sur des pages dédiées :
- Le fichier principal de la page (ou Specific), habituellement rattachés à un gabarit. Il est lancé par le Common en fonction du gabarit trouvé.
- Les composants (ou Classe ou Interface), habituellement rattachés à un composant HTML. Ils sont lancés par le Specific en fonction des composants trouvés (ou par le Common si l'on se passe des Specifics, notamment pour les petits sites).
- Les communs, ils sont chargés sur chaque page et sont :
Rôle et utilisation du HTML
Les gabarits
Les gabarits de page (ou « layouts » / « templates ») incluent exclusivement des composants. Ils ne doivent jamais inclure directement du contenu. Un gabarit est composé de plusieurs zone d'atterrissage (ou « PlaceHolder »).
Un même gabarit peut être utilisé avec quelques différences entre deux rendus de page donc, dans ce cas, une variation de classe (ou « Class Variation ») avec with-* est utile. Nous verrons ça plus loin.
Voici des exemples de gabarit :
Avec un unique zone d'atterissage
Qui pourra servir de gabarit standard pour toutes pages simples.
<head>
<!-- Common CSS file declaration -->
</head>
<body>
<!-- Boot JS file (modernizr, require, etc.) -->
<div class="lyt tmpl-<name-of-template>">
<!-- List of Components -->
</div>
<!-- Common JS file declaration -->
</body>
Note : dans ce cas, tmpl-<name-of-template> n'est pas requis puisqu'il n'y a qu'une seule zone d’atterrissage pour tout.
Avec de multiples zones d'aterissages
Qui servira pour les dispositions les plus complexes.
<head>
<!-- Common CSS file declaration -->
</head>
<body>
<!-- Boot JS file (modernizr, require, etc.) -->
<div class="lyt tmpl-<name-of-template>">
<div class="lyt-area for-header">
<!-- List of Components -->
</div>
<div class="lyt-area for-overview">
<div class="lyt-part for-content">
<!-- List of Components -->
</div>
<div class="lyt-part for-aside">
<!-- List of Components -->
</div>
</div>
<div class="lyt-area for-footer">
<!-- List of Components -->
</div>
</div>
<!-- Common JS file declaration -->
</body>
Note : dans ce cas, tmpl-<name-of-template> est obligatoire pour appliquer du CSS différemment sur chaque zone (lyt-area / lyt-part) en fonction du Gabarit.
Zones atterrissage multiples avec patron de classe
Comme par exemple la Grille de Bootstrap.
<head>
<!-- Common CSS file declaration -->
</head>
<body class="<name-of-template>">
<!-- Boot JS file (modernizr, require, etc.) declaration -->
<div class="container">
<div class="row">
<!-- List of Components -->
</div>
<div class="row">
<div class="col-xs-12 col-sm-8">
<!-- List of Components -->
</div>
<div class="col-xs-12 col-sm-4">
<!-- List of Components -->
</div>
</div>
<div class="row">
<!-- List of Components -->
</div>
</div>
<!-- Common JS file declaration -->
</body>
Note : dans ce cas, class="<name-of-template>" n'est pas obligatoire pour les CSS mais le sera pour le JavaScript qui doit pouvoir différencier (au besoin) un gabarit d'un autre.
Les composants
Les composants —composés de plusieurs sous-partie (ou « Subcomponent »)— sont inclus dans les zones atterrissage et peuvent eux-même inclure des composants (c'est le cas des composants de type « composant de composant »).
Les composants incluent des contenus mais n'incluent jamais directement des patrons de conception en dehors de ses zones de contenu.
Les composants peuvent être incluent plus d'une fois dans de même gabarit et également plus d'une fois dans la même zone d'aterissage.
Les variations de classe
- Un composant peut-être utilisé dans différent contexte d'affichage et avec différent comportement, donc, pour chaque alternative, une variation de classe as-* est utilisée.
- Un composant (ou une sous-partie) peuvent avoir différent états, donc, pour chaque état, une variation de classe is-* ou has-* est utilisée.
- Parce que un composant peut-être trouvé plus d'une fois dans un même zone atterrissage, une variation de classe for-* peut-être utilisée pour différencier les mêmes composants les uns des autres.
Nous reparlerons des trois précédents points plus loin.
Exemple de composant
<!-- Specific CSS file declaration -->
<section class="cmpt-<name-of-component>">
<div class="cmpt-<name-of-component>--ui">
<h1><!-- Contents --></h1>
<div class="cmpt-<name-of-component>--image">
<!-- Contents -->
</div>
<div class="cmpt-<name-of-component>--text">
<!-- Contents -->
</div>
<aside class="cmpt-<name-of-component>--ads">
<ul class="cmpt-<name-of-component>--list">
<li class="cmpt-<name-of-component>--item">
<!-- Contents -->
</li>
<li class="cmpt-<name-of-component>--item">
<!-- Contents -->
</li>
<li class="cmpt-<name-of-component>--item">
<!-- Contents -->
</li>
</ul>
</aside>
</div>
</section>
<!-- Specific JS file declaration -->
Les -- indique une sous-partie du même composant. Il est possible de créer autant de sous-partie que souhaité.
Note : un composant doit avoir au moins deux balises HTML imbriquées pour le représenter afin de pouvoir être totalement habillé avec du CSS de toutes les manières possibles. Voir ce CodePen.
Boucler sur eux même
Un composant peut boucler sur lui même afin d'apparaître comme une liste ou de lier son contenu à une liste d'élément en base de donnée. Un <ul> est grandement recommandé dans ce cas puisque chaque item est censé être identique dans sa structure de composant (mais pas forcément avec un contenu identique).
Exemple de composant
<!-- Specific CSS file declaration -->
<section class="cmpt-<name-of-component>">
<div class="cmpt-<name-of-component>--ui">
<ul class="cmpt-<name-of-component>--multiple">
<!-- start loop -->
<li class="cmpt-<name-of-component>--single">
<h1><!-- Contents --></h1>
<!-- All HTML Here -->
</li>
<!-- some items -->
<li class="cmpt-<name-of-component>--single">
<h1><!-- Contents --></h1>
<!-- All HTML Here -->
</li>
<!-- some items -->
<li class="cmpt-<name-of-component>--single">
<h1><!-- Contents --></h1>
<!-- All HTML Here -->
</li>
<!-- end loop -->
</ul>
</div>
</section>
<!-- Specific JS file declaration -->
Les contenus
Les contenus se trouvent uniquement dans les composants, dans les zones de contenu. Seul les patrons de conception (ou « Class Pattern ») doivent être utilisés dans ces parties et documentés pour être également à la disposition des pousseurs de contenu. C'est par exemple le cas de Bootstrap.
Il y a donc cinq zones de contenu dans ce composant
<section class="cmpt-article">
<div class="cmpt-article--ui">
<div class="cmpt-article--text">
<h2><!-- Content A Title --></h2>
<!-- Contents A Text -->
<div class="cmpt-article--others">
<ul class="cmpt-article--others--list">
<li class="cmpt-article--others--item">
<!-- Content B Image -->
<div class="cmpt-article--others--text">
<h2><!-- Contents B Title --></h2>
<!-- Content B Text -->
</div>
</li>
</ul>
</div>
</div>
</div>
</section>
Les contenus sont par exemple :
Content A Title
<span class="text-large reversed">I'am the<br>Main Title</span>
Content A Text
<h2 class="text-center">I'am a Subtitle</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<ul>
<li>Lorem ipsum dolor sit amet</li>
<li>Lorem ipsum dolor sit amet</li>
</ul>
Content B Image
<img src="source.png" alt="I'am the Image Description" class="img-responsive">
Content B Title
I'am an other Article
Content A Text
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<p><button class="btn btn-hugh">See more</button></p>
Les variations de classe
Il y a beaucoup de prefixe de classe utilisés pour les variations de classe, en voici leur définition.
as-* : défini un comportement alternatif ou un rendu alternatif pour un composant ou une sous-partie de composant. Exemple: class="cmpt-<name-of-component> as-popup" peut signifier que le composant standard cmpt-<name-of-component> est affiché cette fois comme une popup. Ainsi en JavaScript, la classe .as-popup donne la possibilité d'afficher ou de masquer le composant.
with-* : défini un comportement alternatif ou un rendu alternatif pour un gabarit. Exemple: class="tmpl-<name-of-template> with-ads" peut signifier qu'un background publicitaire peut-être affiché dans cette configuration.
is-* : défini un état alternatif sur un composant ou une sous-partie de composant. Exemple: class="cmpt-<name-of-component> is-opened" peut signifier que le composant cmpt-<name-of-component> est actuellement ouvert et visible. Ainsi en JavaScript, l'action du clique sur une zone précise peut enlever la classe .is-opened et par conséquent fermer le composant.
has-* : défini un état alternatif sur un composant ou une sous-partie de composant. Exemple: class="cmpt-<name-of-component> has-more-one-item" peut signifier que le composant cmpt-<name-of-component> contient au moins deux éléments et qu'il doit s'afficher comme une liste.
for-* : défini un id sur un composant parce que si un composant est utilisé plus d'une fois dans la même zone d'atterrissage, il est important d'avoir une classe différente pour les différencier au besoin. Exemple: class="cmpt-<name-of-component> for-article" peut signifier que le composant est utilisé pour afficher du texte faisant office de contenu principale et class="cmpt-<name-of-component> for-aside" peut signifier tout autre chose.
to-* : défini une étape d'attente en début de changement d'état pour gérer une animation. Exemple: class="cmpt-<name-of-component> to-open" peut signifier que cmpt-<name-of-component> est en état d'ouverture depuis son état fermé vers son état ouvert et qu'il faut démarrer une augmentation de l'opacité. Quand l'animation est finie, le is-* peut-être mis. Un exemple ici.
from-* : défini une étape d'attente pour gérer une animation du retour à l'état initial. Exemple: class="cmpt-<name-of-component> from-open" peut signifier que le composant cmpt-<name-of-component> va être fermé et qu'il est temps de diminuer l'opacité de l'élément. Quand l'animation est finie, les classes is-* et from-* sont retirées. Un exemple ici.
Toutes les variations de classe doivent être documentées pour être utilisable par les développeurs back-end.
Lire et comprendre le nommage du DOM
- Si vous voyez une classe qui commence par tmpl- c'est un nom de gabarit.
- Si vous voyez une classe qui commence par cmpt- c'est un nom de composant.
- Si vous voyez une classe finissant par --ui c'est la partie du composant dédié au contenu utilisateur.
- Si vous voyez une classe finissant par --multiple et --single cela concerne un composant répétable.
- Si vous voyez une classe contenant des --, c'est une sous-partie de composant.
- Si vous voyez une classe avec le préfixe as-, c'est un rendu et / ou un comportement alternatif sur un composant par rapport à l'état standard.
- Si vous voyez une classe avec le préfixe with-, c'est un rendu et / ou un comportement alternatif sur un gabarit par rapport à l'état standard.
- Si vous voyez une classe avec le préfixe is-, c'est la description d'un état de composant ou sous-partie de composant alternatif.
- Si vous voyez une classe avec le préfixe has-, c'est la description d'un état de composant ou sous-partie de composant alternatif.
- Si vous voyez une classe avec le préfixe for-, c'est pour un rendu ou comportement spécifique sur une unique version d'un composant.
- Si vous voyez une classe avec le préfixe to-, c'est un état de transition vers un autre état.
- Si vous voyez une classe avec le préfixe from-, c'est un état de transition de retour à l'état initial.
- Si vous voyez une classe qui commence avec lyt-, c'est un patron de conception pour gabarit.
- Si vous ne voyez aucune des classes listées avant, c'est un patron de conception pour contenu.
Choisir la sémantique de vos balise
Il n'y au plus qu'un seul <header> et <footer> sous le <body>. Le <h1> principale se trouve également dans cette partie. Sous le <body> il y a une hiérarchie de <h1-h6> avec un unique <h1> jusqu'à ce que vous trouviez une balise :
- <section>,
- <article>,
- <nav> ou
- <aside>.
Chacune de ces balises arrête la propagation de la hierarchie <h1-h6> des balises parentes et commence leur propre hiérarchie de <h1-h6> (avec seulement un <h1>). Chacune de ces balises peuvent aussi avoir leur propre <header> et <footer>.
De manière à pouvoir rendre déplaçable et réutilisable chaque composant, nous avons deux manières d'organiser notre structure.
Sémantique sur le gabarit
Ici on place <section>, <article>, <nav> et <aside> sur le gabarit dans les aires réservées des zones d'atterrissage. Mais dans ce cas, seuls des composants commençant par des <div> peuvent être insérés.
Gabarit
<div class="lyt tmpl-products">
<section class="lyt-area for-overview">
<!-- One Component Here -->
</section>
</div>
Composant
<div class="cmpt-product">
<div class="cmpt-product--ui">
<header><!-- Content Title --></header>
<!-- Content Text -->
</div>
</div>
Contenus
<h1>A product</h1>
<h2>Subtitle</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>
Sémantique dans le composant
Ici on place <section>, <article>, <nav> et <aside> sur le composant. Dans ce cas, un composant ne peut pas être utilisé dans un autre type que celui pour lequel il est destiné.
Gabarit
<div class="lyt tmpl-products">
<div class="area for-overview">
<!-- Multiple Component Here -->
</div>
</div>
Composant
<section class="cmpt-product">
<div class="cmpt-product--ui">
<header><!-- Content Title --></header>
<!-- Content Text -->
</div>
</section>
Contents
<h1>A product</h1>
<h2>Subtitle</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>
Gommer les inconvénient des deux approches
Comment créer des emails corrects ?
Pour permettre à vos emails HTML d'être lisible dans la plupard des clients mail, il est nécessaire de ne les réaliser qu'avec du HTML4 et d'utiliser des attributs HTML (voir des balises) à la place des attributs style.
N'utiliser pas non plus de <div> brut utilisez une cascade de <table>, <tr>, <td> etc.
Par exemple, n'utilisez pas <div style="text-align: center"> mais <center>. Pour créer une version HTML5 de vos emails HTML4 pour le web, vous pouvez les transformer automatiquement avec NodeAtlas par exemple.
Rôle et utilisation du CSS
La spécificité des sélecteurs
C'est l'aspect le plus fondamental du fonctionnement CSS et c'est celui qui pause le plus problème. Parce que les règles CSS sont comme des variables globales mutables en JavaScript, car une propriété :
- Peut être redéfinie.
- Il y a un niveau de spécifité pour chaque sélecteur (voir les règles). En cas de spécificité égale sur deux règles s'appliquant sur le même élément : c'est la place des lignes qui compte.
- L'ordre de la CSS a une importance.
Exemple
Sélecteur | Spécificité | Description |
---|---|---|
* | 0.0.0.0 | tout |
li | 0.0.0.1 | tag |
ul li | 0.0.0.2 | tag + tag |
li::first-letter | 0.0.0.2 | tag + pseudo-element |
ul li div | 0.0.0.3 | tag + tag + tag |
.teal | 0.0.1.0 | class . |
li:nth-child(1) | 0.0.1.1 | pseudo-class . tag |
ul + *[rel=up] li | 0.0.1.2 | tout + attribut . tag + tag |
li.red.level | 0.0.2.1 | class + class . tag |
ul.blue ol li.blue | 0.0.2.3 | class + class . tag + tag + tag |
li.orange.orange.level | 0.0.3.1 | class + class + class . tag |
#my-id | 0.1.0.0 | id . . |
#my-id.teal | 0.1.1.0 | id . class . |
#my-id.teal | 1.1.0.0 | inline . id . . |
Et si le mot clé !important est ajouté à la fin de n'importe quelle valeur de propriété elle passe devant toute autre. Soyez prudent en l'utilisant (voici un cas d'utilisation justifié).
Vous pouvez voir la spécificité des sélecteurs grâce à cet outil.
Deux approches d'utilisation du CSS
Il y a deux façons d'appliquer des styles CSS sur du HTML. La manière CSS-Driven ou pilotée par la feuille de style ou la manière HTML-Driven ou pilotée par les classes dans les balises HTML.
CSS-Driven
Nous avons une structure HTML figée. Nous nommons tous les éléments de notre structure
<section class="cmpt-<name-of-component>">
<div class="cmpt-<name-of-component>--ui">
<div class="cmpt-<name-of-component>--text">
</div>
</div>
</section>
pour pouvoir y faire référence depuis la feuille CSS. C'est une approche type BEM.
.cmpt-<name-of-component>--ui {
font-weight: bold;
}
Nous venons d'habiller une partie HTML.
HTML-Driven
Nous avons une feuille CSS figée. Nous nommons nos règles de manière à créer des patrons de conception (« Class Pattern »)
.bold {
font-weight: bold;
}
de manière à pouvoir les appliquer comme on le souhaite sur une structure HTML éditable.
<p class="bold">Lorem ipsum</p>
Nous avons créer le patron de conception .bold.
Comment appliquer chaque approche ?
Le plus important est de ne jamais enfermer les composants HTML dans des « patron emprisonnant » car vous ne pourrez pas l'habiller avec du sens dans un autre contexte.
D'un autre côté, parceque les pousseurs de contenu ne doivent jamais toucher au fichier CSS, il est important de leur créer des patrons de conception pour le contenu. Sachez que via des balises <style> ou attribut style il est tout à fait acceptable qu'un pousseur de contenu altère de l'affichage dans une zone de contenu.
Pour finir, pour les gabarits de page, les deux approches sont envisageables, cela dépend se si votre système gère plus facilement le changement de gabarit (« Template ») ou le changement de CSS si vous souhaitez utiliser différentes grilles.
Less / Stylus vous aide à utiliser Bootstrap
Votre meilleur ami pour la maintenance des feuilles CSS et pour garder votre HTML le plus clair possible est Less ou encore mieux Stylus.
Il est possible de gérer les deux avec NodeAtlas avec ces simples webconfig.json
{
"enableLess": true,
"assetsRelativePath": "/"
}
ou
{
"enableStylus": true,
"assetsRelativePath": "/"
}
placés à la racine de votre dossier de site. Et avec la commande CLI nodeatlas --browse.
Exemple de Less pour les gabarits, composants et contenus
Dans le but d'habiller le HTML suivant :
<section class="cmpt-follow-me <!-- Variation Class -->">
<div class="cmpt-follow-me--ui">
<div class="cmpt-follow-me--image">
<!-- Content Image -->
</div>
<div class="cmpt-follow-me--text">
<!-- Content Text -->
</div>
</div>
</section>
vous pourrez utiliser l'implémentation Less suivante dans le fichier cmpt-follow-me.less :
/* Import Bootstrap Class Pattern. */
@import (reference) 'bootstrap/bootstrap.less';
/* Import your own Class Pattern. */
@import (reference) 'common.less';
/* Name your Component. */
@componentName: e('.cmpt-follow-me');
/* Outer Wrapper Design. */
@{componentName} {
/* Initial State and properties. */
display: none;
/* ... */
/* Overload of Class Pattern for this Component. */
.arrowed {
/* ... */
span {
/* ... */
}
}
.gap {
/* ... */
}
/* Opened State. */
&.is-opened {
display: block;
}
/* Clickable State. */
&.is-closable {
cursor: pointer;
}
/* ... */
/* Inner Wrapper Design. */
&--ui {
/* ... */
}
/* Subcomponent Design. */
&--image {
.make-md-col(4);
/* ... */
}
&--text {
.make-md-col(8);
/* ... */
}
&--close,
/* Custom Component Class Pattern usable into Content part. */
.close {
/* ... */
}
/* Variation of this component. */
&.as-v1 {
@{componentName}--ui {
background-color: #da5daf;
}
@{componentName}--text {
border: 1px solid #85214d;
}
/* ... */
}
&.as-v2 {
@{componentName}--ui {
background-color: #80a837;
}
@{componentName}--text {
border: 1px solid #3e6618;
}
/* ... */
}
&.as-v3 {
@{componentName}--ui {
background-color: #ed9364;
}
@{componentName}--text {
border: 1px solid #d6401c;
}
/* ... */
}
}
et avec ce fichier common.less par exemple
/* Same design on all browser. */
@import 'helpers/normalize.less';
/* Use icones. */
@import 'font-awesome/font-awesome-regular.less';
/* Bootstrap Lib. */
@import (reference) 'bootstrap/bootstrap.less';
/* Font Awesome Lib. */
@import (reference) 'font-awesome/font-awesome.less';
/* Use more Font. */
@import (css) 'https://fonts.googleapis.com/css?family=Muli';
/* Just import Bootstrap Class Pattern you needed. */
.text-center {
.text-center;
}
.text-left {
.text-left;
}
.text-right {
.text-right;
}
.img-responsive {
.img-responsive;
display: inline-block;
}
/* Your own Class Pattern for Content. */
.arrowed {
/* ... */
span {
/* ... */
}
}
.gap {
/* ... */
}
/* Your Layout Implementation */
.tmpl-products {
.for-overview {
.make-sm-col(8);
}
.for-aside {
.make-sm-col(4);
}
}
/* ... */
Inclusion par référence
Pour Less, si vous importez en utilisant @import (reference) au lieu de simplement utiliser @import vous génèrerez dans le fichier de sortie uniquement le fragment de CSS souhaité en provenance de votre liste de patron de conception. Par exemple avec la bibliothèque Bootsrtap, vous pouvez importer tous les patrons col-XX-YY par exemple. Cela peut grandement alléger vos feuilles CSS.
La spécificité d'un sélecteur ne doit pas être trop lourde
Nous imaginerons ici que @{componentName} retourne .cmpt-article.
Sélectionner directement la cible
Vous devez accéder à un élément par sa classe :
@{componentName} {
&--others--item {
/* Apply style */
}
}
// CSS
// .cmpt-article--others--item { /* Apply style */ }
Sélectionner un seul élément sans classe par sélecteur
Il est possible qu'il y ai des éléments sans classes dans les composants ou sous-composants. Il devrait être permis de ne faire référence qu'à un, et un seul élément.
Ceci est autorisé
@{componentName} {
&--others--item {
a {
/* Apply style */
}
span {
/* Apply style */
}
}
}
// CSS
// .cmpt-article--others--item a { /* Apply style */ }
// .cmpt-article--others--item span { /* Apply style */ }
mais ceci n'est PAS autorisé...
@{componentName} {
&--others--item {
a {
/* Apply style */
span {
/* Apply style */
}
}
}
}
// CSS
// .cmpt-article--others--item a { /* Apply style */ }
// .cmpt-article--others--item a span { /* Apply style */ } // Too specified selector.
C'est mal. Vous devez ajouter dans ce cas des classes sur vos éléments pour directement pouvoir intéragir avec eux.
@{componentName} {
&--others--item--span {
/* Apply style */
}
}
// CSS
// .cmpt-article--others--item--span { /* Apply style */ }
ou (un élément sans classe autorisé)
@{componentName} {
&--others--item {
span {
/* Apply style */
}
}
}
// CSS
// .cmpt-article--others--item--link span { /* Apply style */ }
Ajouter des variations de classe pour changer le design
Vous pouvez améliorer la précision de la sélection en utilisant une variation de classe.
Sur un composant
@{componentName} {
&.is-opened {
/* Apply style */
}
}
// CSS
// .cmpt-article.is-opened { /* Apply style */ }
ou un sous-composant
.is-opened {
@{componentName}--others--item--link {
span {
/* Apply style */
}
}
}
// CSS
// .is-opened .cmpt-article--others--item--link span { /* Apply style */ }
Note : le code ci-dessous produit la même sortie que celui au dessus.
.is-opened {
@{componentName} {
&--others
&--item
&--link {
span {
/* Apply style */
}
}
}
}
}
}
// CSS
// .is-opened .cmpt-article--others--item--link span { /* Apply style */ }
La règle est la suivante : écrire en une seule ligne les composantes du sélecteur si il n'y a aucun sous-élément voisin utilisant le même chemin à référencer, sinon créer un nouveau sous bloque comme ci-dessous
.is-opened {
@{componentName}--others--item {
&--image {
/* A new sibling Subcomponent */
}
&--link {
span {
/* Apply style */
}
}
}
}
// CSS
// .is-opened .cmpt-article--others--item--image { /* A new sibling Subcomponent */ }
// .is-opened .cmpt-article--others--item--link span { /* Apply style */ }
Rôle et utilisation du JS
Dans le but de produire du code performant et maintenable, vous devriez passer à travers un outil comme JSLint et obtenir une sortie sans erreur. Si vous utilisez Sublim Text, vous pouvez installer SublimeLinter-contrib-jslint pour éviter toute mauvaise pratique.
Règles et patrons
Notez que règles et patrons ci après sont utiles pour une utilisation JavaScript sans bibliothèque ou frameworks. Si vous utilisez par exemple le framework Vue.js, il sera alors plus intéressant de tenir compte de ce qui va suivre dans la limite de ce que l'outil offre déjà en plus de respecter ses conventions
Éviter la surchauffe de cerveau (Brain Overload)
Pour commencer, observer autant que possible les règles suivantes :
- Un fichier ne doit pas dépasser les 1000 lignes (commentaires inclus).
- Une fonction ne doit pas dépasser les 100 lignes (commentaires inclus).
- La complexité d'une fonction ne doit pas dépasser 10 Tester la complexité.
- Il ne doit pas y avoir plus de 3 niveaux de if/else/swich/try/etc. sans qu'on ai recours à une fonction.
Déclarer tout ce que vous utilisez dans la page, et son usage
Dans tous les fichiers JavaScript client, toutes les variables doivent être explicitement déclarée, même si vous savez que l'exécuteur JavaScript connait cette valeur par défaut ou qu'un autre fichier l'a déjà définie. Il est important de savoir ce qui va être utilisé à travers le fichier et de permettre le plus possible de pouvoir vous passer d'un ordre précis dans l'exécution des fichiers.
Exemple
/* jslint browser: true */
/* global $ */
// Define your usage of global object.
var website = website || {},
// Define your usage of `jQuery window` shortcut for this file.
$window = $window || $(window);
// Define your usage of `website.component` namespace.
website.components = website.components || {};
Observez que seulement un var est déclaré au sommet de chaque fonction, pas plus. Si vous définissez vos variables dans le champ lexical globale (« Global Scope »), attachez les à window ou définissez les dans le var global de votre fichier.
L'espace de nom global website va contenir tout le code JavaScript de votre site web. N'hésitez pas à utiliser app ou encore application si c'est une application web et non pas une simple page.
Utiliser des fermetures pour garder le champ lexical global propre
Le code ci-dessus est défini dans le champ lexical global. Si vous écrivez du code dans un fichier, n'écrivez jamais directement dans le champ lexical global. Il y a deux manière de garder le champ lexical global propre.
Avec une fermeture statique anonyme
Si le code n'a pas besoin d'être instancié, faites juste comme ci-après. C'est le cas des contrôlers comme common.js.
// Create a closure to scope all variables or function invisible from global scope.
;(function () {
"use strict";
var iAmAPrivateVariable_ForPublicExpositon = "",
iAmAPrivateVariable = "";
// No accessible in global scope.
iAmAPrivateVariable = "i am only accessible in this closure";
// Accessible to global scope with `website.iAmAPublicVariable`.
website.iAmAPublicVariable = iAmAPrivateVariable_ForPublicExpositon;
}());
ou exposer votre variable publique quand vous créer la Closure (ici website est publique et publics n'est accessible que dans la closure).
// Pass an alias of `website` named `publics` only in this closure.
;(function (publics) {
"use strict";
var privates = {};
// Private Example
privates.addClassIsLoaded = function (view) {
view.classList.add("is-loaded");
}
// Public Example
publics.loadComponents = function () {
/* See this part later */
};
}(website));
Note : on commence notre fichier avec un ; pour éviter qu'un précédent fichier minifié n'interprète notre parenthèse comme une demande d'exécution de fonction.
Avec new Class
S'il est important que le code puisse être instancié en fonction d'un contexte donné, créer une classe est le mieux. Pour ne pas polluer le champ lexical global, attachez la classe à un espace de nom comme website.components.
window.website = window.website || {};
website.components = website.components || {};
// Create a Class for define JavaScript behavior of `<div class="lightbox"></div>`.
website.components.Lightbox = function callee(selector) {
"use strict";
// Create private namespace for instanciable function (publics)
// and inner function (privates).
var publics = this,
statics = callee.prototype,
privates = {};
// Count number of instance for Lightbox.
statics.nbrOfInstance = (statics.nbrOfInstance) ? statics.nbrOfInstance + 1 : 1;
callee.nbrOfInstance = statics.nbrOfInstance;
// Define the selector by default for the HTML view of this Class.
publics.selector = selector || ".information";
// Define all stuff to manage lightbox.
privates.openLightbox = function () { /* ... */ };
privates.moveLightbox = function () { /* ... */ };
privates.closeLightbox = function () { /* ... */ };
// Define a function to launch all mandatory behavior when a HTML view is parsed.
publics.init = function () {
// Set all function you want use to initialization.
privates.openLightbox();
privates.moveLightbox();
privates.closeLightbox();
};
};
website.components.Lightbox.nbrOfInstance = 0;
Et initialisez tous les <div class="lightbox"></div>
var Lightbox = website.components.Lightbox;
(new Lightbox()).init();
ou initialisez tous les <div class="popup"></div>
var Lightbox = website.components.Lightbox;
(new Lightbox(".popup")).init();
ou initialisez <div class="ads"></div>
var Lightbox = website.components.Lightbox,
lightbox = new Lightbox();
lightbox.selector = "ads";
lightbox.init();
ou comptez le nombre d'instance avec
var Lightbox = website.components.Lightbox,
lightbox = new Lightbox();
Lightbox.nbrOfInstance; // `4`
lightbox.nbrOfInstance; // `4`
ou créez une fonction tierce...
// This code is a focus on `website.loadComponents()` defined above.
publics.loadComponents = function () {
var i, currentComponent, currentViews;
// Loop on all components
for (i in website.components) {
// Never forgot to use `hasOwnProperty` to not display other thing that personal object key.
if (website.components.hasOwnProperty(i)) {
// Work on current Component.
currentComponent = new website.components[i]();
// Obtain all `<div class="<currentComponent.selector>"></div>`
currentViews = document.querySelectorAll(currentComponent.selector + ":not(.is-opened)");
if (currentViews.length > 0) {
// Initialize Component.
allComponentsInstance.push(currentComponent.init());
// Tag this component as already initialize.
[].forEach.call(currentViews, addClassIsLoaded); // see before for "addClassIsLoaded" definition.
}
}
}
};
...pour charger tous les composants que vous avez créé de cette manière :
website.loadComponents();
Et si plutard, vous souhaitez ajouter <div class="lightbox"></div> au DOM par XMLHttpRequest (Ajax), vous serez capable d'appliquer le JavaScript suivant :
var Lightbox = website.components.Lightbox;
(new Lightbox(".information:not(.is-loaded)")).init();
et de charger dès lors tous les composants (non déjà chargé) avec :
website.loadComponents();
Use Strict
Le champ lexical global (Global Scope) est une zone sauvage ou toutes les variables vont entrer en conflit ainsi la moindre chose déclarée dans un champ lexical dédié (fermeture ou « Closure ») est une bonne chose. C'est le but des fonctions anonymes comme :
(function () {
"use strict";
// ...
}())
mais pourquoi utiliser le « strict mode » avec "use strict"; ?
Les exécuteurs JavaScript acceptent toute sorte de manière d'écrire du JavaScript ainsi que du code qui n'a pas été standardisé, du code déprécié ou du code non optimisé dans le but de supporter du code JavaScript ancien. Pour ne pas permettre à l'exécuteur de faire tourner du vieux code étrange et de lever des exceptions pour aider au développement, il faut ajouter "use strict"; et toutes les variables dans le champ lexical courant (le contexte d'exécution courant) ainsi que les contextes enfants vont lever une exception si le code ne respecte pas les standards. C'est une bonne pratique de nos jours, ne l'oubliez pas pour ne pas exécuter du code démoniaque !
Note : vous pouvez aussi l'utiliser dans une fonction nommée ou à instancier ainsi que dans le champ lexical global si vous le souhaitez.
Plus de règle JavaScript
Si vous respecez ça, c'est déjà pas mal. Pour respecter toutes les règles, suivez ces règles : Quality JavaScript Rules
Changement d'état des éléments
Si vous souhaitez changer l'état d'un élément, vous pouvez le faire en suivant les étapes suivantes :
Standard
- Créez une classe .is-* avec tout le changement de design associé dans votre fichier CSS (ou Less / Stylus).
- Ajoutez la classe is-* ou retirez la de l'élément en question pour changer son état.
- Vérifiez l'existance de is-* pour savoir dans quel état est actuellement l'élément.
Avec animations simples
Utiliser deux classes de transitions.
- Utilisez avant la classe is-* une classe to-* pour démarrer l'animation de transition entre deux états.
- Utilisez après la classe is-* une classe from-* s'il existe une transition de retour à l'état précédent.
- Utilisez les propriétés CSS3 keyframe, animation, transform, etc. pour créer des animations sur des éléments dont les paramètres sont statiques. Utilisez une librairie d'animation si les valeurs sont variables en fonction du contexte.
Exemple :
- Trouvez dans cet exemple comment permettre un changement d'état animé : Change Element State.
Avec animations complexes
Utilisez six classes de transitions,
pour la transition entrante :
- Utilisez avec ou sans la classe is-* une classe *-enter pour définir l'état avant animation de transition entrante vers l'état modifié.
- Utilisez une classe *-enter-to pour définir l'état après l'animation de transition entrante vers l'état modifié.
- Utilisez une classe *-enter-active pour définir les instructions d'animation de transition entrante de l'état standard vers l'état modifié.
pour la transition sortante :
- Utilisez une classe *-leave pour définir l'état avant animation de transition sortante vers l'état standard.
- Utilisez une classe *-leave-to et retirez la classe is-* (si utilisée) pour définir l'état après l'animation de transition sortante vers l'état standard.
- Utilisez une classe *-leave-active pour définir les instructions d'animation de transition sortante de l'état modifié vers l'état standard.
Exemple :
- Trouvez dans cet article toutes les explications sur la mise en place et l'utilisation de ces six classes : Comprendre et reproduire les animations de transitions Vue.js en CSS et JavaScript.
Validation de formulaire
Pour valider un formulaire il n'est pas nécessaire d'écrire une seule ligne de JavaScript dans la majorité des cas. Utilisez juste ces trois fichiers :
- jQuery //code.jquery.com/jquery-2.1.4.min.js (Utilisez comme une bibliothèque pour wrapper élément Nodes dans le DOM).
- jQuery Validation Plugin //cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.14.0/jquery.validate.min.js (Applique différentes règles pré-définies sur les champs souhaités)
- jQuery Validation Unobstrusive Plugin //ajax.aspnetcdn.com/ajax/mvc/5.2.3/jquery.validate.unobtrusive.min.js (Applique automatiquement les règles en ciblant les éléments avec l'attribut data-*).
Note: ce couple de fichier fonctionne ensemble mais si vous utilisez la dernière version pour l'un, en cas de problème, utilisez la dernière version pour les deux autres.
Vous trouverez ici un cas d'usage complet de l'utilisation de chaque champ : Exemple d'utilisation avec chaque champ
Et si un champ est injecté plus tard dans le DOM, après l'inclusion du fichier unobstrusive, utilisez $.validator.unobtrusive.parse(<selectorForm>); pour faire fonctionner les nouveaux formulaires/champs avec unobstrusive.
Si une validation est plus complexe, vous pouvez trouver la documentation de jQuery Validate ici ou vous tourner vers un lieur de valeur réactive comme Vue.js.
Étendre une classe
Quand vous souhaitez étendre une classe depuis une autre (héritage), vous devez en plus d'étendre la classe, bien penser a étendre les fonctions de son prototype. Si une classe doit hériter de plusieurs classe, alors ce que vous souhaitez est une interface (voir plus loin).
Quand votre classe est un model pour une vue HTML, ajoutez la dans website.components mais si c'est purement une classe de model de donnée, ajoutez là dans website.models.
Essayons donc d'étendre cette classe par celle-ci :
Class Person
/* jslint browser: true */
window.website = window.website || {};
website.models = website.models || {};
website.models.Person = function (firstname, lastname) {
this.firstname = firstname || "No firstname";
this.lastname = lastname || "No lastname";
};
website.models.Person.prototype.age = "No age";
Class User
/* jslint browser: true */
window.website = window.website || {};
website.models = website.models || {};
website.models.User = function (id, password, firstname, lastname) {
website.models.Person.call(this, firstname, lastname);
this.id = id;
this.password = password;
};
website.models.User.prototype = Object.create(website.models.Person.prototype);
website.models.User.prototype.constructor = website.models.User;
Contrôleur
/* jslint browser: true */
window.website = window.website || {};
website.models = website.models || {};
(function () {
var user = new website.models.User("MachinisteWeb", "myPassword");
user.age = 18;
user.id; // "MachinisteWeb"
user.password; // "myPassword"
user.firstname; // "No firstname"
user.lastname; // "No lastname"
user.age; // 18
}());
Implementer un mixin
Quand une fonction est utilisable par plus d'une fonction anonyme, n'hésitez pas à la placer dans l'espace de nom website comme loadComponents dans l'exemple précédent.
Mais quand une fonction est partagée par plus d'une classe, vous pouvez créer cette fonction dans des interfaces ou mixins et l'implémenter dans les différentes classes. Un mixin est comme une classe mais son usage est uniquement d'étendre une classe, jamais d'être instanciée elle-même.
Ainsi nous pouvons implémenter ces deux mixins dans cette classe.
Interface Age
/* jslint browser: true */
var website = window.website || {};
website.mixins = website.mixins || {};
website.mixins.Age = function () {
this.age = "18";
};
Interface Firstname
/* jslint browser: true */
window.website = window.website || {};
website.mixins = website.mixins || {};
website.mixins.Firstname = function () {
this.firstname = "Bruno";
};
Classe Person
/* jslint browser: true */
window.website = window.website || {};
website.models = website.models || {};
website.mixins = website.mixins || {};
website.classes.Person = function () {
website.mixins.Firstname.call(this);
website.mixins.Age.call(this);
var publics = this;
publics.lastname = "Lesieur";
};
Note : si un même nom de propriété ou de fonction existe entre deux interfaces, c'est la dernière interface chargée qui remporte le conflit !
Contrôleur
window.website = window.website || {};
website.models = website.models || {};
(function () {
var person = new website.models.Person();
person.lastname; // "Lesieur"
person.firstname; // "Bruno"
person.age; // 18
}());
Travailler sans espace de nom
Si nous attachons tout dans le namespace website c'est pour ne pas entrer en conflit avec des classes déjà existantes. Le patron pour éviter les conflits est d'exposer une méthode noConflict() dans la classe directement définie dans la portée lexicale global.
Voir plus bas :
;(function (factory) {
var initialClass = window.MyClass,
api = window.MyClass = factory;
api.noConflict = function () {
window.MyClass = initialClass;
return api;
};
}(function () {
// Standard definition of class here.
}));
et si la classe globale MyClass existe déjà, utilisez un nouveau nom de classe avec la méthode noConflict() pour restaurer l'objet MyClass d'origine.
var MyClass2 = MyClass.noConflict();
Classe pour environnement multiple
Dans le but de permettre à la classe MyClass de fonctionner aussi bien avec Vanilla JS, un loader de module JS ou Node.js, créez une factory comme ci-après.
;(function (root, factory) {
var root.MyClass = factory;
if (typeof define === "function" && define.amd) {
define(function () {
return factory;
});
}
if (typeof module === "object" && module.exports) {
module.exports = factory;
}
}(this, function () {
// Normal definition of class here.
}));
Autres comportements
Accesseurs et mutateurs
Le patron d'accession et de mutation vous permettent d'effectuer des actions avant le renvoi d'une valeur ou l'affectation d'une valeur.
Classe
window.website = window.website || {};
website.models = website.models || {};
website.models.Person = function (firstname, lastname) {
var publics = this,
privates = {};
privates.firstname = (firstname) ? publics.firstname(firstname) : "No firstname";
privates.lastname = (lastname) ? publics.lastname(lastname) : "No lastname";
Object.defineProperty(publics, 'firstname', {
get: function () {
// Do what you want before getter
return privates.firstname;
},
set: function (firstname) {
// Do what you want before setter
privates.firstname = firstname;
// Do what you want after setter
}
});
Object.defineProperty(publics, 'lastname', {
get: function () {
// Do what you want before getter
return privates.lastname;
},
set: function (lastname) {
// Do what you want before setter
privates.lastname = lastname;
// Do what you want after setter
}
});
};
Contrôleur
window.website = window.website || {};
website.models = website.models || {};
(function () {
var person = new website.models.Person();
person.firstname = "Bruno";
person.lastname = "Lesieur";
console.log(person.firstname); // "Bruno"
console.log(person.lastname); // "Lesieur"
}());
Chainage
Un patron de chaînage (populaire en jQuery par exemple), consiste à fournir une référence aux propriétés privées d'une classe instanciée depuis l’extérieur. La méthode utilisée renvoi la valeur souhaitée en tant qu'accesseur et permet de la modifier en tant que mutateur. En tant que mutateur, l'objet alors retourné n'est pas la valeur mais l'objet instancié pour permettre « le chaînage ».
Classe
window.website = window.website || {};
website.models = website.models || {};
website.models.Person = function (firstname, lastname) {
var publics = this,
privates = {};
privates.firstname = (firstname) ? publics.firstname(firstname) : "No firstname";
privates.lastname = (lastname) ? publics.lastname(lastname) : "No lastname";
publics.firstname = function (firstname) {
if (typeof firstname === "undefined") {
// Do what you want before
return privates.firstname;
} else {
// Do what you want before
privates.firstname = firstname;
// Do what you want after
return publics;
}
};
publics.lastname = function (lastname) {
if (typeof lastname === "undefined") {
// Do what you want before
return privates.lastname;
} else {
// Do what you want before
privates.lastname = lastname;
// Do what you want after
return publics;
}
};
};
Contrôleur
window.website = window.website || {};
website.models = website.models || {};
(function () {
var person = (new website.models.Person())
.firstname("Bruno")
.lastname("Lesieur");
console.log(person.firstname()); // "Bruno"
console.log(person.lastname()); // "Lesieur"
}());
Forcer le contexte d'exécution
Vous pouvez forcer une classe a ne jamais permettre un autre contexte d'exécution que le sien d'être utilisé. Cela vous permet de ne pas utiliser le mot clé new depuis un appel extérieur.
Classe
var website = window.website || {};
website.models = website.models || {};
website.models.Person = function callee(firstname, lastname) {
if (!(this instanceof callee)) {
return new callee();
}
this.firstname = firstname || "No firstname";
this.lastname = lastname || "No lastname";
};
Contrôleur
var website = window.website || {};
website.models = website.models || {};
(function () {
(new website.models.Person() instanceof website.models.Person); // true
(website.models.Person() instanceof website.models.Person); // true
}());
Gérer les erreurs
Il y a 3 manières de proprement lever les erreurs, pour un code synchrone, un code asynchrone ou un événement : voir par ici.
Documentation
De manière à documenter votre code, utilisez le JSDoc npm CLI. Vous trouverez tout ce qu'il vous faut pour la documentation ici.
Par exemple la page « Home » possède le composant « Hero » qui est documenté pour le CSS et pour le JS.