11.17.5 • Published 12 days ago

@focus4/core v11.17.5

Weekly downloads
193
License
MIT
Repository
github
Last release
12 days ago

Module message

Essentiellement similiaire à celui de Focus V2, avec le même MessageCenter sous forme de Snackbar Material. Il permet de centraliser et d'afficher des messages de succès, de warnings ou d'erreurs issu du reste de l'application. De base, il est alimenté par les formulaires (entity/auto-form) et les erreurs de requêtes serveurs (network/error-parsing). Il est posé par défaut par le Layout.

Module network

Le module propose un RequestStore similiaire à celui de Focus V2 pour suivre la progression des requêtes, ainsi qu'un composant pour les afficher (LoadingBar).

La fonction custom fetch a par contre été remplacée par des wrappers httpGet, httpPost, httpPut et httpDelete autour de window.fetch, qui est le nouveau standard pour les requêtes Ajax. En plus d'être des surcharges plus simples à utiliser, elles mettent également à jour le RequestStore et gèrent les éventuelles erreurs en parsant les messages pour les reporter sur les champs ou dans le MessageStore. Cette dernière reponsabilité était auparavant confiée à l'actionBuilder dans la V2.

Module router

Bases

Le router proposé par Focus V4 est un peu particulier et est basé sur les idées proposées par le créateur de MobX.

Il s'articule autour d'un ou plusieurs store(s) (le ViewStore) qui sert d'intermédiaire entre le "vrai" routeur et le reste de l'application. Ce store expose une propriété observable currentView, qui est le miroir de l'état de l'URL. Modifier l'URL va mettre currentView à jour en conséquence, et inversement.

Exemple :

const viewStore = new ViewStore({view: {page: "", id: "", subPage: "", subId: ""}});
const router = makeRouter([viewStore]);

// Démarrage
async function preload() {
    await router.start();
}

// navigation vers "/structure/1/detail" -> viewStore.currentView = {page: "structure", id: "1", subPage: "detail"}
// action utilisateur
viewStore.setView({id: 5}); // -> URL = "/structure/5/detail"

Il est ensuite très facile dans des composants de se synchroniser au ViewStore en utilisant store.currentView comme n'importe quelle autre observable. Ainsi, on abstrait entièrement le routing des vues et on interagit à la place avec un store (= de l'état), ce qu'on fait déjà pour tout le reste.

Plusieurs ViewStores

Pour pouvoir découpler les différentes parties d'une application, on va vite ressentir le besoin de définir un ViewStore par module de l'application.

Le constructeur du store prend un deuxième paramètre, le prefix, qui représente le nom du store et sera le préfixe de toutes les routes liés à un store. Naturellement, il est obligatoire de spécifier tous les préfixes à partir du moment où on a plusieurs stores.

La fonction makeRouter prend un array de ViewStore comme second paramètre pour cette usage-là. Le premier store de la liste sera le store par défaut : c'est sur ce store-là que l'application va démarrer.

Un routeur avec plusieurs stores gère la notion de "store actif", c'est-à-dire qu'il va déterminer quel store est actif automatiquement. La règle est très simple : c'est le dernier store modifié qui est actif. Chaque store possède une propriété isActive pour savoir s'il est actif. La fonction makeRouter retourne un objet contenant la listes des stores, la méthode start(), une propriété observable currentStore qui contient le store actif, et une méthode to(prefix) permettant de naviguer vers l'état par défaut du ViewStore choisi (cette navigation n'entraînant pas de modification d'état, on est forcé de l'effectuer via le routeur au lieu d'une interaction avec un store).

Exemple d'usage :

export const homeView = new ViewStore({
    prefix: "home",
    view: {page: "" as undefined | "test" | "list", id: "" as string | undefined}
});
export const testView = new ViewStore({prefix: "test", view: {lol: ""}});

const router = makeRouter([homeView, testView]);

const Main = observer(() => {
    const {currentStore} = router;
    if (currentStore.prefix === "home") {
        switch (currentStore.currentView.page) {
            case "test":
                return <Test />;
            case "list":
                return <List />;
            default:
                return <Home />;
        }
    } else if (currentStore.prefix === "test") {
        return <div>Test Store "{currentStore.currentView.lol}"</div>;
    } else {
        return <div>déso</div>;
    }
});

