Releases: klee-contrib/focus4
v10.3
Types de domaines
Le type "réel" du champ, jusqu'ici matérialisé par la proprieté fieldType
d'un champ et pouvant prendre les valeurs "string"
, "number"
ou "boolean"
, est désormais porté par le domaine. Une quatrième valeur, "object"
, a été ajoutée pour tous les types non primitifs qui ne sont pas parmi ces trois là.
Cela veut dire qu'on ne défini plus de type "Typescript" sur le domaine, mais un type "réel" à la place. Concrètement, il faut donc remplacer tous vos domain<string>()({....})
par domain({type: "string", ...})
. Cela implique donc que le domaine ne pourra pas avoir connaissance du type précis du champ associé, au dela de string
, number
, boolean
ou any
(qui correspond à "object"
).
En revanche, le type du champ est désormais toujours contraint par le type du domaine, c'est-à-dire qu'un champ de type nombre doit forcément avoir un domaine associé de type "number"
, y compris pour les champs créés à la volée via makeField
et formNodeBuilder.add
. En l'absence d'un domaine fourni, ces fonctions prendront un domaine de type "string"
par défaut, et ce domaine par défaut ne connait d'ailleurs plus les composants par défaut (Input, Select, Autocomplete, Display et Label) (c'était déjà le cas pour metadata()
dans add
et patch
mais pas makeField
). Vous êtes donc fortement incités à toujours spécifier un domaine lorsque vous créez un champ.
En contrepartie, il n'est plus nécessaire de spécifier fieldType
sur les champs si le type est exactement celui du domaine. Et le domaine étant toujours forcé, il ne devrait plus jamais y avoir de problème avec le type de la valeur retournée par un onChange
(genre un string à la place d'un nombre par exemple).
Il est donc nécessaire de mettre à jour la génération, au moins pour arrêter de renseigner "string"
, "number"
et "boolean"
dans fieldType
. Si vous voulez le renseigner, que ça soit pour spécifier le type du domaine (une liste de codes à la place de string
par exemple, ou un type précis genre number[]
à la place du any
du domaine associé), ou bien pour continuer à le faire systématiquement même quand c'est pas utile, il faudra maintenant toujours le générer sous la forme {} as TypeDuChamp
, comme c'était le cas avant la v9.9. Du coup désolé, j'aurais dû être plus malin à cette époque là et directement faire ce qui est fait aujourd'hui.
Par ailleurs, la méthode metadata()
du FormEntityFieldBuilder
ne permet plus de renseigner le domaine. Il est nécessaire de passer par la nouvelle méthode domain()
qui permet de passer le domaine. Puisqu'il contraint le type du champ, il est nécessaire de l'appeller en premier dans la chaine de méthodes pour patcher ou ajouter un champ dans un FormNode
.
Remarque : makeField
permet toujours de renseigner l'équivalent de metadata
et domain
en même temps, principalement parce qu'il y a moins de contrainte sur le typage du champ créé. Et aussi pour ne pas changer l'API qui est largement utilisée. Mais il est probable qu'un jour on passe à une API équivalent à celle de add
dans un FormNode, avec du fluent et tout. Mais pas tout de suite
Généralisation de l'usage de ReferenceList
Tous les composants d'affichage / select prennent désormais une ReferenceList
comme paramètre, au lieu de la liste + valueKey
+ labelKey
. Ca change pas grand chose en général puisque la grande majorité des champs sont posés par selectFor
, mais ça veut aussi dire qu'il faut proscrire des initialisations du type [] as ReferenceList
, puisque tous les composants derrières supposent qu'ils ont accès à l'ensemble de l'API. Pour contourner le problème, on expose désormais une méthode emptyReferenceList()
, qui permet de faire ces initialisations là proprement.
J'en profiter pour rappeler également que pour vos listes de références stockées dans vos composants qu'il est également indispensable de les stocker dans un @observable.ref
(au lieu d'un @observable
), sinon vous allez avoir de mauvaises surprises.
Réécritures des composants de listes
Les composants de listes ont été intégralement réécrits, dans le but de rationnaliser l'ensemble et de passer sur une implémentation moderne. Le résultat principal de cette réécriture fait qu'il n'y a plus de StoreList
et de StoreTable
, la gestion d'un store (de liste ou de recherche) est désormais native sur la liste et le tableau de base. Il faudra donc simplement remplacer tous vos storeListFor
et storeTableFor
par listFor
et tableFor
, pour le même résultat. Il est également désormais possible de passer un store à une Timeline, qui le gère de la même façon que les deux autres listes.
De plus, les composants ont été séparés de manière plus marquées, ce qui se traduit dans la façon dont le CSS est maintenant réparti. Concrètement, tout le CSS de la liste est dans "list"
, celui du tableau dans "table"
, de la timeline dans "timeline"
et le commun (pour les boutons du bas genre "voir plus") dans "listBase"
). Cela remplace les deux anciens CSS "list"
et "line"
qui mélangeait un peu les deux de façon peu claire.
Petite réorganisation de la recherche avancée
Suite à ces réécritures, quelques nouveautés sont apparues dans la recherche avancée :
- Il est désormais possible de choisir un composant de liste, parmi les 3 disponibles, ou bien le votre si vous êtes chauds. Par défaut, c'est toujours la liste classique qui est utilisée. Cela implique par contre que les props du composant de lignes doivent maintenant être renseignées dans une nouvelle prop
listProps
, puisqu'elles peuvent dépendre du composant choisi. En particulier,itemKey
etLineComponent
sont dedans, il faudra donc tous les déplacer. Dans le même genre,lineOperationList
est désormaisoperationList
danslistProps
par exemple. - Le composant
ListWrapper
, qui permet principalement d'afficher un bouton de création global et un sélecteur de mode (list/mosaic) sur la recherche avancée, a été complètement intégré à la recherche avancée. Son intérêt étant déjà très limité, tout est plus simple comme ça. Et il est possible qu'il soit étendu plus tard pour passer d'autre contexte dans la recherche...
Header de table
Le header d'un table triable a été revu, pour être plus joli et plus clair :
En particulier, tout le titre est désormais cliquable pour changer le sens de tri et les colonnes triables sont indiquées par des pointillés.
Mise à jour mobx-react
Grâce à la refonte des listes qu'il m'a enlevé le principal blocage à la mise à jour, on utilise désormais la v6 de mobx-react. Y a rien qui change à l'usage, à part le fait que les différents hooks sont désormais inclus dedans (useObserver
, useLocalStore
...). Il n'y a donc plus besoin d'aller les chercher dans mobx-react-lite
, et d'ailleurs il ne faut plus le faire.
Pour célébrer l'occasion, l'import focus4
expose désormais useObserver
et useLocalStore
, pour vous motiver à passer aux hooks.
autoSelect
sur Autocomplete
Le composant d'Autocomplete dispose maintenant d'une nouvelle prop autoSelect
, qui a pour effet de,dès que la recherche ne renvoie qu'une seule valeur, de la sélectionner automatiquement (sans cliquer dessus ou appuyer sur Entrée). Cette fonctionnalité est pratique pour forcer une sélection si le champ d'autocomplétion n'est pas obligé de matcher sur une valeur par exemple.
Corrections diverses
- #150
- Les erreurs sur BooleanRadio et SelectRadio sont désormais affichées correctement
- getLabel() fonctionne correctement si la valeur vaut 0
FormActionsBuilder.params(undefined)
fonctionne désormais comme attendu (et indiqué dans la doc d'ailleurs)- Le prop
noForm
sur<Form>
est désormais à l'endroit, c'est à dire que le formulaire HTML est posé par défaut et est enlevé avecnoForm = true
. Attention du coup puisque ça veut dire qu'avant vous n'aviez aucun formulaire par défaut et maintenant vous allez en avoir partout ! - Il n'est plus nécessaire de spécifier le
edit()
d'un sous noeud pendant un (make/use)FormNode (ça plantait avant si on le faisait pas)
Refonte du module legacy
Le meilleur pour la fin, et celui qui ne concerne finalement pas grand monde (on me souffle dans l'oreille qu'il n'y a que moi qui m'en sers) : le module legacy
a été réécrit pour utiliser les composants de champs "classiques" (ceux de forms
) au lieu d'une copie à peine modifiée de tous les composants, et en particulier le Field
en lui même, pour pouvoir gérer les différences entre les deux types de formulaires (les nouveaux et les anciens).
Ce module ne contient donc plus que le bon vieux AutoForm
, ainsi qu'un composant FieldWrapper
qui permet de convertir des champs de StoreNode en champs de FormNode à la volée, ce qui est nécessaire pour faire la compatibilité entre l'AutoForm et les composants "classiques". Via le FieldWrapper
, il est possible de reproduire ce qui été fait à la main avec des ref
pour récupérer les erreurs, et également de patcher des champs à la volée. L'usage direct de fieldWrapperFor
n'est pas recommandé dans le cas général, puisque c'est ce que fait this.fieldFor
, this.selectFor
, ... , qu'il faut continuer à utiliser comme si de rien n'était. Et il paraît même qu'il est possible d'utiliser les même champs via le FieldWrapper
sur des vieux formulaires en v2.... :)
Les APIs de this.fieldFor
, this.selectFor
, this.autocompleteFor
et this.displayFor
ont été homogénéisées avec leurs équivalents sans this
, puisque maintenant elles utilisent les mêmes composants derrière de toute manière. Leur APIs sont donc équivalentes à la fonction de base + un metadata()
+ un domain()
.
v10.2
Refonte interne des modules CSS (#182)
L'une des critiques principales émise à l'encontre des modules CSS a toujours été le fait que toutes les classes définies dans un module sont au même niveau, ce qui ne permet pas de différencier, dans une optique BEM par exemple, quelles classes sont des "blocks", des "elements" ou des "modifiers". Le module CSS en lui-même correspond déjà au niveau "block", et par convention la classe "block" dans le module est la classe qui a le même nom que le module. Il manquait donc essentiellement la notion de "modifier".
C'est donc chose faite. Le CSS des différents modules Focus (et donc pas celui issu de react-toolbox, désolé) a donc été adapté pour avoir des classes qui ont des noms de modifiers. Par exemple, dans le Panel, .top
s'appelle désormais .title--top
(ça a toujours été un modifier sur .title
), ou .loading
qui devient .panel--loading
. Un certain nombre de classes ont donc été renommées. Elles sont visibles dans la PR, et si le besoin se présente je pourrais prendre la peine de les lister ici.
De plus, les exports de CSS et de types ont été renommés : {module}Styles
devient {module}Css
et {Module}Style
devient {Module}Css
.
En interne
Un nouveau générateur de types TS pour les modules CSS a été mis au point, qui permet de typer précisément les classes générées pour en particulier pouvoir déterminer quelles classes sont modifiers de quels elements.
Exemple :
import {CSSElement, CSSMod} from "@focus4/styling";
interface Actions { _f6135: void }
interface Content { _196cd: void }
interface Panel { _353e5: void }
interface Progress { _3cd99: void }
interface Title { _4bf8f: void }
export interface PanelCss {
actions: CSSElement<Actions>;
content: CSSElement<Content>;
panel: CSSElement<Panel>;
"panel--editing": CSSMod<"editing", Panel>;
"panel--loading": CSSMod<"loading", Panel>;
progress: CSSElement<Progress>;
title: CSSElement<Title>;
"title--bottom": CSSMod<"bottom", Title>;
"title--top": CSSMod<"top", Title>;
}
declare const panelCss: PanelCss;
export default panelCss;
Dans Focus (et à terme dans les projets), le module CSS est transformé dans un objet particulier, qui crée une fonction par element, qui prend un objet en paramètre pour spécifier les modifiers associés,
Exemple :
<div className={theme.panel({loading, editing})}>
<div className={theme.title({top: true})}>
<h3>{i18next.t(title)}</h3>
</div>
<div className={theme.content()}>{children}</div>
<div className={theme.title({bottom: true})}>{buttons}</div>
</div>
Toutes les propriétés theme
des composants Focus/RT peuvent prendre en paramètre soit l'objet theme
classique, soit ce fameux objet à la place.
L'objectif final est d'exposer ces fonctionnalités aux projets. Pour l'instant, il n'y a pas encore de chemin clair pour brancher la génération et consommer facilement le CSS comme ça en dehors de Focus.
CSS Timeline + addItemHandler
Ca ressemble à ça maintenant.
En particulier, le CSS de la timeline est plus simple et ne force plus tout un gros bloc dégueulasse (juste les traits violets)
IDs multiples de champs
Il peut arriver parfois dans un formulaire d'avoir plusieurs champs qui ont le même nom. Cela arrivera forcément dans un formulaire sur une liste par exemple. Les noms de champs étant utilisés comme ID dans le HTML, cela pouvait poser différents problèmes, comme les libellés qui ne pointaient pas sur le bon input, ou bien un bon gros warning du navigateur qui explique que c'est interdit.
Par conséquent, à la création d'un nouveau champ, Focus va automatiquement incrémenter l'ID d'un champ qui existe déjà sur la page, indépendemment du fait qu'il soit en édition.
labelRatio
/valueRatio
sur le Form
Il est désormais possible de passer ces paramètres sur le composant <Form>
, qui seront passés par contexte à tous les champs qui sont dedans. Il reste possible de les surcharger par champ, comme d'habitude.
De plus, forceActionDisplay
est désormais directement sur FormActions
, et non plus sur un object formContext
.
Suppression des anciens makeFormActions
/makeFormNode
(pré 10.1)
Tout a été supprimé, il n'est désormais plus possible d'utiliser les anciennes syntaxes. La doc a été mise à jour en conséquence (!!).
v10.1
makeFormNode
makeFormNode
a été reconstruit à partir de FormNodeBuilder
, FormListNodeBuilder
et FormEntityFieldBuilder
, pour offrir une API de type "fluent" pour modifier le FormNode a sa création, qui remplace la fonction "initializer" et des appels répétés à patchField
.
Remarque : L'ancienne façon de construire un FormNode est toujours supportée, néanmoins elle sera retirée dans le futur (proche)
Ci-dessous, un exemple de modification (celui du starter-kit) :
c =>
c
// On change le domaine et le isRequired du champ.
.patch("denominationSociale", (f, node) =>
f.metadata(() => ({
domain: DO_COMMENTAIRE,
isRequired: !!node.capitalSocial.value
}))
)
.patch("capitalSocial", (f, node) =>
f
.value(() => (node.denominationSociale.value && node.denominationSociale.value.length) || 0)
.metadata({validator: {type: "number", max: 20000}})
.edit(() => node.statutJuridiqueCode.value !== "EARL")
)
.patch("adresse", s => s.edit(false))
// On ajoute un champ supplémentaire calculé.
.add("email", (f, node) =>
f
.value(() => node.denominationSociale.value, value => (node.denominationSociale.value = value))
.metadata({
domain: DO_LIBELLE_100,
label: "structure.email",
validator: {type: "email"}
})
)
Cette nouvelle API permet de typer intégralement toutes les modifications, que ça soit des ajouts de champs ou même des modifications de champs (par exemple : composant d'input), et ce quelque soit le niveau (sous objet, sous liste...). Auparavent, aucune modification ne pouvait être tracée, et il n'était possible d'ajouter des champs qu'à la racine et dans les sous-listes (et ces dernières n'étaient pas tracées).
De plus, l'API est plus simple à prendre en main car elle ne dépend plus de fonctions supplémentaires à importer comme patchField
.
FormNodeBuilder
Il permet de construire un FormNode à partir d'un StoreNode. Il sera le paramètre de toute fonction de configuration sur un FormNode (celle de makeFormNode
, ou de patch
et items
qu'on vera plus bas).
Il dispose des méthodes suivantes :
edit(value)
La fonction edit
, qui prend en paramètre soit un booléen, soit une fonction retournant un booléen, permet de modifier l'état d'édition initial (si booléen), ou bien de forcer l'état d'édition (si fonction). Le fonctionnement est identique à ce que faisait patchNodeEdit
, et les valeurs par défaut sont les mêmes (false
pour le noeud racine et true
pour tout le reste).
edit(value, ...props)
Il est également possible de passer des propriétés de l'objet à la fonction edit
. Dans ce cas, la valeur initiale/fonction sera appliquée aux champs/listes/objets demandés au lieu de l'objet en lui-même. Il est donc parfaitement possible d'utiliser edit
plusieurs fois, tant que ça s'applique à des propriétés différentes.
Exemple :
s
.edit(!this.props.egfId) // objet en entier
.edit(false, "id", "isValide", "totalEngagementFinancier", "totalVersementsRecus") // valeur par défaut sur certains champs
.edit(() => !this.props.egfId, "typeEngagementPartenaireCode") // édition forcée sur un champ
La façon standard de modifier l'état d'édition d'un membre d'objet est de passer par patch
(ou add
), décrits en dessous, mais cette version de edit()
permet de spécifier le même état d'édition à plusieurs champs à la fois (désactiver l'édition sur plusieurs champs est un usage très courant). C'est aussi une version plus courte pour un seul champ si l'édition est la seule chose à modifier dessus.
add(name, fieldBuilder)
La fonction add
permet d'ajouter un nouveau champ au FormNode
en cours de construction. Elle prend comme paramètres :
name
, qui est le nom du champ à créerfieldBuilder
, une fonction qui sera appelée avec unFormEntityFieldBuilder
et leFormNode
courant pour paramétrer le champ.
patch(name, builder)
La fonction patch
permet de modifier un membre du FormNode
, que ça soit un champ, une sous-liste ou un sous-objet. Elle prend comme paramètres :
name
, qui est le nom du champ/sous-objet/sous-liste. Ce nom est typé et changera la signature en fonction de ce à quoi il correspond.builder
, qui peut être unfieldBuilder
,listBuilder
ou unobjectBuilder
en fonction du membre choisi. Ces fonctions seront appelé avec le builder correspondant (FormEntityFieldBuilder
,FormNodeBuilder
ouFormListNodeBuilder
), ainsi que leFormNode
courant.
build()
La fonction build
permet de construire le FormNode
. Elle est appelée à la fin de makeFormNode
et à priori il n'y aura jamais besoin de l'appeler manuellement.
FormListNodeBuilder
Il permet de construire un FormListNode à partir d'un StoreListNode. Il sera le paramètre de toute fonction de configuration sur un FormListNode (celle de makeFormNode
ou de patch
)
Il dispose des méthodes suivantes :
edit(value)
Idem FormNodeBuilder
items(objectBuilder)
La fonction items
permet de modifier les items de la liste (qui sont, pour rappel, des FormNode
s). Elle prend comme unique paramètre objectBuilder
, pour préciser la configuration. Comme toujours, cette fonction est donc appelée avec un FormNodeBuilder
ainsi que le FormListNode
courant.
build()
Idem FormNodeBuilder
.
FormEntityFieldBuilder
Il permet de construire un FormEntityFIeld à partir d'un EntityFIeld. Il sera le paramètre de add
et patch
sur le FormNodeBuilder
.
Il dispose des méthodes suivantes :
edit(value)
Idem FormNodeBuilder
value(get, set?)
La fonction value
permet de remplacer la valeur d'un champ (ou bien de la définir pour un champ ajouté) par une valeur calculée, avec un setter éventuel (auparavant, cette possibilité n'était pas offerte au "patch"). Elle prend comme paramètres :
get
, pour spécifier le nouveau getter du champset
, pour spécifier le nouveau setter du champ
metadata($field)
La fonction metadata
permet de remplacer les métadonnées d'un champ (ou bien de les définir pour un champ ajouté). Le fonctionnement est identique à ce que faisait patchField
. Elle prend un seul paramètre, $field
, qui contient soit toutes les métadonnées à remplacer (champ et contenu du domaine), soit un fonction qui les renvoie.
Création via makeFormNode
.
Les signatures correspondantes de makeFormNode
sont donc :
makeFormNode(this, node, objectBuilder, initialState)
makeFormNode(this, listNode, listBuilder, initialState)
Remarques
- Tous les FormNodes sont désormais créés vides. Pour être cohérent, les anciennes signatures de
makeFormNode
précisent égalementisEmpty
àtrue
par défaut. - Tout comme
clear()
,replace()
,set()
,reset()
renvoie également le FormNode lorsqu'il est appelé au lieu devoid
initialState
peut être soit un objet contenant les champs du Store(List)Node, soit une fonction qui retourne ce même objet, ce qui permet de gérer le cas d'une initialisation conditionnelle.
makeFormActions
De la même façon que le FormNode, les FormActions sont maintenant construites via un FormActionsBuilder, qui ressemble à quelque chose comme
a =>
a
.params(() => homeViewStore.withView(({page, id}) => !page && id && +id))
.load(loadStructure)
.save(saveStructure)
Remarque : tout comme makeFormActions
, l'ancienne syntaxe reste utilisable (pour l'instant)
FormActionsBuilder
Il dispose des méthodes suivantes :
params(()=>params)
/params(params)
Il fonctionne à peu près de la même façon que getLoadParams
, à ceci près qu'il est désormais possible de :
- Passer une valeur fixe au lieu d'une fonction, si les paramètres ne changent jamais
- Renvoyer une valeur unique au lieu d'un array s'il n'y a qu'un seul paramètre (via la fonction ou directement)
Cela veut dire que tout ceci est désormais identique :
params(() => [1])
params(() => 1)
params([1])
params(1)
Il est possible d'appeler params()
tel quel sans arguments pour indiquer que le service de chargement ne prend pas de paramètres.
Si vous devez renvoyer plusieurs paramètres non fixes, alors il faudra marquer l'array renvoyé comme as const
(cf. load
juste en dessous). Exemple : params(() => [this.props.id, "test"] as const)
Remarque : Si params
est appelé avec undefined
ou si la fonction passée renvoie undefined
, la service de chargement ne sera pas appelé. C'est cohérent avec le fonctionnement précédent, sauf qu'auparavant, puisqu'on était obligé de retourner un array, le choix de retourner undefined
était délibéré. Par exemple, pour un composant qui prend un id
en props et qui gère de la création/consultation, il fallait spécifier () => this.props.id ? [this.props.id] : undefined
pour que le service de chargement ne soit pas appelé en consultation. Désormais, () => this.props.id
(ou même this.props.id
directement) se comporte exactement de la même manière. Si vous voulez vraiment que le service soit appelé avec undefined
, alors il faut utiliser () => [this.props.id] as const
(ou [this.props.id]
).
load(service)
Permet de préciser le service de chargement. params() doit être appelé avant car il type désormais les paramètres de load.
save(service, name?)
Permet de préciser un service de sauvegarde. Il est toujours possible d'en spécifier plusieurs, pour se faire il suffit de renseign...
v10
Après une série de version "mineures" qui avaient tout plein de breaking changes et qui ont été mal suivies par les projets (après c'est surtout parce qu'elles sont tombées en fin de cycle), voilà maintenant qu'une vraie version majeure arrive pour enfoncer le clou...
... sauf que cette fois-ci y a rien qui change ! Ou presque...
Modularisation de focus4 (#121)
C'est l'heure pour moi de revenir en arrière toute sur la façon dont Focus est construit et architecturé. Jusqu'à présent il n'y avait qu'un seul module, focus4
, qui était buildé de la façon la simple possible avec un simple appel à tsc
et quelques copies de fichiers pour conserver le CSS et ses fichiers de définitions TS. C'était un choix délibéré pour ne pas à avoir à traiter la complexité induite par 2 ou 4 modules (v2/v3) pseudo indépendants, aussi bien pour la maintenance que pour l'utilisation.
Cette réarchitcturation de Focus a été motivée par deux raisons principales :
- Pouvoir donner la possibilité à des projets de ne récupérer que la partie de Focus qui les intéresse, surtout si ce n'est pas pour faire une application React DOM derrière (genre du React Native ou même du Angular/Vue derrière qui ont leurs bindings MobX, si besoin).
- Gérer plusieurs "versions" de Focus via un même repo, en partageant le plus de code possible. C'est évidemment fortement lié au point précédent puisque ça veut dire qu'on aura de quoi développer par la suite des modules spécifiques React Native/Angular/Vue si un projet en a besoin, mais pas que, puisqu'aujourd'hui on a déjà deux versions de Focus (v8 et v9) qui sont a 80-90% identiques mais fondamentalement incompatibles.
6 (+2) modules
La v2 en avait 2, la v3 en avait 4, donc par extension logique la v4 a été divisée en 6 (+2) nouveaux modules ! Les différents modules sont les suivants :
@focus4/core
: il contient tous les petits bouts "génériques" de Focus comme le fetch, le routeur, et les petits stores. Il ne dépend d'aucun autre module, ni de React.@focus4/stores
: il contient les stores d'entités, de formulaires, de références, et de listes. Il dépend decore
mais toujours pas de React.@focus4/styling
: il contient les différents outils liés au CSS ainsi que toutes les variables (y compris celles de React Toolbox, on y reviendra plus tard). Il dépend decore
et de React (comme tous les modules suivants).@focus4/forms
: il contient les composants de saisie, de champs et de formulaires. Il dépend destores
etstyling
.@focus4/collections
: il contient tous les composants de liste de et recherche. Il dépend deforms
, puisqu'il existe un formulaire de saisie de critères de recherche.@focus4/layout
: il contient tous les composants de layout. Il dépend destyling
(mais pas decomponents
oucollections
).
Construction d'un module et changements sur le CSS
Un module est désormais proprement packagé en un seul fichier (avec rollup
) et le CSS est proprement extrait. Cela veut dire que tous les imports ne peuvent se faire que depuis la racine du module ("@focus4/core"
par exemple). Tout est donc exporté depuis la racine. Pour le CSS, cela veut dire que les modules sont déjà compilés (avec des noms de classes non surchageables du coup) et qu'il est possible d'importer les objets générés contenant les classes depuis le module. Par conséquent :
- Il faut importer le CSS à la main dans votre projet, et de préférence dans un fichier CSS unique avec tous les autres CSS des différents modules pour récupérer les variables CSS, toutes exposées par le module
styling
(@focus4/styling/lib/variables.css
). - Il n'y a plus besoin de configurer les modules CSS dans webpack pour utiliser Focus (à moins que vous vouliez en utiliser également). Postcss avec au moins
postcss-preset-env
etpostcss-color-function
sont toujours nécessaires. Si vous voulez compiler les variables CSS (nécessaire si vous ciblez IE, sinon pas besoin), il faut ajouterpostcss-import
et importer le fichier de variables dans tous les CSS qui les utilisent, ainsi quepostcss-custom-properties
(en version 7.0.0...). Si vous voulez utiliser des modules dans votre projet c'est à vous d'ajouter une config pour le faire (par exemple la même quecreate-react-app
avec.module.css
) - Puisqu'il n'est plus possible de compiler soi-même les modules, vous ne pouvez plus donc vous arranger pour avoir des noms stables sur les classes (déso). A la place vous êtes obligés de faire comme prévu par le framework en ajoutant vos classes dans les différentes props
theme
.
@focus4/toolbox
Le 7ème module de Focus est une "réimplémentation" de react-toolbox
qui profite du fait que la librairie a eu le bon sens de découpler leur solution de CSS des composants. Ainsi, ce module wrappe totalement RT et package son CSS comme les autres modules, en plus de l'injecter avec des HoC avec des forward refs et des hooks tous propres (cela veut dire qu'il ne faut plus utiliser innerRef
mais ref
sur les composants RT désormais).
Par conséquent, il faut remplacer tous vos imports de "react-toolbox/lib/****"
par "@focus4/toolbox"
. Ce sont les mêmes composants sinon, à l'innerRef
/ref
près. Il manque certains composants (ceux qui sont remplacés dans les autres modules de Focus), et il y en a en plus, principalement issus de l'ancien components
. Le détail est dans le readme.
@focus4/legacy
Le package legacy
est un module supplémentaire à destination des utilisateurs de la v8 qui souhaiteraient se mettre à jour. L'existence de ce package implique que la v8 ne sera plus maintenue, hormis besoin projet prioritaire insolvable autrement...
Ce module implémente les formulaires < v9 à partir des stores v9+. Il permet donc de bénéficier de toute la "modernité" de Focus v9+ sans avoir à réécrire tous vos formulaires pour s'y accommoder.
Néanmoins :
- La génération de modèle a été mise à jour, c'est désormais la même que la v9+.
- La gestion des stores (mais pas des formulaires) étant désormais la même que la v9+, cela veut dire que :
makeEntityStore
etnew SearchStore()
ont une API claire et qui marche facilement- On a maintenant accès à
node.replace()
etnodeList.replaceNodes()
(du coupnodeList.set()
n'existe plus), à utiliser à la place des clear/set dégueulasses - On a accès à buildNode qui a une API simple pour créer des noeuds à la volée (avant il fallait sortir
buildEntityEntry
qui prend 4 paramètres imbitables) - On a accès à
makeField
, qui peut remplacer astucieusement les fields construits à la main à base de{$entity, value}
. D'ailleurs,$entity
à été renommé en$field
en v9.
- Les définitions de domaines ont été mises à jour comme en v0+
- Il est indispensable d'importer les différents composants de
@focus4/legacy
pour utiliser des formulaires < v9. En particulier,displayFor
,fieldFor
etselectFor
. Ca ne marchera pas sinon, et les APIs sont de toute façon un peu différentes.AutoForm
n'existe que dans ce module donc il n'y aura pas de confusion. - Le
referenceStore
est désormais celui de la v9+. Les listes de références contiennent$valueKey
,$labelKey
etgetLabel
, ce qui rend facultatif de renseignervalueKey
/labelKey
sur lesselectFor
legacy. Le nouveauselectFor
ne permet pas de le faire (il faut utilisermakeReferenceList
), ainsi questringFor
, qui n'a pas de version legacy.
De plus, étant déprécié depuis un moment déjà, l'applicationStore a été retiré. Il est désormais obligatoire de construit ses headers à la main.
focus4
Pour simplifier la vie de tout le monde qui souhaite toujours utiliser l'ensemble de Focus dans ses projets et qui n'a pas envie de se poser trop de questions sur la modularisation, le module focus4
existe toujours et se contente de réexporter les différents modules selon la même structure qu'avant. Pour migrer vers la v10, il devrait donc suffire de remplacer tous vos imports de RT par des @focus4/toolbox
et, à part les petits ajustements sur le build, tout devrait marcher comme avant.
A noter que l'import "focus4"
embarque désormais tout le CSS ainsi que les icônes material (qui étaient dans layout
avant). Il pose aussi le shim de core-js
, de façon à ce que tout ce que vous ayez à faire et d'ajouter un import "focus4"
en haut de votre fichier racine index.js
.
Autres évolutions diverses
node.set/replace/clear
(et les équivalents listes) retournent désormais le noeud au lieu devoid
. Cela peut permettre de chainer (si besoin) les appels, et surtout d'écrire quelque chose commeconst node = buildNode(Entity).set(data)
.registerHeader
etsetHeaderProps
ont été séparés dans la relation Header/Scrollable. Le Header ne s'enregistre plus dans le Scrollable que lorsqu'il est monté (ou sicanDeploy
change). Pour le reste, par exemple si le contenu change, il sera simplement mis à jour. Il n'y aura donc plus, dans aucun cas, d'animation sur le Header quand son contenu change. Grosso modo, cela veut dire qu'il est maintenant obligatoire de faire autant de Header que nécessaire, au lieu d'utiliser le même et de changer son contenu par module. C'était déjà la méthode suggérée depuis la (8|9).10 et la refonte, mais c'est désormais la seule façon correcte de faire.- Il est désormais possible, dans une définition d'entité, de spécifier, en plus de
"field"
,"object"
et"list"
,"recursive-list"
(sans aucune autre propriété) pour créer une sous-liste de l'entité courante. C'était possible en pré-v9 puisqu'on reférençait les entités par leur nom (et non l'entité elle-même) et plus possible après à cause de dépendances circulaires, donc cette fonctionnalité restore cette possibilité. - Le typage de
makeField
a évolué pour avoir moins de problèmes de typage, en séparant le type du champ et celui du domaine.
v9.10 / v8.10
Cette version concerne aussi bien la v9 que la v8, et d'ailleurs seule cette dernière a fait l'objet d'une release propre, étant donné que la 9.9.0 est toujours en attente...
Cette release propose une résolution à #101, et son objectif est de dégager tous les comportements qui se basaient sur window
(le header, le scrollspy, les popins, le scroll infini…) et de les lier à un nouveau composant appelé Scrollable
, qui propose son ScrollableContext
.
Concrètement, ce que ça signifie pour vous c'est que :
- il va probablement falloir refaire votre CSS global de layout qui aura peu de chance de fonctionner tel quel. En particulier il n'y a plus un seul
position: fixed
dans le framework. - Il va falloir un peu modifier certains imports
- Il va falloir refaire vos
tableFor
s - Et beaucoup de choses qui ne marchaient pas/étaient impossibles avant vont désormais marcher/être possible.
C'est parti !
Layout = MainMenu + Scrollable
L'existence du composant <Scrollable>
implique qu'on ne va désormais plus scroller sur la fenêtre entière mais dans un <Scrollable>
. Par conséquent, le Layout a été modifié de façon à ce qu'il prenne toujours 100vw * 100vh, avec le Menu est placé à gauche et le Scrollable à droite. Le menu est donc naturellement fixé à gauche (sans avoir de position: fixed
), et il peut sans problème avoir un overflow (s'il y a trop d'items ou si la fenêtre est trop petite) : une scrollbar va apparaître et la partie scrollable sera naturellement réduite. Il n'y a donc plus besoin de calculer et garder en dur la taille du menu pour placer correctement le contenu.
De plus, le composant <LayoutContent>
a été retiré car il n'est plus nécessaire (il servait à prendre en compte la taille du menu auparavant). Il posait également des marges, qui sont désormais posées par d'autres composants tels que le ScrollspyContainer ou l'AdvancedSearch. Si vous n'utilisez pas de composant de layout global comme ces ceux-là, un composant appelé simplement <Content>
le remplace, dont le simple but est de poser des marges.
Le ErrorCenter
a été retiré, clairement il n'était pas très utile (la plupart des erreurs restaient en console) et il affichait souvent des faux positifs.
De façon plus anecdotique, <LayoutFooter>
a été également retiré pour la même raison. Si besoin, vous pouvez simplement poser un <footer>
HTML standard pour le même résultat.
Pour matérialiser ce changement de Layout, il faut maintenant passer votre composant de menu dans la prop menu
du Layout, au lieu de le poser dedans comme un enfant.
Refonte du MainMenu (#102)
C'est le seul point qui ne concerne pas directement <Scrollable>
donc on va le traiter en premier.
L'API du MainMenu est inchangée, mais son implémentation a évolué : il n'est plus nécessaire que les <MainMenuItem>
soit des enfants directs du <MainMenu>
, ou bien d'un autre item (si sous menu).
De plus, la gestion du sous menu, qui vient avec une nouvelle animation (il y aura beaucoup de nouvelles animations dans cette version), étant désormais déléguée au MainMenuItem lui-même, il devient possible (même si ce n'est pas forcément une bonne idée) d'avoir autant de niveau de sous menus que nécessaire.
API du ScrollableContext
Le Scrollable
propose une API "publique" dans son contexte, dans le sens ou vous pouvez l'utiliser dans votre projet mais qu'elle existe surtout pour les composants de layout internes de Focus :
registerHeader(Header, headerProps, nonStickyHeader, canDeploy)
: méthode utilisée par le<HeaderScrolling>
pour s'enregistrer dans le<Scrollable>
. A priori il n'y a aucune raison de l'utiliser directement puisque c'est presque la seule utilité du composant associé.registerIntersect(node, onIntersect)
: cette méthode permet d'enregistrer un handler d'évènement sur un nœud HTML, qui sera appelé régulièrement quand la section visible de ce noeud dans le viewport du<Scrollable>
change. En général, via un évènement de scroll, mais pas nécessairement. Cela permet de faire des choses (on verra ce que fait Focus avec plus tard) lorsqu'un élément entre ou quitte le viewport par exemple.scrollTo(options)
: même chose quewindow.scrollTo
, pour le<Scrollable>
. Vous pouvez toujours essayer d'utiliserwindow.scrollTo
mais ça ne marchera plus, puisque la fenêtre est fixe.portal(node, parentNode?)
: cette méthode permet de poser un Portal React vers le<Scrollable>
. Il y a deux possibilité d'usage :- Soit
parentNode
n'est pas spécifié, dans ce cas le composant sera ajouté au nœud racine du<Scrollable>
(la partie fixe). - Soit
parentNode
est renseigné, dans ce cas le nœud sera également ajouté à la partie fixe, mais il sera positionné automatiquement au même niveau que leparentNode
, jusqu'à ce que le nœud arrive au sommet du<Scrollable>
: il sera bloqué en sticky à ce moment-là.
- Soit
Contenu et props du <Scrollable>
Il est assez rare que vous ayez à poser votre propre <Scrollable>
puisqu'il est posé par le <Layout>
ainsi que par les <Popin>
. En effet, le fait que chaque <Popin>
pose son <Scrollable>
implique que tout ce qui va être présenté ici marche également dans une popin ! Bon en effet c'était quand même le but premier recherché…
Néanmoins, les props du Scrollable se retrouve dans les props du Layout et de la Popin, donc ça vaut le coup d'en parler un peu :
hideBackToTop
: par défaut, le Scrollable affiche un bouton de retour en haut de page, comme le Scrollspy et l'AdvancedSearch le faisait avant. Désormais, il n'existe qu'ici (ce qui est assez logique tout compte fait), et il est possible de le désactiver avec cette prop.backToTopOffset
: Offset avant l'apparition du bouton de retour en haut. Par défaut : 200.className
: Permet de poser une classe CSS sur le nœud racine. Correspond àtheme.container
.scrollBehaviour
: Comportement du scroll pourscrollTo
.resetScrollOnChildrenChange
: Réinitialise le scroll (à 0) dès que les children changent. Cette prop est activée pour le scrollable du Layout mais pas pour les autres. En pratique un changement de children sur le layout correspond à un changement de page pour lequel on voudra toujours commencer en haut de page, d'où ce comportement.
Ce qu'on remarque surtout, c'est que ces props et les comportements associés existaient déjà avant, mais ils étaient dans des composants spécialisés (surtout le Scrollspy). Maintenant, tout ceci est standard dans <Scrollable>
.
Header
Le header se pose toujours dans le Layout (ou dans un <Scrollable>
comme la popin désormais), mais son mode replié s'affiche désormais dans la partie fixe du Scrollable une fois que le header déplié est entièrement sorti de l'écran (via registerIntersect
). Il apparaît à l'écran avec une petite animation :
A noter aussi la petite animation lorsque le bouton back to top apparait 😄
De plus, l'animation des boutons d'actions est déclenchée automatiquement par contexte (react-pose c'est vraiment de la bombe), donc si vous voulez changer le composant d'actions pour mettre le votre vous aurez aussi la possibilité de faire votre propre animation.
D'ailleurs, vous pouvez aussi complètement réimplémenter le header facilement, puisque toute la logique est dans le <Scrollable>
et que tout ce que fait le <HeaderScrolling>
est un useEffect
qui appelle context.registerHeader
...
A noter qu'il est toujours possible de spécifier canDeploy
à false
pour avoir un header toujours sticky. Ce header-là n'a pas d'animation (même si vous changez de page avec le même composant de header et juste la prop qui change).
ScrollspyContainer
Le ScrollspyContainer a été déplacé dans le module focus4/layout
.
Il utilise désormais registerPortal
pour placer le menu en sticky, comme on peut le voir sur l'animation suivante :
A noter qu'il est impossible de reproduire ce comportement dans IE ou Edge à cause d'un délai trop important entre le scroll et le déclenchement de l'évènement associé (aucun problème sous Chrome ou Firefox), donc un mode "dégradé" ou le scroll est "debouncé" avec une animation pour déplacer le menu a été mis au point. Les deux navigateurs étant sur leur lit de mort, tant pis s'ils ont un mode dégradé.
De plus, via registerIntersect
, il est possible de correctement déterminer quel Panel est actif à l'écran, ce qui permet (enfin !) de faire marcher ceci :
Sur un sujet similaire, l'AdvancedSearch
propose une nouvelle valeur pour facetBoxPosition
, "sticky"
, qui reproduit le comportement du menu du Scrollspy pour la liste des facettes. A noter qu'il vous faudra tout de même surcharger son CSS pour déterminer sa hauteur maximale et l'overflow auto qui va avec.
Scroll infini sur les listes
Le scroll infini fonctionne désormais avec registerIntersect
sur le n-ième dernier élément de la liste. Dès qu'il apparaît à l'écran, la suite de la liste est chargée (ce qui résout également #111). La prop offset
a donc été remplacée par pageItemIndex
qui correspond à ce "n" (par défaut placé à 5).
Il est également disponible sur les listes de groupes de la recherche avancée, via les deux nouvelles props groupListPageSize
(défaut à 10) et groupPageItemIndex
(défaut à 2).
A noter que pour des limitations techniques (merci React…) il est nécessaire d'utiliser listFor
et autres (à la place du composant directement) pour bénéficier du contexte du ListWrapper
dans une liste (le truc utilisé dans la recherche avancée pour...
v9.9
Le but de cette release est d'essayer de combler les derniers points qui posent problème autour des formulaires. En ce sens, elle ne concerne donc que la v9.
Cette version n'est pas encore finalisée car elle est en attente de deux correctifs (côté typescript et côté mobx-react), mais cela ne vous empêchera pas de l'utiliser.
<Input type="number" />
Le composant d'Input standard supporte maintenant un type "number", qui va permettre de saisir des nombres proprement et de gérer des contraintes de formatage. En particulier, les props suivantes ont été ajoutées :
type
, qui peut valoir"string"
ou"number"
. Le type"string"
correspond à l'existant et reste le seul qui supporte les masques de saisies.hasThousandsSeparator
, qui va afficher les séparateurs de milliers de la locale courante (via la config de numeraljs) dans le champ d'input.maxDecimals
, qui va permettre de limiter le nombre de décimales qu'il est possible de saisir. On peut bien sûr y mettre0
si on ne veut que des entiersnoNegativeNumbers
, pour empêcher la saisie de nombres négatifs (qui est autorisée par défaut)
Ces options n'ont pas de lien avec le process de validation, donc il conviendra dans la plupart des cas de renseigner les contraintes dans les props de l'Input et dans le validateur.
A noter que si la locale est "fr", les points sont automatiquement remplacés par des virgules à la saisie, ce qui permet d'utiliser les deux.
fieldType: "number" | "string" | "boolean"
Dans une définition d'entité, il est désormais possible (et fortement conseillé) de renseigner le fieldType
d'un champ comme étant "string"
, "number"
ou "boolean"
, au lieu de mettre systématiquement {} as number
. Ce type sera passé à tous les composants d'input (et en particulier le composant standard d'Input qui vient de se doter d'un mode de saisie pour les nombres), ce qui leur permet désormais de retourner directement le bon type dans leurs onChange
, au lieu de string
systématiquement. A noter que seuls "string"
et "number"
sont gérés par les composants existants (Input
, Autocomplete
et les différents Select
), et que tout ce qui n'est pas "number"
sera considéré comme "string"
.
Domaines et validateurs
Il faut désormais créer un domaine via la fonction domain<T>()
, qui s'utilise de la manière suivante :
const DO_DATE = domain<string>()({
InputComponent: InputDate,
inputProps: {
inputFormat: "DD/MM/YYY"
}
});
Cela permet de connaître le type du champ dans le domaine, d'arrêter de devoir écrire le type à la main (ce qui est pénible lorsqu'on met des composants custom), et cela va permettre certaines des évolutions suivantes :
displayFormatter
à la bonne signature et les bons types d'Input sont choisis
displayFormatter
est maintenant (value: T | undefined) => string
, au lieu d'avoir un value: any
qui pouvait changer selon si la valeur venait d'un onChange ou non (le problème se posait pour les nombres). Un domaine de type number
choisira un Input de type "number"
.
Attention cependant, le type du domaine n'existe que pour Typescript et la vraie valeur de "type"
qui sera passée à l'Input sera celle de $field.fieldType
. Dans le cas général c'est transparent, mais attention à makeField
auquel il faudra préciser le fieldType
, sinon cela ne marchera pas comme attendu (et il y aurait une erreur TS).
Exemple :
makeField(() => this.myNumber, {fieldType: "number"} as const, value => this.myNumber = value, true)
Les validateurs sont maintenant liés au type du domaine
FunctionValidator<T>
est disponible pour tous les types, NumberValidator
pour les nombres et StringValidator
, RegexValidator
, EmailValidator
et DateValidator
pour les strings.
NumberValidator
remplace les regex
NumberValidator
propose désormais les options min
, max
et maxDecimals
(qui remplace isInteger
) pour valider les nombres. Il n'est de toute façon plus possible d'utiliser des regex puisqu'elles sont limitées aux strings et qu'on traite enfin les nombres comme des nombres de bout en bout.
inputFormatter
et unformatter
ont été retirés
S'il y a quelque chose à faire de cet ordre, c'est au composant d'input de s'en charger. En particulier, la quasi-totalité des usages ce ces fonctions sont maintenant gérés nativement dans le composant d'Input.
patchField
merge désormais les validateurs
Un validateur ajouté par patchField
est maintenant ajouté en plus des éventuels validateurs existants, au lieu de les remplacer. Il n'y a pas vraiment de raison de vouloir retirer un validateur existant et bien souvent on s'embête à vouloir garder les deux. C'est maintenant le comportement standard et on ne peut pas d'en échapper.
cloneField
La fonction cloneField(field, isEdit)
a été ajoutée et est un raccourci pour makeField(() => field.value, field.$field, value => field.value = value, isEdit)
qui est un pattern utilisé de temps en temps qui pouvait être simplifié.
Dépréciation de util
en faveur de @disposeOnUnmount + réaction
Vous êtes désormais priés de ne plus utiliser @classAutorun
et consorts et de vous rapatrier sur le décorateur standard @disposeOnUnmount
qui s'utilise ainsi :
@disposeOnUnmount
r = autorun(async () => store.replace(await load(store2.id)));
// ou
disposeOnUnmount(this, autorun(/* … */));
Le problème des décorateurs existants est qu'ils ne couvrent qu'une partie des possibilités de réactions, le font plutôt mal (@classReaction en particulier) et qu'ils ne se transfèreront pas vers les nouveaux usages de React dans le futur.
Attention : le correctif attendu sur mobx-react concerne @disposeOnUnmount
qui se comporte mal si vous avez plusieurs instances du même composant. Vous pouvez rester sur @classAutorun
si vous êtes concernés en attendant la release finale
makeFormNode(this, …)
et makeFormActions(this, …)
Sur le même sujet, chacune de ces deux méthodes crée une réaction (synchronisation et chargement), qu'il conviendra de disposer lorsque le composant qui l'a créé sera démonté. Jusqu'à présent, c'était fait dans le <Form>
associé aux actions, qui disposait les deux. Maintenant qu'on peut complètement dissocier formNode
, actions
et Form
(depuis la v9 et surtout la 9.7), ce moyen de procéder peut poser beaucoup de problèmes en pratique lorsqu'on commence à vraiment les dissocier (surtout si le <Form>
n'est pas dans le même composant que les autres).
Il n'y a pas beaucoup de solution pour contourner le problème, à moins de lier explicitement makeFormNode
et makeFormActions
à leur composant, et la manière la plus standard (pour ne pas déjà contredire le point précédent) est de passer this
en premier paramètre de ces fonctions. En interne, elles appellent simplement disposeOnUnmount(this, reaction)
une fois l'objet créé.
(La migration est simple, ça se fait très bien avec un search and replace)
Saves multiple sur makeFormActions
Il est maintenant possible, via la propriété save
de ActionConfig
, de renseigner plusieurs services de sauvegarde dans les actions. Ils seront accessibles directement sur l'objet d'actions. Cela permet de partager l'état de chargement entre toutes les actions et de ne pas avoir a reproduire ce que fait le save pour d'autres actions "secondaires". Tous les saves ont exactement la même implémentation, ils ne diffèrent que par le service qu'ils appellent.
Exemple :
const actions = makeFormActions({save: {
default: saveObject, // Le service `default` est obligatoire, ce sera le save par défaut
delete: deleteObject // Oui, une suppression peut être considérée comme une sauvegarde
});
actions.save();
actions.delete();
referenceStore.item.getLabel()
et typage valueKey
/labelKey
Les items d'un ReferenceStore sont maintenant munis d'une méthode getLabel
qui permet de résoudre un libellé à partir d'un code.
De plus, à condition de typer correctement les types de référence générés, valueKey
et labelKey
peuvent à nouveau être typé par le nom de la propriété qui correspond. Cela permet de vérifier l'appel à getLabel
, ainsi l'usage de makeReferenceList
pour être sur que valueKey
et labelKey
pointent sur des propriétés qui existent.
Mise à jour de la génération
Certaines nouvelles fonctionnalités nécessitent des évolutions de la génération de code. Il faut :
- Renseigner "number", "string" et "boolean" dans le fieldType quand les types correspondent
- Typer littéralement
fieldType
(commetype
qui vaut aujourd'hui"field" as "field"
), ainsi quevalueKey
etlabelKey
dans les liste de référence.
TS 3.4 ajoute une fonctionnalité d'assertion en "const
", qui permet de considérer une propriété ou tout en objet comme constant, en ajoutant des readonly partout et surtout en utilisant des types littéraux pour tous les champs, ce qui correspond exactement à ce qu'on souhaite avec nos types générés.
Attention : Il est nécessaire de rester sur la RC (3.4.0-rc
) de TS 3.4 car une régression violente c'est introduite dans la version finale qui fait exploser le typage de tous les stores (en v9). Le fix a été réalisé et mergé sur master
chez eux mais il n'a pas été encore release.
L'évolution de la génération devrait donc générer maintenant les entités comme
const MyEntity = {
name: "myEntity",
fields: {/* … */}
} as const
et les références comme
const myType = {labelKey: "libelle", valueKey: "id", type: {} as MyType} as const
Cela forcerait cependant tout le monde à se mettre à jour vers TS 3.4, mais à priori c'est une mise à jour simple pour tous les projets concernés (qui devraient déjà tous être en 3.3 !).
Breaking changes
Quasiment tout ce dont je parle est un breaking change, mais y a rien de vraiment méchant cette fois-ci (...
v9.8.3
v9.8.1
Passer un getter de la forme () => boolean
à makeFormNode
, et à patchField
/makeField
/patchNodeEdit
dans sa fonction d'initialisation remplace désormais l'état interne d'édition du nœud/champ au lieu de s'y ajouter. Cela veut dire qu'appeler node.form.isEdit = true/false
ou field.isEdit = true/false
n'a plus d'effet dans ce cas, puisque l'état est contrôlé depuis "l'extérieur". En revanche, cela ne permet toujours pas de bypasser l'état d'édition du parent, qui est toujours requis.
Cela ne devrait impacter personne puisque c'était le fonctionnement attendu/compris, et on peut maintenant passer () => true
à des nœuds pour qu'ils restent toujours en édition, ce qui n'était pas possible avant (seul () => false
fonctionnait).
v9.8 / v8.8
Une partie des travaux présentés dans cette release date en réalité de la 9.7.1, mais n'ayant de toute façon jamais fait l'objet d'une vraie release, on va dire que tout fait partie de la 9.8.0)
Cette release contient un bon nombre d'améliorations et de nouvelles fonctionnalités autour des listes et de la recherche, suite au feedback et aux demandes des projets en cours.
Performances et animations (#96, #99, entre autres)
La liste (ainsi que l'ActionBar dans une moindre mesure) a été partiellement réécrite dans un objectif d'amélioration des performances, en particulier au niveau des animations. La librairie d'animation a été remplacée (react-motion
par react-pose
), la nouvelle proposant une API simplifiée (plus besoin de calculer des hauteurs à la main, y compris pour vous avec le detailHeight
) et de meilleures performances.
Pour assurer des performances correctes et réparer une erreur de conception qui date depuis longtemps, la prop itemKey
est maintenant une fonction de data
et est désormais obligatoire.
FacetBox : sections et composants d'affichage personnalisés
Sections
La FacetBox peut maintenant être découpée en sections via la nouvelle prop facetSections
, qui est une liste de la forme {name: string, facets: string[]}
. Cela permet de contrôler l'ordre d'affichage des facettes et de le regrouper par catégories directement depuis la SPA (l'ordre était auparavant forcé par le serveur). Il est possible d'avoir une section sans nom (""
), et si elle est renseignée, toutes les facettes non incluses dans une section seront ajoutées dedans.
Il sera probablement question, dans le futur, de proposer de pouvoir "plier" les facettes et les sections, mais on en est pas encore là.
Composants customs (#81)
Via la prop customFacetComponents
, il est maintenant possible de remplacer le composant d'affichage standard d'une facette par le votre, qui doit respecter la même API que le composant standard. Libre à vous ensuite d'implémenter le comportement que vous voulez, que ça soit un (ou plusieurs) slider(s), un autocomplete… C'est à vous également de gérer le caractère multi-sélectionnable de la facette.
ActionBar : groupableFacets
Une nouvelle prop a été ajoutée sur l'ActionBar, groupableFacets
, qui permet de préciser la liste des facettes sur lesquelles il est possible de grouper. Auparavant, toutes les facettes apparaissaient obligatoirement dans la liste sans pouvoir en enlever.
SearchChip
avec chipThemer
et chipKeyResolver
(#64)
Il est maintenant possible, à travers la recherche avancée, de pouvoir personnaliser les Chips en fonction de leur position, du champ qu'ils représentent ainsi que de la valeur (si applicable) :
chipThemer(type, code, value?) => ChipTheme
permet de modifier le rendu de n'importe quel chip, par exemple pour afficher une facette en couleur uniquement dans le Summary, ou encore afficher une valeur de filtre en particulier en gras…chipKeyResolver(type, code, value) => Promise<string>
permet de fournir une fonction de résolution de clé pour n'importe quelle valeur affichée dans un Chip. Le besoin initial était de pouvoir résoudre des IDs dans des filtres (qui ne bénéficient pas de libellés donnés par le serveur comme les facettes), mais on peut également s'en servir pour remplacer un libellé existant de facette. LechipKeyResolver
sera utilisé pour tous les chips si renseigné, mais il suffit de ne rien retourner pour les cas où on ne veut pas surcharger des libellés.
Améliorations diverses sur les listes
LoadingState
L'état de chargement (le "chargement..." qui apparait en dessous de la liste lors d'une recherche) est maintenant porté par la List (et non la recherche), et est pilotable par le nouvelle prop isLoading
si on n'utilise pas un store de recherche. Cela veut dire qu'on n'affiche plus l'empty state pendant le chargement.
Il est également possible, comme pour l'empty state, de surcharger le rendu du loading state par son propre LoadingComponent
.
Modes d'affichage pour les boutons d'actions (évoqué dans #95)
L'API des operationLists
a été modifiée et étendue pour permettre plus de libertés sur les types de boutons, et propose désormais des "modes" d'affichage. La propriété mode
remplace showIcon
et isSecondary
et peut prendre les valeurs suivantes :
"icon"
: affiche un IconButton simple"label"
: affiche un Button simple"icon-label"
: affiche un Button simple avec l'icône (valeur par défaut)"icon-tooltip"
: affiche un IconButton avec une tooltip"secondary"
: affiche l'action dans la liste des actions secondaires
Le mode mosaïque utilise toujours des boutons "FAB" et ne respectera donc pas les modes d'affichage demandés. Par contre, la tooltip sera bien posée.
Réinitialisation de la pagination quand la liste change (#100)
La pagination interne d'une liste (qui est indépendante de la pagination serveur de la recherche) sera maintenant réinitialisée si le contenu affiché de la liste a été modifié (la pagination ne sera pas réinitialisée si on ne fait qu'ajouter des données à la suite de la liste existante).
Améliorations diverses
Tooltip sur ButtonMenu (évoqué dans #95)
Il est maintenant possible d'afficher une tooltip sur le bouton d'un ButtonMenu.
Personnalisation du boutons d'actions secondaires du header (demande originale de #95)
Via la nouvelle prop secondaryButton
du HeaderActions
, il est possible de personnaliser entièrement le rendu du bouton des actions secondaires, au même titre qu'il était déjà possible de personnaliser les boutons d'actions principales et les actions secondaires. Cela inclut la possibilité d'y mettre une tooltip.
Bugfixes
TLDR : Breaking changes
itemKey
est maintenant une fonction obligatoire pour toutes les listesdetailHeight
n'existe plus sur la liste (c'est fait automatiquement maintenant)showIcon
etisSecondary
sont remplacé parmode
dansOperationListItem
8.8.0
Focus v4 a beaucoup évolué depuis le lancement de la v9, non rétro-compatible avec le v8, et même si certains de ses changements resteront à jamais inaccessible aux projets bien avancés en v8, il doit quand même y avoir moyen de reporter une bonne partie des améliorations réalisées en dehors du cadre des formulaires.
Cette version 8.8.0, à jour sur la version courante de la v9 (aujourd'hui 9.8.0
) a été publiée après de nombreux tests. En particulier, elle contient :
- React 16 (obligatoire avec react-pose de toute manière)
- Toutes les autres mises à jour de dépendances, y compris celle de MobX qui a eu des breaking changes.
- Toutes les évolutions sur les composants et les collections, donc au moins tout ce qui a été présenté dans cette release note.
En plus de ne pas toucher au module entity
, cette version ne modifie aucun comportement qui est dérivé de code généré, donc les améliorations sur l'API de fetch
et du store de référence sont également absentes.
Le @autobind
sur AutoForm
a été retiré car il posait des problèmes avec la mise à jour de MobX et de chaînes d'héritage trop longues. Faites attention à bien rebinder ses méthodes à l'utilisation (en particulier this.save
...)
La mise à jour d'un projet existant en 8.7 vers 8.8 ne sera probablement pas immédiate, mais l'idée est de limiter le plus possible les breaking changes inutiles et de se concentrer sur l'essentiel.
La mise à jour vers la 8.8 permettra également aux projets qui souhaitent la mener de bénéficier de toutes les nouveautés qui viendront par la suite, genre ça par exemple...
v9.7
Nouveautés sur les formulaires
"transform" => "initializer" (#88)
La fonction de transformation de makeFormNode
est maintenant appelée initializer, pour refléter le fait que son scope d'action n'est pas limité à du patchField
ou du makeField
, mais qu'on peut bien s'en servir pour initialiser le nœud de formulaire au sens large (valeurs initiales en particulier)
Création d'un FormNode vide (#85)
Il est maintenant possible de créer un FormNode vide, au lieu de toujours recopier le contenu du nœud source à la création.
La signature de makeFormNode
à changé en conséquence et est maintenant makeFormNode(node, {isEdit?, isEmpty?}, initializer)
.
onClickCancel
, onClickEdit
(#87)
makeFormActions
propose maintenant deux callbacks onClickCancel
et onClickEdit
à la place de onToggleEdit
. L'usage à montré que dans l'écrasante majorité des cas les deux actions provoquaient des effets distincts.
Synchronisation du FormNode (#86)
La synchronisation du FormNode sur son nœud source est maintenant réalisée champ par champ et même si la mise à jour du champ source n'a pas d'effet sur lui-même. Pour référence, jusqu'à présent une simple autorun sur le reset()
global sur FormNode était posée à sa création.
Les ajouts et suppressions d'éléments de listes (sur le nœud source, ce qui est quand même peu fréquent en dehors d'un replaceNodes
) sont gérés séparément, puisque tous les éléments de la liste du formulaire ne se retrouvent pas forcément dans le nœud source. De façon générale, tout élément ajouté ou supprimé de la liste source sera ajouté ou supprimé (s'il ne l'a pas déjà été) dans la liste du formulaire. Les ajouts/suppressions de la liste du formulaire ne seront annulés que si on réinitialise totalement la liste source, via un replaceNodes
ou un reset()
.
Concrètement, cela apporte deux bénéfices :
- Une modification d'une partie du nœud source ne réinitialisera que la partie affectée par la modification (au champ près)
- Une réinitialisation du nœud source via un
replace
du même contenu (par exemple si je recharge le même contenu dans unload
) réinitialisera bien toujours le formulaire, même si le nœud source reste inchangé (au champ près)
De plus, les réactions de réinitialisation seront disposées au démontage du <Form>
qui utilise les champs et les listes concernées. Cela correspond au comportement précédent qui disposait la réaction globale au démontage du <Form>
qui utilisait le nœud global. Il n'y a pas vraiment d'autre solution, mais je doute fortement que cela pose un problème en pratique.
Breaking changes
StoreListNode#$transform
=>StoreListNode#$initializer
makeFormNode(node, isEdit)
=>makeFormNode(node, {isEdit, isEmpty})
. Il faudrait donc retirer lesentity.clear()
qui traînent et qui sont maintenant inutiles.FormActionsConfig.onToggleEdit
=>FormActionsConfig.onClickCancel
+FormActionsConfig.onClickEdit
FormActionsConfig.clearBeforeInit
supprimé au profit deisEmpty
surmakeFormNode
- En théorie l'intégralité de #86 est un breaking change, mais en pratique je pense qu'on évitait les cas qui posaient problème et qui sont maintenant améliorés. Il y aurait possiblement des
entity.clear()
à retirer, ajoutés pour forcer des mises à jour suite à un rechargement.
Exemple de migration
Voir klee-contrib/focus4-starter-kit@c99efb8
Autres
#93 : Migration vers le nouveau contexte React, en vue de la suppression prochaine de l'ancien. De toute façon, depuis qu'il y a static contextType
, il n'y avait plus de raison de ne pas migrer.