Démarrer avec Tango.js


LES BASES

TangoJS est un framework basé sur le DOM et JavaScript, de manière à pouvoir fonctionner sur tout support pouvant lire une page Web (y compris NodeJS à travers le module JsDOM). L'accent est mis sur le côté simple et durable, le coeur est programmé en JavaScript standard pour offrir la meilleure compatibilité avec les supports datant de quelques années, qu'ils soient ordinateur de bureau ou appareils mobiles.

Le framework s'inclut comme un fichier JavaScript dans une page web côté client et démarre de manière automatique, en chargeant les ressources JavaScript et CSS selon la configuration. Côté serveur, pour créer une instance TangoJS il suffit d'installer NodeJS et lancer une simple ligne de commande. Le coeur du système est le même côté client et serveur et propose globalement les mêmes outils. Un fichier de configuration en JSON définit les ressources à charger au démarrage et les propriétés propres à l'instance.

Client : <script type="text/javascript" src="./includes/tango.js/src/classes/generic/tgxcore.js"></script>
Serveur : node "tango_folder/server/server.js"

Il n'impose pas de nouvelle syntaxe de language, tout développeur qui est familier avec la manipulation du DOM et les événements avec JavaScript peut utiliser TangoJS avec le minimum d'apprentissage. Les balises HTML et leurs attributs sont ceux du standard HTML, produisant du code valide sans besoin de pré-traitement ou de compilation préalable.
Ainsi les composants Tgx sont identifiés l'attribut "name" et des paramètres peuvent être passés par attributs data, par exemple "data-tgx-object" pour le type de composant. La CSS peut bien sûr être utilisée pour la présentation. Le système se veut le moins intrusif possible (il ne change pas les prototypes et le fonctionnement interne de JavaScript, ni le celui du DOM) pour pouvoir facilement être utilisé avec d'autres librairies.

Composant vue Tgx : <div name="myView" data-tgx-object="tgxView" style="width: 100%; height: 100%;"></div>

