Skip to content

Commit

Permalink
Merge branch 'feature/form-arrays' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
MrWolfZ committed Oct 29, 2017
2 parents 54a42b1 + 8539ea4 commit 19d4e49
Show file tree
Hide file tree
Showing 58 changed files with 2,776 additions and 1,031 deletions.
74 changes: 74 additions & 0 deletions docs/FORM_ARRAYS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
## Form Arrays

Form arrays are collections of controls. They are represented as plain state arrays. The state of an array is determined almost fully by its child controls (with the exception of `errors` which an array can have by itself). Array states have the following shape:

```typescript
export class FormArrayState<TValue> extends AbstractControlState<TValue[]> {
readonly controls: Array<AbstractControlState<TValue>>;
}
```

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 an array.

|Property|Negated|Description|
|-|-|-|
|`id`||The unique ID of the array.|
|`value`||The aggregated value of the array. The value is computed by aggregating the values of all children and is itself an array.|
|`isValid`|`isInvalid`|The `isValid` flag is `true` if the array does not have any errors itself and none of its children have any errors.|
|`errors`||The errors of the array. 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. `{ arrayError: true, _0: { childError: true } }`). If neither the array nor any children have errors the property is set to `{}`.|
|`isEnabled`|`isDisabled`|The `isEnabled` flag is `true` if and only if at least one child control is enabled.|
|`isDirty`|`isPristine`|The `isDirty` flag is `true` if and only if at least one child control is marked as dirty.|
|`isTouched`|`isUntouched`|The `isTouched` flag is `true` if and only if at least one child control is marked as touched.|
|`isSubmitted`|`isUnsubmitted`|The `isSubmitted` flag is set to `true` if the containing group is submitted.|
|`controls`||This property contains all child controls of the array. 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.|
|`userDefinedProperties`||`userDefinedProperties` work the same for arrays as they do for controls.|

