NgRX
This chapter gives guidance about NgRx. NgRx is a framework for building reactive applications in Angular.
OneCx uses NgRx to manage the state of an applet. The communication with the store is done by smart components, usually by the page components.
Guidelines for NgRx
-
An action is only dispatched at one place.
-
Actions are never dispatched conditionally.
-
For every component that is getting data from the store there is an interface for the viewmodel that is called like the component class but with postfix "ViewModel".
-
For every component that is getting data from the store there is a selector for the viewmodel that is called like the viewmodel interface but with the prefix "select".
-
Every component that is getting data from the store selects only the viewmodel via the viewmodel selector. The observable that is storing the result is called
viewModel$
. -
Every HTML file of a component that is getting data from the store has an outer ng-container tag that is using the ngrxLet directive e.g.
*ngrxLet="viewModel$ as vm"
. -
The store never stores data that can be computed from other data in the store. This is done in selectors.
-
Selectors are only accessing the data on top-level of the part of the store that is past to it.
-
Every feature module has its own feature store.
-
The store is the model of the applet and is modeled like that. Data that is relevant for all pages in the feature are stored on top-level. Data that is only relevant for one page is stored in a subsection of the store which is called like the page.
-
All HTTP calls are done in effects.
-
Routing or changing the URL parameters via the angular router is done in effects.
-
Selectors, reducers and effects are tested.
-
For reducers there is at least a test for the initial state and for one state that is not the initial state.
-
Effects are tested via marble testing.
-
Selector tests are testing the projector function of the selectors.
Project structure
-
See example for NgRx project The following examples shows a potential folder structure, that might be extended based on the need.
Folders should only be created and used if they have content. [Example Angular NgRx structure] |
├── src │ ├── app │ │ ├── featuremodule1 // one of feature modules, might have many of them │ │ │ ├── components // optional - components shared between pages of this feature module │ │ │ │ ├── featurecomponent1 │ │ │ │ │ ├── featurecomponent1.component.html │ │ │ │ │ ├── featurecomponent1.component.scss │ │ │ │ │ ├── featurecomponent1.component.spec.ts │ │ │ │ │ ├── featurecomponent1.component.ts │ │ │ ├── directives // optional - directives used in specific feature module │ │ │ ├── pages │ │ │ │ ├── page1 // every page gets a subfolder, might have many of them │ │ │ │ │ ├── page1subcomponent1 // one of subcomponent for page, optional + might have many of them │ │ │ │ │ │ ├── page1subcomponent1.component.html │ │ │ │ │ │ ├── page1subcomponent1.component.scss │ │ │ │ │ │ ├── page1subcomponent1.component.spec.ts │ │ │ │ │ │ ├── page1subcomponent1.component.ts │ │ │ │ │ │ ├── page1subcomponent1.viewmodel.ts │ │ │ │ │ ├── dialogs │ │ │ │ │ │ ├── page1dialog1 // dialog that is only used by this page, optional + might have many of them │ │ │ │ │ │ │ ├── page1dialog1.component.html │ │ │ │ │ │ │ ├── page1dialog1.component.scss │ │ │ │ │ │ │ ├── page1dialog1.component.spec.ts │ │ │ │ │ │ │ ├── page1dialog1.component.ts │ │ │ │ │ │ │ ├── page1dialog1.viewmodel.ts │ │ │ │ │ ├── page1.component.html │ │ │ │ │ ├── page1.component.scss │ │ │ │ │ ├── page1.component.spec.ts │ │ │ │ │ ├── page1.component.ts │ │ │ │ │ ├── page1.reducer.ts │ │ │ │ │ ├── page1.reducer.spec.ts │ │ │ │ │ ├── page1.selectors.ts │ │ │ │ │ ├── page1.selectors.spec.ts │ │ │ │ │ ├── page1.state.ts │ │ │ │ │ ├── page1.effects.spec.ts │ │ │ │ │ ├── page1.effects.ts │ │ │ │ │ ├── page1.actions.ts │ │ │ │ │ ├── page1.viewmodel.ts │ │ │ ├── dialogs // dialogs which are used across pages, optional │ │ │ │ ├── dialog1 // every dialog gets a subfolder, might have many of them │ │ │ │ │ ├── dialog1subcomponent1 // one of subcomponent for dialog, optional + might have many of them │ │ │ │ │ │ ├── dialog1subcomponent1.component.html │ │ │ │ │ │ ├── dialog1subcomponent1.component.scss │ │ │ │ │ │ ├── dialog1subcomponent1.component.spec.ts │ │ │ │ │ │ ├── dialog1subcomponent1.component.ts │ │ │ │ │ │ ├── dialog1subcomponent1.viewmodel.ts │ │ │ │ │ ├── dialog1.component.html │ │ │ │ │ ├── dialog1.component.scss │ │ │ │ │ ├── dialog1.component.spec.ts │ │ │ │ │ ├── dialog1.component.ts │ │ │ │ │ ├── dialog1.reducer.ts │ │ │ │ │ ├── dialog1.reducer.spec.ts │ │ │ │ │ ├── dialog1.selectors.ts │ │ │ │ │ ├── dialog1.selectors.spec.ts │ │ │ │ │ ├── dialog1.state.ts │ │ │ │ │ ├── dialog1.effects.spec.ts │ │ │ │ │ ├── dialog1.effects.ts │ │ │ │ │ ├── dialog1.actions.ts │ │ │ │ │ ├── dialog1.viewmodel.ts │ │ │ ├── featuremodule1.module.ts │ │ │ ├── featuremodule1.reducers.ts │ │ │ ├── featuremodule1.reducers.spec.ts │ │ │ ├── featuremodule1.routes.ts │ │ │ ├── featuremodule1.selectors.ts │ │ │ ├── featuremodule1.selectors.spec.ts │ │ │ ├── featuremodule1.state.ts │ │ ├── shared // (technical) components and directives used in multiple feature modules │ │ │ ├── components │ │ │ │ ├── sharedcomponent1 │ │ │ │ │ ├── sharedcomponent1.component.html │ │ │ │ │ ├── sharedcomponent1.component.scss │ │ │ │ │ ├── sharedcomponent1.component.spec.ts │ │ │ │ │ ├── sharedcomponent1.component.ts │ │ │ ├── directives │ │ │ │ ├── directive1.ts │ │ │ │ ├── directive1.spec.ts │ │ │ ├── generated //generated from openApi │ │ │ │ ├── models │ │ │ │ ├── services │ │ │ ├── shared.module.ts │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── app.state.ts │ │ ├── app.reducers.ts │ │ ├── app.reducers.spec.ts │ │ ├── app.selectors.ts │ │ ├── app.selectors.spec.ts │ ├── assets │ │ │ ├── i18n │ │ │ ├── fonts │ │ │ ├── images │ │ │ ├── scss │ │ │ ├── yamls │ ├── environments │ │ │ ├── dev │ │ │ ├── prod
Subcomponents
Usually subcomponents have a single input for the viewmodel called vm
. This viewmodel is a member of the components viewmodel and is build via selectors. If a subcomponent has also a subsubcomponent, the viewmodel of the subcomponent has a member containing the viewmodel of the subsubcomponent.
Dialogs
There are two kinds of dialogs:
-
dialogs only used in one page
-
dialogs used in multiple pages
Both dialog types have in common that:
-
they are opened in effects
-
the result is handled in the effects and is converted into an action
-
they have a viewmodel
-
they are opened with
PortalDialogService.openDialog(…)
-
they are communicating with the code that opened the dialog by implementing the interface
DialogResult
-
Implementing
DialogButtonClicked
can help setting the dialog result before the dialog closes
Dialogs used in one page
The components used for dialogs are subcomponents of the page they are used in. Theses subcomponents are placed in a subfolder called 'dialogs'. They do not have a separate section in the store because they are sharing the data of the page they are belonging to. The viewmodel can be passed via an input which is called 'vm'. The viewmodel is read from the store via a viewmodel selector in the effects or is build with data from the action which is triggering the opening of the dialog.
The dialogs can also dispatch actions but only if they are not used to react on the closing of the dialog. Reacting to the closing is always done via the Observable returned by openDialog(…)
Dialogs used in multiple pages
The structure and inner workings of a dialog that is used in many pages is similar to the structure of a page. They have their own section in the store, own effects, own selectors,… Dialogs that are used by multiple pages are always dispatching an action that is informing about the opening of the dialog in the constructor of the dialog component. The viewmodel is read from the store inside of the component via a selector like it is done for a page.
Further information about NgRx
For a further introduction, please checkout the NgRX documentation.
Furthermore, the following video tutorial might be helpful.