(oui oui, le currentStore est statiquement typé avec le bon store lorsqu'on distingue par préfixe !)

beforeEnter

Il est possible de définir un hook beforeEnter sur un ViewStore (dans le constructeur) qui va s'exécuter juste avant une navigation (que ça soit par URL ou par setView()), qui peut retourner 3 choses :

  • {redirect: view}, pour rediriger vers la vue retournée. Par exemple : {redirect: {page: "home"}}.
  • {errorCode: "code"}, pour rediriger vers la page d'erreur avec le code demandé (voir plus bas).
  • undefined, pour ne rien faire. Ce hook permet d'ajouter de la logique pour par exemple bloquer l'accès à certaines pages si l'utilisateur n'a pas les droits, ou pour combler une URL qui n'existe pas.

Page d'erreur

Le routeur gère, en plus des différents ViewStores, une page spéciale destinée aux erreurs. Cela correspond au cas ou aucun store n'est actif : dans ce cas, currentStore vaut {prefix: "error", errorCode: "your_code"}. On y accède soit par une erreur personnalisée retournée dans un beforeEnter, soit lorsqu'une route n'est pas matchée (errorCode = "notfound"). C'est donc a l'utilisateur, dans le switch principal de l'application, de concevoir ses propres pages d'erreurs en fonction du code. (le nom de la page "error" et le code "notfound" sont configurables)

API du ViewStore

export declare class ViewStore<V, N extends string> {
    /** Préfixe éventuel du store. */
    readonly prefix?: N;

    /**
     * Construit un nouveau ViewStore.
     * @param config La configuration du store.
     */
    constructor({beforeEnter, view, prefix}: ViewStoreConfig<V, N>);

    /** Calcule l'URL en fonction de l'état courant. */
    readonly currentPath: string;

    /** Représente l'état courant de l'URL. */
    readonly currentView: View<V>;

    /** Précise si le store est actuellement actif dans le router. */
    readonly isActive: boolean;

    /**
     * Récupère l'URL pour la vue donnée.
     * @param view La vue à récupérer.
     * @param replace Ne fusionne pas la vue demandée avec la vue courante.
     */
    getUrl(view?: Partial<V>, replace?: boolean): string;

    /**
     * Met à jour la vue courante.
     * @param view La vue souhaitée.
     * @param replace Ne fusionne pas la vue souhaitée avec la vue courante.
     */
    setView(view: Partial<V>, replace?: boolean): void;

    /**
     * Effectue l'action fournie à partir de la vue courante et filtre les résultats "faux".
     * @param block L'action à effectuer.
     */
    withView<T>(block: (view: View<V>) => T | undefined | "" | false): T;
}

Il est important de noter que currentView est totalement immutable et que la seule manière de modifier l'état de la vue est de passer par la fonction setView() (ou bien de passer par l'URL directement).

Etendre un ViewStore

Un ViewStore représente l'état global du module de l'application auquel il est associé, et est par conséquent visible et utilisable par tous les composants de ce module. Par défaut, il ne contient que l'état stocké dans l'URL, mais il peut être intéressant d'y stocker également d'autres informations liées, qui dérivent fonctionnellement de l'état de la vue courante.

Par exemple, si le ViewStore gère un module qui concerne un objet métier en particulier, on va certainement stocker son ID dans l'URL. De plus, il y aura probablement d'autres infos générales sur cet objet métier que l'on va vouloir partager dans tout le module, comme son nom, son identifiant métier, son état... Des infos que l'on peut récupérer facilement via une requête au serveur la plupart du temps. L'idéal, ça serait de les garder toujours à portée de main dans le ViewStore. On pourrait donc faire quelque chose du genre de :

class MyViewStore extends ViewStore<{id: string}, "objet"> {
    @observable resume: ObjetResume = {};
    constructor() {
        super({prefix: "objet", view: {id: ""}});
        autorun(() => this.withView(async ({id}) => id && (this.resume = await loadObjetResume(+id))));
    }
}

