NgForm
leaves state management entirely in the developer's hands, which quickly results in unmanageable client-side complexity: a modern single-page application (SPA) may have to keep track of hundreds of pieces of information, any of which may need to be updated based on the user's actions and kept in sync with permanent storage on the server.How should we manage client-side state in an asynchronous web application that uses forms?
Use NgRx Store to represent state as a sequence of snapshots, each of which is created in response to a single action.
As we will see below, we can often avoid the need to copy all of the state. If we divide it into logically-separate chunks, we can re-use the chunks that don't change. In our experience, the amount of data that actually has to be duplicated will grow slowly with the size of the application so long as we think carefully about how to organize it.
Factory functions must be exported, named functions. The AOT compiler does not support lambda expressions ("arrow functions") for factory functions.
StoreModule
and the trafficLight.reducer
file in your app.module.ts
. Then Add the StoreModule.forRoot
function in the imports array of your AppModule. The StoreModule.forRoot()
method registers the global providers needed to access the Store throughout your application.this
, which in turn encourages us to think more functionally.)ICharacter
interface, and another that conforms to the IForm
interface:characterInitialState
into initialState
to create one large literal object, but again, since we're likely to add more state information (like the equipment the character is carrying), we have decided that initialState
will only ever be a list of top-level objects conforming to separately-defined interfaces.We have defined our initial state in a file of its own to make it easier to find and update. In a real application, it would probably be created programmatically so that we could swap in something else for testing. Note that in real life, it would not come from a database: instead, it's the blank slate that would be updated from a database as the application is being bootstrapped. (And yes, that bootstrap process would be implemented as a series of update actions.) However the initial state is created, it should be as empty as possible.
type
(which NgRx requires), and a sub-object called payload
that will contain the path to the part of the store it applies to and whatever extra values are needed to update the state. NgRx doesn't mandate this, but again, experience teaches us that creating literal objects in many different places throughout our program is just as risky as using magic numbers rather than defining a constant and referring to it.The nametype
is a requirement: NgRx mandates it by exporting anAction
interface defined as:1interface Action {2type: string;3}Copied!The namepayload
is not required, but is widely used and strongly encouraged, since in some cases we need to add extra info for our actions to be completed:1interface PayloadAction extends Action {2payload: any;3}Copied!
ThecreateAction
function returns a function, that when called returns an object in the shape of theAction
interface. Theprops
method is used to define any additional metadata needed for the handling of the action.
props
helper method to define the structure of the would be payload.assocPath
: makes a shallow clone of an object,mergeRight
: create a shallow copy of one object with properties merged in from a second object.assocPath
, but using a second object to get multiple changes at once.)path
: retrieve the value at a specified location in a structured object.formReducer
looks like this:on
method supplied by NgRx to add a case to the reducer. If the action's type is not safeForm
(The action object we previously defined)), then formReducer
won't make any changes to the state. This means that we can safely combine reducers together with others that handle other parts of our user interface: as long as they all use distinct keys for their actions, they will watch the state fly by without doing anything we don't want them to.formReducer
uses path
to get the part of the state that needs to be updated, then uses mergeRight
and assocPath
to create a shallow copy of the state, replacing that element, and only that element, with a different value. The most important word in the previous sentence is "create": at this point, the state that was passed in is thrown away and a new one created. Some parts of the old state are recycled–assocPath
does a shallow clone–but those are the parts that weren't changed, and if other reducers do their jobs properly, they will replace those parts rather than updating them in place when it's their turn to act.@Component
decorator to weld some metadata to it:ngForm
and character
in sync with each other using its own dark magic, which we will see in a moment. In order to synchronize that with our state, we create a standard observer/observable connection to make character
watch the character portion of our NgRx state.character
and the form's actual internal data are the same.@Component
decorator in the code above tells Angular that this class is used to fill in <character-form>
elements in the character-form.html
template snippet.@ViewChild
decorator on ngForm
gives this class an instance variable that watches a form. We need this because we are going to subscribe to event notifications from that form later on.character
instance variable is our working copy of the form's state.private store: Store<IAppState>
triggers Angular's dependency injection and gives us access to the NgRx store. When our application is busy doing other things, our data will live in this store, and when we're testing, we can inject a mock object here to give us more insight.Thesubscription
instance variable is the odd one out in this class. Its job is to store the observer/observable subscription connecting our state to our form so that we can unsubscribe cleanly when this component is destroyed. Angular will automatically unsubscribe on our behalf, but it's always safer to put our own toys away when we're done playing with them…
CharacterForm
is an Angular component, it needs a template to define how it will be rendered. In keeping with Angular best practices, we will use a template-driven form and bind its inputs to objects retrieved from the NgRx state. The first part of that form looks like this:ngModel
is a magic word meaning "the value of this field in the form". The expression [ngModel]
therefore makes a form control and binds the value of the public data member of our form object. The final step in wiring all of this together is therefore to subscribe to the NgRx state so that whenever it changes, the changes are automatically pushed into the form (which triggers a DOM update and a re-rendering of the page). Equally, since we're listening to the form for changes and writing those to our state by creating and dispatching actions, anything the user does will be reflected in the state. The beauty of this is that if anyone else does an update anywhere, everything will keep itself in sync.For the sake of simplicity we are synchronizing the whole form object in a single action, which sets the character attribute in the state with a brand new object every time. We could instead sync specific attributes to improve performance by using thepath
mechanism introduced earlier.
ngOnInit
, which is called after all the objects in the system have been created but before any of them are used. The ngOnInit
for CharacterForm
does the two pieces of wiring described above in an arbitrary order:store.select
with a fat arrow function as callback to pick out the character portion of the state, then subscribe to that with a another fat arrow callback that updates the form data (which as explained above triggers redisplay).ngOnInit
does the binding in the opposite direction: whenever the form changes, we dispatch an action created by saveForm
that has [Form] Save
as the type and ['character']
as the path to the part of the state we want to modify. ngForm
's valueChanges
method automatically gives us a change
value that contains all of the form values. These values arrive in a JavaScript object that mirrors the structure of the HTML, which is exactly what we want (because we defined the name attributes to get it).props
helper function:addIntoArray
therefore has two keys called path
and value
, which are in turn bound to whatever was passed in for the parameters with the corresponding names.lensForProp
is the location of the skills array in our state, propValue
is its current value which we concat
the new value to, wrapped up in assocPath
gives us the old skills with the new one appended. The additions to handle updating and removing skills have a similar shape.For those who wish to be more precise, we should distinguish between selectors as a general concept and selectors as implemented byNgRx
. Memoization benefits are built into selectors created using NgRx, but the general benefit of using a selector for validation is that you can derive the validity of your form from the data you already have without having to dispatch extra actions likeVALIDATE_FIELD
.
form.character
:isFormValid$
with an asynchronous pipe in our template:$
on the end of isFormValid$
is a naming convention used in the RxJS library and elsewhere meaning, "This is an observable." We don't have to use it, but we find it helps make code easier to understand.)isBetweenNumber
that does exactly what you'd expect:FormControl
states, we can use them to show error messages: