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.
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!
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",
[...]
]
}
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>
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>
(function(globalSpace){
"use strict";
// Votre code JavaScript
})(this.tgxGlobalSpace);
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.(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);
globalSpace["tgxControllers"]
.$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 "$".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.
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);
$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.globalSpace["tgxClasses"]
.propagateSuccess' par exemple pour l'étape "create":
self.propagateSuccess("created");`.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);
}
);
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);
globalSpace["tgxDataModels"]
.Les fonctions de bases appelées de façon automatiques sont:
Il est aussi possible d'ajouter des fonctions personnalisées, qui pourront être appelées à travers le service dataApi.
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"
}
}
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éfixepopup:
. Il peut aussi s'activer automatiquement au chargement de sa vue parente. _Un "route shortcut" popup : _`Raccourci popup test` -->