Généralités
Javascript est un langage objet mais orienté prototype : on va créer une instance d'un objet et on le modifie par la suite (contrairement au Java où on va créer le "moule" d'un objet -la classe).
Le langage est standardisé par la spécification Ecmascript (European Computers Manufacturers Association) et continue d'évoluer en rajoutant des nouvelles fonctions, syntaxes, etc. Dernièrement on a une nouvelle version d'Ecmascript tous les ans.
Objets
L'objet global est l'objet window, un seul pour chaque fenêtre (ou onglet si on utilise un navigateur à onglet). Si on tape
toto=10
puis
toto
dans une console javascript, on voit que toto est égal à 10. Notre variable toto est lié à cet objet window, il suffit de taper window pour trouver notre variable toto.
Il y a trois façons de créer un objet :
1)Création d'un objet façon JSON
var personne={
nom:'Crasby',
prenom:'Bob',
direNom:function(){
console.log("Je m'appelle "+this.nom);
}
}
personne.direNom();
2)Création d'un objet via son prototype
function Personne() {
this.nom = 'Crasby';
this.moprenom = 'Bob';
}
Personne.prototype.direNom = function () {
console.log("Je m'appelle "+this.nom);
}
var p=new Personne();
p.direNom();
3)Création avec une classe
class Personne{
constructor(nom, age = 0){ //sera appelé quand on fera "new Personne()", ici age a une valeur par défaut
this.setName(nom); //setters et getters comme en Java, attention il faut le "this"
this.setAge(age);
}
seDecrire(){
return "Je m'appelle "+this.nom+" et j'ai "+this.age+" ans.";
}
}
const p=new Personne("Bob");
Il y a plusieurs façons de créer une string, soit directement soit en instanciant un objet de type String :
a="lol";
"lol"
typeof a;
"string"
Et le deuxième :
b=new String("lol");
String { "lol" }
typeof b;
"object"
Dans les 2 cas on aura accès aux méthodes de String (par ex toLowerCase()).
On peut accéder/modifier les attributs d'un objet très facilement :
let o={};
o.monAttribut="ma belle valeur";
Les clefs peuvent être des objets :
o['cette clef est une chaine'] = "ma belle valeur";
Cloner un objet
Pour par exemple modifier un objet sans modifier l'original.
Cloner l'objet mais conserver les liens vers les objets qu'il contient :
Object.assign({}, monObjet);
Cloner l'objet en le convertissant en JSON, cela fait donc une copie de tout l'objet et de ce qu'il contient :
JSON.parse(JSON.stringify(food));
Nouveautés ES6
Liste de nouveautés
On ne fait plus "var" en es6 mais "let" qui change des choses par rapport au scope.
"var" avait le scope de TOUTE LA FONCTION (on parle de "hoisting" -"hissage" en français car on hisse la portée de la variable) :
function lol() {
if(true){
var a=1; //a est déclaré dans le bloc if mais en fait a un scope dans toute la fonction
}
alert(a); //affiche 1
}
"let" a un scope entre les accolades :
function lol() {
if(true){
let a=1; //a est déclaré dans le bloc if et ne sera pas utilisable en dehors
}
alert(a); //ReferenceError, aucune alert n'est affichée.
}
//il y a désormais des constantes
const user={name:"bob"};
console.log(user.name);
user.name="bill"; //on peut quand même changer des trucs dans l'objet vers lequel pointe la constante
console.log(user.name);
//user={name:"John"}; //par contre on peut pas changer ce vers quoi pointe la constante
//Il y a également maintenant des fonctions fléchées (Arrow functions)
const action= () => { //autrefois on avait "var action=function() {", c'est pareil sauf qu'elle a pas son propre this
console.log("test");
}
action();
const sayHello = prenom => { //s'il n'y a qu'un argument on peut se passer des parenthèses
return `Hello ${prenom}`; //les `` sont une nouvelle façon de concaténer avec des variables
}
console.log(sayHello("John"));
const add = (a,b) => a+b; //sans accolades après la flèche, la valeur est retournée direct
console.log((4+4));
let tab=[1,2,3];
let tab2=[...tab,4,5,6]; //les ... pour merger les tableaux
console.log(tab2);
let css={color:'red', display: 'inline-block'}; //ca marche aussi pour fusionner les fichiers
let style = {...css, width:'400px'};
console.log(style);
const {color:recupColor,width:recupWidth}=style; //plutôt que de faire recupColor=style.color, recupWidth=style.width
console.log(recupColor,recupWidth);
const {color,width}=style; //si on garde le même nom de variable, on peut se passer des 2 points
console.log(color,width);
On est d'accord que la fonction
()=>{}
ne se déclenche pas, elle est juste déclarée, il faut faire
(()=>{})()
pour l'exécuter.
Classes :
//Nouveauté ES6 : les classes !
class Personne{
constructor(nom, age = 0){ //sera appelé quand on fera "new Personne()", ici age a une valeur par défaut
this.setName(nom); //setters et getters comme en Java, attention il faut le "this"
this.setAge(age);
}
setName(nom){
this.nom=nom;
return this; //permet de continuer à manipuler directement derrière le set
}
getName(){
return this.nom;
}
setAge(age){
this.age=age;
return this;
}
getAge(){
return this.age;
}
seDecrire(){
return "Je m'appelle "+this.nom+" et j'ai "+this.age+" ans.";
}
}
const p=new Personne("Bob");
console.log(p.setAge(18).setName("Bobby")); //comme this est renvoyé par setAge on peut y chainer setName
class User extends Personne{
constructor(nom, age, mail, mdp){
super(nom,age);
this.mail=mail;
this.mdp=mdp;
}
seDecrire(){
return "Je m'appelle "+this.nom+" et j'ai "+this.age+" ans. Mon mail : "+this.mail;
}
}
const u=new User("Kev97",14,"kev97_le_vrai@hotmail.com","pass123");
console.log(u.seDecrire());
Construction de variables selon des tableaux (destructuring) :
tableau=["ab","b","c","d","e","f"];
const [,,valeur1,,valeur2]=tableau;
console.log(valeur1, valeur2); //affichera "c e"
Marche aussi avec des objets :
monObjet={attribut1:"un",attribut2:"deux",attribut3:"trois",attribut4:"quatre"};
let {attribut1, attribut2, attribut4}=monObjet; //les var doivent avoir le même nom que les champs !
Template literals
Pour éviter l'injection de code (permet aussi de passer à la ligne sans devoir écrire \n) :
const personne = {nom:"Bob"};
let chainePresentation = `Mon nom est ${personne.nom}.
Bonjour !`;
console.log(chainePresentation); //"Mon nom est Bob."
Fonctions fléchées
argument
⇒ valeur de retour
On met les parenthèses dans 2 cas : s'il n'y a aucun paramètres ou s'il y en a plus un.
Si la valeur de retour est une création d'objet avec des accolades, il faut la mettre entre parenthèse.
//exemple de fonction fléchée
ages=[10,12,13,14,19,20,17];
minors=ages.filter(age => age <18); //"age => age <18" est une fonction de callback qui sera appelée pour chaque élément du tableau
//la fonction filter retourne un tableau qui sera composé des éléments pour lequels la fonction sera true
Equivalent à :
ages=[10,12,13,14,19,20,17];
minors=ages.filter( function (age){return age<18;});
Par défaut this est l'objet "global" (window pour les navigateurs). Sinon, il est interne à chaque fonction :
function a(){
monThis=this;
function b(){
//le "this" ici est le this de la fonction b.
console.log(monThis); //là on récupère le this d'avant
}
}
SAUF dans le cas des fonctions fléchées.
Portée des variables
Il y a une différence entre un var et un let.
Un var déclaré en dehors des fonctions sera toujours global. Ce n'est pas une condition d'accolades, mais bien de fonctions !
Exemple :
for(var i=0;i<10;i++){
}
console.log(i); //on verra la valeur de i dans toute la fonction, même en dehors du for (et si ce n'est pas dans une fonction, dans tout "window").
Pour éviter la problématique du dessus, on peut utiliser let :
for(let j=0;j<10;j++){
}
console.log(j); //j est undefined
Tableaux
La façon la plus moderne de faire est
let monTableau=[]
. Attention
typeof monTableau
renverra "object", pour savoir si c'est un tableau il faut voir s'il a la propriété length et les méthodes de tableaux comme push.
Pour boucler :
for(let i in monTableau){
console.log(monTableau[i]);
}
for(let val of monTableau){
console.log(monTableau);
}
For each appelle une fonction pour chaque élément du tableau.
monTableau.forEach((val) => console.log(val));
Equivalent à :
monTableau.forEach(function(element) {
console.log(element);
});
Encore plus détaillé, équivalent à :
function callBack(element) {
console.log(element);
}
monTableau.forEach(callBack);
Il y a des paramêtres supplémentaires optionels dans la fonction de callback : l'index de l'élément et le tableau utilisé.
function callBack(element, index, tableau) {
console.log("Valeur de l'élément n°"+index+" : "+ element);
console.log("Pour rappel, on s'occupe du tableau "+tableau);
}
monTableau.forEach(callBack);
La function map() permet de triturer l'intérieur des tableaux pour retourner un tableau avec les éléments modifieés :
let a=[1,2,3].map(item=>item*10); //a est un tableau qui vaudra 10,20,30
Autre exemple :
tabMonObjet=[1,2,3].map(itemDuTableau => ({sonAttribut:itemDuTableau*10}));
Plus complexe :
const tab=[1,2,3,4];
const mapped=tab.map((item, index) => {
return index > 2 ? { //si l'index est sup à 2 on renvoie un objet
clef:index
}:'' //sinon une chaine vide
});
Une autre méthode intéressante, reduce, ne retourne qu'une seule valeur mais permetd e faire des opérations sur les éléments du tableau.
Divers
Au lieu de faire
parseInt(maVariable)
, on peut faire
+(maVariable)
.
Ajax :
function X()
{
//création d'un objet pour XMLHttpRequest
var xhr;
try { xhr = new ActiveXObject('Msxml2.XMLHTTP'); }
catch (e)
{
try { xhr = new ActiveXObject('Microsoft.XMLHTTP'); }
catch (e2)
{
try { xhr = new XMLHttpRequest(); }
catch (e3) { xhr = false; }
}
}
xhr.onreadystatechange = function()
{
if(xhr.readyState == 4)
{
if(xhr.status == 200){
//si on a récupéré quelque chose on l'affiche dans une alert
alert(xhr.responseText);
}
else{
document.ajax.dyn="Error code " + xhr.status;
}
}
};
//demande quelque chose à la page XXX.php
xhr.open( "GET", "XXXX.php", true);
xhr.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT");
xhr.send(null);
}
On peut ajouter un truc en cas de timeout, par exemple :
xhr.timeout = 20000;
xhr.ontimeout = function (e) {
alert("Délai de réponse dépassé.");
};
Manipulation DOM
On peut modifier et même générer des éléments HTML uniquement en Javascript.
Génération d'éléments DOM
Pour créer des éléments y a plusieurs étapes :
1)Création de l'élement :
const li = document.createElement('li');
2)Eventuellement, modification de ses attributs (texte affiché, style, nom de la classe, etc) :
i.className="choseAFaire";
3)Récupération de son élément parent
parent=document.getElementById('parent');
4)Enfin, on l'attache au parent :
parent.appendChild(li);
Ci-dessous un système de "todo list" avec des éléments générés dynamiquement :
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Titre de ma page</title>
<style>
.xSupression{
font-size:1.2em;
cursor: pointer;
color:red;
}
</style>
</head>
<body>
<div class="todo">
<div>
<input type="text" class="todo_input">
<ul class="todo_items">
</ul>
</div>
</div>
<script>
const todoInput=document.querySelector(".todo_input");
const todoItems=document.querySelector(".todo_items");
todoInput.addEventListener('keyup', evenement =>{
if (evenement.keyCode === 13){
const texteRecupere=evenement.target.value;
if(texteRecupere){
const li = document.createElement('li');
li.className="choseAFaire";
const texte=document.createTextNode(texteRecupere); //on créé un texte
const span = document.createElement('button');
li.contentEditable=true;
span.className="xSupression";
span.innerText=" 🗑️";
span.addEventListener('click',suppressionNote);
li.appendChild(texte);
li.appendChild(span);
todoItems.appendChild(li);
todoInput.value='';
}
}
},false);
function suppressionNote(){
elementASupprimer=this.parentNode;
elementASupprimer.parentNode.removeChild(elementASupprimer);
}
</script>
<script>
window.onload = () =>{
const elem=document.getElementById('element'); //on récupère l'élement
elem.style.backgroundColor='yellow'; //on change sa couleur de fond
const texte=document.createTextNode('Mon texte'); //on créé un texte
const contenuP=document.createElement('p'); //on créé une partie paragraphe
contenuP.appendChild(texte); //à ce P, on rajoute le texte en tant qu'enfant
elem.appendChild(contenuP); //à l'élément du début, on y met notre Ps
}
</script>
<div id="element">coucou</div>
</body>
</html>
Bubbling et capturing
Si on met des listener sur des éléments superposés, il est logique qu'ils soient tous déclenchés. Par exemple, si on clique sur p qui est lui même dans une div lui-même dans body, avec chacun un listener, les 3 seront déclenchés. Dans quel ordre ? Bubbling : de bas en haut, celui de p en premier. Capturing : de haut en bas.
Dans l'exemple ci-dessous on a un p dans un article dans une section :
<section onclick="alert(this.tagName);">
<article onclick="alert(this.tagName);">
<p onclick="alert(this.tagName);">Lorem ipsum dolor sit amet.</p>
</article>
</section>
<section class="section2">
<article>
<p>Lorem ipsum dolor sit amet.</p>
</article>
</section>
<script>
const tabElements = document.querySelectorAll('.section2, .section2 *');
console.log(tabElements);
for(let element of tabElements){
element.addEventListener('click', evenement =>{ alert(element.tagName); },true); //true=capturing (de bas en haut), false=bubbling (de haut en bas).
}
</script>
Local storage
Pour stocker des variables dans le navigateur de l'utilisateur. Il vaut mieux utiliser ça que des cookies, car ces derniers sont envoyés à chaque requête au serveur.
localStorage.setItem("prenom", "Bob");
alert(localStorage.getItem("prenom"));
Récupérer les coordonnés de la souris :
e.clientX avec firefox, event.clientX avec IE.
<script language="JavaScript1.2">
//on assigne la fonction getMouseXY si la souris est bougé sur le document
document.onmousemove = getMouseXY;
function getMouseXY(e) {
mouseX=0;
mouseY=0;
if(e){ //si argument passé, pas IE
mouseX=e.clientX ;
mouseY=e.clientY ;
}
else
{ //sinon, ie, donc event
mouseX=event.clientX ;
mouseY=event.clientY ;
}
document.getElementById('vaisseau').style.top=mouseY ;
document.getElementById('vaisseau').style.left=mouseX ;
}
</script>
//l'image qui suit la souris
<img src="vaisseau.jpg" id="vaisseau" style="position:absolute;">
document.onclick
"document.onclick = fonction;" Signifie que si on clique, on fera le contenu de fonction.
"document.onclick = fonction();" Signifie que si on clique, on fera la fonction retournée par "fonction".
Pour passer un argument, ça sera :
document.onclick = function (evt) {
fonction('arg');
};
document.write
Pose problème quand est appelé une fois la page chargé, il efface tout.
Il existe document.body.insertAdjacentHTML('beforeEnd',"Hello"); comme alternative.
document.body.insertAdjacentHTML
Soustraire une seconde à une date UTC
var e=new Date();
var e = new Date(new Date().getTime() -60000);
document.write(e.toUTCString());
Taille de l'écran
function largeur() { return screen.width } function hauteur() { return screen.height }
Script au démarrage
Pour lancer le script au tout début du chargement de la page, il suffit de l'appeler directement.
Mais souvent on a besoin que la page soit chargée, surtout pour récupérer des éléments :
window.onload = function() {
alert(document.getElementById("monElement"));
};
Un pop in en html
Mais autant utiliser prompt() en javascript....
#divFondPopIn{
position:fixed;
padding:0;
margin:0;
top:0;
left:0;
width: 100%;
height: 100%;
background:#000000;
opacity:.70;
filter: alpha(opacity=70);
}
#divCadrePopIn{
background:#FFFFFF;
padding:1em;
position:fixed;
top:40%;
left:40%;
}
#boutonsOkAnnulerPopIn{
float:right;
}
<div id="divFondPopIn"></div>
<div id="divCadrePopIn">
<form action="action_page.php">
Nom du nouvel écran ?:<br>
<input type="text" name="nom_ecran" value="Ecran"><br>
<br>
<span id="boutonsOkAnnulerPopIn"><input type="submit" value=" Créer l'écran "> <input type="button" value=" Annuler "></span>
</form>
</div>
Redimensionner des iframes selon leur contenu
Il faut déjà s'assurer que la page qu'on va appeler dans l'iframe se redimensionne correctement lorsqu'on change la taille du navigateur.
Pour ça, ne pas oublier de définir des tailles pour tous les éléments parents (body, html, puis les div jusqu'au div qu'on veut redimensionner, car si on demande "20%" à un div en position relative dont le parent n'a pas de taille définie, il ne saura pas faire).
window.onload = function() {
var tableau_iframe_element = document.getElementsByClassName("iframe_element");
for(var i = 0; i < tableau_iframe_element.length; i++) {
tableau_iframe_element[i].style.height = (tableau_iframe_element[i].parentNode.offsetHeight * (100-8))/100+'px';
tableau_iframe_element[i].style.width = (tableau_iframe_element[i].parentNode.offsetWidth * (100-1))/100+'px';
tableau_iframe_element[i].contentWindow.location.reload(true);
}
};
setTimeout avec argument
setTimeout(function() {
redimensionner_iframe('iframe'+event.target.id.substring(3));
}, 500);
Envoyer du JSON en Ajax avec Jquery
$.ajax({
type: 'POST',
url: 'index.php?sauver_affichage_ecran_num=<?=$page_actuelle->get_id_ecran() ?>',
data: {elements_que_l_on_veut_envoyer:mon_super_json},
success: function(msg) {
alert(msg);
}
});
Augmenter la police d'un select
.contactselect option:checked {
background: #ffb7b7; /* WebKit/Blink Browsers */
font-size:4em;
}
Boucle i avec variable locale
var tabNombres=[293,34,233,423,44,56,32];
var max=0;
for(var i=0, tailleTableau=tabNombres.length;i<tailleTableau;i++){
if(max<tabNombres[i]){
max=tabNombres[i];
}
}
alert(max);
Héritage avec prototypes
//
/*prototype Vehicule*/
//
function Vehicule(marque, modele, prix) {
this.marque = marque;
this.modele = modele;
this.prix = prix;
}
Vehicule.prototype.seDeplacer = function () {
console.log("Le véhicule "+this.modele+" se déplace");
}
Vehicule.prototype.seDecrire = function () {
console.log("Je suis un véhicule de marque "+this.marque+" et de modele "+this.modele+", mon prix est de "+this.prix);
}
//
/*prototype Voiture qui hérite de Vehicule*/
//
function Voiture(marque, modele, prix) {
Vehicule.call(this,marque,modele,prix); //sorte de super pour appeler le constructeur de Vehicule en lui donnant le contexte "this"
}
Voiture.prototype = new Vehicule(this.marque,this.modele, this.prix); //
Voiture.prototype.seDeplacer = function () {
console.log("La voiture "+this.modele+" se déplace sur 4 roues.");
}
/*prototype Moto qui hérite de Vehicule*/
//
function Moto(marque, modele, prix) {
Vehicule.call(this,marque,modele,prix);
}
Moto.prototype = new Vehicule(this.marque,this.modele, this.prix);
Moto.prototype.seDeplacer = function () {
console.log("La moto "+this.modele+" se déplace sur 2 roues.");
}
/*Création des objets*/
var v1 = new Voiture('Opel','Astra',15000);
var v2 = new Voiture('Renaud','Twingo',6000);
var m1 = new Moto('Suzuki','SV650',25000);
//démarrage
v1.seDecrire();
v2.seDecrire();
v2.seDeplacer();
m1.seDecrire();
m1.seDeplacer();
Utiliser "this" dans une fonction de callback
var Monstre={
name:'Crasby',
executer:function(callback){
console.log(this.name+" se prépare...");
callback.call(this); //on execute la fonction de callback en lui donnant le this actuel
//"apply" fait la même chose, avec un tableau par contre
}
}
function attack(){
console.log(this.name+" attaque !");
}
Monstre.executer(attack);
jQuery
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Titre de ma page</title>
<script src="jquery-3.3.1.js"></script>
<style>
.favcolor {
color: royalblue;
}
.boite,
.accordeon-boite {
border: 1px solid black;
max-width: 30em;
}
.boite-titre,
.accordeon-boite-titre {
padding: 0;
margin: 0;
background-color: mediumblue;
color: white;
}
.boite-contenu,
.accordeon-boite-contenu {
background-color: royalblue;
color: white;
margin: 0;
display: inline-block;
}
.accordeon {
border: 1px solid red;
max-width: 30em;
}
</style>
</head>
<body>
<div>
<p>Bonjour !</p>
<p>Hello !</p>
</div>
<div class="boite">
<h2 class="boite-titre">Titre</h2>
<div class="boite-contenu">
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptatem quas provident repellendus perspiciatis
blanditiis omnis harum.</p>
</div>
</div>
<hr>
<div class="accordeon">
<div class="accordeon-boite">
<h2 class="accordeon-boite-titre">Titre</h2>
<div class="accordeon-boite-contenu">
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptatem quas provident repellendus perspiciatis
blanditiis omnis harum.</p>
</div>
</div>
<div class="accordeon-boite">
<h2 class="accordeon-boite-titre">Titre</h2>
<div class="accordeon-boite-contenu">
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptatem quas provident repellendus perspiciatis
blanditiis omnis harum.</p>
</div>
</div>
<div class="accordeon-boite">
<h2 class="accordeon-boite-titre">Titre</h2>
<div class="accordeon-boite-contenu">
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptatem quas provident repellendus perspiciatis
blanditiis omnis harum.</p>
</div>
</div>
</div>
<hr>
<div class="accordeon">
<div class="accordeon-boite">
<h2 class="accordeon-boite-titre">Titre</h2>
<div class="accordeon-boite-contenu">
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptatem quas provident repellendus perspiciatis
blanditiis omnis harum.</p>
</div>
</div>
<div class="accordeon-boite">
<h2 class="accordeon-boite-titre">Titre</h2>
<div class="accordeon-boite-contenu">
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptatem quas provident repellendus perspiciatis
blanditiis omnis harum.</p>
</div>
</div>
<div class="accordeon-boite">
<h2 class="accordeon-boite-titre">Titre</h2>
<div class="accordeon-boite-contenu">
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptatem quas provident repellendus perspiciatis
blanditiis omnis harum.</p>
</div>
</div>
</div>
<script>
//deux façons d'appeler la fonction jquery : $() et jQuery()
$(document).ready(function () { //pareil que windows.onload, on attends le chargement du DOM
$('p').addClass('classeAjoutee'); //on ajoute une classe sur les éléments p
$('p').css('cursor', 'pointer'); //on modifie la css en mettant le curseur en pointeur
fermerTousAccordeons();
});
//si un clique sur un p la couleur change
$('p').on('click', function (e) {
console.log(this);
$(this).toggleClass('favcolor'); //on peut en ajouter une 2e
});
//ouvre et ferme une boite
$('.boite-titre').on('click', function (e) {
console.log($(this).find().tagName);
$(this).next(".boite-contenu").slideToggle();
});
$.fn.accordeoniser = function() {
this.css( "color", "green" );
return this;
}
$('.accordeon-boite-titre').on('click', function (e) { //on met le listener sur les titres d'accordéon
//alert("Parent :"+$(this).parent().parent().prop('class'));
fermerAccordeon(this);
//et on ouvre uniquement celui qu'on veut
$(this).next().slideDown();
});
//fonction typiquement appelée à l'ouverture de la page pour fermer les accordéons
function fermerTousAccordeons(){
$('.accordeon').children("*").each(function () {
//alert("Enfant : "+$(this).prop('class'));
//pour chaque accordeon-boite-contenu dans l'enfant
$(this).children(".accordeon-boite-contenu").each(function () {
//alert("Enfant intéressant : "+$(this).prop('class'));
$(this).slideUp();
//on le ferme
});
});
}
//fermer tous les éléments d'un accordéon dont on a cliqué sur le titre d'une boîte
function fermerAccordeon(ac){
$(ac).parent().parent().children("*").each(function () {
//alert("Enfant : "+$(this).prop('class'));
//pour chaque accordeon-boite-contenu dans l'enfant
$(this).children(".accordeon-boite-contenu").each(function () {
//alert("Enfant intéressant : "+$(this).prop('class'));
$(this).slideUp();
//on le ferme
});
});
}
</script>
</body>
</html>
Ajoute un listener qui se déclenchera à chaque appel Ajax :
$( document ).ajaxComplete(function(xhr, status, args) {
alert(xhr);
});
Parcourir toutes les propriétés d'un objet
for (var key in items) {
console.log(items[key]);
}
Closure
En JS on peut mettre une fonction dans une variable :
toto=function(){ return 10; }
. Pour l'appeler :
toto()
function test(param){
var option=param;
return function (val){ //une fonction sera retournée, donc on
return val*option;
}
}
var mult10=test(10); //mult10 EST une fonction
var mult5=test(2); //mult5 EST une fonction
var result1 = mult10(2);
var result2 = mult5(2);
console.log(result1,result2);
TypeScript
Un surensemble syntaxique strict de javascript, sera "transpilé" (traduit avec une phase de contrôle) pour un navigateur. Open source, de Microsoft. Rajoute des notions aux classes ES6 (private/protected, les interfaces, etc).
Fait de l'inférence de type : on peut déclarer une variable sans la typer, mais elle sera typée lors de sa première affectation (on dit aussi "assertion de type").
Types primitifs
let isDone: boolean = false;
let decimal: number=6;
let hex: number=0xf00d;
let binary: number=0b0101;
let octal: number=0o744;
let maChaine: string='valeur de ma chaine';
Le type "any" stocke n'importe quoi.
On peut donner plusieurs types à un objet ("Union Types") :
product: Product | undefined; //la variable product peut contenir un product ou undefined
Ca fonctionne aussi avec des fonctions :
function printId(id: number | string) { //la fonction peut prendre un paramètre number ou string
console.log("Your ID is: " + id);
}
printId(101);
printId("toto");
printId(true); //Argument of type 'boolean' is not assignable to parameter of type 'string | number'.
A ne pas confondre avec les pipes utilisés par Angular pour transformer l'affichage d'une valeur (
exemple) !
On peut faire des sortes de cast :
let uneValeur: any = "C'est une string stocké dans un type any";
//puis, si on a besoin d'une méthode de string, il faut caster :
let longueurChaine: number = (<string>uneValeur).length;
alert(longueurChaine);
On peut typer les retours des fonctions et donner des valeurs par défaut aux arguments :
function add(x:number, y:number, sertarien?:number, sertarienAvecValeurParDefaut='valParDefaut'):number{ //point d'interrogation pour facultatif, undefined par défaut. On peut spéficier la valeur par défaut.
return x+y;
}
On peut utiliser les 3 points comme en java (rest parameters) :
function maFonction(a, b, ...plusDArguments) {
console.log("a", a);
console.log("b", b);
console.log("plusDArguments", plusDArguments);
}
maFonction("un", "deux", "trois", "quatre", "cinq");
Pour les classes :
class Personne{
nom: string;
age: number;
constructor(nom, age = 0){ //sera appelé quand on fera "new Personne()", ici age a une valeur par défaut
this.nom = nom; //setters et getters comme en Java, attention il faut le "this"
this.age = age;
}
seDecrire(){
return "Je m'appelle "+this.nom+" et j'ai "+this.age+" ans.";
}
}
const p=new Personne("Bob");
console.log(p.seDecrire()); /je m'appelle bob et j'ai 0 ans
On peut faire ça aussi :
class Personne{
constructor(private nom: string, private age: number){}
seDecrire(){
return "Je m'appelle "+this.nom+" et j'ai "+this.age+" ans.";
}
}
const p=new Personne("Bob",18);
console.log(p.seDecrire()); //je m'appelle bob et j'ai 18 ans
Interface :
interface Speak{
say(words: string);
}
class Dog implements Speak{
say(words: string){
console.log("Wouf"), words);
}
}
Les getters et les setters sont appelés en modifiant directement la variable :
class Personne{
constructor(private nom: string, private age: number){}
get leNom(){
return this.nom;
}
set leNom(nouveauNom: string){
this.nom = nouveauNom;
}
}
let personne = new Personne("Bob",18);
personne.leNom = "John";
alert(personne.leNom);
Angular
Créé par google en 2012, grosse évolution en 2016 qui donnera Angular 2 fonctionnant avec des composants (comme son concurrent react.js), des services, bref, des petites entités qui vont communiquer entre elles.
Les numéros de versions suivent le Semantic Versionning : MAJOR.MINOR.PATCH.
Permet de faire de faire du progressive web app (client entre le léger et lourd), native (exécute les instructions natives du processeur mobile sur lequel l'application tourne, compilée pour marcher sur un type de téléphone spécifique grâce à Ionic Framework, NativeScript, React Native... Ce n'est pas un interpréteur de js, l'application est compilée pour fonctionner sur le téléphone cible), desktop (des applications de bureau Mac, Windows, Linux).
JQuery était une première abstraction (au lieu de devoir écrire plusieurs versions du code javascript, on écrivait une seule en Jquery), Angular est une couche d'abstraction au dessus (mais ça ne veut pas dire qu'il se sert de Jquery ! Il y a quand même un Jquery minimal émulé à l'intérieur) : avant, on chargeait nous même les scripts avec des balises scripts (JQuery, bootstrap, etc) et c'était à nous d'ordonner ça dans la page HTML. A chaque chargement de la page, le navigateur allait chercher les js, il ne fallait pas se tromper de sens dans la déclaration des js... Avec angular, il n'y a plus tout ça, ce sera installé par npm.
En angular on va faire des composants (de préférence réutilisables), définir une nouvelle balise HTML avec des nouveaux attributs pour récupérer des informations de l'extérieur. Angular offre un accès en ligne de commande (cli) qui permet la transpilation du TypeScript, l'internationalisation, l'utilisation de webpack, le guide du style angular, du scaffholding (générer tout le nécessaire -fichiers, arborescence... pour créer un composant et notamment le fichier pour les tests qui nous permettrons de modifier l'application en étant confiant de ne pas introduire de bugs. En théorie, on devrait avoir 2 consoles tout le temps ouvertes, une qui lance la compilation et l'autre qui lance le test. Angular facilite les tests mais aussi l'accessibilité, l'internationalisation.
Pour installer le cli angular :
sudo npm install -g @angular/cli
ng new [options]
: crée un nouveau répertoire contenant une nouvelle application angular. Par défaut angular créé le projet en utilisant css, si on veut utiliser less ou scss il faudra faire :
ng new bookstore --style=scss
ng serve
: simule un serveur http, build et lance l'application dans le navigateur, rebuild automatique si des fichiers sont modifiés.
ng serve -o --port 4402
pour ouvrir automatiquement dans le navigateur avec un certain port.
ng build
: build pour déployer l'application.
ng generate [options]
: ou
ng g
, création d'un nouvel élément angular (component, directive, interface, module). m pour module, exemple pour générer un module books :
ng g m books
.
ng g c books/booksList
pour créer un composant booksList dans le module books.
ng g s services/auteur
pour créer un service auteur.
ng test
: exécute la suite de test de l'application. A faire pendant le
ng xi18n
: extraction des messages i18n du code source.
ng e2e
: tests end to end (fonctionnels, simuler les clics de l'utilisateur sur notre application).
Un grand nombre de fichiers vont être téléchargés/générés.
A la racine :
-package.json : 2 parties, tout ce qui va servir à faire marcher le projet + tout ce qui va servir à le construire. La commande "npm install" va lire ce fichier et télécharger les dépendances.
Partie "scripts" : des raccourcis vers les commandes, par exemple "ng serve" est raccourci en "start". Il va chercher les commandes dans le path (au cas où ng serait installé globalement), s'il ne trouve pas, il ira dans dossier node/module/bin. On peut taper
npm run start
pour lancer "ng serve". On peut ajouter nous même des commandes (par exemple une commande pour lancer json server).
"dependencies" : tout ce qui va être chargé dans le navigateur.
"devDependencies" : ce dont on a besoin en tant que dev (linter pour controler les sources, jasmine/karma pour les tests.
-angular.json : ne jamais le toucher à la main ! Par exemple,
"prefix": "app"
sera le prefixe de nos composants (balise "app-root" par ex),
"styleext": "scss"
indique qu'on voudra utiliser du scss et pas juste du css.
-tslint.json : pour la correction syntaxique de notre code, on peut changer les règles.
⇒Dans le dossier src on aura un fichier index.html avec une balise app-root (ne pas rajouter d'autres balises ! pas besoin de touche ce fichier sauf si on veut rajouter des balises meta etc), tsconfig.spec.json (tout ce qui contient "spec" concerne les tests). "Polyfill.ts" pour émuler les tests. main.ts (le point d'entrée de l'application) ne pas y toucher.
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment'; //on importe l'objet environnement
if (environment.production) { //si on est en production
enableProdMode(); //Disable Angular's development mode, which turns off assertions and other checks within the framework.
}
platformBrowserDynamic().bootstrapModule(AppModule) //on se sert de app.module.ts
.catch(err => console.error(err));
Modules angular
Un fichier *.module.ts annoté de @NgModule.
Il y a au minimum un NgModule (root), appelé par convention AppModule. Chaque module aura un ou plusieurs composants (dont un par défaut), des imports qu'il va utiliser et éventuellement des exports qu'il va mettre à disposition.
app.module.ts est là par défaut :
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [ //les classes de vues appartenant au module (components, directives, pipes), ce qui est en dessous de lui.
AppComponent
],
imports: [ //les autres modules utilisés par notre module, nécessaires pour le faire marcher, il est en dessous d'eux.
BrowserModule
],
providers: [], //autrefois pour les services, on ne s'en sert plus vraiment depuis angular6, maintenant par défaut si on créé un service il est à disposition de tout le monde
bootstrap: [AppComponent] //le composant qui sera chargé, celui qui sera chargé. il sera forcément dans la liste de déclaration des composants en haut.
}) //ATTENTION cette ligne et celle du dessous doivent être accolées.
export class AppModule { }
Composant
Les composants sont en général formés de 4 fichiers : html, css, ts (controleur) et spec.ts (tests). Ils sont contenus dans un module.
Annotés de @Component.
Pour créer un :
ng g c books/booksList
. Il sera ajouté aux déclarations du module (soit le module dans le même dossier, soit le module app-root), mais on voudra sans doute également l'exporter.
Exemple du contrôleur du composant app-root :
import { Component } from '@angular/core';
@Component({
selector: 'app-root', //la balise html pour appeler ce composant
templateUrl: './app.component.html', //le fichier html pour définir la structure du composant, on peut aussi mettre le code html directement ici
styleUrls: ['./app.component.scss'] //le style à appliquer au html
})
export class AppComponent {
title = 'bookstore'; //une variable qui sera affichée dans le template html
//il y aura un constructeur, des méthodes, etc
}
Cyle de vie d'un composant :
Le navigateur requière la page de la route et execute les js qu'il y aura sur cette page. Il travaille sur le "shadowdom", un dom qui sera chargé en arrière plan puis qui remplacera le vrai dom (sinon, on verrait les composants se charger petits à petits).
Quand Angular charge le module principal, il va faire un dictionnaire de tous les modules qui existent. Quand il analysera la page html, il va détecter les balises et les remplacer avec les modules correspondants dans son dictionnaire.
->Angular créé le composant.
->Construit et insère le code html dans le DOM.
->Gère récursivement la création et le rendu des composants.
->Gère le changement des propriétés.
->Détruit les composants.
Des "fonctions" comme ngOnInit() (appelé lors de l'initialisation, avant même que le composant soit dans le shadowdom), ngOnDestroy() (appelé à la destruction de l'élément) sont dans les composants.
Le fichier browserlist pour définir les navigateurs que l'on veut viser.
⇒Un dossier styles.scss : pour appliquer des styles communs à tous les composants qu'on va créer.
⇒Le dossier app contiendra notre code.
⇒environnement : comment on va lancer le serveur et quelles variables vont être différentes entre le mode production et le mode local. Par exemple, si on a des urls différentes entre le test en local pour accéder à une api et une vraie url vers une vraie api en prod :
export const environment = {
production: true,
BASE_URL:'http://localhost:3000/books'
};
Autre exemple, en prod on veut afficher 5 articles par pages mais en debug on voudra 20 articles par pages.
⇒le dossier assets ne contient rien par défaut (sauf un fichier .gitkeep vide, il y a un fichier pour s'assurer que git copie le dossier -par défaut il ne prend pas les dossiers vide sinon). Ce dossier assets contiendra tous les médias (il convertira les images les plus petites en base 64 et les incluera dans le code).
Ne pas modifier l'intérieur du dossier "node-module".
Nous n'allons travailler que dans "app". Lorsqu'on créé un composant, il y aura automatiquement 4 fichiers. nomdelappli.typedecomposant.extension, par exemple app.component.html ou app.component.scss.
Le css et le html est souvent inclut dans le composant, décrit directement dans le js.
Pour intégrer un composant dans la page :
-Eventuellement, créer son module :
ng g m books
.
-Créer le composant :
ng g c books/booksList
-Dans notre module, le composant doit avoir été ajouté, mais pas exporté. Rajouter l'annotation
@NgModule
, rajouter notre ligne pour exporter le composant :
exports:[ BooksListComponent]
-A la racine, dans
app.module.ts
, ajouter les imports dans
@NgModule
(on aura donc
imports: [BrowserModule,BooksModule
)
-Intégrer la balise
dans app.component.html
Exemple de books-list.component.ts :
import { Component, OnInit } from '@angular/core';
import { Book } from 'src/app/models/book.model';
@Component({
selector: 'app-books-list',
templateUrl: './books-list.component.html',
styleUrls: ['./books-list.component.scss']
})
export class BooksListComponent implements OnInit {
monLivre: Book = { //ici on instancie un livre, juste pour l'exemple il faudrait utiliser un service plutôt
id: 1,
titre: 'Germinal',
nomAuteur: 'Zola',
description: 'Livre sur les grèves dans les mines'
}
constructor() { }
ngOnInit() {
}
}
Modèles
Pour les classes modèles, il faut le faire manuellement (un dossier "models" dans "app", avec un fichier "book.model.ts" par exemple) :
export class Book {
id: number;
titre: string;
nomAuteur: string;
description: string;
}
Services
Une class exportée avec l'annotation @Injectable. Grâce à l'injection de dépendance, le service sera accessible partout. On l'injectera dans le constructeur de la classe où on en aura besoin.
Le service sera un Singleton.
Exemple de service :
import { Injectable } from '@angular/core';
import { Book } from '../models/book.model';
import { HttpClient } from '@angular/common/http';
import { Observable, from } from 'rxjs';
import {environment} from '../../environments/environment';
const BASE_URL: string=environment.BASE_URL;
@Injectable({
providedIn: 'root'
})
export class BookService {
//va chercher un httpclient et retourner un observable de tableau de Book
constructor(private http: HttpClient) {
console.log("Nous sommes dans le constructeur de BookService");
}
putBooks(livre: Book){
return this.http.post<Book>(`${BASE_URL}/books/`,livre);
}
getBook(numLivre){
console.log("Ici le book.service, on va chercher",BASE_URL+"/"+numLivre);
return this.http.get<Book>(`${BASE_URL}/books/${numLivre}`);
}
getBooks(){
return this.http.get<Book[]>(`${BASE_URL}/books/`);
}
}
Puis dans la classe du livre (books-list.component.ts), injection et utilisation du service :
import { Component, OnInit } from '@angular/core';
import { BookService } from 'src/app/services/book.service'; //on a besoin du service pour récupérer le livre
import { Book } from 'src/app/models/book.model'; //il faut connaitre le model
@Component({
selector: 'app-books-list',
templateUrl: './books-list.component.html',
styleUrls: ['./books-list.component.scss']
})
export class BooksListComponent implements OnInit {
livreAAfficher: Book ; //la variable est en attribut pour pouvoir s'en servir en dehors du constructeur, l'afficher
constructor(private bookService: BookService) { //ici le type BookService sera détecté comme injectable et sera INJECTE
this.livreAAfficher= bookService.getBook(1); //on récupère le livre 1
console.log("Nous sommes dans le constructor de BooksListComponent");
}
ngOnInit() {
console.log("Nous sommes dans le ngOnInit de BooksListComponent");
}
}
Directives
Se met dans le template html.
Directives structurelles : ngIf va instancier selon une condition, ngFor va instancier tant qu'il y a dans un tableau, ngShow va montrer des choses si une valeur mise, ngSwitch pour afficher des choses selon que ou selon que.
Exemple (si article vaut null ou undefined ou false, il ne va pas afficher cette div) : <div *ngIf="article">{{article.title}}</div>
Pour afficher un tableau :
<ul>
<li *ngFor="let article of articles">{{article.title}}</li> //va générer autant de lignes que d'articles, on peut rajouter : "let i= index" après "let article of articles"
</ul>
Directives attributes : Pour définir les attributs personnalisées, par exemple pour mettre un texte en majuscule on pourrait faire <p appUppercase>texte qu'on veut en maj</p>
Il faudra la directive quelque part, dans un dossier Directives dans notre élément par exemple :
import { Directive,ElementRef } from '@angular/core';
@Directive({
selector: '[appUppercase]'
})
export class UppercaseDirective {
constructor(el: ElementRef){
el.nativeElement.style.textTransform = 'uppercase';
}
}
On pourrait faire une directive pour du drag and drop par exemple.
Pour créer une directive : ng generate directive shared/Bold
. Les fichiers bold.directive.ts et bold.directive.spec.ts vont se créer dans le dossier app/shared.
Pour pouvoir utiliser notre directive dans toute notre appli, on peut la mettre dans un module "Shared" par exemple. Il faudra y déclarer notre directive et l'exporter, puis importer ce shared module partout où on voudra l'utiliser :
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UppercaseDirective } from './uppercase.directive';
@NgModule({
imports: [
CommonModule //ce module est important, il y aura les directives angular
],
declarations: [
UppercaseDirective //on déclare notre directive dans ce module
],
exports: [
UppercaseDirective //on l'exporte
]
})
export class SharedModule { }
Data binding/Event binding
Notation moustache pour afficher un objet/une variable : {{monObjet}}
Sauf si c'est dans un composant :
<appMoncomposant [attributAuquelOnVeutPasserQuelqueChose]="instanceDunObjet" attributString="uneString" (action)="uneAction()" (action2)="uneActionAvecParam($event)"></appMoncomposant>
Dans le composant il faudra :
@input() attributAuquelOnVeutPasserQuelqueChose: Type;
Exemple pour détecter un clic :
//dans la vue, moncomposant.component.html
<div (click)="methodeAAppelerSiClick()"> //click est un event préprogrammé dans angular, se déclenche si clic de souris sur l'élément
Cliquez-moi !
</div>
//dans le contrôleur, moncomposant.component.ts
import { Component, OnInit, Input, Output,EventEmitter } from '@angular/core';
import { Book } from 'src/app/models/book.model';
@Component({
selector: 'app-book',
templateUrl: './book.component.html',
styleUrls: ['./book.component.scss']
})
export class BookComponent {
methodeAAppelerSiClick(){
console.log("Un clic a été fait !");
}
}
On peut générer nous même des events avec un objet "EventEmitter". Exemple typique : un composant enfant détecte un évènement clic -> la fonction associée se lance et demande à un objet EventEmitter d'envoyer un évènement -> cet évènement est détecté chez un composant parent->la fonction associée se lance chez le composant parent.
//Dans la vue du composant enfant
<div (click)="monActionATransferer()">
<p appUppercase>
Titre : {{livre.titre}}
</p>
</div>
Dans le contrôleur du composant enfant :
import { Component, OnInit, Input, Output,EventEmitter } from '@angular/core';
import { Book } from 'src/app/models/book.model';
@Component({
selector: 'app-book',
templateUrl: './book.component.html',
styleUrls: ['./book.component.scss']
})
export class BookComponent {
@Input() livre: Book;
@Output() monEventEmit=new EventEmitter(); //c'est lui qui émettra vers le parent
monActionATransferer(){
console.log("transfert de l'action !");
this.monEventEmit.emit(this.livre); //ici l'évènement est émit vers le parent
}
}
Dans la vue du composant parent :
<div>
Livre cliqué : {{livreSelectionne.titre}}
<app-book [livre]="livreAAfficher" (monEventEmit)="monAction($event)" *ngFor="let livreAAfficher of tabLivres" class="book"></app-book>
</div>
Dans le contrôleur du composant parent :
import { Component, OnInit } from '@angular/core';
import { BookService } from 'src/app/services/book.service';
import { Book } from 'src/app/models/book.model';
@Component({
selector: 'app-books-list',
templateUrl: './books-list.component.html',
styleUrls: ['./books-list.component.scss']
})
export class BooksListComponent {
tabLivres: Book[] ;
livreSelectionne: Book;
constructor(private bookService: BookService) {
this.tabLivres= bookService.getBooks(); //récupère un tableau de livres
console.log("Nous sommes dans le constructor de BooksListComponent");
}
monAction(livreTransfere:Book){
console.log(livreTransfere);
console.log(livreTransfere.titre);
this.livreSelectionne=livreTransfere;
}
}
JSON
json-server permet de simuler un serveur REST.
Pour interroger notre serveur REST, on va utiliser un objet de type HttpClient. Attention, il ne nous retourne pas directement les objets mais un objet Observable, auquel devra souscrire quelqu'un.
Dans le service on aura :
getMonBelObservable(){
return this.http.get<Book[]>(BASE_URL);
}
Dans le ts du composant :
bookService.getMonBelObservable().subscribe( //on s'abonne à l'observable
(books:Book[]) => {this.vraiTabLivres = books}, //on récupère les Objets, ici, des livres, et on en fait ce qu'on en veux
(error) => this.errorMessage = <any>error); //on peut afficher des erreurs
Si on veut juste les lister dans un tableau, on peut utiliser async qui fera la subscription :
*ngFor="let livre of tabDeMesObservables|async"
Routes
Fonctionne avec un module interne à Angular, RouterModule, à importer là où on veut l'utiliser.
Lors de l'importation il faudra utiliser sa méthode forRoot() pour lui donner un tableau de routes. Pour chaque route, on aura path/composant, ou path/redirect...
Les composants qui seront chargés selon la route seront affichés sous une balise <router-outlet></router-outlet>
.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http'; //rajout perso
import { RouterModule, Routes } from '@angular/router'; //ajout, pour les routes
import { AppComponent } from './app.component';
import { BooksModule } from './books/books.module';
import { SharedModule } from './shared/shared.module';
import { BookDetailsComponent } from './books/book-details/book-details.component';
import { BooksListComponent } from './books/books-list/books-list.component';
const appRoutes: Routes = [ //on peut aussi mettre les routes dans un fichier à part, par exemple app.routes.ts
{ path: 'livres/:id', component: BookDetailsComponent }, //puis faire import {appRoutes} from './app.routes.ts'
{ path: 'livres', component: BooksListComponent },
{ path: '', redirectTo:'livres',pathMatch:'full' } //exemple de redirection
];
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
BooksModule
SharedModule,
HttpClientModule,
RouterModule.forRoot( //ne pas oublier d'importer notre module !
appRoutes, //ici le tableau contenant les routes qu'on veut charger
{ enableTracing: true } // affiche des messages d'infos concernant les routes
)
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Pour faire nos liens, on utilisera la directive "routerLink" et non plus href. Par exemple <a routerLink="/livres">Retour</a>
.
Les liens dynamiques se présenteront comme ça (notez les crochets pour indiquer qu'il s'agit d'un objet, en l’occurrence un tableau composé d'une string et d'un livre) : <a [routerLink]="['/livres',livre?.id]">Voir les détails du livre</a>
On peut utiliser la directive "RouterLinkActive" pour charger une classe sur le lien qui correspond à la route sur laquelle on se trouve (imaginer une barre de menu, le lien correspondant à la page sur laquelle on se trouve serait coloré).
Il existe aussi une façon d'appliquer une classe seulement si une condition est remplie. Ci-dessous, les liens prendront une couleur selon la valeur d'une variable :
<a [ngCLass]="{nomDeLaClasse: locale === 'en' }">
<a [ngCLass]="{nomDeLaClasse: locale === 'fr' }">
Pour récupérer un paramètre, il y a 2 façons.
Snapshot, la plus simple :
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-book-details',
templateUrl: './book-details.component.html',
styleUrls: ['./book-details.component.scss']
})
export class BookDetailsComponent implements OnInit {
constructor(
private route: ActivatedRoute, //la route actuelle sera injectée
) { }
ngOnInit() {
const numLivreAAfficher=this.route.snapshot.params.id; //on récupère le paramètre (la route sera "/book/:id", noter les 2 points)
console.log("FAUT AFFICHER LE LIVRE N°",numLivreAAfficher);
}
Exemple avec récupération d'un livre sur monsite.com/livres/2 :
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { Book } from 'src/app/models/book.model';
import { BookService } from 'src/app/services/book.service';
@Component({
selector: 'app-book-details',
templateUrl: './book-details.component.html',
styleUrls: ['./book-details.component.scss']
})
export class BookDetailsComponent implements OnInit, OnDestroy {
subscription: Subscription=null; //on doit avoir la subscription en attribut pour pouvoir se désabonner par la suite
private livreAAfficher: Book; //sur la page on affichera {{livreAAfficher.description}} par exemple
private errorMessage: string=""; //pour les erreurs si rien n'est renvoyé
constructor(
private route: ActivatedRoute,
private bookService: BookService
) {
}
ngOnInit() {
const numLivreAAfficher=this.route.snapshot.params.id;
console.log("FAUT AFFICHER ",numLivreAAfficher);
this.subscription=this.bookService.getBook(numLivreAAfficher).subscribe(
(book:Book) => {this.livreAAfficher = book; console.log("On a recup",book);}, //une fois qu'on a récup le book on l'affiche
(error) => this.errorMessage = <any>error);
}
ngOnDestroy() {
this.subscription.unsubscribe; //il faut absolument unsuscribe sinon des observateurs vont s'accumuler chaque fois qu'on voudra les détails d'un livre !
}
}
Sous-routes
Pour que chaque composant soit isolé, il s'occupera lui même des routes le concernant.
On aura le module de l'app avec sa balise router-outlet qui contiendra un module admin apr exemple avec sa propre balise router outlet. Il aura un tableau de route avec la route principale qui contiendra la ou les sous-routes.
Exemple de module enfant :
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router'; //ajout, pour les routes
//a été ajouté automatiquement lors de la génération du composant
//en général on saute une ligne pour lister nos propres composants
import { BooksListComponent } from './books-list/books-list.component';
import { SharedModule } from '../shared/shared.module';
import { BookComponent } from './books-list/book/book.component';
import { BookDetailsComponent } from './book-details/book-details.component';
import { BooksComponent } from './books.component';
import { bookRoutes } from './books.routes';
@NgModule({
imports: [
CommonModule,
SharedModule,
RouterModule,
RouterModule.forChild( //ce n'est plus "forRoot"
bookRoutes //un tableau de routes
)
],
declarations: [
BooksListComponent,
BookComponent,
BookDetailsComponent,
BooksComponent
],
exports:[ //rajouté par moi
BooksListComponent //faut lui dire ce qu'on veut mettre à disposition
]
})
export class BooksModule {}
import { Routes } from "@angular/router";
import { BooksListComponent } from "./books-list/books-list.component";
import { BookDetailsComponent } from "./book-details/book-details.component";
import { BooksComponent } from "./books.component";
export const bookRoutes: Routes = [
{
path: 'livres',
component: BooksComponent,
children: [ //la sous-route
{ path: ':id', component: BookDetailsComponent },
{ path: '', component: BooksListComponent }
]
}
];
Lazy loading
Les composants listés dans le module racine seront TOUS chargés au lancement de l'application. Elle sera donc plus rapide lors de son utilisation au détriment d'un temps de chargement au premier lancement.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http'; //rajout perso
import { RouterModule, Routes } from '@angular/router'; //ajout, pour les routes
import { AppComponent } from './app.component';
import { BooksModule } from './books/books.module';
import { SharedModule } from './shared/shared.module';
import { BooksListComponent } from './books/books-list/books-list.component';
import { AdminModule } from './admin/admin.module';
const appRoutes: Routes = [ //on peut aussi mettre les routes dans un fichier à part, par exemple app.routes.ts
{ path: '', redirectTo:'livres',pathMatch:'full' },
{ path: 'admin', loadChildren:'./admin/admin.module#AdminModule'} //si le chemin correspond à "admin", le module sera chargé
];
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
BooksModule, //à parti de là, rajouts perso
SharedModule,
HttpClientModule,
// AdminModule, //il faut pas mettre l'AdminModule ici pour qu'on puisse le lazyloader
RouterModule.forRoot(
appRoutes,
{ enableTracing: false }
)
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Du côté de ce module admin :
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AdminComponent } from './admin.component';
import { NewBookComponent } from './new-book/new-book.component';
import { NewAuthorComponent } from './new-author/new-author.component';
import { RouterModule } from '@angular/router';
import { adminRoutes } from './admin.routes';
@NgModule({
imports: [
CommonModule,
RouterModule.forChild(
adminRoutes
)
],
declarations: [AdminComponent, NewBookComponent, NewAuthorComponent]
})
export class AdminModule { }
Et les routes du module admin :
import { Routes } from "@angular/router";
import { AdminComponent } from "./admin.component";
export const adminRoutes: Routes = [
{
path: '',
component: AdminComponent,
children: [
{ path: '', component: AdminComponent }
]
}
];
Formulaires
Deux façons de faire des formulaires : template driven et reactive form.
Exemple template driven (pensez à importer FormsModule et ReactiveFormsModule dans votre app.module ! sinon vous allez avoir des "Can't bind to 'ngModel'....") :
<div *ngIf="messageDErreur">{{messageDErreur}}</div>
<form (ngSubmit)="envoyerForm()" #monFormulaire="ngForm">
<p>Titre : <input name="titre" [(ngModel)]="livreACreer.titre" required></p>
<p>Nom de l'auteur : <input name="nomAuteur" [(ngModel)]="livreACreer.nomAuteur" required></p>
<p>Description : <textarea name="description" [(ngModel)]="livreACreer.description" required></textarea></p>
<p><button type="submit">Créer le nouveau livre</button></p>
</form>
import { Component, OnInit } from '@angular/core';
import { Book } from 'src/app/models/book.model';
import { BookService } from 'src/app/services/book.service';
import { Router } from '@angular/router';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-new-book',
templateUrl: './new-book.component.html',
styleUrls: ['./new-book.component.scss']
})
export class NewBookComponent implements OnInit {
private livreACreer: Book;
subscription: Subscription=null;
messageDErreur: string="";
constructor(private bookService: BookService, private routeur:Router) {
this.livreACreer=new Book();
}
ngOnInit() {
}
envoyerForm(){
//il faudrait griser le bouton dès l'envoi pour éviter que si le serveur met 10s à répondre, on puisse cliquer 3 fois sur le bouton et envoyer 3 livres
console.log("envoi du formulaire, il contient le nouveau livre ",this.livreACreer);
this.subscription=this.bookService.putBooks(this.livreACreer).subscribe( //ATTENTION IL FAUT ABSOLUMENT SUBSCRIBE POUR POUVOIR POST
() => {this.routeur.navigateByUrl("/")}, //on redirige vu qu'on a eu un succès
(error)=>{this.messageDErreur=error.message;}); //ya eu une erreur, on la met dans notre attribut
}
}
Exemple reactive form :
<div *ngIf="messageDErreur">{{messageDErreur}}</div>
<Form [formGroup]="monFormulairePourAuteur" (ngSubmit)="envoiFormulaire()">
<input placeholder="Name" formControlName="nomauteur">
<p><button type="submit">Créer le nouvel auteur</button></p>
</Form>
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
import { Auteur } from 'src/app/models/auteur.model';
import { Subscription } from 'rxjs';
import { AuteurService } from 'src/app/services/auteur.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-new-author',
templateUrl: './new-author.component.html',
styleUrls: ['./new-author.component.scss']
})
export class NewAuthorComponent implements OnInit {
private subscription: Subscription=null;
private messageDErreur="";
private monFormulairePourAuteur: FormGroup = new FormGroup({ //Construction du formulaire !
nomauteur: new FormControl()
}
);
constructor(private auteurService: AuteurService, private routeur:Router) { }
ngOnInit() {
}
envoiFormulaire(){
console.log("Il faut envoyer le formulaire !");
const formModel=this.monFormulairePourAuteur.value;
const auteurACreer:Auteur = {
id:0, //comme notre modèle a une ID, on est forcé de la spécifier. En la mettant à 0 le serveur va l'incrémenter. On pourrait faire new Auteur(nomauteur) aussi...
nomAuteur: formModel.nomauteur as string
};
this.subscription=this.auteurService.putAuteur(auteurACreer).subscribe( //ATTENTION IL FAUT ABSOLUMENT SUBSCRIBE POUR POUVOIR POST
(auteur: Auteur) => { //c'est l'auteur renvoyé par le serveur une fois qu'il est créé, il aura un id
this.routeur.navigateByUrl("/")
console.log('Auteur créé : ', auteur); //on le display pour le fun
},
(error)=>{this.messageDErreur=error.message;});
}
}
Validation
Ici, on a un composant affiche-erreur-valid-forms qui affiche les messages d'erreurs.
Le formulaire à remplir :
<div *ngIf="messageDErreur">{{messageDErreur}}</div>
<form (ngSubmit)="envoyerForm()" #monFormulaire="ngForm">
<p>Titre : <input name="titre" [(ngModel)]="livreACreer.titre" required minlength="4" appForbiddenName="bob" #titre="ngModel"></p>
<app-affiche-erreur-valid-forms [objetATester]="titre"></app-affiche-erreur-valid-forms> <!-- on met le champ "desc" dans l'objet objetATester. -->
<p>Nom de l'auteur : <input name="nomAuteur" [(ngModel)]="livreACreer.nomAuteur" required minlength="4"#nomauteur="ngModel"></p>
<app-affiche-erreur-valid-forms [objetATester]="nomauteur"></app-affiche-erreur-valid-forms>
<p>Description : <textarea name="description" [(ngModel)]="livreACreer.description" required minlength="4" #desc="ngModel"></textarea></p>
<app-affiche-erreur-valid-forms [objetATester]="desc"></app-affiche-erreur-valid-forms>
<p>
<button type="submit" [disabled]="!monFormulaire.form.valid">Créer le nouveau livre</button></p> <!-- par contre ya une verif à faire dans le contrôleur derrière car l'attribut disabled peut être enlevé dans l'inspecteur-->
</form>
Le composant affiche-erreur-valid-forms :
<div *ngIf="objetATester.invalid && (objetATester.dirty || objetATester.touched)"
class="alert alert-danger">
<div *ngIf="objetATester.errors.required">
Champ requis.
</div>
<div *ngIf="objetATester.errors.minlength">
Longueur incorrecte.
</div>
<div *ngIf="objetATester.errors.forbiddenName">
Interdit de rentrer cette valeur.
</div>
Transclusion
Les composants se présentent avec une balise app-moncomposant ouvrante et une fermante. Entre les 2 on peut mettre du texte qui sera remplacé par le javascript. Mais on peut aussi utiliser une balise ng-content pour y mettre des bouts d'autres composants.
Build
ng build
et ng build --prod
.
Just In Time : transpilateur lancé à chaque execution du code. Les librairies sont rechargées à chaque utilisation.
Ahead of time : une phase de build qui va inclure les librairies etc. Plus légér à charger et rapide à exécuter.
Des fichiers js seront générés.
main : notre code.
polyfill : emulation navigateurs anciens.
Les modules lazy loadés seront dans des js à part.
Les fichiers map permettent de faire la correspondance avec les lignes non-minifiées et de débugger.
On peut tester en installatn un serveur http : npm i -h http-server
Traduction
Angular i18n : statique (il faudra générer un site par langue !), mais du coup très rapide. Ne peut être utilisé que dans le HTML.
Préférer la librairie NGX-Translate dont les traductions peuvent être substituées dynamiquement dans l'application. Peut être utilisé aussi au niveau des contrôleurs. Pour installer : npm i -S @ngx-translate/core
et npm install @ngx-translate/http-loader --save
Mettre un dossier i18n dans assets, avec un fichier json par langage.
Exemple avec "en.json" :
{
"APP":{
"BOOKS_LINK":"Books zone",
"ADMIN_LINK":"Admin zone"
},
"BOOK":{
"BIENVENUE":"Welcome in our bookstore!",
"LIST":{
"TITRE":"Book title:",
"AUTEUR":"Author :",
"DETAILS":"Show book details"
},
"DETAILS":{
}
},
"ADMIN":{
}
}
Puis un fichier :
import {TranslateLoader } from '@ngx-translate/core';
import {TranslateHttpLoader } from '@ngx-translate/http-loader';
import { HttpClient} from '@angular/common/http';
export function createTranslateLoader(http: HttpClient){
return new TranslateHttpLoader(http,'./assets/','.json');
}
export const appTranslate = {
loader: {
provide: TranslateLoader,
useFactory: (createTranslateLoader),
deps: [HttpClient]
}
};
Et enfin, dans le app.module.ts :
import { Component, OnInit } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { environment} from '../environments/environment';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit{
title = 'bookstore';
langage=environment.langageParDefaut;
constructor(private traduction:TranslateService){}
ngOnInit(){
this.traduction.setDefaultLang(this.langage);
this.traduction.use(this.langage);
}
changerLangue(){
console.log(this.langage);
this.traduction.use(this.langage);
}
}
Platform-server
Angular a un système pour générer des pages html statiques (server side rendering) au lieu de notre interface en JS. Utilise pour les moteurs de rechere, les preview facebook, etc.
Tests
Avec Jasmine (exécute les tests) et Karma (test runner, enchaine les tests en lançant Jasmine) + Testbed (créé un environnement pour les tests).
Tests principaux dans app.component.spec.ts :
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('AppComponent', () => {
beforeEach(async(() => { //s'éxécutera avant chaques tests, on peut faire "beforeAll" si on veut que que quelque chose soit fait avant tous les tests
TestBed.configureTestingModule({
declarations: [
AppComponent
],
schemas:[ //on créé ça pour qu'il n'essaie pas de tester les composants du dessous
NO_ERRORS_SCHEMA //on veut pas qu'il essaie d'instancier tous les composants en dessous
//a mettre pour chaque tests concernant des composants ayant eux-même des composants
//pour éviter cascade de test
]
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'bookstore'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance; //ici on fonctionne sur le componentInstance, càd une fois instanciée mais pas forcément dans le dom
expect(app.title).toEqual('bookstore');
});
it('should render title in a h1 tag', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges(); //c'est seulement une fois que le DOM est affiché...
const compiled = fixture.debugElement.nativeElement; //...qu'on peut tester des balises HTML normales genre H1
expect(compiled.querySelector('h1').textContent).toContain('Welcome to bookstore!');
});
it('should render two routerlinks tags', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelectorAll('a[routerLink]').length).toEqual(2); //il devrait y avoir 2 balises routerLink sur la page principale (index et accès admin)
});
});
Pour ne lancer qu'un test en particulier, utiliser fdescribe au lieu de describe ci-dessus par exemple.
Teste le retour d'une fonction d'un composant :
const fixture = TestBed.createComponent(AppComponent);
const monComposant= fixture.debugElement.componentInstance;
let monRetour = monComposant.maBelleFonction();
expect(monRetour.id).toContain('quelqueChose');
Teste l'appel à une fonction depuis une autre fonction :
fixture = TestBed.createComponent(MonBeauComponent);
component = fixture.componentInstance;
...
...
it('devrait appeler maBelleFonction', () => {
spyOn(component, 'maBelleFonction');
component.autreFonctionQuiAppelleMaBelleFonction();
expect(component.maBelleFonction).toHaveBeenCalled();
});
Teste l'appel à une fonction privée depuis une autre fonction :
fixture = TestBed.createComponent(MonBeauComponent);
component = fixture.componentInstance;
blabla...
blabla...
it('devrait appeler maBelleFonction', () => {
spyOn<any>(component, 'maBelleFonction');
component.autreFonctionQuiAppelleMaBelleFonction();
expect(component.['maBelleFonction']).toHaveBeenCalled();
});
Test l'appel à une fonction dans un service :
TestBed.configureTestingModule({
let monbeauServiceMock= mock(MonbeauServiceMock);
declarations: [DashboardComponent],
providers: [
{ provide: MonbeauService, useValue: instance(monbeauServiceMock) },
blabla...
blabla...
it('devrait appeler MonbeauService.laFunctionDeMonbeauService', () => {
//GIVEN
let service = TestBed.inject(MonbeauService);
spyOn(service, 'laFunctionDeMonbeauService').and.callThrough();
//WHEN
component.laMathodeQuiAppelleLeService();
//THEN
expect(service.laFunctionDeMonbeauService).toHaveBeenCalled();
});
Test l'appel à une fonction dans un service, mais mocké (on peut aussi utiliser callFake au lieu de callThrough) :
TestBed.configureTestingModule({
declarations: [DashboardComponent],
providers: [ { provide: EventService, useValue: { laFunctionDeMonbeauService: () => {console.log("mocké");} } }]
blabla...
blabla...
it('devrait appeler MonbeauService.laFunctionDeMonbeauService', () => {
//GIVEN
let service = TestBed.inject(MonbeauService);
spyOn(service, 'laFunctionDeMonbeauService').and.callThrough();
//WHEN
component.laMathodeQuiAppelleLeService();
//THEN
expect(service.laFunctionDeMonbeauService).toHaveBeenCalled();
});
Mock d'une méthode privée :
const maBelleMethodePriveeMockee = spyOn<any>(component, 'maBelleMethodePrivee');
maBelleMethodePriveeMockee.and.callFake(() => [{
monChamp1: 'abc',
monChamp2: 'lol'
}]);
component.methodeAppelantMaBelleMethodePrivee();
});
Créer et utiliser un mock sur un service :
describe('FiltresComponent', () => {
let monBeauService: MonBeauService;
let monBeauServiceMock: MonBeauService;
beforeEach(async(() => {
monBeauServiceMock = mock(MonBeauService);
TestBed.configureTestingModule({
declarations: [UnComponent],
providers: [
{
provide: MonBeauService,
useValue: instance(monBeauServiceMock)
}
]
}).compileComponents();
monBeauService= TestBed.inject(MonBeauService);
}));
}
//ensuite, plus loin dans le test :
when(monBeauServiceMock.maBelleMethode()).thenReturn(monBelObjetDeRetour);
Ci-dessous on créé un faux service HttpClient pour les tests :
import { TestBed } from '@angular/core/testing';
import { BookService } from './book.service';
import { of } from 'rxjs';
import { HttpClient } from '@angular/common/http';
describe('BookService', () => {
//il faut simuler le HttpClient et ses méthodes
//on pourrait mettre un vrai HttpClient qui ferait des vrais requêtes mais le test ne serait plus isolé
const mockHttpClient = jasmine.createSpyObj('HttpClient', {
get: of([{ //on créé une fausse fonction get qui renverra un faux observale de tableau de Book
id:1,
titre:'Title1',
nomauteur:'Auteur',
description:'blah'}])
});
beforeEach(() => TestBed.configureTestingModule({ //dans notre environnement de test, on va utiliser des choses
providers: [
{provide: HttpClient, useValue: mockHttpClient}] //on dit qu'on va utiliser (provide) notre faux http client "mockHttpClient" (on pourrait utiliser le vrai mais cf plus haut) en tant que (usevalue) HttpClient
}));
it('should be created', () => {
const service: BookService = TestBed.get(BookService);
expect(service).toBeTruthy();
});
});
Ci-dessous on simule un attribut qui est undefined :
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { BookComponent } from './book.component';
describe('BookComponent', () => {
let component: BookComponent;
let fixture: ComponentFixture<BookComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BookComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BookComponent);
component = fixture.componentInstance;
component.livre= { //pour le test on va générer cet attribut
id:1,
titre:'titre test',
nomAuteur:'auteur test',
description:'description test'
}
fixture.detectChanges(); //laisser ça à la fin
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
Modifier une propriété en lecture seule :
Object.defineProperty(monBeauMock, 'laProprieteDeMonMockQueJeVeuxChanger', { value: 'valeurADonner' });
Pour une directive :
import { UppercaseDirective } from './uppercase.directive';
import { ElementRef } from '@angular/core';
describe('UppercaseDirective', () => {
it('should create an instance', () => {
const el:ElementRef = new ElementRef({ //on fait un élément sur lequel il peut travailler
style:{
textTransform:""
}
}); //on doit crééer l'élément ici
const directive = new UppercaseDirective(el); //il attend un argument donc il faut lui donner un élément
expect(directive).toBeTruthy();
});
});
Test d'un clic sur un bouton :
it('devrait appeler maBelleFonction si on clique sur le bouton', fakeAsync(() => {
spyOn(component, 'maBelleFonction');
const button = fixture.debugElement.query(By.css('#mon-beau-bouton'));
button.nativeElement.click();
tick();
fixture.detectChanges();
expect(component.maBelleFonction).toHaveBeenCalled();
}));
Pour une checkbox :
//clic sur une checkbox :
fixture.debugElement.nativeElement.querySelector("#maBelleCheckbox").click();
//ou bien :
fixture.debugElement.query(By.css("#maBelleCheckbox")).triggerEventHandler('change', { target: { checked: true }});
Remplir un input et s'assurer qu'il change le ngModel :
it("doit changer l'état du ngModel lorsqu'on rempli le champ", fakeAsync(() => {
// GIVEN
monModel.monChamp = "0"; //on reset le champ
fixture.detectChanges();
//WHEN
let champDeSaisie = fixture.debugElement.query(By.css('#monBelInput')).nativeElement
champDeSaisie .value = "1234"; //on saisit un montant dans le champ input
champDeSaisie .dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
//THEN
expect(monModel.monChamp).toEqual("1234"); //le modèle a bien été mis à jour quand on a saisi
});
}));
Pour éviter d'avoir des imports dans chaque tests, on peut faire un module "passe-plat" :
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { RouterTestingModule } from '@angular/router/testing';
@NgModule({
imports: [
CommonModule,
FormsModule,
RouterTestingModule,
HttpClientTestingModule
],
exports:[
FormsModule,
RouterTestingModule,
HttpClientTestingModule
],
declarations: []
})
export class TestsModule { }
Angular Route Guard
Bon tuto
Le principe : sur la route, on va appeler un service "guard" qui va nous renvoyer true ou false. Il va lui même vérifier auprès d'un service "authentification" qu'on a les droits.
On commence par créer le service autentification : ng g s agent/auth
. Ci-dessous son auth.service.ts :
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private authenticated=false;
constructor() { }
isAuthenticated(){
return this.authenticated;
}
setAuthenticated(authenticated:boolean){
this.authenticated=authenticated;
}
}
Ensuite on va créer le service qui va renvoyer le booléen : ng g s agent/loginGuard
. Dans son login-guard.service.ts :
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, Route } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root'
})
export class LoginGuardService implements CanActivate {
constructor(private _authService: AuthService, private _router: Router) {
}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
if (this._authService.isAuthenticated()) {
return true;
}
// navigate to login page
this._router.navigate(['/login']);
// you can save redirect url so after authing we can move them back to the page they requested
return false;
}
}
Puis, une page de login et son controleur ng g c agentLogin
:
<form (ngSubmit)="envoyerForm()" #monFormulaire="ngForm">
<p>Identifiant : <input name="login" [(ngModel)]="login" required></p>
<p>Mot de passe : <input name="password" type="password" [(ngModel)]="password" required></p>
<p><button type="submit">Connexion</button></p>
</form>
import { Component, OnInit } from '@angular/core';
import {Router} from "@angular/router"
import { AuthService } from '../auth.service';
@Component({
selector: 'app-login',
templateUrl: './agent-login.component.html',
styleUrls: ['./agent-login.component.css']
})
export class AgentLoginComponent implements OnInit {
private messageDErreur: string;
private login: string;
private password: string;
constructor(private authService:AuthService, private router: Router) { }
ngOnInit() {
}
envoyerForm(){
if(this.login=="agent" && this.password=="password"){
console.log("Redirection vers la page de l'agent...");
this.authService.setAuthenticated(true);
this.router.navigate(['/agent/login'])
}else{
console.log("Mauvais login ou mot de passe !");
}
}
}
Enfin, dans les routes, il suffit de mettre notre service :
{ path: 'login', component: AgentLoginComponent},
{ path: 'customers', component: AgentCustomersManagComponent, canActivate: [LoginGuardService]},
{ path: '**', component: AgentLoginComponent, canActivate: [LoginGuardService]},
Debug
Se fait dans chrome, F12, debug. Dossier webpack/./src/.
Node.js/npm
On en aura besoin pour utiliser nos outils Angular.
Le gestionnaire de paquets de Node est npm. Il existe aussi Yarn, une alternative. Les 2 travaillent sur le fichier package.json.
Pour installer sous Ubuntu : sudo apt-get install nodejs npm
puis sudo npm i -g n
, enfin n latest
pour se mettre sur la dernière version.
Font Awesome
La librarie fortawesome permet d'importer une seule icône de la librairie font awesome : https://www.npmjs.com/package/@fortawesome/angular-fontawesome
ReactiveX
Reactive programming.
Asynchrone : on lance une tâche qui va aboutir un jour. On ne l'attend même pas. Par exemple onreadystatechange pour ajax, ou le mousemove... on demande à être notifié quand un évènement arrivera.
Synchrone : on appelle une fonction et on ATTEND son retour, c'est bloquant.
Promesses : pour éviter le "callback hell".
ReactiveX : permet de regarder un flux d'évènement. ReactiveX c'est le pattern Observer. Par exemple un élément "photo_profil" observe si l'user est connecté et affichera son avatar si c'est le cas.
Le type principal de RxJS est Observable (qui va observer des flux), avec les type Observers, Subject... qui gravitent autours.
Observable : représente la source d'une collection de future valeurs ou d'évènement. C'est un flux qui émet des valeurs (exemple : pourcentage de quelque chose mis à jour chaque seconde). Il peut aussi émettre une fin de flux/une erreur.
Single : comme un observable mais avec une seule valeur/évènement émis(e) (exemple, retour de l'id d'une personne d'un webservice).
Completable : émet uniquement une info indiquant qu'il est terminé ou en erreur (par exemple, lancement d'un batch).
On peut utiliser Observable à la place d'un Single et d'un Completable mais c'est un peu démesuré.
import { Observable } from 'rxjs'; //Observable est une classe
const source=Observable.create(observer => { //on créé une source d'évènement
observer.next(33); //lorsqu'il va s'inscrire, l'observeur va recevoir "33". ici pas très intéressant avec "33" mais une source d'évènement pourrait être du du REST par exemple.
});
const subscription=source.subscribe(val=>{
console.log("Observer1 :"+val);
});
Une source d'véènement sur lesquels on va greffer des opérateur qui vont modifier ou filtrer les evénements qui arrivent et donner (si applicable) les éléments filtrés à l'abonné.
On peut chainer avec pipe :
const inputObservable = Observable.of(1,2,6,4,12,3); //fonction statique de la classe Observable, émet une séquence d'évènements, il existe par exemple Observable.interval (1000), Observable.fromEvent, Observable.concat.... of sert souvent en tests unitaires pour injecter un faux flux de données. Ici on envoie n° par n° mais on reçoit souvent des tableaux.
observable.pipe(
//par exemple notre valeur est A
map(x=>x*5), //si c'est 1, je le multiplie par 5
filter(x=>x<45) //puis ce résultat je vérifie s'il est inférieur à 45
scan((acc,x)=>acc+x), //on l'aditioonne avec la valeur accumulé
tap(x=>console.log('TAP:',x)),
bufferCount(5)//une fois qu'on en a 5 on renvoie à l'observer, si on l'élève elles seront retournées à chaque fois
Les différentes opérations sur les sources sont représentées par des diagrammes :
Modules
Un ensemble de fonctions dans un fichier. On va importer ces fonctions dans notre appli.
Exemple : on a un module pour gérer les zips. On pourrait vouloir se servir d'une seule fonction, par exemple celle pour compresser.
Le point d'entré de notre appli est un module.
Là où on va utiliser le module : import { sayHello } from './say';
, s'il ne trouve pas say.js, il va chercher un répertoire "say" et lancer son "index.js" (on peut nous même y importer plein de choses, cette technique est le "barrel" ou "passe-plat")
Dans le module : export function sayHello(){ console.log("Hello!");
On peut aussi exporter des classes, des const, des interfaces...
Si on fait import { Observable } from 'rxjs';
, sans ., ça signifie que c'est le module RxJS dans le dossier des node_modules(et pas à la racine de notre projet).
Webpack
Bundler de modules : rassemble les modules js pour diminuer le nombre de fichiers (parfois rassemblés en un seul fichier). Si on build pour publier le site, webpack met les fichiers dans un dossier. Si on build juste pour tester, webpack ne génère pas réellement les fichiers mais les gardes juste en ram, ce qui est beaucoup plus rapide.
Il est capable de générer juste les modifications.
Angular js
Module
Utile pour isoler différentes parties de l'application. Est un conteneur pour les contrôleurs.
Un contrôleur fera forcément partie d'un et un seul module :
var appli = angular.module("maBelleAppli", []);
appli.controller("monBeauController", function($scope) { //"controller" est une fonction de module
$scope.maBelleVariableDeScope = "valeurDeLaVariable"; //l'objet scope fait le lien entre la vue et le contrôleur
});
La vue (cela affichera "valeurDeLaVariable"):
<div ng-app="maBelleAppli" ng-controller="monBeauController">
{{ maBelleVariableDeScope }}
</div>
Divers
Same-Origin Policy / CORS
La "Same-Origin Policy" est un mécnaisme mis en place dans les navigateurs pour éviter le cas suivant :
-on se logue sur sa banque, identifié par un cookie ;
-on va sur un www.sitedechatonsmignon.com, qui effectue en arrière plan un appel AJAX vers une page de sa banque pour transférer de l'argent ;
Les navigateurs empêchent ça, mais en mettant en place CORS côté serveur, on peut l'autoriser. La banque peut ainsi vouloir autoriser certaines requêtes venant de www.sitedechatonsmignon.com, par exemple l'appel un une api pour faire un don.
En Spring, ça se configure avec une annotation : @CrossOrigin(origins="http://localhost:4200", maxAge=3600)
Promesses
Une mécanique mise en place pour éviter d'enchainer des callbacks.
Une promesse a 2 attributs internes : state (pending/fullfilled/rejected) et result (undefined si pending, une valeur si fullfilled ou rejected).
Exemple ci-dessous on teste que la condition est vraie et on appelle la fonction (interne, déjà définie) resolve si oui ou reject (fonction interne à js également) si non. Selon que resolve ou reject ait été appelé, ça sera la première ou la 2e fonction qui sera appellée dans le .then qui suit.
let promesse = new Promise((resolve, reject) => {
if (true) { //changer ici par false pour voir un rejet
resolve("Valeur de retour en cas de resolution");
} else {
reject("Valeur de retour en cas de rejet");
}
});
promesse.then(
(valeurDeRetour) => alert("La promesse a été résolue avec comme valeur de retour " + valeurDeRetour), //fonction en cas de resolve
(valeurDeRetour) => alert("La promesse a été rejetée avec comme valeur de retour " + valeurDeRetour) //fonction en cas de rejet
);
On peut chainer les promesses :
let promesse = new Promise((resolve, reject) => {
if (true) { //changer ici par false pour voir un rejet
resolve("Valeur de retour en cas de resolution");
} else {
reject("Valeur de retour en cas de rejet");
}
});
promesse.then(
(valeurDeRetour) => {
alert("La promesse a été résolue avec comme valeur de retour " + valeurDeRetour);
return "Nouvelle valeur";
},
(valeurDeRetour) => {
alert("La promesse a été rejetée avec comme valeur de retour " + valeurDeRetour);
throw "Nouvelle erreur";
//comme on a un throw ici cela va déclencher la 2e fonction dans le then suivant, avec un return ça déclencherait la première
}
).then(
(nouvelleValeur) => alert("Chaîne réussie : " + nouvelleValeur),
(nouvelleErreur) => alert("Chaîne échouée : " + nouvelleErreur)
);
La version non chaînée peut se traduire comme ça avec async/await qui est un peu du sucre syntaxique pour les promesses :
async function simulateOperation() {
if (true) {
return "Valeur de retour en cas de réussite";
} else {
throw "Valeur de retour en cas d'échec";
}
}
async function main() { //pour pouvoir utiliser await il faut que ça soit dans une fonction async
try {
const result = await simulateOperation();
alert("L'opération a réussi avec comme valeur de retour : " + result);
} catch (error) {
alert("L'opération a échoué avec comme valeur de retour : " + error);
}
}
main();
alert("Sera exécuté en premier");