Ainsi, à chaque fois que je change l'id de la currentView, mon résumé est rechargé et le reste du module pourra toujours compter sur ces informations à jour.

Synchroniser un composant sur un ViewStore

Puisque l'on vient d'établir un ViewStore comme étant l'unique source de vérité sur les informations de base du module courant, il faudrait donc faire dépendre tous les composants directement du ViewStore, quitte à devoir l'importer (ou l'injecter si on veut vraiment être rigoureux) partout.

Par exemple, si je veux charger des données qui dépendent de l'ID courant dans un composant, alors il serait idéal de faire comme ceci :

class Component extends React.Component {
    @disposeOnUnmount
    load = autorun(() => {
        viewStore.withView(async ({id}) => id && (this.data = await loadData(+id)));
    });
}

à la place d'un componentWillMount classique. Ce fonctionnement permet d'assurer la synchronisation du composant avec le store sans passer par une prop id et gère directement l'initialisation comme la mise à jour (on aurait eu besoin d'à la fois componentWillMount et componentWillReceiveProps pour obtenir le même comportement sans réaction, du coup ça fait relativiser la syntaxe).

Le gros point fort en faisant ça, c'est qu'on peut librement modifier l'id dans le ViewStore (via l'URL ou dans le code) et voir tous nos composants se remettre à jour sans remonter le moindre composant, sans effort supplémentaire.

L'AutoForm créé nativement une réaction pour le chargement à partir de la fonction getLoadParams qu'on lui passe dans la configuration. Donc si cette fonction ressemble à quelque chose comme () => viewStore.withView(({id}) => id && [+id]), alors il bénificiera de la synchronisation.

Note : la synchronisation par réaction n'est pas à faire en toute circonstances. Par exemple, si un module affiche des composants différents selon un état global qui peut changer avec l'ID, alors ces composants ne doivent pas être synchronisés. Dans ce cas, la meilleure solution est de synchroniser le composant racine et de s'assurer que l'on remonte tous les composants enfants à chaque changement d'ID. fromPromise est pratique pour ça.

A propos de ce qu'on vient de faire

Cela veut dire que l'on va utiliser de moins en moins de props à nos composants et que l'on va d'avantage s'appuyer directement sur l'état de stores définis globalement. Il faut bien comprendre que MobX est infiniment meilleur que React pour décrire et déterminer comment un composant doit réagir à des changements de state ou de props, donc on va essayer de l'utiliser le plus possible. Rien n'empêche de définir des stores spécialisés dérivés pour restreindre l'accès au state global pour des composants spécialisés si on veut être plus propre, l'important est bien de n'avoir aucun intermédiaire entre la source de vérité (le store) et son usage.

En gros, si on exagérait un peu, on pourrait dire qu'il ne faudrait surtout pas faire ça :

render() {
    return <MyComponent id={+viewStore.currentView.id} />;
}

Cela isole MyComponent de viewStore, bloque l'établissement de réactions sur le changement d'ID (forçant à utiliser du cycle de vie genre componentWillReceiveProps à la place) et force un intermédiaire non nécessaire entre les deux, qui sera inclus dans la chaîne de réaction alors qu'il n'utilise pas la valeur. Après, il y aura bien un moment où il faudra se dissocier du store. L'important c'est d'essayer de le faire le plus tard possible.

Si on ne veut pas coupler en dur MyComponent avec viewStore, il vaut mieux faire :

render() {
    return <MyComponent view={+viewStore.currentView} />;
}

Ces recommandations ne sont pas absolues et à utiliser en tout circonstances, mais ce sont des idées qu'il faut avoir en tête.

Module user

Ce module propose une base de store utilisateur, pour y stocker les données de la session. La seule fonctionnalité prévue est la gestion des rôles / permissions (avec roles et hasRole()), et c'est à chaque application d'y rajouter leurs informations métiers pertinentes.

11.17.5

12 days ago

11.17.0

23 days ago

11.16.4

2 months ago

11.16.1

2 months ago

11.16.0

3 months ago

11.15.0

3 months ago

11.14.0

