Skip to content

Commit 4ccd3d4

Browse files
committed
chore: update README with detailed usage examples and explanations
1 parent 5154deb commit 4ccd3d4

File tree

1 file changed

+201
-14
lines changed

1 file changed

+201
-14
lines changed

README.md

Lines changed: 201 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ npm install ngrx-forms --save
2323
This library depends on versions `^4.0.0` of `@angular/core`, `@angular/forms`, and `@ngrx/store`, and version `^5.0.0` of `rxjs`.
2424

2525
## <a name="2"></a>2 Design Principles
26-
This library is written to be as functional and as pure as possible. Most of the heavy lifting is done in pure reducer functions while the directives are only a thin layer to connect the form states to the DOM.
26+
This library is written to be as functional and as pure as possible. Most of the heavy lifting is done in pure reducer functions with the directives being only a thin layer to connect the form states to the DOM.
2727

2828
This library also tries to be as independent as possible from other libraries/modules. While there is of course a dependency on ngrx the touching points are small and it should be possible to adapt this library to any other redux library without too much effort. There is also a peer dependency on `@angular/forms` from which we re-use the `ControlValueAccessor` concept to allow easier integration with other libraries that provide custom form controls.
2929

30-
Conceptually this library borrows heavily from `@angular/forms`, specifically the concepts of form controls and form groups. Groups are simply a collection of named form controls. The state of a group is determined almost fully by its child controls (with the exception of errors which a group can have by itself).
30+
Conceptually this library borrows heavily from `@angular/forms`, specifically the concepts of form controls and form groups (see the [User Guide](#3) below for a more detailed description of these concepts).
3131

3232
## <a name="3"></a>3 User Guide
3333

@@ -36,18 +36,18 @@ Conceptually this library borrows heavily from `@angular/forms`, specifically th
3636
Import the module:
3737

3838
```typescript
39+
import { StoreModule } from '@ngrx/store';
3940
import { NgrxFormsModule } from 'ngrx-forms';
4041

42+
import { reducers } from './reducer';
43+
4144
@NgModule({
4245
declarations: [
4346
AppComponent,
44-
...,
4547
],
4648
imports: [
47-
...,
4849
NgrxFormsModule,
4950
StoreModule.forRoot(reducers),
50-
...,
5151
],
5252
providers: [],
5353
bootstrap: [AppComponent]
@@ -146,29 +146,214 @@ Set the control states in your template:
146146

147147
### Form Controls
148148

149-
- explain whole state concept
150-
- mention that state is sync'ed immediately
149+
Form controls in ngrx-forms are represented as plain state objects. Control states have the following shape:
150+
151+
```typescript
152+
export type FormControlValueTypes = string | number | boolean | null | undefined;
153+
export interface ValidationErrors { [key: string]: any; }
154+
155+
export class AbstractControlState<TValue> {
156+
id: string;
157+
value: TValue;
158+
isValid: boolean;
159+
isInvalid: boolean;
160+
errors: ValidationErrors;
161+
isEnabled: boolean;
162+
isDisabled: boolean;
163+
isDirty: boolean;
164+
isPristine: boolean;
165+
isTouched: boolean;
166+
isUntouched: boolean;
167+
isSubmitted: boolean;
168+
isUnsubmitted: boolean;
169+
}
170+
171+
export class FormControlState<TValue extends FormControlValueTypes> extends AbstractControlState<TValue> {
172+
isFocused: boolean;
173+
isUnfocused: boolean;
174+
lastKeyDownCode: number;
175+
}
176+
```
177+
178+
The following table explains each property.
179+
180+
|Property|Negated|Description|
181+
|-|-|-|
182+
|`id`||The unique ID of the form control. Usually this is the name of the field in the form value prefixed by the ID of the containing group, e.g. `MY_FORM.someTextInput`.|
183+
|`value`||The value of the form control. Controls only support values of type `string`, `number`, `boolean`, `null`, and `undefined` to keep the state string serializable.|
184+
|`isValid`|`isInvalid`|The `isValid` flag is `true` if the control does not have any errors.|
185+
|`errors`||The errors of the control. This property always has a value. If the control has no errors the property is set to `{}`.|
186+
|`isEnabled`|`isDisabled`|The `isEnabled` flag indicates whether the control is enabled. When `isEnabled` is `false` the `errors` are always `{}` (i.e. the control is always valid if disabled).|
187+
|`isDirty`|`isPristine`|The `isDirty` flag is set to `true` as soon as the value of the control changes for the first time.|
188+
|`isTouched`|`isUntouched`|The `isTouched` flag is set to `true` based on the rules of the underlying `ControlValueAccessor` (usually on `blur` for most form elements).|
189+
|`isSubmitted`|`isUnsubmitted`|The `isSubmitted` flag is set to `true` if the containing group is submitted.|
190+
|`isFocused`|`isUnfocused`|The `isFocused` flag is set to `true` if the control currently has focus. Note that this feature is opt-in. To enable it you have to add ```[ngrxEnableFocusTracking]="true"``` to your form element.|
191+
|`lastKeyDownCode`||The `lastKeyDownCode` is set to the key code of the last key that was pressed on the control. Note that this feature is opt-in. To enable it you have to add ```[ngrxEnableLastKeydownCodeTracking]="true"``` to your form element. This feature can be used for example to react to `Enter` key events. Note that this feature is likely to be changed in the near future.|
192+
193+
Control states are associated with a form element via the `NgrxFormControlDirective` (applied with `[ngrxFormControlState]="controlState"`). This directive is reponsible keeping the view and the state in sync. When the state is changed the update is always immediately sync'ed to the view. Currently this also always happens immediately when the view is changed (e.g. as soon as the value of an `input` changes the state is updated), but there are plans to allow specifying the event on which the sync happens (e.g. `change`, `blur` etc.).
151194

152195
### Form Groups
153196

154-
- explain exactly how each property of a group is computed
155-
- form directive for submission status tracking (mention preventDefault())
197+
Groups are collections of controls. Just like controls groups are represented as plain state objects. The state of a group is determined almost fully by its child controls (with the exception of `errors` which a group can have by itself). Group states have the following shape:
198+
199+
```typescript
200+
export interface KeyValue { [key: string]: any; }
201+
export type FormGroupControls<TValue> = {[controlId in keyof TValue]: AbstractControlState<TValue[controlId]> };
202+
export class FormGroupState<TValue extends KeyValue> extends AbstractControlState<TValue> {
203+
controls: FormGroupControls<TValue>;
204+
}
205+
```
206+
207+
As you can see most properties are shared with controls via the common base interface `AbstractControlState`. The following table explains each property in the context of a group.
208+
209+
|Property|Negated|Description|
210+
|-|-|-|
211+
|`id`||The unique ID of the group.|
212+
|`value`||The aggregated value of the group. The value is computed by aggregating the values of all children.|
213+
|`isValid`|`isInvalid`|The `isValid` flag is `true` if the group does not have any errors itself and none of its children have any errors.|
214+
|`errors`||The errors of the group. This property is computed by merging the errors of the control with the errors of all children where the child errors are a property of the `errors` object prefixed with an underscore (e.g. `{ groupError: true, _child: { childError: true } }`). If neither the group nor any children have errors the property is set to `{}`.|
215+
|`isEnabled`|`isDisabled`|The `isEnabled` flag is `true` if and only if at least one child control is enabled.|
216+
|`isDirty`|`isPristine`|The `isDirty` flag is `true` if and only if at least one child control is marked as dirty.|
217+
|`isTouched`|`isUntouched`|The `isTouched` flag is `true` if and only if at least one child control is marked as touched.|
218+
|`isSubmitted`|`isUnsubmitted`|The `isSubmitted` flag is set to `true` if the group is submitted. This is tracked by the `NgrxFormDirective` (which needs to be applied to a form via `[ngrxFormState]="groupState"`). Note that applying this directive to a form prevents normal form submission since that does not make much sense for ngrx forms.|
219+
|`controls`||This property contains all child controls of the group. As you may have noticed the type of each child control is `AbstractControlState` which sometimes forces you to cast the state explicitly. It is not possible to improve this typing until [conditional mapped types](https://github.com/Microsoft/TypeScript/issues/12424) are added to TypeScript.|
220+
221+
Group states are usually completely independent of the DOM (with the exception of root groups that are associated with a `form` via `NgrxFormDirective`). They are updated by intercepting all actions that change their children (i.e. the group's reducer is the parent reducer of all its child reducers and forwards any actions to all children; if any children change it recomputes the state of the group). A group state can be created via `createFormGroupState`. This function takes an initial value and automatically creates all child controls recursively.
156222

157223
### Updating the State
158224

159-
- explain all helper functions
225+
All states are internally updated by ngrx-forms through dispatching actions. While this is of course also possible for you there exist a set of utility functions that can be used to update states. This is mainly useful to change the state as a result of a different action in your reducer. Note that ngrx-forms is coded in such a way that no state references will change if nothing inside the state changes. It is therefore perfectly safe to repeatedly call any of the functions below and the state will be updated exactly once or not at all if nothing changed. Each function can be imported from `ngrx-forms`. The following table explains each function:
226+
227+
|Function|Description|
228+
|-|-|
229+
|`setValue`|This curried function takes a value and returns a function that takes a state and updates the value of the state. Note that setting the value of the group will also update all children including adding and removing children on the fly for added/removed properties. Has an uncurried overload that takes a state directly as the second parameter.|
230+
|`validate`|This curried function takes a validation function as a parameter and returns a function that takes a state and updates the errors of the state with the result of the provided validation function applied to the state's value. Has an uncurried overload that takes a state directly as the second parameter.|
231+
|`enable`|This function takes a state and enables it. For groups this also recursively enables all children.|
232+
|`disable`|This function takes a state and disables it. For groups this also recursively disables all children.|
233+
|`markAsDirty`|This function takes a state and marks it as dirty. For groups this also recursively marks all children as dirty.|
234+
|`markAsPristine`|This function takes a state and marks it as pristine. For groups this also recursively marks all children as pristine.|
235+
|`markAsTouched`|This function takes a state and marks it as touched. For groups this also recursively marks all children as touched.|
236+
|`markAsUntouched`|This function takes a state and marks it as untouched. For groups this also recursively marks all children as untouched.|
237+
|`markAsSubmitted`|This function takes a state and marks it as submitted. For groups this also recursively marks all children as submitted.|
238+
|`markAsUnsubmitted`|This function takes a state and marks it as unsubmitted. For groups this also recursively marks all children as unsubmitted.|
239+
|`focus`|This function takes a control state and makes it focused (which will also `.focus()` the form element).|
240+
|`unfocus`|This function takes a control state and makes it unfocused (which will also `.blur()` the form element).|
241+
|`setLastKeyDownCode`|This function takes a control state and sets the last keydown code.|
242+
|`addControl`|This curried function takes a name and a value and returns a function that takes a group state and adds a child control with the given name and value to the state.|
243+
|`removeControl`|This curried function takes a name and returns a function that takes a group state and removes a child control with the given name from the state.|
244+
245+
These are very basic functions that perform simple updates on states. The last two functions below contain the real magic that allows easily updating deeply nested form states.
246+
247+
`updateGroup`:
248+
This curried function takes a partial object in the shape of the group's value where each key contains an update function for that child and returns a function that takes a group state, applies all the provided update functions recursively and recomputes the state of the group afterwards. As with all the functions above this function does not change the reference of the group if none of the child update functions change any children. The best example of how this can be used is simple validation:
249+
250+
```typescript
251+
import { updateGroup, validate } from 'ngrx-forms';
252+
253+
export interface NestedValue {
254+
someNumber: number;
255+
}
256+
257+
export interface MyFormValue {
258+
someTextInput: string;
259+
someCheckbox: boolean;
260+
nested: NestedValue;
261+
}
262+
263+
function required(value: any) {
264+
return !!value ? {} : { required: true };
265+
}
266+
267+
const updateMyFormGroup = updateGroup<MyFormValue>({
268+
someTextInput: validate(required),
269+
nested: updateGroup({
270+
someNumber: validate(required),
271+
}),
272+
});
273+
```
274+
275+
The `updateMyFormGroup` function has a signature of `FormGroupState<MyFormValue> -> FormGroupState<MyFormValue>`. It takes a state, runs all validations, updates the errors, and returns the resulting state.
276+
277+
In addition, the `updateGroup` function allows specifying as many update function objects as you want and applies all of them after another. This is useful if you have dependencies between your update functions where one function's result may affect the result of another function. The following (contrived) example shows how to set the value of `someNumber` based on the `errors` of `someTextInput`.
278+
279+
```typescript
280+
const updateMyFormGroup = updateGroup<MyFormValue>({
281+
someTextInput: validate(required),
282+
nested: updateGroup({
283+
someNumber: validate(required),
284+
}),
285+
}, {
286+
// note that the parent form state is provided as the second argument to update functions;
287+
// type annotations added for clarity but are inferred correctly otherwise
288+
nested: (nested: AbstractControlState<NestedValue>, myForm: FormGroupState<MyFormValue>) =>
289+
updateGroup<NestedValue>({
290+
someNumber: (someNumber: AbstractControlState<number>) => {
291+
if (myForm.controls.someTextInput.errors.required) {
292+
// sets the control's value to 1 and clears all errors
293+
return validate(() => ({}), setValue(1, someNumber));
294+
}
295+
296+
return someNumber;
297+
};
298+
})(cast(nested))
299+
// the `cast` (utility function exported by `ngrx-forms`) helps the type checker to recognize the
300+
// `nested` state as a group state
301+
});
302+
```
303+
304+
`groupUpdateReducer`:
305+
This curried function combines a `formGroupReducer` and the `updateGroup` function by taking update objects of the same shape as `updateGroup` and returns a reducer which first calls the `formGroupReducer` and afterwards applies all update functions by calling `updateGroup`. Combining all we have seen so far our final reducer would therefore look something like this:
306+
307+
```typescript
308+
const myFormReducer = groupUpdateReducer<MyFormValue>({
309+
someTextInput: validate(required),
310+
nested: updateGroup({
311+
someNumber: validate(required),
312+
}),
313+
}, {
314+
nested: (nested, myForm) =>
315+
updateGroup<NestedValue>({
316+
someNumber: someNumber => {
317+
if (myForm.controls.someTextInput.errors.required) {
318+
return validate(() => ({}), setValue(1, someNumber));
319+
}
320+
321+
return someNumber;
322+
}
323+
})(cast(nested))
324+
});
325+
326+
export function appReducer(state = initialState, action: Action): AppState {
327+
const myForm = myFormReducer(state.myForm, action);
328+
if (myForm !== state.myForm) {
329+
state = { ...state, myForm };
330+
}
331+
332+
switch (action.type) {
333+
case 'some action type':
334+
// modify state
335+
return state;
336+
337+
default: {
338+
return state;
339+
}
340+
}
341+
}
342+
```
160343

161344
### Custom Controls
162345

163-
- control value accessors
346+
As mentioned above ngrx-forms re-uses the `ControlValueAccessor` concept of `@angular/forms`. ngrx-forms ships its own variants of all default value accessors (most of which simply inherit the implementation from `@angular/forms`. Most libraries providing custom value accessors should also work with ngrx-forms out of the box as long as they properly export the value accessor. However, in case a library does not do this you may have to write your own value accessor. See the example app for such a custom value accessor (in this case for the `md-select` from `@angular/material` which in version `2.0.0-beta.8` does not properly export the `md-select`'s value accessor).
164347

165348
## <a name="4"></a>4 Open Points
166349

350+
* providing a simple set of common validation functions (e.g. required, min, max, pattern, etc.) and error composition
167351
* providing option to choose when the view is sync'ed to the state (e.g. `change`, `blur` etc.)
168-
* proper value conversion
352+
* proper value conversion for converting values between the view and the state
169353
* async validation (although already achievable via effects)
170-
* providing some global configuration options
171-
* some tests for directives
354+
* providing some global configuration options (e.g. enabling focus tracking globally)
355+
* add `isFocused` to groups to track whether any child is focused
356+
* some more tests for directives
172357
* tests for example application
173358

174359
## <a name="5"></a>5 Contributing
@@ -208,3 +393,5 @@ npm run compodoc-serve
208393

209394
## License
210395
MIT
396+
397+
Copyright &copy; 2017 Jonathan Ziller

0 commit comments

Comments
 (0)