@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

Readme

Keywords

none

Package Sidebar

Install

npm i @mondosha1/feature-store-kit

Weekly Downloads

0

Version

0.0.1-beta-032

License

none

Unpacked Size

366 kB

Total Files

40

Last publish

Collaborators

  • yannickglt