4 months ago

11.13.0

4 months ago

11.12.6

4 months ago

11.12.4

5 months ago

11.12.2

6 months ago

11.12.0

6 months ago

11.10.0-rc.0

8 months ago

11.12.0-beta.0

6 months ago

11.11.0

7 months ago

11.11.3

7 months ago

11.9.0-rc.0

8 months ago

11.9.0

8 months ago

11.10.2

7 months ago

11.10.1

7 months ago

11.11.0-beta.0

7 months ago

11.10.0

8 months ago

11.8.4

9 months ago

11.8.2

9 months ago

11.8.1

10 months ago

11.8.0

12 months ago

11.7.5

1 year ago

11.7.6

1 year ago

11.7.3

1 year ago

11.7.4

1 year ago

11.7.0-rc.0

1 year ago

11.7.0

1 year ago

11.6.6

1 year ago

11.6.2

1 year ago

11.6.0

1 year ago

11.6.0-beta.0

1 year ago

11.5.1

2 years ago

11.5.2

2 years ago

11.5.6

1 year ago

11.4.6

2 years ago

11.5.0

2 years ago

11.4.0

2 years ago

11.4.1

2 years ago

11.4.5

2 years ago

11.3.1

2 years ago

11.3.0

2 years ago

11.2.0-alpha.0

2 years ago

11.2.0

2 years ago

11.1.3

3 years ago

11.1.1

3 years ago

10.9.2

3 years ago

11.0.0

3 years ago

11.0.0-rc.0

3 years ago

11.0.0-preview.9

3 years ago

11.0.0-beta.0

3 years ago

11.0.0-beta.1

3 years ago

11.0.0-6.0

3 years ago

11.0.0-preview.6

3 years ago

11.0.0-preview.7

3 years ago

11.0.0-preview.4

3 years ago

10.9.0

3 years ago

10.9.0-beta.1

3 years ago

10.9.0-beta.0

3 years ago

10.8.1

3 years ago

10.8.8

3 years ago

10.8.0-preview.1

3 years ago

10.8.0-beta.0

3 years ago

10.8.1-beta.0

3 years ago

10.8.0-preview.0

3 years ago

10.8.0-test.0

3 years ago

10.7.5

3 years ago

10.8.0-alpha.0

3 years ago

10.7.4

3 years ago

10.7.2

3 years ago

10.7.1

3 years ago

10.7.1-rc.0

3 years ago

10.7.0-rc.0

4 years ago

10.7.0-beta.5

4 years ago

10.7.0-beta.2

4 years ago

10.7.0-beta.0

4 years ago

10.7.0-alpha.0

4 years ago

10.6.1

4 years ago

10.6.0-rc.1

4 years ago

10.6.0-rc.0

4 years ago

10.6.0-beta.9

4 years ago

10.6.0-beta.7

4 years ago

10.6.0-beta.8

4 years ago

10.6.0-beta.6

4 years ago

10.6.0-beta.5

4 years ago

10.6.0-beta.4

4 years ago

10.6.0-beta.2

4 years ago

10.6.0-beta.0

4 years ago

10.6.0-alpha.0

4 years ago

10.5.0

4 years ago

10.5.1

4 years ago

10.5.0-alpha.0

4 years ago

10.4.6

4 years ago

10.4.0

4 years ago

10.3.3

4 years ago

10.3.2

4 years ago

10.3.1

4 years ago

10.3.0-rc.2

4 years ago

10.3.0-rc.0

4 years ago

10.3.0-alpha.0

4 years ago

10.2.0

4 years ago

10.2.0-rc.1

4 years ago

10.2.0-rc.0

4 years ago

10.1.5

4 years ago

10.1.0

5 years ago

10.0.0

5 years ago

10.0.0-rc.2

5 years ago

10.0.0-rc.0

5 years ago

10.0.0-beta.8

5 years ago

10.0.0-beta.7

5 years ago

10.0.0-beta.2

5 years ago

10.0.0-beta.1

5 years ago

10.0.0-alpha.2

5 years ago

10.0.0-alpha.1

5 years ago

10.0.0-alpha.0

5 years ago