v9
Avant de commencer
Numéro de version
Cette v9 aura été en cours de développement pendant plusieurs mois (premier commit le 01/12/2017), les versions betas se sont succédées, les numéros de versions ont monté, monté, pour arriver à la release finale qui portera le numéro 9.6.0. Les versions 9.0 -> 9.2 n'auront été que des versions de transitions vers la version complète de la nouvelle API de formulaires. La première version utilisée dans un vrai projet aura été la 9.3, la 9.4 a apporté la refonte du typage des stores avec TS2.8, la 9.5 a été la première bêta publique utilisée dans 3 projets. Pour marquer le coup (et aussi parce que la 9.6 apporte vraiment quelque chose sur la 9.5), on passera donc directement de 8.7 à 9.6.
Pas de rétrocompatibilité
Autant être clair dès le début : il n'est pas prévu de faire monter de version les projets existants en v8. Les changements proposés sont trop importants pour les mitiger facilement, et les projets existants sont trop avancés pour justifier une telle mise à jour.
La maintenance de la v8 est assurément toujours maintenue, selon les besoins projets, et les features demandées par les projets (et les bugfixes) seront toujours développées en premier dessus (puis rebasées sur la v9). 90% des changements sont contenus dans le module entity
, ce qui veut dire que tout ce qui n'y a pas attrait est facilement reportable d'une version à l'autre.
De plus, à l'initiative des projets, si une quelconque feature présentée ici intéresse quelqu'un en v8, il est tout a fait envisageable de la backporter pour en faire bénéficier le plus de monde.
Documentation
La documentation pour cette nouvelle version est à jour dans les différents readme du repo. Cette release note à pour vocation de présenter les changements v8 -> v9, si vous n'avez jamais fait de v8 et moins vous pouvez sauter la suite et lire directement la doc.
Mise à jour des dépendances (majeures)
La v9 apporte :
- React 16.x
- MobX 4.x
- Typescript 2.8+
React 16 ne change rien (du moins pour l'instant) à Focus et la mise à jour et tout à fait transparente (à part les warnings de peer dependencies sur React Toolbox, mais en pratique il n'y a aucun problème).
MobX 4 vient avec quelques changements d'API, qui ont changé quelques implémentations dans Focus mais qui devraient être transparentes dans les usages simples des projets.
Typescript 2.8 ne change rien mais apporte des nouvelles fonctionnalités de typages qui seront utilisées dans les stores.
Rappel des majeures précédentes
Pour rappel, les dernières majeures (enfin, pour lesquelles la dénomination "majeure" avait du sens) étaient :
- (1.)5.0 : Refonte de la recherche
- (1.)6.0 : Refonte du routeur
- 7.0 : Migration vers React-Toolbox/PostCSS/Variables CSS
- 8.0 : Retrait définitif de MDL
On remarque que ces majeures ont touché à tous les composants clés de Focus à l'exception d'un seul : le formulaire. Et bien voilà, c'est l'heure d'y faire un tour :)
Présentation
Motivation
Précédemment, un formulaire était entièrement encapsulé dans une classe de composant abstraite (AutoForm
), initialisée à partir un noeud de store (créé par makeEntityStore()
) et une définition de services (chargement/sauvegarde). AutoForm
avait pour vocation d'être une solution "clé en main" immédiate pour construire un formulaire, complet avec de la validation automatique, sur le modèle des usages établis par les précédentes versions de Focus. Cette dernière motivation a été une raison jusqu'ici suffisante pour simplifier l'implémentation du formulaire du côté framework, en créant un couplage fort entre AutoForm
, this.entity
et this.fieldFor
.
Le problème, c'est que l'usage forcé d'AutoForm
bloque ou complexifie certains usages parfaitement valides (exemple : des sous-formulaires) et repose sur une API qui n'est pas forcément la plus limpide (héritage). L'usage forcé de this.fieldFor
restreint la validation automatique aux seuls champs posés par cette méthode, rendant inutilement complexe une validation impliquant une quelconque sous entité (ou sur un formulaire basé sur une liste, le problème est le même). Oui, ces usages n'étaient pas non plus couverts par les versions antérieures, mais ce sont des besoins valides et qui ont déjà été implémentés plusieurs fois au dessus d'AutoForm
dans des projets. Une bonne partie de la problématique avait déjà automatiquement résolue avec la possibilité de créer un this.entity
à partir de n'importe quelle structure de données, donc c'était réellement dommage de bloquer sur des problèmes à priori moins complexes (spoiler alert : en vrai c'était carrément plus dur).
Solution adoptée
Pour créer un formulaire, il faut maintenant suivre les étapes suivantes :
- Créer un store d'entité avec
makeEntityStore
(inchangé) - Créer, à priori dans un composant, le noeud de formulaire (
FormNode
) à partir d'un noeud d'EntityStore
:- Il correspond à
this.entity
, vous êtes encouragés à garder la même convention de nommage - C'est une copie du noeud de base, observable de la même façon, réinitialisée à chaque fois que le noeud de base change (inchangé)
- Il contient maintenant, ainsi que chacun de ses sous nœuds, une référence vers son nœud équivalent de base dans
sourceNode
et une méthodereset()
pour se réinitialiser dessus (nouveau, sauf pour la racine qui avait déjàreset()
) - Il contient le résultat de validation des différents champs contenus dans le
FormNode
(nouveau) :- Chaque
EntityField
est muni d'une propriétéerror
(la même qui était dans leField
avant) - Le
FormNode
, ses sous-nœuds, ainsi que chaqueEntityField
sont muni d'une propriétéform.isValid
(isValid
pourEntityField
), résultat de la validation des champs contenus dans ce nœud (ou juste du champ).
- Chaque
- Il contient l'état d'édition du
FormNode
et de tous ses constituants (nouveau). A un endroit donné, la valeur deisEdit
(node.form.isEdit
oufield.isEdit
) est l'intersection de :- La valeur du
isEdit
du parent (sauf la racine qui n'en est pas, naturellement) - La valeur du
isEdit
propre (modifiable) - Une éventuelle condition supplémentaire, sous forme de dérivation.
- Note : l'état principal est initialisé à
false
et tous les sous-états àtrue
, ce qui conserve le principe existant duisEdit
qui pilote toute l'édition.
- La valeur du
- Il correspond à
- Créer, à priori dans un composant, les actions du formulaire (
FormActions
), à partir d'un noeud issu d'unFormNode
(donc pas forcément le noeud en entier) et d'une définition de services :- C'est une extraction des méthodes d'
AutoForm
dans un objet séparé (rien de nouveau) - Il contient les méthodes suivantes :
load()
, muni du hookonFormLoaded()
save()
, muni du hookonFormSaved()
toggleEdit()
, muni du hookonToggleEdit()
clean()
, équivalent de ce qu'il y avait danscomponentWillUnmount()
- Il expose les propriétés suivantes :
panelProps
, les props à passer à unPanel
(inchangé)formProps
, les props à passer à unForm
(voir ci-dessous) (nouveau)
- C'est une extraction des méthodes d'
- Poser dans le rendu d'un composant le composant
Form
(nouveau)- Il pose le formulaire HTML (si
hasForm = true
, qui est le défaut) - S'il reçoit ses props depuis un
FormActions
, il appelera :load()
aucomponentWillMount()
save()
à la validation du formulaireclean()
aucomponentWillUnmount()
- Il s'agit une fois encore d'une extraction d'
AutoForm
, cette fois-ci de la partie "composant"
- Il pose le formulaire HTML (si
- (optionnel, inchangé) Poser un
Panel
et lui donneractions.panelProps
.
Remarque importante : L'ajout de la validation et de l'état d'édition ne concerne bien que le FormNode. Un nœud d'EntityStore
de base est totalement inchangé par cette nouvelle version. Par la suite, à chaque fois qu'on mentionne l'un ou l'autre sur un champ ou un nœud, ce qui est dit ne s'applique que s'il est question d'un FormNode
.
Nouveautés
node.replace
/ listNode.replaceNodes
Les nœuds simples (et l'EntityStore
lui-même) sont maintenant munis d'une méthode replace
qui remplace tout le contenu du store par l'intégralité du contenu qui lui est passé, contrairement à set
qui ne remplace que les propriétés renseignées. Cela correspond à peu près à faire clear()
puis set(data)
, à la différence près que c'est fait en une seule opération (ce qui marche beaucoup mieux dans un autorun
). Le fonctionnement de set
et de replace
est inversé : set
parcourt l'objet passé et met à jour en conséquence le nœud, alors que replace
parcourt le nœud et se met à jour avec ce qui lui a été passé (et si une propriété est vide, alors le champ est vidé).
Les nœuds de listes ont une méthode équivalente replaceNodes
, (replace
existe déjà pour l'array de StoreNode lui même), qui est en fait la même chose que ce qu'était listNode.setNodes
(anciennement set
, renommé pour l'homogénéité du nommage). Ce dernier a maintenant le même fonctionnement que set
sur un nœud simple, en mettant à jour les nœuds avec les propriétés passées plutôt que de remplacer. Si la liste passée à plus d'éléments que le noeud de liste, alors les éléments manquants seront créés.
Construction d'EntityStore
TS 2.8 apporte une fonctionnalité très puissante au système de types, lui permettant de décrire des transformations conditionnelles de types. Auparavant (depuis la 2.1 pour être précis), il était simplement possible de définir des transformations uniformes de types, comme Partial<T>
par exemple, qui transforme toutes les propriétés du type T en propriétés optionnelles, ou encore Readonly<T>
dont le nom est explicite. Dans Focus, on s'en servait pour transformer les objets de définitions de stores d'entités et de références en listes de ces mêmes objets. Avec les transformations conditionnelles, il est possible de distinguer les cas selon la propriété à transformer.
Il y a un cas d'usage tout trouvé dans Focus pour ça : les EntityStores. Il est donc maintenant possible de définir complètement un EntityStore à partir des objets d'entités seuls.
Cela implique que :
Le code généré par entité s'est beaucoup simplifié. Par exemple, on peut maintenant générer seulement ça :
import {EntityToType, StoreNode} from "../types";
import {DO_ID, DO_CODE} from "../domains";
import {Structure, StructureEntity} from "./structure";
export type Operation = EntityToType<typeof OperationEntity>;
export type OperationNode = StoreNode<typeof OperationEntity>;
export const OperationEntity = {
name: "operation",
fields: {
id: {
type: "field" as "field",
fieldType: 0,
domain: DO_ID,
isRequired: false,
name: "id",
label: "operation.id"
},
numero: {
type: "field" as "field",
fieldType: "",
domain: DO_CODE,
isRequired: true,
name: "numero",
label: "operation.numero"
},
structure: {
type: "object" as "object",
entity: StructureEntity
}
}
};
En particulier, on peut remarquer que StoreNode<typeof OperationEntity>
remplace intégralement toute la (longue) définition du OperationNode
et que le type résultant est exactement le même. Pareillement, EntityToType<typeof OperationEntity>
récupère le type. Il est à priori souhaitable de continuer à générer les 2 alias par praticité.
Un EntityStore peut se déclarer simplement à partir des entités
Par exemple :
const subStore = makeEntityStore({
structure: StructureEntity,
operationList: [OperationEntity]
});
return makeEntityStore({
operation: OperationEntity,
projetTest: ProjetEntity,
structureList: [StructureEntity],
subStore
});
(L'exemple montre que les sous stores sont toujours gérés (même si personne ne s'en sert), ce qui est en réalité beaucoup moins trivial qu'avant, surtout pour le typage.)
Fini donc le constructeur à 4 paramètres avec des assertions bizarres et des listes d'entités composées à n'en plus finir.
Toutes les méthodes set
/replace
sont typées
En particulier celles qui ne l'étaient pas avant :
- Le set/replace global du store
- Le set/replaceNodes d'une liste
L'implémentation interne du store est simplifiée
Ca n'impacte pas directement les utilisateurs, mais un buildNode
(anciennement buildEntityEntry
) peu toujours servir dans des cas à la marge et le fait qu'il ne prenne que l'entité en paramètre au lieu de 4 paramètres peut clairs (comme makeEntityStore
en fait) est un plus.
De plus, la plupart des types génériques ont été simplifiés, en particulier fieldFor
/Field
qui n'en a plus qu'un seul (l'EntityField
entier). Loin est maintenant l'époque ou il y en avait 7 :) De manière générale, certains types internes (parfois utilisés à l'extérieur) ont vu la complexité des génériques s'encapsuler dans le type contenu. Exemple : EntityField<T, Domain>
est devenu EntityField<FieldEntry>
avec FieldEntry
qui contient le type et le domaine.
L'usage des stores n'a pas été modifié
Transformation d'entité
La première grosse nouveauté (et probablement celle qui sera la plus impactante) est la transformation d'entité. C'est une fonctionnalité qui va permettre de modifier un noeud de store au moment de la création du FormNode
.
Elle se présente comme une nécessité si on veut dissocier la validation et l'état d'édition du composant de formulaire. La raison est simple : avec la validation dans le FormNode
, il n'est plus possible de faire fieldFor(field, {isRequired: true})
, et avec l'état d'édition dans le FormNode
, il n'est plus possible de faire selectFor(field, values, {isEdit: false})
...
Avant de se demander "comment on s'en sort" avec des contraintes pareilles, voyez-donc ce qui à été proposé pour mitiger la problème, et restez pour tous les nouvelles possibilités que la transformation d'entité va apporter :)
Renommages
$entity
(deEntityField
, celui de l'objet{$entity, value}
) à été renommé en$field
. Parce que le nom n'était pas le bon.translationKey
(deFieldEntry
) à été renommé enlabel
, pour aligner le nom de la propriété de surcharge sur la surchargée.ViewModel
(l'objet dethis.entity
) à été renommé enFormNode
(ce qui avait déjà été suggéré plus haut), etcreateViewModel
enmakeFormNode
.
patchField
, fromField
et makeField
La transformation d'entité repose sur ces 3 fonctions de base, à partir desquelles vous pourrez probablement exprimer tous vos besoins :
patchField(field, $field, isEdit?)
: Cette fonction modifie le champ existant pour remplacer des métadonnées (dont le domaine et ses constituants), ($field
), ainsi que l'état d'édition du champ (isEdit
).fromField(field, $field)
: Equivalent depatchField
, sauf qu'il crée une copie du champ au préalable pour ne pas le modifier.makeField(value, $field?, setter?, isEdit?)
: Crée un champ à partir d'une valeur. Ce champ peut être calculé, dans ce cas il est possible de fournir également un setter et un état d'édition.
Le paramètre $field
peut être soit un objet simple, contenant les propriétés à remplacer, soit une dérivation (ici une fonction sans paramètres) sur même objet. Ainsi, il est possible de facilement gérer tous les cas où les métadonnées d'un champ dépendent de l'état d'un autre (libellé, caractère obligatoire...).
Le paramètre isEdit
peut être soit un booléen, auquel cas il sera l'état initial du champ (dans un FormNode
), soit une dérivation qui agira comme une condition supplémentaire pour que le champ soit en édition. On rappelle que par défaut isEdit = true
, mais que isEdit === true
seulement si field.isEdit === true && node.isEdit === true
.
Attention : Puisqu'il est maintenant possible de spécifier tous ces paramètres avec ces fonctions, il n'est donc plus possible de le faire dans fieldFor
. On ne peut plus donc modifier le libellé, le isRequired
ou encore le InputComponent
depuis fieldFor
. Par exemple, fieldFor(field, {isRequired: true})
devient fieldFor(fromField(field, {isRequired: true}))
.
De plus, tous les signatures (qui marchaient moyen d'ailleurs) de fieldFor
qui acceptaient des primitives ont été retirées (ce qui, en plus du retrait des surcharges locales, à grandement simplifié l'API). Par exemple, fieldFor(value, {label: "yolo"})
devient fieldFor(makeField(value, {label: "yolo"}))
Remarques : fromField
est conçu pour être utilisé "à la volée", à l'extérieur d'un formulaire, tandis que patchField
est conçu pour être utilisé à la création d'un FormNode, il est très important de ne pas confondre les usages de ces deux méthodes qui se ressemblent.
makeFormNode(node, isEdit?, transform?)
Ci-dessus, la nouvelle signature d'une méthode que vous n'avez jusqu'à présent jamais utilisé et qui va dorénavant arborer tous vos nouveaux formulaires (les 2 derniers paramètres sont nouveaux).
isEdit
fonctionne comme patchField
ou patchNodeEdit
.
transform
est une fonction du node qui sera votre "terrain de jeu" pour appliquer vos transformations d'entité. C'est ici qu'il faut appeler patchField
et patchNodeEdit
, une autre nouvelle fonction qui ne sert qu'à préciser l'état d'édition (computed ou initial, comme pour un field) d'un sous-noeud. Cette fonction peut retourner un objet contenant de nouveaux champs créés par makeField
, qui seront ajoutés à l'entité telle qu'elle sera utilisable dans le formulaire.
Ci-dessous, un exemple mettant en scène la plupart des choses qu'on peut faire dans une transformation :
entity = makeFormNode(mainStore.structure, false, entity => {
// On change le domaine et le isRequired du champ.
patchField(entity.denominationSociale, () => ({
domain: DO_COMMENTAIRE,
isRequired: !!entity.capitalSocial.value
}));
// On ajoute un champ supplémentaire calculé.
return {
email: makeField(
() => entity.denominationSociale.value,
{
domain: DO_LIBELLE_100, // Pour le fun
label: "structure.email",
validator: {type: "email"}
},
email => entity.denominationSociale.value = email, // Setter.
() => entity.statutJuridiqueCode.value === "SARL") // Champ éditable que si sjuCode === "SARL"
};
});
Remarque : le champ ajouté email
sera utilisable exactement comme un champ classique, malgré le fait qu'il soit calculé !
Dans le cas d'un FormNode
à partir d'un nœud de liste (StoreListNode
), la fonction de transformation s'applique à chaque élément de la liste. De manière plus générale, une propriété $transform
a été ajoutée aux StoreListNode
, à laquelle on peut affecter une fonction de transformation comme celle présentée au dessus. Elle sera appelée à chaque ajout d'élément dans la liste, et à l'initialisation du FormNode
si la liste n'était pas vide. Cette propriété ne devrait à priori être affectée manuellement qu'au sein d'une fonction de transformation qui agirait sur un nœud englobant.
Note : l'utilisation de patchField
ou de $transform
n'est pas limitée au scope d'une fonction de transformation, il est également possible de les utiliser pour modifier directement un nœud d'EntityStore. Attention tout de même à ne pas faire n'importe quoi avec ça. De plus, les champs calculés ($field
ou value
) perdront leur définition dans la construction d'un FormNode
associé.
valueKey
et labelKey
dans le ReferenceStore
Cette évolution n'a pas vraiment de lien avec le reste, mais elle a été motivée par la simplification de l'API de fields (et de manière sournoise par une modification de modèle à cause de la validation).
Les clés nécessaires à l'utilisation d'une liste de référence se spécifient maintenant directement dans makeReferenceStore
. Chaque propriété correspondant à une liste de référence dans la fonction de création est remplacée par un objet dont le type est {type: T, labelKey?: string, valueKey?: string}
. Il est conseillé de générer directement depuis le modèle pour tous les listes de référence ces objets correspondants, pour garder une description simple, du style :
/* Généré */
const civilite = {type: {} as Civilite, labelKey: "libelle", valueKey: "code"};
const refStore = makeReferenceStore(refLoader, {civilite});
L'usage reste le même, sauf qu'il n'est maintenant plus possible de spécifier les labelKey
et autres dans les Field. S'il y a besoin de modifier les clés, la méthode proposée est d'utiliser la méthode makeReferenceList(list, {valueKey, labelKey})
mise à disposition. (En pratique, tout ce qu'on fait est ajouter deux propriétés $valueKey
et $labelKey
sur la liste).
Validation
Comme présenté en introduction, la validation est maintenant gérée dans le FormNode
. Au fond, elle n'était que l'intersection de champs calculés définits dans chaque Field, et puisqu'on empêche maintenant de surcharger la validation dans un Field, on peut déplacer ces champs calculés dans le FormNode
.
Note : si vous avez un peu suivi ce qui est raconté, vous aurez peut être l'impression que quelqu'un a essayé de vous embobiner avec ses transformations d'entités "nécessaires". Oui, c'est obligatoire pour la validation (et l'édition), mais pas pour le reste (composants, libellés...). C'est un choix délibéré, premièrement pour simplifier le framework (c'est toujours pénible de gérer des surcharges) et deuxièmement pour faire en sorte qu'il n'y ait plus plusieurs façons (valides) de faire la même chose. Tu veux juste surcharger un libellé ? Ecris quand même une transformation. La porte n'est pas fermée et on peut toujours négocier. J'imagine bien des développements futurs qui pourront s'appuyer sur cette contrainte, genre un certain explorateur de stores par exemple...
Nouvelles propriétés
Comme présenté, on a maintenant, uniquement dans un FormNode, les deux nouvelles propriétés suivantes :
EntityField#error
, qui est strictement équivalent à ce qu'étaitField#error
.EntityField#isValid
, qui est vaut!EntityField#error || !EntityField#isEdit
(ce qui veut dire qu'un champ qui n'est pas en édition est toujours valide)Store(List)Node#form.isValid
, qui contient le résultat de validation de tous les champs + les sous-nœuds du nœud en question. Naturellement, il vauttrue
si aucun champ n'a d'erreur. La validation prend en compte tous les champs présents, y compris ceux ajoutés et modifiés par une transformation d'entité (évidemment, pourquoi se serait-on donné autant de mal sinon ?!).
Validation continue
La validation étant maintenant portée par le store, les champs n'ont plus besoin d'être posés pour être validés. Attention donc aux champs non affichés qui pourrait bloquer la validation sans afficher d'erreur !
Les champs qui ne sont pas en édition sont maintenant exclus de la validation d'un nœud. Cela permet de mitiger les problèmes de "pourquoi je peux pas cliquer sur save???" et de rendre la validation plus "claire", en particulier le fait que l'on puisse activer ou désactiver des règles en changeant l'état d'édition du champ associé. Cela veut aussi dire qu'on délègue la responsabilité à l'utilisateur et son serveur de vérifier que les champs non-éditables ne vont pas avoir des effets indésirables sur la sauvegarde.
Si des champs non affichés, obligatoires mais non renseignés car le serveur s'en chargera bloquent la validation, une solution peut être de ne pas être cheap et de faire un bean dédié à la création peut être de rendre les champs non éditables ou non obligatoires. D'ailleurs, ce comportement impose de modifier le générateur de modèle pour rendre toutes les IDs (clés primaires, pas étrangères) non obligatoires, ce qui correspond plus à la réalité et permet de ne pas avoir ce problème systématiquement sur les IDs en création.
Personnaliser la validation
Pour changer les règles de validation, il suffit d'ajouter/modifier la fonction de transformation pour obtenir le résultat souhaité. Si la règle s'applique à un champ, on modifie sa règle, et si c'est une règle transverse, on ajoute un (ou plusieurs) champ(s) calculé(s) à l'entité pour leur associer des règles de validation supplémentaires. Comme rappelé au paragraphe précédent, ils seront inclus dans la validation de l'entité, qu'ils soit affichés ou non.
Affichage des erreurs
Les erreurs restent affichées par défaut, néanmoins :
- Tant qu'un champ est actif (= l'utilisateur est en train d'effectuer une saisie), l'erreur est masquée (pour ne pas perturber l'utilisateur qui est en train de saisir sa valeur).
- Tant que l'utilisateur n'a pas interagi avec un Field vide initialisé en édition (= cas standard du mode création), l'erreur est également masquée.
- Le précédent comportement peut être surchargé par un formulaire, qui possède une propriété
forceErrorDisplay
pour cet usage. Par défaut, elle est initialisée àfalse
et devienttrue
après le premier clic surSave
.
API des validateurs
De plus, l'API des validateurs à été un peu simplifiée :
- Les paramètres des différents validateurs se mettent à la racine de l'objet de validateur
- Le type
"regex"
n'a plus besoin d'être spécifié, il suffit d'écrire un validateur de la forme{regex: /regex/}
- Le validateur de type
"function"
a été remplacé (enfin, il existe toujours pour des questions de compatibilité v2/v4, genre y a un projet qui fait ça) par une fonction de validation qui retourne directement l'erreur (par exemplevalidator: data => data && isString(data) && data.length > 10 && "validation.tooLong"
). - Il n'est plus nécessaire de wrapper un validateur unique dans un array dans un domaine.
En lien avec l'édition dans un FormNode
Précisions
Tous les isEdit
possèdent un setter, qui permet de modifier l'état propre. Ainsi, il est tout à fait normal que isEdit === false
après avoir demandé isEdit = true
si une condition ou le parent s'y oppose. L'état propre a quand même bien été affecté (il est caché dans une propriété _isEdit
si besoin).
displayFor
et changements d'API
displayFor
displayFor
n'ayant plus vraiment de sens dans un monde ou l'état d'édition est dans le champ, il a donc été retiré.
Sans FormNode, fieldFor
est maintenant équivalent à ce qu'était displayFor
. A noter donc qu'il n'est maintenant plus possible d'afficher un champ de StoreNode
en édition (il faut un FormNode
). Ce qui est une très bonne chose, puisque cela évitera les confusions entre le store de base et l'état du formulaire (surtout en création).
En revanche, il est possible de créer un champ en édition avec makeField
, avec quelque chose comme makeField(() => this.value, {label: "Test"}, value => this.value = value, true)
. Cet usage de makeField
est très utile pour afficher des champs de saisies qui ne sont pas liés à un formulaire (comme un champ de recherche ou un filtre par exemple).
@autobind
n'est plus utilisé ni exporté par Focus
@autobind
n'est plus utilisé ni exporté par Focus. C'était pratique pendant que ça a duré mais ça posait à peu près autant de problèmes que ça n'en résolvait. Si vous voulez toujours en utiliser un décorateur, alors il faudrait plutôt se rabattre sur class-autobind-decorator qui est beaucoup mieux.
MobX expose @action.bound
, pour binder une méthode à une classe, et il se trouve que les actions (quelles soient décorées ou non) représentent la quasi-totalité des méthodes qui ont besoin d'être bindées. De plus, les lambdas sont toujours bindés automatiquement, et c'est ce qu'on utilise le plus lorsque l'on veut passer des fonctions entre objets/composants/autres fonctions (qui est le seul cas où on a besoin du binding).
En résumé, ce n'est pas bien compliqué. Si vous avez des méthodes de classes passées directement :
- Soit ce sont déjà des actions => il suffit d'ajouter
.bound
derrière - Soit ce sont des méthodes qui devraient être des actions =>
@action.bound
- Soit ce sont des petites méthodes d'une ligne => soit
@action.bound
pour être propre, soit on remplace par un lambda.
Attention quand même avec @action.bound
qui a une implémentation beaucoup plus simple et moins exhaustive qu'@autobind
, en particulier :
- Il est impossible de surcharger une méthode décorée avec
@action.bound
(ça fera une erreur). Si le besoin existe, il faut enlever le.bound
et rebinder à l'usage avec un lambda. - Il est impossible de mettre un
@classReaction
au-dessus d'un@action.bound
(d'ailleurs, MobX renvoie maintenant une belle erreur sur unautorun(action(())
).@classAutorun/Reaction
binde déjà correctement les méthodes à l'appel, et ils sont maintenant utilisables sur des méthodes d'instance (des lambdas), ce qui est la solution si la réaction est aussi passée en handler quelque part.
Exemple de formulaire
Au final, un formulaire ressemble maintenant à ça (l'exemple est un formulaire de création dans une popin, un cas très classique) :
import {fieldFor, Form, makeFormActions, makeFormNode, observer, Panel, React} from "focus4";
import {createEvenement} from "../../../../services";
import {mainStore} from "../../../../stores";
@observer
export class SuiviCreation extends React.Component<{close: () => void}, void> {
entity = makeFormNode(mainStore.evenement, () => ({}), true);
actions = makeFormActions(
this.entity,
{save: createEvenement},
{
clearBeforeInit: true,
onFormSaved: () => this.props.close(),
onToggleEdit: edit => !edit && this.props.close()
}
);
render() {
return (
<Form {...this.actions.formProps}>
<Panel hideOnScrollspy title="Ajouter un évènement" {...this.actions.panelProps}>
{fieldFor(this.entity.commentaire)}
{fieldFor(this.entity.date)}
</Panel>
</Form>
);
}
}
Breaking changes
Génération de modèle
- Il n'y a plus besoin de générer l'interface et l'interface du noeud. A la place, générer des alias avec
EntityToType
etStoreNode
. translationKey
a été renommé enlabel
(cela nécessite une modif du générateur de modèle). Il s'est toujours agit d'un libellé et avec le passage versfromField
il fallait que le nom soit conservé.- Il faut générer un champ
fieldType
en plus dans lesFieldEntry
qui porte le type de champ. - Dans
ObjectEntry
etListEntry
, qui faut mettre une référence vers l'Entity composée au lieu de son nom. - Les IDs clés primaires doivent être générées en tant que propriétés non obligatoires (il me semble bien que c'est le cas d'ailleurs sur le serveur, elles n'ont pas d'annotation
[Required]
). - Il faut générer un objet en plus pour les types de références (qu'ils soient statiques ou non), qui porte le nom de l'objet en camelCase, qui contient le type et les valueKey/labelKey du type de référence.
Node
StoreListNode#set
a été renommé enStoreListNode#setNodes
et ne remplace plus toute la liste par celle qui lui a été passée, la méthode réalise un "merge" à la place de la même façon queStoreNode#set
. La méthodereplaceNodes
remplace l'ancienne méthodeset
.
Champs
- Il n'est plus possible de surcharger des infos des métadonnées (et donc du domaine) directement dans un champ.
- Tout champ en dehors d'un formulaire ne peut plus être en édition.
fieldFor
n'a plus qu'une seule signature, comme déjà évoqué.displayFor
n'existe plusvalueKey
,labelKey
etvalues
ne sont plus disponibles dansFieldOptions
.stringFor
prend maintenant la liste de référence en second paramètre optionnel (il n'y a plus d'objet d'options).
Petits détails
$entity
a été renommé en$field
(sauf pour lesStoreListNode
où c'est bien toujours$entity
, en même temps les deux propriétés avaient le même nom mais ne représentait pas la même chose).ViewModel
->FormNode
etcreateViewModel
->makeFormNode
, comme déjà évoqué.- API des validateurs (voir section détaillée).
Changelog 9.5 -> 9.6 (à destination des projets en cours)
- Un domaine peut maintenant spécifier un
SelectComponent
et unAutocompleteComponent
(ainsi que les props associées), qui sera utilisé parselectFor
etautocompleteFor
. Cela implique également qu'il est possible de les patcher avecfromField
/patchField
- Les props de composants (
inputProps
,labelProps
...) sont maintenant surchargeables dans le champ (fieldFor
,selectFor
...) et seront "shallow mergées" avec celle du domaine (exactement commefromField
/patchField
). De plus, l'autocomplétion de ces props n'inclus plus les props qui sont gérées par le Field (genrevalue
,onChange
...) (note : il n'a jamais été possible de surcharger ces props par là de toute façon) - Les classes CSS contenues dans
theme
dans les props de composants seront maintenant toujours fusionnées lors d'un patch ou d'une surcharge dans un Field. Par exemple, si je donne "classe1" dans le domaine, que je patche pour spécifier "classe2" puis que dansfieldFor
je passe "classe3" (toujours{inputProps: {theme: {input: "classe"}}}
par exemple), la classe finale qui sera posée sera "classe1 classe2 classe3". - 3 composants ont été ajoutés : BooleanRadio, SelectRadio et SelectCheckbox, par @c3dr0x. (note : ces composants sont aussi disponibles en 8.7.9)
- L'API du fetch à été mise à jour pour prendre en compte le body et les query params (au lieu de la surcouche projet qu'on avait tous copié/collé) et supprimer les presets
httpGet
et autres qui n'étaient finalement plus utilisés. De plus,coreFetch
renvoie maintenant la response en retour au lieu du response.text() si le content type n'est pas du json ou du text. - Pour le SearchStore, la sélection ne se vide plus en cas de recherche au scroll (8.7.9 également), et le critère personnalisé est maintenant renseignable dans
setProperties
, et donc aussi à la création du store dans le constructeur.