Array states are completely independent of the DOM. They are updated by intercepting all actions that change their children (i.e. the array'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 array). An array state can be created via `createFormArrayState`. This function takes an initial value and automatically creates all child controls recursively.

#### Dynamic Form Arrays

Sometimes you will have to render a variable number of fields in your form. Form arrays support adding and removing controls dynamically. This is done by setting the value of the form array via `setValue` which will automatically update the form array based on the value you provide.

Below you can find an example of how this would look. Assume that we have an action that provides a variable set of objects which each should be mapped to an array with two form controls.

```typescript
import { Action } from '@ngrx/store';
import { FormArrayState, setValue, updateGroup, cast } from 'ngrx-forms';

interface DynamicObject {
someNumber: number;
someCheckbox: boolean;
}

interface DynamicObjectFormValue {
someNumber: number;
someCheckbox: boolean;
}

interface SetDynamicObjectsAction extends Action {
type: 'SET_DYNAMIC_OBJECTS';
objects: DynamicObject[];
}

interface AppState {
someOtherState: string;
someOtherNumber: number;
dynamicFormArray: FormArrayState<DynamicObjectFormValue>;
}

export function appReducer(state: AppState, action: Action): AppState {
switch (action.type) {
case 'SET_DYNAMIC_OBJECTS': {
const newFormValue: DynamicObjectFormValue[] = (action as SetDynamicObjectsAction).objects;

// the `setValue` will add and remove controls as required; existing controls that are still
// present get their value updated but are otherwise kept in the same state as before
const dynamicFormArray = cast(setValue(newFormValue, state.dynamicFormArray));
return { ...state, dynamicFormArray };
}

default:
return state;
}
}
```
2 changes: 1 addition & 1 deletion docs/FORM_GROUPS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## Form Groups

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:
Groups are collections of named 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:

```typescript
export interface KeyValue { [key: string]: any; }
Expand Down
41 changes: 25 additions & 16 deletions docs/UPDATING_THE_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,32 @@ All states are internally updated by ngrx-forms through dispatching actions. Whi

|Function|Description|
|-|-|
|`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.|
|`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 a group or array will also update all children including adding and removing children on the fly for added/removed properties/items. Has an uncurried overload that takes a state directly as the second parameter.|
|`validate`|This curried function takes either a single validation function or an array of validation functions 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.|
|`enable`|This function takes a state and enables it. For groups this also recursively enables all children.|
|`disable`|This function takes a state and disables it. For groups this also recursively disables all children.|
|`markAsDirty`|This function takes a state and marks it as dirty. For groups this also recursively marks all children as dirty.|
|`markAsPristine`|This function takes a state and marks it as pristine. For groups this also recursively marks all children as pristine.|
|`markAsTouched`|This function takes a state and marks it as touched. For groups this also recursively marks all children as touched.|
|`markAsUntouched`|This function takes a state and marks it as untouched. For groups this also recursively marks all children as untouched.|
|`markAsSubmitted`|This function takes a state and marks it as submitted. For groups this also recursively marks all children as submitted.|
|`markAsUnsubmitted`|This function takes a state and marks it as unsubmitted. For groups this also recursively marks all children as unsubmitted.|
|`enable`|This function takes a state and enables it. For groups and arrays this also recursively enables all children.|
|`disable`|This function takes a state and disables it. For groups and arrays this also recursively disables all children.|
|`markAsDirty`|This function takes a state and marks it as dirty. For groups and arrays this also recursively marks all children as dirty.|
|`markAsPristine`|This function takes a state and marks it as pristine. For groups and arrays this also recursively marks all children as pristine.|
|`markAsTouched`|This function takes a state and marks it as touched. For groups and arrays this also recursively marks all children as touched.|
|`markAsUntouched`|This function takes a state and marks it as untouched. For groups and arrays this also recursively marks all children as untouched.|
|`markAsSubmitted`|This function takes a state and marks it as submitted. For groups and arrays this also recursively marks all children as submitted.|
|`markAsUnsubmitted`|This function takes a state and marks it as unsubmitted. For groups and arrays this also recursively marks all children as unsubmitted.|
|`focus`|This function takes a control state and makes it focused (which will also `.focus()` the form element).|
|`unfocus`|This function takes a control state and makes it unfocused (which will also `.blur()` the form element).|
|`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.|
|`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.|
|`setUserDefinedProperty`|This curried function takes a name and a value and returns a function that takes a state and sets the property with the given name to the given value on the state's user defined properties.|

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.
These are the basic functions that perform simple updates on states. The functions below contain the real magic that allows easily updating deeply nested form states.

`updateArray`:
This curried function takes an update function and returns a function that takes an array state, applies the provided update function to each element and recomputes the state of the array afterwards. As with all the functions above this function does not change the reference of the array if the update function does not change any children. See the section below for an example of how this function can be used.

`updateGroup`:
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:

```typescript
import { updateGroup, validate } from 'ngrx-forms';
import { updateArray, updateGroup, validate } from 'ngrx-forms';

export interface NestedValue {
someNumber: number;
Expand All @@ -36,21 +39,25 @@ export interface MyFormValue {
someTextInput: string;
someCheckbox: boolean;
nested: NestedValue;
someNumbers: number[];
}

function required(value: any) {
return !!value ? {} : { required: true };
}

function min(value: number, minValue: number) {
return value >= minValue ? {} : { min: [value, minValue] };
function min(minValue: number) {
return (value: number) => {
return value >= minValue ? {} : { min: [value, minValue] };
};
}

const updateMyFormGroup = updateGroup<MyFormValue>({
someTextInput: validate(required),
nested: updateGroup({
someNumber: validate([required, min]),
someNumber: validate([required, min(2)]),
}),
someNumbers: updateArray(validate(min(3))),
});
```

Expand All @@ -62,8 +69,9 @@ In addition, the `updateGroup` function allows specifying as many update functio
const updateMyFormGroup = updateGroup<MyFormValue>({
someTextInput: validate(required),
nested: updateGroup({
someNumber: validate(required),
someNumber: validate([required, min(2)]),
}),
someNumbers: updateArray(validate(min(3))),
}, {
// note that the parent form state is provided as the second argument to update functions;
// type annotations added for clarity but are inferred correctly otherwise
Expand All @@ -90,8 +98,9 @@ This curried function combines a `formGroupReducer` and the `updateGroup` functi
const myFormReducer = groupUpdateReducer<MyFormValue>({
someTextInput: validate(required),
nested: updateGroup({
someNumber: validate(required),
someNumber: validate([required, min(2)]),
}),
someNumbers: updateArray(validate(min(3))),
}, {
nested: (nested, myForm) =>
updateGroup<NestedValue>({
Expand Down
2 changes: 2 additions & 0 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ The following sections will explain and showcase the different features of `ngrx
* [Value Conversion](FORM_CONTROLS.md#value-conversion)
* [Form Groups](FORM_GROUPS.md#form-groups)
* [Dynamic Form Groups](FORM_GROUPS.md#dynamic-form-groups)
* [Form Arrays](FORM_ARRAYS.md#form-arrays)
* [Dynamic Form Arrays](FORM_ARRAYS.md#dynamic-form-arrays)
* [Updating the State](UPDATING_THE_STATE.md#updating-the-state)
* [Validation](VALIDATION.md#validation)
* [Custom Controls](CUSTOM_CONTROLS.md#custom-controls)
Expand Down
10 changes: 10 additions & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ module.exports = function (config) {
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['spec'],

specReporter: {
maxLogLines: 10,
suppressErrorSummary: true,
suppressFailed: false,
suppressPassed: true,
suppressSkipped: true,
showSpecTiming: false,
failFast: false
},

// web server port
port: 9876,

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"pack-lib": "npm pack ./dist",
"publish-lib": "npm publish ./dist",
"compodoc": "compodoc -p tsconfig.json",
"compodoc-serve": "compodoc -s"
"compodoc-serve": "compodoc -s",
"example-install": "npm run build && move ./dist/@ngrx ./dist/ngrx && npm pack ./dist && cd example-app && npm install ../ngrx-forms-1.1.1.tgz && move ./node_modules/ngrx-forms/ngrx ./node_modules/ngrx-forms/@ngrx && cd .."
},
"typings": "./forms.d.ts",
"author": "",
Expand Down
Loading

0 comments on commit 19d4e49

Please sign in to comment.