Naivete Precedes Misrepresentation

    @mondosha1/feature-store-kit
    TypeScript icon, indicating that this package has built-in type declarations

    0.0.1-beta-032 • Public • Published

    FSK: Feature Store Kit

    Intro

    What is the FSK?

    FSK is a set of helpers and tools to help building NgRx feature stores for lazy-loaded libraries.

    Features:

    • Allows synchronization between NgRx stores, Angular forms and Angular route parameters (cf Managing State in Angular Applications by Victor Savkin).
    • Avoid boilerplate: write actions, reducer and effects only if you need them!
    • Not a replacement of NgRx but built on top of it. No inheritance, only composition: easily add or remove FSK from a feature store without impacting its behavior.

    What is it based on?

    FSK takes benefits of standard technologies used in an Nx application:

    How to use it?

    Today you can choose how you want to integrate the FSK in your own code.

    The traditional way: by your own bare hands.

    This means you have to add all configuration needed inside your module, and implement all you need (state, reducer, structure, effects, etc…)

    This will be necessary if you have to implement it inside an old module.

    But if you create a new fresh module, we recommend you to use our schematic !

    This will create you the entire module customized for your needs.

    Let’s see that more in details

    With the Mondosha1 library schematic

    We recommend you to use Nx Console for VSCode or for Webstorm when you want to use our schematic. This tool provide you a nice UI and let you check a dry run version of what you will generate with the schematic.

    Let’s see what you have to to generate your FSK library.

    As described by the gif below, first go to generate → library .

    The only mandatory information is the name of your library.

    image

    Now let’s see in details what options you can use in order to customize your new library.

    image

    With your little hands

    1. Declare the state, initial state and structure
    2. Inject the FeatureStoreModule
    3. Add FSK onto route definition
      1. Angular Routes
      2. Dialog paths
    4. Generate the form with the form helper
    5. Use effect helpers
    6. Create your selectors
      1. The standard NgRx way (with createFeatureSelector)
      2. Reselect on FSK (selector based on getStateWithoutMetaData w/ props)
    7. Create a facade (optional but advised)

    How does it work?

    Update flow

    image

    Source: https://www.lucidchart.com/documents/edit/c71861f2-377f-4a4a-89b3-661e45090461/0?beaconFlowId=CCE36D22DB246700#

    1. An effect on ROUTER_NAVIGATION (see ngrx/router for more information) parses router parameters (see difference with router params and query params) from the URL, formats them depending on the defined structure and triggers the updateStoreFromParams action to update the store. The effect and its mechanic is only triggered if the router segment which parameters are attached on correspond to the feature store key. Then, the updateStoreFromParams action is handled by the feature store meta-reducer and updates the store for the matching feature store key. \
    2. In the feature store form helper, the form subscribes the stateWithoutMetaData$ method from the facade (listening to the feature store stateWithoutMetaData selector). When a new state is emitted by the selector, the form will patch its value and possibly make some additional controls changes (for form arrays for example). \
    3. Still in the feature store form helper, a subscription on form value changes

    Reset

    Submit

    Documentation

    Module

    FSK provides an Angular Module named FeatureStoreModule which is given a configuration object.

    Below is the list of available options:

    Name Description Mandatory
    featureStoreKey The identifier of the current FSK library. It must correspond to the featureName injected in the NgRx store during StoreModule.forFeature declaration. Yes
    initialState The initial state used as default state in the NgRx reducer and optionally injected with the feature name during StoreModule.forFeature declaration. Yes
    structure The description of the store with the name of the fields, their type and their validators. Yes
    structurePathsForForm A whitelist of the paths (dot-separated) in the structure to create form controls for. If not given or empty, then controls will be generated for all paths in the structure. No
    structurePathsForParams A whitelist of the paths (dot-separated) in the structure to generate route parameters for. If not given or empty, then all paths in the structure will be forwarded as route parameter. No
    children A list of feature stores which will be initialized, submitted and reset at the same time as the current store. We says this one acts as the parent feature store of these child feature stores. No

    Structure

    Intro

    The FSK structure file is a serializable representation of the state and its typing. It should make the state and its possible sub-parts easily understandable.

    Why?

    The structure is used for form groups, arrays and controls generation on the one hand. And it’s used for router params parsing and formatting in the other hand.

    Structure format

    The format of the structure is opinionated and is presented as a TypeScript interface.

    Why don’t we use the format of controls configs from the Angular Form Builder?

    Because, we want the structure to be serializable in order to possibly retrieve structures dynamically, from HTTP requests for example.

    Controls configs are not serializable as they may contain Angular validators which are functions.

    Why don’t we use a JSON file following the JSON Schema specification?

    The main benefit of a plain object is that it’s strongly typed. Interfaces and types describe how should be declared the structure. Same types are used by the structure helper in order to generate the reactive forms. But we could handle a JSON schema format additionally for dynamic structures retrieval.

    Form controls generation

    Depending on the given structure, a reactive form will be generated with form controls, form arrays or child form groups.

    Default values of form controls are handled by the NgRx initialState and are not duplicated in the structure to keep DRY.

    See the part To complete

    Effects

    Intro

    To complete

    updateStoreFromParams

    To complete

    navigateToStore

    To complete

    updateParamsFromForm

    To complete

    initStoreFromParent

    To complete

    submitIfValid

    To complete

    resetStoreOnLeave

    To complete

    Form

    Form groups, controls and arrays

    Form controls are declared in the structure

    Form group

    The root level of the structure must be a plain object and will inevitably generate a form group. It’s the obvious behavior as the root level of an Angular Form is a form group.

    It is also possible to generate child form groups, for example for structuring the store into sub-parts or into split smart components. To achieve this, the structure part corresponding to the child form group should also be an object where each key is associated a form control to.

    In the example below, engine is a child form group of the structure where name and cylinders are controls of the child form.

    {
      brand: 'Peugeot',
      engine: {
        name: 'string',
        cylinders: 'number'
      }
    }

    Form control

    Controls are always leaves of a form group. In this way, it can be declared:

    • In the simplest way, as a simple field type: 'string' **| **'number' **| **'boolean' **| **'object' **| **'date'

      {
        brand: 'Peugeot'
      }
    • With the combination of a type and validators:

      {
        brand: {
          type: 'Peugeot',
          validators: ValidatorName.Required
        }
      }
    • As an array of simple fied types:

      {
        valves: {
          items: 'number',
          type: 'array'
        }
      }

    For array of complex types, a form array will be generated instead of a form control.

    Form array

    Array of complex items will generate Angular Form Arrays where each item will be represented as a form group with associated controls, following the item structure.

    // structure of an array of complex field types
    {
      wheels: {
        type: 'array',
        items: {
          width: 'number',
          height: 'number',
          diameter: 'number'
        }
      }
    }
    
    // will generate the Angular Form Array
    new FormArray([
      new FormGroup({
        width: new FormControl(),
        height: new FormControl(),
        diameter: new FormControl()
      }),
      
    ])

    Array of objects do not always need to be represented as an array of complex types. A form control of type object may be sufficient.

    To conclude, it really depends on the need to edit or not values of the array items themselves.

    Validators

    Structure validators are serializable and used for Angular Forms generation only.

    There are two kinds of validators:

    • Angular validators
    • Formulas

    Angular validators

    To remain serializable, the angular validators are declared as string in the structure.

    {
      name: {
        type: 'string',
        validators: 'required'
      }
    }

    A string enum helps retrieving them.

    {
      name: {
        type: 'string',
        validators: ValidatorName.Required
      }
    }

    Validators requiring parameters (like maxLength) can be declared using a plain object:

    {
      name: {
        type: 'string',
        validators: {
          name: ValidatorName.MaxLength
          params: { maxLength: 4 }
        }
      }
    }

    Like standard Angular Form controls declaration, several validators can be given.

    {
      url: {
        type: 'string',
        validators: [ValidatorName.Required,ValidatorName.Url]
      }
    }

    Sometimes, you want to apply your validator only for some use cases. For that, you can use the condition property for a validator definition. This one accept formulas.

    {
      id: 'number',
      url: {
        type: 'string',
        validators: [{
          name: ValidatorName.Required,
          condition: 'ISEMPTY(id)'
        }]
      }
    }

    Formulas

    Formulas are a way to validate a form control using an expression as a string (still serializable) in the same manner as we write formulas in Excel.

    It uses the amazing Javascript Expression Evaluator (or expr-eval) library for formulas parsing and evaluation. Formula expressions accept either values, arithmetic or logical operators, and functions. Each property of the feature store state is available as a value and the list of available functions can be read below.

    An should evaluate to a boolean value and is accompanied by an error message which will be displayed if the expression evaluates to TRUE. In other words, the expression can be read as “What should throw an error?”.

    {
      url: {
        type: 'string',
        validators: {
          formula: 'NOT(REGEX(url, "^http[s]?://datastudio\\\\.google\\\\.com/embed(/[a-z]/[0-9])?/reporting/[^/]/page/[a-zA-Z]{4}$"))',
          message: 'The entered URL is not a valid Google Data Studio dashboard URL'
        }
      }
    }

    Below are the set of functions are available in formulas.

    Standard

    Name Type Description
    ISEMPTY boolean Return true if the given expression is empty (null, undefined, false, 0, NaN, '', [])
    ISINTEGER boolean Return true if the given expression is an integer
    ISNUMBER boolean Return true if the given expression is a number
    ISURL boolean Return true if the given expression is a valid URL
    REGEX boolean Return true if the given expression matches the pattern

    ex: REGEX(ip, “\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b”)

    GET any
    LTE number
    INARRAY any[] ex: NOT(fileTypes, INARRAY(["image/jpeg", "jpeg", "jpg"]))
    MAP any[], fn

    Constants

    Name Type Description
    TODAY number The today’s date represented as a timestamp (in seconds)
    NOW number The date at time T represented as a timestamp (in milliseconds)

    Helpers

    Name Type Description
    LENGTH number Return the length of a string or an array
    MAX number Return the maximum value from the given set of numbers.

    eg: MAX(1, 2, 3)

    MIN number Return the minimum value from the given set of numbers.

    eg: MIN(1, 2, 3)

    Logical functions

    Name Type Description
    NOT boolean Return the negation of the given boolean expression or function.

    eg: NOT(age > 18)

    eg: EVERY(names, NOT(ISEMPTY))

    OR boolean Return the disjunction of the given boolean expressions.

    eg: OR(ISEMPTY(age), age < 18)

    AND boolean Return the conjunction of the given boolean expressions.

    eg: AND(age >= 18, NOT(ISEMPTY(firstName)), NOT(ISEMPTY(lastName)))

    Advanced functions

    Name Type Description
    EVERY boolean Returns true if the each value of given array evaluates the given expression to true.

    eg: EVERY(names, NOT(ISEMPTY))

    Store binding

    On value changes

    The Angular form is tightly coupled to the store. Every change in any field of the form will trigger an update of the corresponding field in the store.

    The form updates the store in two different ways depending on the chosen FormUpdateStrategy.

    • ToStore: form values are directly sent to the store which is updated thanks to an action and a reducer case.
    • ToParams: form values are sent to as segment parameters of the current route before updating the store. The route parameters are updated from an action and an effect, then the store is updated thanks to an effect listening to the router actions (see https://ngrx.io/guide/router-store) and a reducer.
    this.formGroup = this.featureStoreFormFactory.getFormBuilder(MY_FEATURE).create({
      takeUntil$: this.destroy$,
      updateStrategy: FormUpdateStrategy.ToParams
    })

    To trigger params or store update, a subscription to the valueChanges observable provided by the form is used.

    To avoid leaks, the subscription should be cancel when the component handling the form creation is destroyed, that’s why a takeUntil$ prop should be fed and correspond to an observable which completes at the component destroy.

    While the form values are updated in the store, the form validity is also synchronized in the metadata of the store.

    Formatter

    It is possible to give a custom formatter function to store values which differ (a bit) from the values returned from the form.

    The example below store the foo property as a number while the form field returned it as a string.

    FeatureStoreModule.forFeature<MY_FEATURE>({
      featureStoreKey,
      initialState,
      structure,
      formatter: state => ({ foo: parseInt(state.foo, 10), ...state })
    })

    It is also possible to get the previous form value in order to compare them and by the way, format the data differently.

    FeatureStoreModule.forFeature<MY_FEATURE>({
      featureStoreKey,
      initialState,
      structure,
      formatter: (newState, oldState) =>
        newState.color !== oldState.color ? { colorUpdated: true, ...newState } : newState
    })

    Ask for validation

    The feature store form subscribes to the askForValidation$ selector which returns true when the askForValidation action is called (manually or via the submitBeforeLeaving guard).

    Once validation asked, the form marks all its controls as dirty (which usually shows form fields errors if they have) and call the submit action. This action is responsible for checking the validity of the form (thank to the corresponding metadata) before triggering any user-defined side-effect stream.

    Patch value

    The last responsibility of the form is to listen to the store changes and update the corresponding form fields. Only the fields specified in the structurePathsForForm property are updated (an empty array or no property given will whitelist all the structure).

    Such updates also work for Angular FormArrays and will create or remove items depending on the changes made in the store. This is a tricky part of the form update as Angular does not help us a lot (mutability, event emitting, etc).

    Meta-reducer & emptyReducer

    Meta-reducer

    To complete

    emptyReducer

    To complete

    Selectors and facade

    Facade

    To complete

    Selectors

    To complete

    Child store

    To complete

    FAQ

    Why is a form array created while I expect a simple form control?

    You probably declared your array entry in the structure using a flatten object as items instead of the simple type object. As explained above, a flatten object will create a form group for each object and by the way will create a form array as parent.

    Glossary

    Keywords

    none

    Install

    npm i @mondosha1/feature-store-kit

    DownloadsWeekly Downloads

    64

    Version

    0.0.1-beta-032

    License

    none

    Unpacked Size

    366 kB

    Total Files

    40

    Last publish

    Collaborators

    • yannickglt