- Notifications
You must be signed in to change notification settings - Fork15
Keep your Angular2+ form state in Redux
License
angular-redux/form
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Please note that this repo has been deprecated. Code and issues are being migrated to a monorepo athttps://github.com/angular-redux/platform where we are beginning work on a new and improved v10. To file any new issues or see the state of the current code base, we would love to see you there! Thanks for your support!
This library is a thin layer of connective tissue between Angular 2+ forms andRedux. It provides unidirectional data binding between your Redux state andyour forms elements. It builds on existing Angular functionality likeNgModelandNgControl
This supports bothTemplate driven forms andReactive driven forms.
For the simplest use-cases, the API is very straightforward. Your templatewould look something like this:
<formconnect="myForm"><inputtype="text"name="address"ngControlngModel/></form>
The important bit to note here is the[connect] directive. This is the only thingyou should have to add to your form template in order to bind it to your Redux state.The argument provided toconnect is basically a path to form state inside of youroverall app state. So for example if my Redux app state looks like this:
{"foo":"bar","myForm": {"address":"1 Foo St." }}Then I would supplymyForm as the argument to[connect]. If myForm were nesteddeeper inside of the app state, you could do something like this:
<form[connect]="['personalInfo', 'myForm']"> ...</form>
Note that ImmutableJS integration is provided seamlessly. IfpersonalInfo is animmutable Map structure, the library will automatically useget() orgetIn() tofind the appropriate bits of state.
Then, in your application bootstrap code, you need to add a provider forthe class that is responsible for connecting your forms to your Redux state.There are two ways of doing this: either using anRedux.Store<T> object oranNgRedux<T> object. There are no substantial differences between theseapproaches, but if you are already using@angular-redux/store or you wish to integrateit into your project, then you would do something like this:
import{NgReduxModule}from'@angular-redux/store';import{NgReduxFormModule}from'@angular-redux/form';@NgModule({imports:[BrowserModule,ReactiveFormsModule,FormsModule,NgReduxFormModule,NgReduxModule,],bootstrap:[MyApplicationComponent]})exportclassExampleModule{}
Or if you are using Redux without@angular-redux/store, then your bootstrap call would lookmore like this (substitute your own store creation code):
import{provideReduxForms}from'@angular-redux/form';conststoreCreator=compose(applyMiddleware(logger))(createStore);conststore=create(reducers,<MyApplicationState>{});@NgModule({imports:[BrowserModule,ReactiveFormsModule,FormsModule,NgReduxFormModule,],providers:[provideReduxForms(store),],bootstrap:[MyApplicationComponent]})exportclassExampleModule{}
The essential bit of code in the above samples is the call toprovideReduxForms(...).This configures@angular-redux/form and provides access to your Redux store or NgReduxinstance. The shape of the object thatprovideReduxForms expects is verybasic:
exportinterfaceAbstractStore<RootState>{/// Dispatch an actiondispatch(action:Action&{payload?}):void;/// Retrieve the current application stategetState():RootState;/// Subscribe to changes in the storesubscribe(fn:()=>void):Redux.Unsubscribe;}
BothNgRedux<T> andRedux.Store<T> conform to this shape. If you have a morecomplicated use-case that is not covered here, you could even create your own storeshim as long as it conforms to the shape ofAbstractStore<RootState>.
The bindings work by inspecting the shape of your form and then binding to a Reduxstate object that has the same shape. The important element isNgControl::path.Each control in an Angular 2 form has a computed property calledpath which usesa very basic algorithm, ascending the tree from the leaf (control) to the root(the<form> element) and returning an array containing the name of each group orarray in the path. So for example, let us take a look at this form that lets theuser provide their full name and the names and types of their children:
<formconnect="form1"><inputngControlngModelname="fullname"type="text"/><templateconnectArraylet-indexconnectArrayOf="dependents"><div[ngModelGroup]="index"><inputngControlngModelname="fullname"type="text"/><selectngControlngModelname="type"><optionvalue="adopted">Adopted</option><optionvalue="biological">Biological child</option></select></div></template></form>
Our root<form> element has aconnect directive that points to the state elementform1. This means that the children within your form will all be bound to somebit of state inside of theform1 object in your Redux state. Then we have a childinput which is bound to a property calledfullname. This is a basic text box. Ifyou were to inspect it in the debugger, it would have apath value like this:
['form1', 'fullname']And therefore it would bind to this piece of Redux state:
{"form1": {"fullname":"Chris Bond" }}So far so good. But look at the array element inside our form, in the<template>element. It is bound to an array property calleddependents. The elements insideof the<template> tag contain the template that will be instantiated for eachelement inside of thedependents array. ThengModelGroup specifies that we shouldcreate aFormGroup element for each item in the array and the name of that groupshould be the value ofindex (the zero-based index of the element that is beingrendered). This is important because it allows us to create a form structure thatmatches our Redux state. Let's say our state looks like this:
{"form1": {"fullname":"Chris Bond","dependents": [ {"fullname":"Christopher Bond Jr.","type":"biological" } ] }}If you think about the 'path' to the first element of the dependents array, it wouldbe this:
['form1', 'dependents', 0]The last element,0, is the index into thedependents array. This is ourngModelGroup element. This allows us to create a form structure that has thesame structure as our Redux state. Therefore if we pause the debugger and look atthepath property on our first<select> element, it would look like this:
['form1', 'dependents', 0, 'type']From there,@angular-redux/form is able to take that path and extract the value forthat element from the Redux state.
The value in "connect" attribute is the value that will show up in the Redux store. The formGroup value is the name of the object in your code that represents the form group.
<formconnect="myForm"[formGroup]="loginForm"><inputtype="text"name="address"formControlName="firstName"/></form>
If you are having trouble getting data-binding to work for an element of your form,it is almost certainly because thepath property on your control does not matchthe structure of your Redux state. Try pausing the debugger inConnect::resetStateand check the value ofpath on the control that has failed to bind. Then make sureit is a valid path to the state in question.
The library will automatically bind your state to value of your form inputs. This isthe easy part and is unlikely to cause any problems for you. Slightly more difficultisupdating your Redux state when the form values change. There are two approachesthat you can take in order to do this.
The first, and by far the simplest, is to use the reducer that comes with@angular-redux/formand uses the value supplied inconnect and the form input names in order to updateyour Redux state automatically. If you do not need to do any special processing onyour data when the user updates form inputs, then you should use this default reducer.To use it, you need to combine it with your existing reducers like so:
import{composeReducers,defaultFormReducer}from'@angular-redux/form';constreducer=composeReducers(defaultFormReducer(),combineReducers({foo:fooReducer,bar:barReducer}));
The important bits of code here are the calls tocomposeReducers anddefaultFormReducer.The call tocomposeReducers essentially takes your existing reducer configuration andchains them together withdefaultFormReducer. The default form reducer only handles oneaction,{FORM_CHANGED}. You can think of it like so:
functiondefaultFormReducer(state,action:Redux.Action&{payload?}){switch(action.type){caseFORM_CHANGED:[returnnewstatewithformvaluesfromaction.payload];default:break;}returnstate;}
If you have a more complex use-case that the default form reducer is incompatible with,then you can very easily just handle the FORM_CHANGED actions in your existing reducersand manually update your state with the form values fromaction.payload.value, whichhas the shape of an object containing all of your raw form values:
{"address1":"129 Spadina Ave","address2":"Toronto, Ontario M4Y 1F7","otherGroup": {"foo":"bar","biz":1 }}This would match a form that looks like this:
<formconnect><inputname="address1"ngControlngModeltype="text"/><inputname="address2"ngControlngModeltype="text"/><formname="otherGroup"><inputname="foo"ngControlngModeltype="text"/><inputname="biz"ngControlngModeltype="number"/></form></form>
Note: If you implement your own reducer instead of using the default one provided byng2-form-redux, the state you return still needs to match the shape of your form,otherwise data-binding is not going to work. This is why it probably makes sense tojust use the default reducer in almost every case - because your custom reducer wouldhave to implement the same logic and produce a state object that is the same shape.But if you are having trouble with the default reducer, or if you find the fact thatyou have to usecomposeReducers distasteful, then this is another route availableto you.
The unit tests in*.test.ts files also contain useful examples of how to buildforms using@angular-redux/form.
About
Keep your Angular2+ form state in Redux
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors8
Uh oh!
There was an error while loading.Please reload this page.