Les vues tgxView sont un élément central du framework. Elles servent de structure hiérarchique (conteneur parent pour d'autres composants Tgx) et d'éléments d'interface (associées à un élément du DOM). Les vues sont chargées avec le contenu de fichiers HTML à la volée, ce qui facilite la structuration et la réutilisation des modules.

Chaque composant Tgx est associé à un élément du DOM, qu'il soit affiché ou non, qui peut être utilisé pour stocker des propriétés ou être lié à des évènements. Pour des raisons historiques, notamment de rétro-compatibilité, le framework est basé sur jQuery et supporte donc nativement le syntaxe basée sur '$' comme '$("#myDiv").on("click", ...)', etc. Si vous avez déjà des routines écrites en jQuery vous pourrez les utiliser sans avoir à les modifier. La réflexion pour supporter d'autres outils est toutefois ouverte pour l'avenir.

La conception tente de s'affranchir au maximum d'une architectures de programmation imposée: on pourrait le voir comme un modèle MVC (Modèle-Vue-Contrôleur) dans lequel ni le Modèle ni le Contrôleur ne sont vraiment obligatoires (ils peuvent aussi ne pas être uniques, une vue peut par exemple avoir 2 contrôleurs si il y a un intérêt pour la modularité du code). Les composants sont accessibles entre eux et offrent une totale liberté d'interaction, permettant la mise en place de nombreuses architectures différentes selon le besoin.

Application Tango minimale "Hello World!" :

index.html :

<!DOCTYPE html> 
<html>

<head>
<!-- BALISES META etc... -->

<script type="text/javascript" src="./includes/jquery/jquery-3.1.1.min.js"></script>
<script type="text/javascript" src="./includes/tango.js/src/classes/generic/tgxcore.js"></script>
<style>
html, body, body > div { height: 100%; width: 100%; margin: 0; padding: 0; }
</style>
</head>

<body>
<div name="appView" data-tgx-object="tgxView" data-tgx-url="{{appDir}}/app.html"></div>
</body>

</html>

app/app.html :

Hello World!
  • Une vue sert de conteneur à toute l'application:
  • L'attribut data-tgx-url des vues précise le chemin du fichier à charger dans la vue. Le contenu sera injecté dans la div comme contenu HTML. L'utilisation d'URL pour les contenus de vue permet de structurer vos modules par dossiers et sous-dossiers.
  • Un système de placeholders délimités par {{}} permet de rendre certains éléments d'une vue paramétrables pour la réutilisation d'un module dans différents contextes. Dans l'exemple le placeholder "{{appDir}}" est utilisé pour charger un fichier de contenu différent selon la valeur. La valeur des placeholders peut être définie dans la config ou dans le code lui-même.
  • Pour des raisons de performance et de robustesse les placeholders sont remplacés lors du chargement de la vue uniquement, il n'y a pas de cycles de mise à jour tournant en arrière plan. Une fois la vue chargée, seul votre code est actif, nous n'avons pas implémenté de double bindings car à vrai dire, nous n'en avons jamais vraiment eu besoin; les bindings se font par l'interaction des composants Tgx.
  • Les styles sont laissés à la liberté de l'utilisateur, ici nous avons le minimum pour une application responsive, mais on peut utiliser des librairies CSS en les ajoutant dans le fichier config (bootstrap est inclus par défaut).


LA CONFIG DU NOYAU - Les fichiers "app.tgxconfig.json" et "server.tgxconfig.json"

Les fichiers de configuration sont des fichiers JSON qui définissent des options propres au fonctionnement du framework, des placeholders par défaut pour les chemins de ressources pour permettre d'organiser l'application avec une autre hiérarchie de dossiers. Ils précisent aussi les fichiers JavaScript et CSS à charger et les règles de CSP (Content Security Policy) pour la page. Le dernier point est important car un script d'installation du framework permet d'installer plusieurs instances sur un serveur, dans des dossiers différents; le script d'installation injecte automatiquement le nom du dossier dans les CSP pour que tout corresponde.

Les ressources sont chargées de manière ordonnée pour que chaque fichier puisse se baser sur le précédent (comme l'héritage depuis Tgx). Elles sont aussi séparées entre "preDependencies" et "dependencies", à savoir les ressources nécessaires au démarrage du noyau et les autres. Les fichiers de config précisent aussi à quel serveurs se connecter, en précisant à quel dossier et port se connecter. On peut définir plusieurs serveurs par un nom qui pourra être utilisé dans le code, et préciser s'ils sont ou non fournisseurs de services. On peut donc travailler localement pour développer et passer en production facilement.

La config serveur est similaire à celle client avec un propriété "serverConfig" (sans le "s") avec les options de configuraion du serveur.

Version simplifiée de app.tgxconfig.json :

{ 
"generalConfig": {
"tgx_version": "1.0.1",
"defaultApplication": "",
"includes_dir": "./includes/",
"core_dir": "./includes/tango.js/src/classes/",
"modules_dir": "./modules/",
"services_dir": "./services/",
"dataModels_dir": "./dataModels/",
"tango_css_dir": "./includes/tango.js/css/",
"active_developper_mode": true,
[...]
},
"serversConfig": {
"Tango-dev.fr": {
"type": "nodejs-server",
"protocol": "http",
"host": "localhost",
"path": "tango-dev",
"folder": "server/",
"port": "9560",
"http_port": "80",

"service_provider_capability": true,
"service_provider_auto_connect": true,
"service_provider_persistent_connection": false,
},
[...]
},
"debugLevel": {
"db": 1,
[...]
},
"contentSecurityPolicies": [
{ "origin" : "'self'", "policies" : "frame-src default-src style-src media-src connect-src font-src img-src" },
{ "origin" : "'unsafe-inline'", "policies" : "default-src style-src" },
{ "origin" : "'unsafe-eval'", "policies" : "default-src" },
{ "origin" : "http*://localhost:*", "policies" : "frame-src default-src connect-src" },
{ "origin" : "ws://localhost:*", "policies" : "frame-src default-src connect-src" },
[...]
],
"classes": [
"{{tgxCoreDir}}ui/tgx.js",
"{{tgxCoreDir}}ui/tgxbutton.js",
[...]
],
"preDependencies": [
"{{tgxTangoCSSDir}}tango.css",
[...]
],
"dependencies": [
"{{tgxIncludesDir}}fullscreen.js/screenfull.min.js",
"{{tgxIncludesDir}}socket.io/socket.io.js",
[...]
]
}


LE HTML - Les vues "tgxView"

Vue de structuration d'interface :

<div name="personForm" data-tgx-object="tgxForm" data-tgx-orientation="horizontal" data-tgx-splitter="true" data-tgx-size1="50%" data-tgx-collapse-responsive-size="500" class="full"> 
<div name="gridView" data-tgx-object="tgxView" data-tgx-url="{{tgxModulesDir}}res/res_person/lists/view.grid.html" class="full"></div>
<div name="tabsView" data-tgx-object="tgxView" data-tgx-url="{{tgxModulesDir}}res/res_person/forms/view.html" class="full"></div>
</div>
  • Le contenu d'une vue peut être simplement un élément de structure d'interface voire un conteneur non visible. ici un formulaire "tgxForm" sépare la vue en 2 sous-vues séparées par un "splitter" (élément de séparation vertical ou horizontal qu'on peut repositionner). Ça permet de séparer la structure du contenu et de ré-utiliser facilement un module dans plusieurs présentations différentes.
  • Le fichier est chargé par le framework et exécuté dans une sandbox avant injection dans le DOM (une fois débarassé d'éventuelles balises JavaScript). Les composants Tgx vont alors démarrer automatiquement et passer par différentes étapes de chargement.

Vue d'interface :

<div name="dataset" data-tgx-object="tgxClientDataSet" data-tgx-database="db_work" data-tgx-datamodel="mod_formation"></div> 
<div name="grid" data-tgx-object="tgxGrid" class="full"></div>
<div name="controller" data-tgx-object="tgxController" data-tgx-controller="modFormationBounded" data-tgx-dependencies="{{tgxModulesDir}}mod/mod_formation/lists/bounded.controller.js"></div>

<script type="text/javascript">

{{tgxParams}}["grid"] = {
columns: [
{ datafield: "_ref_mod_formation", width: "50%", cellsrenderer: function(row, columnfield, value, defaulthtml, columnproperties){
return '<span style="color: red;">' + value + '</span>';
} },
{ datafield: "_ref_mod_formation_level", width: "35%" },
{ datafield: "date_getting", width: "15%" },
],
options: {
editable: true,
editmode: "dblclick",
},
};

</script>
  • 3 composants Tgx sont ici présents (un dataset "tgxClientDataSet", une grille "tgxGrid" et un contrôleur "tgxController"). La plupart des objets reçoivent les paramètres par des attributs data, comme l'url du fichier à charger pour le contrôleur, mais qu'il y a possibilité d'ajouter des paramètres par JavaScript à travers le placeholder {{tgxParams}}. Cela permet par exemple de définir des fonctions, tableaux ou objets comme paramètres, sans avoir besoin de nouvelles syntaxes de language incluses dans le HTML. Cette solution a l'avantage de pouvoir rassembler toutes les informations pertinentes à la présentation dans le même fichier sans avoir à faire des allers-retours, ici par exemple pour définir quelles colonnes sont affichées dans la grille et une fonction custom de rendu de cellule.
  • Le couplage MVC est ici très libre, on pourrait très bien ne pas avoir de contrôleur, en avoir 2, etc. Le fichier chargé par le contrôleur pourrait aussi si c'est utile contenir plusieurs fonctions de contrôleur, ici on précise qu'on veut utiliser la fonction "modFormationBounded". L'aspect Modèle du framework est porté par un composant spécial, le dataset. On peut avoir plusieurs datasets dans une vue, chacun lié à son modèle de données et à une base de données si on en utilise plusieurs.
  • Le dataset représente une liste de données chargées, dont on peut influencer le contenu par un domaine de données "dataDomain" passé au chargement (filtrage des données, ordre, etc). Les datasets peuvent communiquer avec d'autres composants via une API interne ("addrow", "updaterow", "deleterow", "onBeforeUpdate" etc), pour que la grille puisse récupérer automatiquement les données à afficher, lancer une sauvegarde quand on fait une modification dans la grille ou à l'inverse mettre à jour la grille quand le dataset change. Il est ainsi possible de réaliser des bindings en temps réel entre composants à travers une série de fonctions auquelles le développeur a accès. Contrairement aux frameworks où le système est interne et opaque, on a ici la possibilité de contrôler ces bindings, et même d'annuler l'opération.


LE JAVASCRIPT

Le tgxGlobalSpace :

(function(globalSpace){ 
"use strict";

// Votre code JavaScript

})(this.tgxGlobalSpace);
  • Le "tgxGlobalSpace" est lié au sandboxing du code. C'est un objet passé à la sandbox qui permet d'accéder à des variables d'environnement comme: if (globalSpace.is_nodejs) et d'exporter le résultat du code. C'est généralement une bonne pratique d'encapsuler le code dans une fonction invoquée immédiatement (IIFE) afin de s'assurer qu'il n'y ait pas de collision de variables dans le globalSpace. Nous recommendons aussi l'utilisation de "use strict"; dans les fichiers JavaScript contrôleurs / composants / modèles de données.
  • Les codes présentés utilisent le mot-clé "var", c'est un choix de notre part pour une meilleure rétro-compatibilité, mais rien n'empêche l'utilisation de "let" et "const" si cette considération est moins importante pour vous.

Les contôleurs "tgxController" :

(function(globalSpace){ 
"use strict";

globalSpace["tgxControllers"].modFormationBounded = function(context, tools)
{
var self = context;
var $tgx = tools.$tgx, $ = tools.$;

self.start = function()
{
self.params = {};
self.params["dataset"] = self.$tgx("dataset");
self.params["grid"] = self.$tgx("grid");

self.params["dataset"].onBeforeAdd = function(rowdata)
{
rowdata["title"] = "TEST";
};

self.propagateSuccess("started");
};

self.load = function(params)
{
var dataDomain = $tgx.getDataDomain();
dataDomain.filters.push(
$tgx.createFilter({ operator: "and", field: "title", condition: "=", value: params.value })
);

self.params["dataset"].getData( function(){

self.params["grid"].show({ clientDataSet: self.params["dataset"] });

}, { dataDomain: dataDomain });

};

};


})(this.tgxGlobalSpace);
  • Un contrôleur est simplement une fonction constructeur, à laquelle sont passés la nouvelle instance d'objet "tgxController" et les outils du framework. Dans cette fonction on peut ajouter, voire surcharger des fonctions du contrôleur, des propriétés etc.
  • L'export du contrôler se fait dans l'objet globalSpace["tgxControllers"].
  • L'outil de base du framework est $tgx, une fonction qui permet de rechercher un composant par nom ou id dans la hiérarchie absolue de l'application $tgx("/appView/controller") ou de manière relative self.$tgx('dataset') qui récupère un objet dans la vue courante. Il contient aussi une panoplie de fonctions, comme des opérations sur les dates et leur format, de gestion de processus asynchrones, le log $tgx.log("Hello World!"); et beaucoup d'autres. jQuery est aussi proposé dans les outils dans la variable "$".
  • Nous verrons dans le point suivant les étapes de chargement des composants Tgx et la fonction "propagateSuccess", ici le contrôleur récupère au "start" certains objets Tgx de la vue pour pouvoir intéragir avec. Il n'y a pas de restriction inhérente au framework dans l'interaction entre les composants, un contrôleur peut supprimer des propriétés, des fonctions, pratiquement tout ce que JavaScript peut faire est possible, sauf toucher au prototype d'un objet.
  • Ce contrôleur ajoute une fonction "onBeforeAdd" au dataset, qui sera automatiquement appelée chaque fois qu'on ajoutera une entrée au dataset, et y fixe une valeur arbitraire. On ajoute aussi une fonction "load" qui pourra être appelée depuis un autre composant. Cette fonction crée un "dataDomain" comme vu plus haut, y ajoute un filtre sur les données, puis lance le chargement des données et les utilise pour lance le rendu de la grille.
  • Ce contrôleur avec le code HTML de l'exemple de vue d'interface suffisent pour afficher une grille qui se chargera à l'appel de la fonction "load" du contrôleur avec des données filtrées, qui sauvegardera automatiquement les données à la modification de la grille et qui ajoutera un champ dans chaque nouveau rang créé.

TangoJS impose le minimum possible de paradigme de développement, dans le contrôleur donné en exemple, la fonction "load" a été ajoutée comme une fonction d'interface, pouvant être appelée directement depuis un autre composant avec la syntaxe :
$tgx("/appView/controller").load({ value: "TEST" });.

Mais on pourrait avoir le même code basé sur un évènement :

 self.start = function() 
{
// [...]

self.jQelement.on("load", function(e, params){
var dataDomain = $tgx.getDataDomain();
dataDomain.filters.push(
$tgx.createFilter({ operator: 'and', field: 'idc', condition: '=', value: params.value })
);

self.params['dataset'].getData( function(){

self.params['grid'].show({ clientDataSet: self.params['dataset'] });

}, { dataDomain: dataDomain });
});

// [...]
};

Et être appelé depuis un autre composant avec le code:
$tgx("/appView/controller").jQelement.trigger("load", [ { value: "TEST" } ]);

A noter la propriété jQelement présente dans tous les composants Tgx, qui contient l'objet jQuery de l'élément du DOM. D'autres propriété sont ajoutées automatiquement, comme les paramètres récupérés depuis le placeholder "{{tgxParams}}" de la vue. Il est aussi possible de passer des paramètres aux composants par URL.

Les composants Tgx :

Exemple minimal d'un composant :

(function(globalSpace){ 
"use strict";

var $tgx = globalSpace.$tgx, Tgx = globalSpace["tgxClasses"].Tgx;

"tgx-compile_START";

function tgxButton(params, jqObj)
{
var self = this;
self.init((params) ? params : {}, jqObj);

self.isToggleButton = false;

self.jQelement.on("click", function(e){
$tgx.log("Thou clicked: ", self.element_name, self.jQelement);
});

self.propagateSuccess("created");
}

$tgx.extend(tgxButton, Tgx);

tgxButton.prototype.prepare = function()
{
var self = this;

self.jQelement.show();

self.propagateSuccess("prepared");
};

tgxButton.prototype.destroy = function()
{
var self = this;

self.jQelement.off("click");

self.propagateSuccess("destroyed");
};

globalSpace["tgxClasses"].tgxButton = tgxButton;

"tgx-compile_END";

})(this.tgxGlobalSpace);
  • Le framework implémente un système d'héritage avec fonctions constructeurs avec la fonction $tgx.extend(tgxButton, Tgx);. Chaque composant hérite de la classe Tgx qui implémente les fonctions basiques du framework, dont la liaison avec l'élément du DOM. Il est donc possible de créer des composants génériques qui pourront servir de parent à leur tour pour un héritage, par exemple "tgxMenuBase" qui hérite de "Tgx" et apporte les fonctionnalités de chargement de menus, et "tgxMenu" et "tgxNavigationBar" qui héritent de "tgxMenuBase" et diffèrent par le rendu visuel.Il a été choisi d'utiliser fonctionnement "natif" par prototype de Javascript, avec héritage du conbstructeur. Il est donc possible d'ajouter des fonctions au prototype des futurs composants, sur l'objet d'instance ou statiques sur la classe elle-même selon les besoins du développeur.
  • L'export du composant se fait dans l'objet globalSpace["tgxClasses"].
  • Les composants démarrent automatiquement quand ils sont chargés dans une tgxView, en lançant une série de fonctions constructeurs suivant des étapes successives paramétrables (par défaut "create/prepare/start...destroy"). Les fonctions pour chaque étape est ajoutée par le framework au composant, pour pouvoir y définir des opérations depuis sa construction à sa destruction. Pour une utilisation facile avec les fonctions asynchrones, c'est le rôle du composant de prévenir qu'il a fini son étape via la fonction propagateSuccess' par exemple pour l'étape "create":self.propagateSuccess("created");`.
  • De manière optionnelle, on peut ajouter des "balises" pour le compilateur ("tgx-compile_START"; / "tgx-compile_END";). Le compilateur permet d'injecter les composants dont on a besoin (définis dans le fichier de config) et de les minifier de manière automatique. Ces "balises" permettent de n'injecter qu'une partie du code dans la version compilée.
  • Il faut garder à l'esprit que les composants sont fait pour être chargés par le coeur de TangoJS, localement. Le globalSpace ici n'est pas le même que celui des contrôleurs (qui sont dans une sandbox), il contient directement les outils "$tgx" et des objets système comme les classes de composants.
  • Une série de composants est proposée par le framework, mais Il est facile de créer de nouveaux composants, il suffit ensuite de les ajouter dans la liste des ressources à charger dans le fichier de config et ils sont directement chargés par le coeur.


LES SERVICES

  • Les services sont avant tout définis par une API simple qui permet d'appeler des fonctions avec des paramètres et de récupérer un résultat de manière uniforme.
  • En pratique un service est simplement une vue + contrôleur qui sera exécutée dans une vue spéciale (non visible). On peut ensuite demander d'appeler des fonctions de ce contrôleur par l'API, localement ou à distance par la même syntaxe, ce qui permet de choisir si le service est exécuté sur le client ou le serveur de manière totalement interchangeable. Ça permet aussi à un serveur de faire exécuter le service par un autre serveur, et peut rendre possible par exemple le SSR (Server-Side Rendering), puisqu'un DOM existe sur une instance serveur.
  • Entres instances distantes la communication se fait par websockets et JSON, il est donc possible de communiquer en temps réel avec un service persistant (qui ne se détruit pas après exécution de la tâche) et d'utiliser différents niveaux de complexité dans les paramètres / réponses.
  • En plus de pouvoir se répartir les services, les instances NodeJS ont aussi la possibilité de créer des workers pour les tâches lourdes pour éviter de surcharger le thread principal.

Le service dataApi :

Un service spécial, le dataApi gère à la fois les bases de données et l'application des Modèles. Il offre une syntaxe simplifiée pour ajouter, mettre à jour, copier, supprimer et appeler des fonctions personnalisées, sur plusieurs bases de données différentes si besoin. Il permet aussi certains modes de "broadcasting" comme de prévenir les datasets d'autres instances client connectées que des données ont été mises à jour pour les recharger.
La fonction d'appel est asynchrone, et reçoit un objet de paramètres, une fonction de callback de succès et une d'erreur. Un quatrième paramètre peut être ajouté pour passer des options propres à l'appel.

Exemple appel dataApi "GET" :

var dataDomain = $tgx.getDataDomain(); 
dataDomain.filters.push(
$tgx.createFilter({ operator: "and", field: "title", condition: "=", value: "TEST" })
);
dataDomain.specialFilters["query:codes"] = true;

$tgx.dataApi(
{
dataModel: { name: "mod_formation" },
action: "get",
database: { id: 1 },
dataDomain: dataDomain,
},
function(message) {
$tgx.log("OK: ", message.serviceResponse.result);
},
function(message) {
$tgx.error("ERROR: ", message.serviceResponse.message);
}
);
  • Le système de modèles de données est actuellement fait pour fonctionner avec SQL, mais il est possible d'implémenter d'autres systèmes de bases de données à l'avenir. Le composant base de données possède sa propre API interne qui est implémentée par des "traducteurs". Les traducteurs existants (MySQL/MariaDB par PHP, MySQL/MariaDB sous NodeJS, Sqlite pour NodeJS/Client) paramétrisent bien sûr les valeurs utilisées dans les requêtes.
  • Le dataDomain est un objet utilisé pour le filtrage des données, mais peut aussi servir pour passer des informations plus complexes dans les "specialFilters". C'est donc généralement le rôle du modèle de données de s'assurer que les données sont correctes.

Les Modèles de données :

Actuellement les modèles de données sont faits pour être utilisés soit avec le service dataApi (à priori serveur), soit avec des datasets (à priori client). On pourrait tout à fait coder un service ou des composants qui les utilise d'une autre manière qu'aujourd'hui, car il se base sur des fonctions automatiquement appelées en cas d'ajout, de suppression, de copie ou de mise à jour, ce qui est commun à la plupart des systèmes de stockage de données.
Les modèles de données ressemblent aux contrôleurs, à savoir des fonctions constructeur qui enrichissent une instance fournie en paramètre, dans laquelle sont déjà présentes des fonctions de base.

Exemple minimal d'un Modèle de données :

(function(globalSpace){ 

globalSpace["tgxDataModels"].mod_formation = function(context, tools)
{
var self = context;
var $tgx = tools.$tgx, $ = tools.$;


self.applyDatabaseModel("mod_formation");

self.sqlDefault = "SELECT mod_formation.*, mod_formation_type.title AS _mod_formation_type ";

self.sqlDefault += " {{AUTO_SQL:EXTRA_FIELDS}} FROM {{db}}mod_formation ";

self.sqlDefault += " LEFT JOIN mod_formation_type ON (mod_formation_type.id = mod_formation.mod_formation_type_id) ";

self.sqlDefault += " {{AUTO_SQL:EXTRA_JOIN}} ";
self.sqlDefault += " {{AUTO_SQL:EXTRA_WHERE}} ";
self.sqlDefault += " {{AUTO_SQL:EXTRA_GROUP_BY}} ";
self.sqlDefault += " {{AUTO_SQL:EXTRA_ORDER_BY}} ";
self.sqlDefault += " {{AUTO_SQL:EXTRA_LIMIT}} ";

self.orderDefault = "id ASC";


self.onPrepareQuery = function(queryType, dataDomain)
{
if ("select" === queryType)
{
var sql = self.sqlDefault;


//retourne seulement les codes
if (dataDomain.specialFilters["query:codes"]) {
sql = "SELECT mod_formation.code FROM {{db}}mod_formation {{AUTO_SQL:EXTRA_WHERE}} {{AUTO_SQL:EXTRA_ORDER_BY}} {{AUTO_SQL:EXTRA_LIMIT}}";
}


return sql;
}
};


self.onBeforeApplyDelete = function(databaseDomain, data, validationFunc)
{
validationFunc(false, { message: "Il est interdit de supprimer une formation!" });
};

self.customDeleteAll = function(params, validationFunc)
{
//fonction personnalisée

validationFunc(true, { message: "customDeleteAll OK" });
};

};

})(this.tgxGlobalSpace);
  • L'export du contrôler se fait dans l'objet globalSpace["tgxDataModels"].
  • La première commande utilisée la fonction applyDatabaseModel pour définir le nom de la table qui sera utilisée par le modèle.
  • Des propriétés par défaut peuvent être utilisées, comme sqlDefault pour la requête à exécuter ou orderDefault pour l'odre des résultats.
  • Un système de placeholders permet d'utiliser la requête pour plusieurs bases de données, d'ajouter des champs, des jointures, des groupages, des conditions etc.

Les fonctions de bases appelées de façon automatiques sont:

  • onPrepareQuery / onPrepareQueryAsync: permet de modifier la requête par défaut selon la présence de filtres, voire de retourner une requête complètement différente. La première est synchrone et la deuxième retourne le résultat par une fonction callback.
  • onAfterQuery: s'il y a besoin d'effectuer du traitement sur le résultat de la requête.
  • onBeforeApplyAdd / onAfterApplyAdd / onBeforeApplyUpdate / onAfterApplyUpdate / onBeforeApplyDelete / onAfterApplyDelete / onBeforeApplyCopy / onAfterApplyCopy: fonctions qui permettent d'implémenter des actions avant et après les ajouts, mises à jour, suppressions et copies, voire d'annuler l'opération pour les fonctions "before". Elles sont appelées par le service (généralement côté serveur)
  • onBeforeClientAdd / onAfterClientAdd / onBeforeClientUpdate / onAfterClientUpdate / onBeforeClientDelete / onAfterClientDelete: fonctions similaires appelées par les datasets (généralement côté client).

Il est aussi possible d'ajouter des fonctions personnalisées, qui pourront être appelées à travers le service dataApi.

Les définitions de champ :

Les types de champ de données et une définition en JSON sont soit enregistrées en base de données, soit fournis de manière statique par le modèle. Selon le type plusieurs options sont disponibles, depuis { nullable: true } pour un champ "date" qui peut être null, jusqu'à des définitions plus complexes permettant l'auto-complétion et d'ouvrir un assistant de sélection en précisant quelles données charger pour un champ de type "assistant". Le format JSON autorise beaucoup de liberté et l'implémentation de nouvelles propriétés. L'utilisation de placeholders permet aussi à la définition de s'adapter au contexte, comme le fait de récupérer des données qui dépendent de certaines valeurs du rang que l'on veut modifier.

Ce système permet aussi la création de champs "virtuels", qui n'ont pas d'existence physique en base de données. Ils sont généralement issus de jointures avec une autre table selon des champs de relation. Par exemple, une table stocke les ids d'une autre table pour enregistrer le type d'entrée, et une jointure récupère le titre du type pour affichage. On peut modifier le titre du type et il sera automatiquement à jour lors des récupérations de données suivantes, jusqu'ici rien d'inhabituel. Mais on peut ajouter ce champ titre comme champ virtuel, ce qui permet de lui attribuer une définition. À la modification du champ de titre du type, on pourra choisir parmi la liste des types applicables à cette entrée et le système saura quels champs d'ids mettre à jour automatiquement.

Exemple d'une définition de champ :

{ 
"autocomplete": true,
"initDisplayField": "_mod_formation_type",

"autocompleteProperties": {
"source": {
"dataModelName": "mod_formation_type",
"specialFilters: {
"filter:applicableFor": { "id": "{{dataset:id}}" }
},
"displayTemplate":"{{code}} - {{title}}",
"dataFieldsAssignement": { "mod_formation_type_id": "id", "_mod_formation_type": "title" }
},
"listTextOnly": true
},

"assistantProperties": {
"mode": "comboAuto"
}
}
  • La propriété "autocomplete active l'auto-complétion, et initDisplayField précise quel champ est utilisé pour l'affichage par défaut.
  • autocompleteProperties précise les options d'auto-complétion, comme listTextOnly qui, si activé, interdit la saisie manuelle (on ne peut que choisir dans la liste proposée).
  • assistantProperties quant à lui précise les propriétés de l'assistant de sélection, le mode comboAuto veut dire qu'on utilisera les mêmes caractéristiques pour l'assistant que pour l'auto-complétion
  • La source supporte de nombreux paramètres, cet exemple simple précise que les données à charger sont celles du modèle "mod_formation_type", qu'on utilisera un specialFilter "filter:applicableFor" dont la valeur sera un objet avec l'id du rang en cours d'édition. "displayTemplate" fournit un template d'affichage de la liste de sélection, ici le code, un tiret et le titre. Enfin "dataFieldsAssignement" précise quels champs mettre à jour, avec quels champs des données sélectionnées.


LE ROUTEUR ET LA NAVIGATION

Le concept de route est un peu différent dans TangoJS de ce qu'il est habituellement. Il est issu de la même volonté de laisser toute liberté d'interaction et de structure. Les étapes de la route ne s'appliquent pas à l'application de manière globale, mais directement aux vues. Les tgxRouteLink sont des liens cliquables qui définissent une vue cible, un nom d'état et l'URL à charger dans la vue.

Un "route link" : <a name="tgxRouteLinkTest" data-tgx-object="tgxRouteLink" data-tgx-state="test" data-tgx-target="/appView/moduleView" data-tgx-url="{{tgxAppModulesDir}}app/test/test.html" data-tgx-default="true">Lien test</a>

Le lien provoquera le chargement de la vue ciblée avec un nouveau contenu, mais ce n'est pas tout: s'il est défini comme lien par défaut, il s'activera automatiquement au démarrage de la vue qui le contient. Ça permet aux différentes vues de démarrer de manière organique, de manière indépendante si besoin. Si l'élément DOM du routeLink est un lien, il mettra aussi automatiquement sont attribut "href" avec la bonne URL et "ouvrir dans un nouvel onglet" marchera correctement.

Les intéractions d'états sont gérées par le routeur, le tgxRouteProvider. Il coordonne le chargement et applique aussi la route passée à l'URL au chargement de la page. Ainsi le lien ci-dessus serait activé avec l'URL index.html?route=test. La route est une succession d'états à activer dans l'ordre, ainsi index.html?route=test1/test2 activera "test1", puis quand tout est démarré activera "test2". Il est aussi possible pour un contrôleur d'enregistrer des états à charger pour une vue, ou d'influer sur l'état qui sera activé, mais la syntaxe est plus compliquée.

Un troisième élément, le tgxRouteShortcut, est un raccourci qui permet de réinitialiser la route globale de l'application, comme si elle était chargée depuis l'URL. A noter que ça ne recharge pas forcément tout, le routeur se contente d'essayer d'appliquer les états aux vues concernées, de telle sorte que la route test1/inutile/test2 mènera au même endroit que test1/test2 si l'état du milieu n'existe nulle part. Au cours de la navigation, le routeur met à jour l'URL dans la barre d'adresse et enregistre les routes dans l'historique, ainsi faire "Précédent" ré-appliquera simplement la route sauvée au lieu de recharger la page.

Un "route shortcut" : <a name="tgxRouteShortcutTest" data-tgx-object="tgxRouteShortcut" data-tgx-shortcut="test1/test2" data-tgx-default="true">Raccourci test</a>

Le routeur et les liens ne sont pas nécessaires au fonctionnement du framework. Nous n'en avons pas mis côté serveur pour le moment.

Il permet aussi d'ouvrir une fenêtre popup si le composant "tgxWindowProvider" existe. Dans ce cas c'est une URL qui est attendue au lieu d'une route, avec un préfixe popup:. Il peut aussi s'activer automatiquement au chargement de sa vue parente. _Un "route shortcut" popup : _`Raccourci popup test` -->