Giter Club home page Giter Club logo

ngrx-forms's Introduction

ngrx-forms

npm version Build Status codecov Docs license

ngrx-forms brings the strengths of the redux state management model to the world of forms in applications that are using Angular and ngrx. The mechanisms that Angular provides for working with forms are inherently mutable, local, and hard to debug. This library offers a different model for working with forms. Instead of storing the state of form controls inside the components we put them in the ngrx store. We update the state with actions which allows easy debugging just like any other redux application. ngrx-forms also provides powerful mechanisms to update, validate and generally manage large complex forms. It contains APIs for synchronous and asynchronous validation, creating dynamic forms, integrating with custom form elements, and much more.

To get to know more you can either read the official documentation or visit the example application.

Installation

npm install ngrx-forms --save

This library has a peer dependency on @angular/core, @angular/common, @angular/forms, and @ngrx/store, so make sure appropriate versions of those packages are installed.

Bug reports

To report a bug please provide a reproduction of the issue in a code sandbox. You can fork this example.

Contributing

Please see the documentation.

License

Everything in this repository is licensed under the MIT License unless otherwise specified.

Copyright (c) 2017-present Jonathan Ziller

ngrx-forms's People

Contributors

dependabot[bot] avatar dzonatan avatar icepeng avatar lucax88x avatar mr-eluzive avatar mrwolfz avatar mucaho avatar philjones88 avatar r-khr avatar scott-wyatt avatar sloff avatar solnat avatar tashoecraft avatar tbroadley avatar tomvandezande avatar wbhob avatar ymekesser avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ngrx-forms's Issues

Question/Idea about form state dirty values

Is there a way, or should there be a way to to filter formState.values for only dirty values? What I am trying to do is take the same array as formState.value but where only dirty values exist. I thought of traversing the controls tree for isDirty including children. I figured it could be a common task (?) so could be part of the library. Am I following an anti-pattern or any ideas how to achieve this easily?

Dynamic FormGroup should be able to work with Array as FormGroupValue

Dynamic FormGroup should be able to work with Array as FormGroupValue

  • Here is quick info, but screenshot and code below will probably tell the whole story:
    • When using Array as FromGroupValue object, it works, I can access that array substate as object literal form state with Array indexes as string keys
    • Even the FormValue is Array after form creation
    • But after first modification of some Array element, form value Array gets converted to object literal with Array indexes as string keys => but that should be converted to Array instead, because source value was array and not object literal

img

// reducer.ts

import * as actions from '../actions/form-with-array.actions';
import { FormGroupState, createFormGroupState, groupUpdateReducer, setValue, cast } from 'ngrx-forms';

export interface State {
    entity: MyEntity,
    form: FormGroupState<MyEntity>,
}

export interface MyEntity {
    field: string,
    entities: MyEntity[],
}

export interface OtherEntity {
    simpleField: string,
}

const initialEntity: MyEntity = {
    field: '',
    entities: [],
};

const apiEntity: MyEntity = {
    field: 'foo',
    entities: [
        { field: 'bar', entities: [] },
        { field: 'baz', entities: [] },
    ]
};

export const initialState: State = {
    entity: initialEntity,
    form: createForm(initialEntity),
};

const formReducer = groupUpdateReducer<MyEntity>();

export function reducer(
    state = initialState,
    action: actions.Actions,
): State {
    state = reduceForm(state, action);

    switch (action.type) {
        case actions.LOAD_SUCCESS: {
            const entity = apiEntity; // just example - get it from real api instead
            const form = createForm(entity);
            const newState = reduceForm({ ...state, form }, action);
            return { ...newState, entity };
        }

        case actions.SAVE: {
            const entity = {
                ...state.entity,
                ...state.form.value,
            };
            return { ...state, entity };
        }

        default: {
            return state;
        }
    }
}

function createForm(
    entity: MyEntity,
): FormGroupState<MyEntity> {
    return createFormGroupState<MyEntity>(
        'MyFormName',
        entity,
    );
}

function reduceForm(
    state: State,
    action: actions.Actions,
): State {
    const form = formReducer(state.form, action);
    return form === state.form ? state : { ...state, form };
}
// component.ts

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Store } from '@ngrx/store';
import { State } from '../../reducers/index';
import { FormGroupState, FormGroupControls } from 'ngrx-forms';
import * as actions from '../../actions/form-with-array.actions';
import { Observable } from 'rxjs/Observable';
import { MyEntity } from '../../reducers/form-with-array.reducer';

@Component({
    selector: 'app-form-with-array-container',
    templateUrl: './form-with-array-container.component.html',
})
export class FormWithArrayContainerComponent implements OnInit {
    vm$: Observable<FormGroupState<MyEntity>>
    form: FormGroupState<MyEntity>

    constructor(
        private store$: Store<State>,
    ) { }

    ngOnInit() {
        this.store$.dispatch(new actions.LoadSuccessAction());
        this.vm$ = this.store$
            .select(state => state.patient.formWithArray.form)
            .do(form => { this.form = form; })
    }

    get entities() {
        const entitiesGroupState = this.form.controls.entities as FormGroupState<MyEntity[]>;
        return entitiesGroupState.controls as FormGroupControls<MyEntity[]>;
    }

    get entitiesKeys() {
        return Object.keys(this.entities);
    }

    getEntity(key: string) {
        return this.entities[key] as FormGroupState<MyEntity>;
    }
}
<!-- Component HTML -->

<div *ngIf="vm$ | async"></div>

<app-form [form]="form" colLabel="3" colControl="9">
    <div class="col-md-6">
        <app-panel>
            <app-panel-heading>MyEntity</app-panel-heading>
            <app-panel-body>
                <app-form-group [control]="form.controls.field">
                    <app-form-label>Field</app-form-label>
                    <app-form-control-input></app-form-control-input>
                </app-form-group>
            </app-panel-body>
        </app-panel>
        <app-panel>
            <app-panel-heading>MyEntity.Entities</app-panel-heading>
            <app-panel-body *ngIf="!entitiesKeys.length">
                There are no entities yet.
            </app-panel-body>
            <app-panel-body *ngIf="entitiesKeys.length">
                <app-panel *ngFor="let key of entitiesKeys; let index = index">
                    <app-panel-heading>Entity {{index+1}}</app-panel-heading>
                    <app-panel-body>
                        <app-form-group [control]="getEntity(key).controls.field">
                            <app-form-label>Field</app-form-label>
                            <app-form-control-input></app-form-control-input>
                        </app-form-group>
                    </app-panel-body>
                </app-panel>
            </app-panel-body>
        </app-panel>
    </div>
</app-form>

Using select options break the application

I'm pretty new to ngrx-forms, but today when I tried it, I got into a pretty serious bug.

It seems that using a select component with options breaks the application when it has options with values. My code looks as follows:

<form novalidate [ngrxFormState]="(formState$ | async)">
    <select
        id="option"
        [ngrxFormControlState]="(formState$ | async).controls.myOption">
        <option [value]="">Default</option>
        <option [value]="A">A</option>
    </select>
</form>

When I use this code, I keep getting this error.

TypeError: this._renderer.setElementProperty is not a function
at NgrxSelectOption.webpackJsonp.../../../forms/@angular/forms.es5.js.NgSelectOption._setElementValue (forms.es5.js:1402)
at NgrxSelectOption.set [as value] (forms.es5.js:1389)

Is there anything I'm missing to make the select work or is there a bug?

Angular service extension typing error in vscode

Hi just got started with the library, it's good. Just a small issue:

This line causes an angular service extension error/warning in vscode [Angular] Identifier 'controls' is not defined. 'AbstractControlState' does not contain such a member although it does exist and works just fine. Anything I can do about this? Thanks

Form with a dynamic array of objects inside plus validation

Hi,

I have a model which is something like this:

Player {
ebuNumber: number;
firstName: string;
lastName: string
}

EntryForm{
comments: string;
heat: number;
players: Player[]
}

How would I go about creating the ngrx form to model this? I also need to be able to do the following:

  1. Dynamically add players to the players array - also default the size of this when the form is initially created to a known number of players which depends on the event they are entering (stored in another part of the store)

  2. Perform validation on each of the players - i.e., ebuNumber must be of a particular format and firstName/lastName are required fields for all players

  3. The heat is a required field based on the value which is stored in another part of the store - i..e, the event they are entering may or not have associated heats and only if the event has heats is the heat id mandatory

Any help you could give would be very much appreciated. :-)

Dynamic Array of Objects

I'm still wrapping my head around everything here and i'm curious how one would implement a dynamic list of objects.

interface Person {
surname: string;
givenName: string;
middleName: string;
}

image
i'd like to add a new entity each time the last one is filled in. I'm just not sure about the form implementation. As usual, i'm probably over-thinking this..

would i just use a normal array and handle it in 3's with the reducer and such and parse out the values into the model structure?

Reducer Decorator

Curiosity - Question
@MrWolfZ This seems very superficial and purely cosmetic, but I'm curious. Would it be possible to attach the form reducer methods with their own Decorator instead of the current spreading before the switch?

Set async error on control using effect

I'm currently trying to set an async error on a control using an effect. I would like, for instance, to check if a control is unique. I tried several things, but none worked. This is one of the tries I made using an effect.

 @Effect() asyncValidateForm$: Observable<Action> = this.action$
  .ofType(MarkAsSubmittedAction.TYPE)
  .filter((action: MarkAsSubmittedAction) => action.controlId === FORM_ID)
  .delay(2000)
  .map(() => {
    const errors: ValidationErrors = {
      duplicated: true
    };

    return new SetErrorsAction(`${FORM_ID}.id`, errors);
  });

I tried the following:

  • Setting the controlId to: FORM_ID.controlId using a flat error object.
  • Using the name of the controlId in the error object.
  • Using the name of the controlId prefixed with an underscore in the error object (with FORM_ID as the controlId).

There seem to be code to handle the last case, but an if statement prevents me from reaching that code. Is there a way to do it?

Ordering controls

How would one go about doing this best?

Lets say we have an array of Person and Person looks like:

export class Person {
    id: number;
    name: string;
    age: number;
}

Do it inside the reducer perhaps?

Complex object type as value for Autocomplete form control

Hi @MrWolfZ

I have a form with material mat-autocomplete fields which allow to select a user. When I bind the user instance as value for mat-option, then the SetValueReducer complains about the non-primitive value:

<mat-form-field>
  <input matInput placeholder="Author" [matAutocomplete]="auto" [ngrxFormControlState]="form.controls.author">
  <mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
      <mat-option *ngFor="let user of users" [value]="user">{{user.displayName}}</mat-option>
  </mat-autocomplete>
</mat-form-field>

So for now the only way to use ngrx-forms in this case is to manage just the user id in the form state and then lookup the display name somewhere else (e.g. in the same store section where the user suggestions are kept). But this requires a lot of additional effort, especially when a route is opened directly by url and no user suggestions have been queried so far. Or the data must be stored in a normalized form by default.

Another solution is to intercept 'ngrx/forms/SET_VALUE' action and handle it by myself. Will it break something when I store complex object values in the form state?

Thanks,
Dominik

Validation: CSS classes?

Hallo,
from Angular ReactiveForms I'm used to it that the CSS classes are set on the form controls.
Can this be enabled with this library somehow? Is it an out-of-the-box feature that I have overlooked?

ExpressionChangedAfterItHasBeenCheckedError when using user defined properties

I have a custom autocomplete control that I use like this:

<app-autocomplete 
    label="Beschikbaarheid" 
    id="availability" 
    [ngrxFormControlState]="(controls$ | async).availability" 
    [values]="availabilities$ | async">
</app-autocomplete>

Next to that I just created a directive to set the values to the state of the control to validate if the input is actually in the list of values:

import { Directive, Input, OnInit } from "@angular/core";
import { FormControlState, SetUserDefinedPropertyAction } from "ngrx-forms";
import { ReferenceData } from "../../services/reference-data/reference-data.model";
import { ActionsSubject } from "@ngrx/store";

@Directive({
    selector: 'app-autocomplete[ngrxFormControlState][values]',
})
export class AutocompleteValuesDirective implements OnInit {
    @Input() ngrxFormControlState: FormControlState<string>;
    @Input() values: ReferenceData[];

    constructor(private actionsSubject: ActionsSubject) { }

    ngOnInit() {
        this.actionsSubject.next(new SetUserDefinedPropertyAction(this.ngrxFormControlState.id, 'AUTOCOMPLETE_VALUES', this.values));
    }
}

Since then, Angular is showing ExpressionChangedAfterItHasBeenCheckedError in the console. This seems to make sense since the state of the control (ngrxFormControlState) is actually changed because of the added values by the directive.

Is this behavior something you're aware of and do you maybe have a suggestion to work around it? Or, am I doing something wrong myself?

Question: Not getting any validation messages (Angular Material - mat-error).. why?

My form is binding to ngrx correctly and I've added the FormGroupState into my reducer state but when deleting required values I'm not getting any mat-error messages appearing despite the fact that my form model does contain the correct error child value.

reducer.ts

import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { createFormGroupReducerWithUpdate, createFormGroupState, FormGroupState, validate } from 'ngrx-forms';
import { required } from 'ngrx-forms/validation';

import { System } from '../../../model/fusion.dtos';
import { FusionCoreAppState } from '../../index';
import { SystemsActions, SystemsActionTypes } from '../actions/systems.actions';

const SystemEditFormId = 'SystemEditFormId';
const InitialSystemEditFormValue: System = {
  id: 0,
  name: '',
  initials: '',
  colour: ''
};

export const InitialSystemEditFormState = createFormGroupState<System>(SystemEditFormId, InitialSystemEditFormValue);

const validationFormGroupReducer = createFormGroupReducerWithUpdate<System>({
  name: validate(required),
  initials: validate(required),
});

export interface State extends EntityState<System> {
  selectedSystemId: number;
  systemEditForm: FormGroupState<System>;
  loaded: boolean;
  loading: boolean;
  error: string | null;
}

export const initialState: State = adapter.getInitialState({
  selectedSystemId: null,
  systemEditForm: InitialSystemEditFormState,
  loaded: false,
  loading: false,
  error: null,
});

export function reducer(state = initialState,
                        action: SystemsActions): State {

  const systemEditForm = validationFormGroupReducer(state.systemEditForm, action);

  if (systemEditForm !== state.systemEditForm) {
    state = { ...state, systemEditForm };
    return state;
  }

  switch (action.type) {
    case SystemsActionTypes.LoadSingle:
    case SystemsActionTypes.Load: {
      return {
        ...state,
        loading: true,
        error: null,
      };
    }

    case SystemsActionTypes.LoadSuccess: {
      return adapter.addMany(action.payload, {
        ...state,
        loaded: true,
        loading: false,
        error: null,
      });
    }

    case SystemsActionTypes.LoadSingleSuccess: {
      return adapter.addOne(action.payload, {
        ...state,
        loaded: true,
        loading: false,
        error: null,
      });
    }

    case SystemsActionTypes.Select: {
      return {
        ...state,
        selectedSystemId: action.payload,
      };
    }

    default: {
      return state;
    }
  }
}

`

component

import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ActionsSubject } from '@ngrx/store';
import { FormGroupState, ResetAction, SetValueAction } from 'ngrx-forms';

import { System } from '../../../../model/fusion.dtos';
import { InitialSystemEditFormState } from '../../../../store/systems/reducers/systems.reducer';

@Component({
  selector: 'fusion-system-edit-form',
  template: `    
    <div class="content p-24">
      <div fxLayout="column" fxLayoutAlign="start start" fxLayout.gt-md="row">

        <form class="mat-white-bg mat-elevation-z4 p-24 mr-24 mb-24" fxLayout="column" fxLayoutAlign="start"
              fxFlex="1 0 auto" name="form" novalidate [ngrxFormState]="systemEditState"
              (submit)="submit()">

          <div fxLayout="row" fxLayoutAlign="start center" fxFlex="1 0 auto">

            <mat-form-field fxFlex="50">
              <input matInput
                     placeholder="Name"
                     [ngrxFormControlState]="systemEditState.controls.name" required>
              <mat-error *ngIf="systemEditState.errors._name?.required">
                A name is required
              </mat-error>
            </mat-form-field>

            <mat-form-field fxFlex="50">
              <input matInput placeholder="Initials"
                     [ngrxFormControlState]="systemEditState.controls.initials" required>
              <mat-error *ngIf="systemEditState.errors._initials?.required">
                Initials are required
              </mat-error>
            </mat-form-field>

          </div>

          <ul *ngIf="systemEditState.isInvalid"
              class="error-messages">
            <li *ngIf="systemEditState.errors._name?.required">
              A name is required
            </li>
            <li *ngIf="systemEditState.errors._initials?.required">
              Initials are required
            </li>
          </ul>

          <div class="buttons">
            <div>
              <button mat-raised-button
                      color="primary"
                      type="submit"
                      [disabled]="systemEditState.isInvalid && systemEditState.isSubmitted">
                Register
              </button>
              <button mat-raised-button
                      type="button"
                      [disabled]="systemEditState.isPristine
                          && systemEditState.isUntouched
                          && systemEditState.isUnsubmitted"
                      (click)="reset()">
                Reset
              </button>
            </div>
          </div>

        </form>

        <div class="form-errors-model mat-white-bg p-24 mat-elevation-z4">

          <div class="h2 mb-24">Reactive Form Errors Model</div>

          <pre><code>{{systemEditState.errors | json}}</code></pre>
        </div>

      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SystemEditFormComponent {
  @Input() systemEditState: FormGroupState<System>;
  @Input() originalValue: System;
  submittedValue: System;

  constructor(private actionsSubject: ActionsSubject) {
  }

  reset() {
    this.actionsSubject.next(new SetValueAction(InitialSystemEditFormState.id, this.originalValue));
    this.actionsSubject.next(new ResetAction(InitialSystemEditFormState.id));
  }

  submit() {
    if (this.systemEditState.isInvalid) {
      return;
    }

    this.submittedValue = this.systemEditState.value;
  }
}

Is there something I'm doing wrong? I suspect that I'm not adding the ngrx-forms reducer into my existing reducer correctly?

An array of FormGroupState?

Let's say you want to build state like:

export interface MyForm {
   id: number;
   name: string;
}

export interface CustomState {
   foo: string;
   bar: string;
   myForm: FormGroupState<MyForm>;
}

export function reducer(state: CustomState[] = [], action: myActions.Actions): CustomState[] {
...
}

How would I go about applying formGroupReducer in this scenario where it's inside an array

Nested form: 'The control state must not be undefined!'

Hallo,
I've got a nested form that is several levels deep:

export const initialShippingEditorFormState = createFormGroupState<ShippingEditorForm>(SHIPPING_EDITOR_FORM_ID, {
	ups: {
		extraServices: {
			...
		}...
	},
	...
});

I have a root Angular component that's bound to the root form state. Each nested form state has an own component that's also bound. In the root component the binding looks like this:

<form [ngrxFormState]="formState$ | async">
     ....
     <ups-overview [formState]="(formState$ | async).controls.ups"></ups-overview>
     ...

The binding in ups-overview looks like this:

<ups-extra-services [formState]="formState.controls.extraServices"></ups-overview>

Please note that formState$ in the root is an Observable<FormGroupState> and in the child components it's just an @input() FormGroupState. Now when I run the application I get the error 'The control state must not be undefined!' within ngrx-forms code for the ups-extra-services component.

I debugged it by setting a breakpoint at the location where the exception is thrown and within the setter of the formState-input-property:
ngrx-form is called first, before the data binding took place.

Do you have any suggestion how I could fix this problem?

Composing custom and built-in reducers / howto

I've built a reducer function that composes a custom and a built-in ngrx/forms reducer. The reason for that is to have some updates done before the state is handled by the built-in reducers.

Therefore, I created two custom reducers:

  • formPrepareReducer (This reducer function clears any error in the control after the touched, dirty or setValue action is called. This function must be called before any other ngrx/forms reducer.)

  • formReducer (This is the replacement of the default formGroupReducer. Instead of formGroupReducer, the custom formReducer is called)

The code looks like this:

// The prepare reducer has to be called before the other reducers
function formPrepareReducer<T>(formGroupState: FormGroupState<T>, action: any) {
    switch (action.type) {
        case MarkAsTouchedAction.TYPE:
        case MarkAsDirtyAction.TYPE:
        case SetValueAction.TYPE:
            return formGroupReducer(formGroupState, new SetErrorsAction(action.controlId, {}));
    }
    return formGroupState;
}

// The formReducer replaces the formGroupReducer, beeing used in the main reducer
export function formReducer<TValue extends object>(...updateFnsArr: Array<StateUpdateFns<TValue>>) {
    return (state: FormGroupState<TValue>, action: any) =>
        compose<FormGroupState<TValue>>(
            s => formPrepareReducer(s, action), // calls the prepare reducer first
            s => formGroupReducer(s, action), // calls the default form group reducer second
            updateGroup<TValue>(...updateFnsArr) 
        )(state);
}

Altough the above code is working (I haven't tested it with 2.0), I'm wondering if there is a more efficient solution to compose reducers, especially in version 2.0 of ngrx/forms.

Thank you very much for your help in advance.

Property 'controls' does not exist on type 'AbstractControlState<T>'

Got back from holiday and upgraded to Angular 5.0.5 and Angular CLI 1.5.5 as they've fixed a few issues I had with building in AOT and I thought I'd give it another go.

I get these errors:

Error at /Users/phil/Documents/Projects/vq/src/src/app/foo/components/template-form/template-form.component.html(95,5): Property 'controls' does not exist on type 'AbstractControlState<TemplateEntry<number>>'.

Strangly for this one module but not the others that use NGRX-Forms.

I suspect it might be due to the convulted typings that I have to use to match the API?

The component accepts the FormGroupState as an @Input():

  @Input()
  public template: FormGroupState<TemplateViewModel>;

and the template looks like:

  <input type="number"
    [disabled]="template.value.allowWrite === false"
    [ngrxFormControlState]="template.controls.foo.controls.value">

the model is a little convulted:

export class TemplateViewModel {
    public allowWrite: boolean;

    public foo: TemplateEntry<number> = new TemplateEntry<number>(0);
}

export class TemplateEntry<T> {
    public value: T;

    public allowEdit = false;

    constructor(val?: T, allow: boolean = false) {
        this.value = val;
        this.allowEdit = allow;
    }
}

Any ideas?

RemoveArrayAction does not update child controls ID

Ciao Wolfz :)

I'm handling an array of entities, and I have a remove button on each entity row, so for example, when I have 3 rows I can remove the middle one, and so on.

What's going on is that when I dispatch a RemoveArray action it's indeed updating the ID of the subsequents items, but not the id of their inner controls.

simple example is below:

3 items on array, with nested fields of Name & Surname

    ITEM0
     Name0
     Surname0
    ITEM1
     Name1
     Surname1
    ITEM2
     Name2
     Surname2

we now remove ITEM1 with a Remove_Array action from the library, ITEM2 gets correctly renamed to ITEM1, but the nested fields remains the same.

    ITEM0
     Name0
     Surname0
    ITEM1
     Name2
     Surname2

we now add a 3rd item, it's get ITEM2, and the nested fields? guess it..

    ITEM0
     Name0
     Surname0
    ITEM1
     Name2
     Surname2
    ITEM2
     Name2
     Surname2

This situation now leads to duplicated data, because when I modify Name on ITEM1 also modifies the one in ITEM2.

Regards,
Luca

subsequent focus actions

I'm firing off the below effect to reset the form and set focus, it works for the first submit, but all subsequent the focus action dispatches but the control is not focused.

  @Effect({dispatch: false})
  addDistrictSuccess$ = this.actions$
  .ofType(districtActions.DistrictActionTypes.AddDistrictSuccess)
  .do(() => {
    this.actionSubject.next(new FocusAction(districtForm.initialFormState.controls.districtName.id));
    this.actionSubject.next(new SetValueAction(districtForm.initialFormState.id, districtForm.initialFormState.value));
    this.actionSubject.next(new ResetAction(districtForm.initialFormState.id));
  });

How to do conditional validation based on other state values?

Here's a use cases I have that I'm wondering how to implement via ngrx-forms:

Let's say you have a user profile information page where they can update their phone number, email address, etc.

There's certain validations that only need to run conditionally, both based on state from the original user model (stored elsewhere in the state, not in the form state as it isn't a form).

For eg:

  1. Confirmation field for email is not required unless the email address has been changed. This means that when the email address field is updated, the value needs to be compared to the original email address (sitting elsewhere in the state on the user model returned from the server) to determine whether the confirmation field should be marked as required or not.

  2. The phone number field is required -- but only if the user already had a phone number set. This means that the required validator for this field should only be enabled if the user model returned from the server to begin with has a phone number present.

Sync validation of array?

I've been digging through the example app and docs but I can't see an example of how to validate an array of objects?

class Item {
  name: string; // Needs to be required
  description: string;
}

class Template {
  name: string;
  items: Item[];
}

Question: A user edit form initial state

Hi, just discovered this repo today and I like it. I am considering using it as it looks quite beneficial.

Just after a bit of guidance here. I would be creating a user edit form (after selecting a user from a list) and have the following questions about how I would do that here:

  1. How do I initialize the form state using user data? None of the examples seem to do this - probably something simple I'm just missing philosophically speaking. An initial action dispatched somewhere else perhaps?
  2. Do all users' edit forms share the same state key? i.e. something like selectedUser: User? That will need to be cleared/reset/updated when another user is selected.
  3. If the user's data were to change during editing - how to ensure a good user experience (i.e. not suddenly replace the values you're changing with the latest ones)
  4. If the form is updating selectedUser, wouldn't other components now display unsaved data? Seems wrong - as I would expect to update that state only after successfully updating the user. Need both a selectedUser and a selectedUserForm ?

Hopefully my questions are clear and that someone can help fill in my quirky knowledge gaps. Thanks

Error is not updated when FormArrayState has empty array value

When FormArrayState has [] value, error is not updated with SetErrorsAction() or setErrors().

This is necessary because there are use-case of "Array must have one or more value".

If this issue is resolved, validate(required) would be extended to validate FormArrayState.

<select> value is empty

Hi,

I have an issue with all select fields in my application which I didn't have earlier, but I'm not sure when the issue appeared first. I haven't managed to reproduce that issue in a clean project and I cannot publish my code, however I started debugging the issue and maybe someone can point me into the right direction on what I might do wrong.

First of all, I am aware of #23, however I debugged setViewValue and it uses the correct value.

The initial situation is that I have a form state which already has a value assigned to the field which is rendered as a select field.

The control state:

{
  "id": "airline.createRfp.proposalCurrencyLabor",
  "value": "USD",
  ...
}

The template:

<select [ngrxFormControlState]="controlState">
    <option  value="USD">USD</option>
    <option  value="EUR">EUR</option>
</select>

I'll try to explain the issue with screenshots I made while debugging:

First, setViewValue is called with the initial value (USD) but the optionsMap is still empty so the element's value is set to null.

image

Then the vlaue of the "USD" option is set to "0" via the ' NgrxSelectOption. This fixes the optionMap` problem above from what I understand.

image

Then setViewValue is called again with "USD" and because the optionMap now has the correct value, the select is rendered with the correct value

image

image

Now is the point were I suspect the problem. For whatever reason NgrxSelectMultipleOption's ngOnInit fires and sets the options value to "". Note: The select is not configured to allow multiple values! Maybe, because both NgrxSelectOption and NgrxSelectMultipleOption have the same selector?

image

Now, when the setViewValue method is called the third time around, selectedId is "0", which is correct, however the option's value has been changed back to the original "USD" value and not "0"

image

In the result, the select loses its value.

Any ideas?

FormGroupState missing in Version 2x

Just updated to 2.0.1. Webpack gives me following warning in the console:

WARNING in ./src/app/foo.component.ts
44:84-98 "export 'FormGroupState' was not found in 'ngrx-forms'

After looking at the bundle .js files, I noticed that FormGroupState is not exported.

But type definitions are working.

setValue should accept partial value

The setValue function definition was defined as the following:

export declare function setValue<TValue>(value: TValue): ProjectFn<AbstractControlState<TValue>>;
export declare function setValue<TValue>(value: TValue, state: AbstractControlState<TValue>): AbstractControlState<TValue>;

However it will be better to provide partial values, so the definition can be rewrite as the following (added Partial):

export declare function setValue<TValue>(value: Partial<TValue>): ProjectFn<AbstractControlState<TValue>>;
export declare function setValue<TValue>(value: Partial<TValue>, state: AbstractControlState<TValue>): AbstractControlState<TValue>;

No provider for ControlContainer

I am trying to replace some form code within my project to use ngrx-forms but am having the following template parsing error. Do you have any idea what I am missing?

No provider for ControlContainer (" [ERROR ->]<form class="builder" [ngrxFormState]="formState$ | async">

<mat-tab la"): ng:///QueryBuilderModule/QueryBuilderComponent.html@0:2
Error: Template parse errors:
No provider for ControlContainer (" [ERROR ->]<form class="builder" [ngrxFormState]="formState$ | async">

<mat-tab la"): ng:///QueryBuilderModule/QueryBuilderComponent.html@0:2

Issue with <select> options

Hey,

I'm having an issue using the library with <select> elements.

Here are the two problems I've encountered so far:

  1. When initialising my state with a certain value, the correct select option is not selected on the DOM element, despite the state being correct. The selected option in the DOM is just the first one on the list.

  2. Seems somewhat related to the issue above. When using the dev tools to time travel, I noticed that the <select> stops selecting the option as per the state object.

Please let me know if I can provide any more details.

FormViewAdapter for custom control

Hi,

first of all: thank you for this great library! I'm surprised that it hasn't gotten more traction yet. It is by far the best solution for combining reactive forms and ngrx that I've seen yet.

I have a question regarding custom controls. The documentation states...

However, since many third party libraries are using the ControlValueAccessor already, ngrx-forms integrates with these libraries by converting the value accessor internally into a view adapter

To me, this reads like "if the custom control component implements ControlValueAccessor, everything should work out of the box". However, in my project this fails with No valid view adapter!. And when looking at the view adapters in src/view-adapter I don't understand how it could work as all adapters have a selector like selector: 'input:not([type=checkbox])[ngrxFormControlState],textarea[ngrxFormControlState]',, so of course no adapter can be found for my custom component.

Am I missing something?

Question: how to handle api submission errors?

I'm using ngrx-forms (great code btw!) and have a situation where administrators can sign up new users in a user creation form.

The thing is, if they submit a username that already exists our API returns an exception.

I'm handling the exception with ngrx effects and displaying a snackbar (angular app) but the exception seems to leave my form.value in an inconsistent state.

Should ngrx handle this scenario out of the box?

How to implement nested form groups more than 2 levels deep

Hi,

I was using the dynamic forms reducer example, On my project I need 3 levels deep to add and remove items on my form. Are there any examples you can provide?

In the code, entityLocations should be a member of requestEntities. How do assign new EntityLocation to a specific RequestEntity?

RequestEntities['7df2d750-31c6-4314-860d-c07fe9a34c7c'].EntityLocations['6ddcb217-446b-4acb-b6a5-c92bde1b3282']: {
"Country": {
"PrimaryKey": "",
"BusinessEntityNK": ""
},
"State": {
"PrimaryKey": "",
"BusinessEntityNK": ""
},
"City": "Test CITY",
"IsCrossBorder": false,
"CreatedByUser": "",
"UpdatedByUser": ""
}

I also attached a sample of my reducer code.

service-request.reducer.txt

2018-01-07_15-07-09

<select /> for number with *ngFor number

Hey,

Can't seem to get this working.

Lets say you have the model:

export class Template {
   barId: number
}

and an array of Bar options:

export class Bar {
   id: number;
   label: string;
}

and you try to combine them in a form like:

template$: Observable<FormGroupState<Template>>;
bars$: Observable<Bar[]>;
<select [ngrxFormControlState]="(template$ | async)?.controls?.barId">
    <option *ngFor="let b of (bars$ | async)" [ngValue]="b.id">{{b.label}}</option>
</select>

I cannot get it to select the saved value.

Any ideas?

Ngrx-forms and Angular Material

Hi,

Thanks for the help you gave me earlier - much appreciated. I have a new problem now:

I am trying to change my entry form to use angular material. I have the following:

<mat-form-field>
  <input matInput placeholder="Name"
    [ngrxFormControlState]="entryForm.controls.name" required/>
  <mat-error *ngIf="!entryForm.controls.name.isValid && entryForm.controls.name.isTouched">
    You must enter a name.
  </mat-error>
</mat-form-field>

but the error message is not being displayed even though I can see in devtools that 'isValid' is false and 'isTouched' is true.

Is there something I need to do to get this to work properly?

Question: How to bind to mat-selection-list

Hi

I'm having difficulties binding my ngrx-forms formState to a mat-selection-list. If I bind to the [selected] input the list doesn't display at all.

Any ideas where I'm going wrong?

Model

export class ShouldSearchType {
  type: SearchType;
  selected: boolean;
}


export class Search {
  searchPhrase: string;
  searchTypes: ShouldSearchType[];
}

Reducer

const SearchFormId = 'SearchFormId';
export const InitialSearchFormValue: Search = {
  searchPhrase: '',
  searchTypes: [
    {
      type: SearchType.Project,
      selected: true,
    },
    {
      type: SearchType.Customer,
      selected: true,
    },
    {
      type: SearchType.Person,
      selected: true,
    },
  ],
};
export const InitialSearchFormState = createFormGroupState<FusionSearch>(SearchFormId, InitialSearchFormValue);

export interface State {
  searchForm: FormGroupState<FusionSearch>;
  searchResults: {
    customers: SearchResponse,
    people: SearchResponse,
    projects: SearchResponse
  };
  loaded: boolean;
  loading: boolean;
  error: string | null;
}

export const initialState: State = {
  searchForm: InitialSearchFormState,
  searchResults: {
    customers: null,
    people: null,
    projects: null,
  },
  loaded: false,
  loading: false,
  error: null,
};

const validationFormGroupReducer = createFormGroupReducerWithUpdate<FusionSearch>({
  searchPhrase: validate(required),
});

export function reducer(state = initialState, action: SearchActions): State {

  const searchForm = validationFormGroupReducer(state.searchForm, action);

  if (searchForm !== state.searchForm) {
    state = { ...state, searchForm };
    return state;
  }
...

Component

export class SearchFiltersListComponent {
  @Input() formState: FormGroupState<Search>;
  @Output() onFilterChanged = new EventEmitter();

  constructor() {
  }

  filterChanged() {
    this.onFilterChanged.emit();
  }

  getSearchType(searchTypeId: number) {
    return SearchType[searchTypeId];
  }
}

This view displays the search types correctly, but without the checkboxes bound

  <form name="search-filters-form" novalidate
        [ngrxFormState]="formState">

      <mat-selection-list [ngrxFormControlState]="formState.controls.searchTypes">
        <mat-list-option *ngFor="let st of formState.value.searchTypes" [value]="st.type">
          {{getSearchType(st.type)}}
        </mat-list-option>
      </mat-selection-list>

...and when I look to bind the checkboxes like below I get nothing displayed at all

  <form name="search-filters-form" novalidate
        [ngrxFormState]="formState">

      <mat-selection-list [ngrxFormControlState]="formState.controls.searchTypes">
        <mat-list-option *ngFor="let st of formState.value.searchTypes" [value]="st.type" [selected]="st.selected">
          {{getSearchType(st.type)}}
        </mat-list-option>
      </mat-selection-list>

Control Value Accessor

Good Day,

I'm a little confused with the control value accessor to be used. I am trying to create a custom form control but sadly it isn't working. I also tried using the form view adapter in the documentation. It logs the writeValue() method but when I am triggering the propagateChange(newValue) function it's not updating the state.

`
clearSelection() {
const selected = _.cloneDeep(this.selected)
this.selected = []
// this.selectDeselect.emit(this.selected)
this.propagateChange(this.selected)
}

writeValue(value: any) {
if (value !== undefined) {
this.selected = value
}
}

propagateChange = (value: any) => {}
registerOnChange(fn: (value: any) => void): void {
this.propagateChange = fn
}

registerOnTouched(fn: () => void) {}
`
My custom component

<comp [ngrxFormControlState]="(myFormState$ | async).controls.group"></comp>

Thanks!

New user requires review

Hi,

I'm trying to implement ngrx-forms into an application I'm working on and so far everything is working and a whole bunch of code has been removed or moved from components to reducers, which is great. Before I continue the implementation I'd like a quick review of the way I'm working since I'm not 100% sure it's done the way it should be done, especially the validation and adding/updating addresses. The reason for the unusual approach with address is because there is only 1 form for adding/editing them and not a form per address.

export const FORM_ID = 'asset';
export const SUBADDRESSFORM_ID = 'asset_address';

export interface State {
    currentSubaddress: FormGroupState<AssetAddress>;
    formState: FormGroupState<Asset>;
}

const initialState: State = {
    currentSubaddress: undefined,
    formState: createFormGroupState<Asset>(FORM_ID, new Asset())
};

const validationFormGroup = updateGroup<Asset>({
    location: updateGroup<AssetLocation>({ 
        address: updateGroup<AssetAddress>({ 
            countryCode: validate(required) 
        })
    })
});

const assetFormReducer = createFormGroupReducerWithUpdate<Asset>({
    location: updateGroup<AssetLocation>({ 
        address: updateGroup<AssetAddress>({ 
            countryCode: validate(required) 
        })
    })
  });

export const addOrReplaceAddressInAssetIdentification = (asset: Asset, address: AssetAddress): Asset => {
    const addressIndex = asset.identification.addresses.findIndex(c => c.id == address.id);
    let newAddresses: AssetAddress[];
    if(addressIndex != -1) {
        newAddresses = [
            ...asset.identification.addresses.slice(0, addressIndex),
            address,
            ...asset.identification.addresses.slice(addressIndex + 1)
        ];
    }
    else {
        newAddresses = [
            ...asset.identification.addresses,
            address
        ];
    }

    return new Asset({
        ...asset,
        identification: new AssetIdentification({
            ...asset.identification,
            addresses: newAddresses
        })
    });
}

export function reducer(state = initialState, action: assetDetail.All): State {
    state = {
        ...state,
        formState: assetFormReducer(state.formState, action),
        currentSubaddress: state.currentSubaddress != undefined ? formGroupReducer(state.currentSubaddress, action) : undefined
    };

    switch (action.type) {
        case assetDetail.SAVE_SUBADDRESS:
            const addressToSave = state.currentSubaddress.value;
            const currentAsset = state.formState.value;

            return {
                ...state,
                currentSubaddress: undefined,
                formState: validationFormGroup(createFormGroupState<Asset>(FORM_ID, addOrReplaceAddressInAssetIdentification(currentAsset, addressToSave)))
            }
        case assetDetail.DONE_WITH_SUBADDRESS:
            return {
                ...state,
                currentSubaddress: undefined
            }
        case assetDetail.EDIT_SUBADDRESS:
            return {
                ...state,
                currentSubaddress: createFormGroupState<AssetAddress>(SUBADDRESSFORM_ID, action.payload.value)
            }
        case assetDetail.ADD_SUBADDRESS:
            return {
                ...state,
                currentSubaddress: createFormGroupState<AssetAddress>(SUBADDRESSFORM_ID, new AssetAddress())
            }
        case assetDetail.LOAD_COMPLETE:
            return {
                ...state,
                progress: new Set<string>(),
                formState: validationFormGroup(createFormGroupState<Asset>(FORM_ID, action.payload))
            };
        default:
            return state;
    }
}

Thanks in advace for your time!

radio buttons don't mark as touched

Given:

<div class="form-group">
    <label class="col-xs-3 control-label">Change state</label>
    <div class="col-xs-5">
        <div class="radio">
        <label>
            <input type="radio" name="stateRadios" value="online" [ngrxFormControlState]="(template$ | async)?.controls?.newTemplateState">
            Online
        </label>
        </div>
        <div class="radio">
        <label>
            <input type="radio" noBootstrap name="stateRadios" value="offline" [ngrxFormControlState]="(template$ | async)?.controls?.newTemplateState">
            Offline
        </label>
        </div>
    </div>
</div>

It issues a SET_VALUE and MARK_AS_DIRTY action but not a MARK_AS_TOUCHED action and I disable my button based on the form being touched.

If I focus another form element I then get a MARK_AS_TOUCHED action that enables my save button

Does this work with lazy loaded modules?

I have an Angular 5 + NGRX v4.x app that uses lazy loaded module + the per feature store states.

I can't get it working as it keeps throwing the error The form state must not be undefined!.

Does ngrx-forms work with this combination?

UpdateArray() function does not provide access to index of array's item

UpdateArray() function does not provide access to index of array's item

Hi, I am working with form arrays and I have use case, that I want to sync certain property in array's item with its index (position in array). Problem is, that updateArray(func) func does not have index parameter, like I am used from JS map((item, index) => ({...})) function.

I have came up with workaround using setValue() for whole array, but I would prefer to add index as a third parameter to updateArray(). Is this possible?

const formReducerFn = (dateTimeSettings: DateTimeSettings) =>
    createFormGroupReducerWithUpdate<InvoiceFormValue>(
        {
            // this is how I would prefer to do it
            lineItems: updateArray<InvoiceLineItem>(lineItem => ({
                ...lineItem,
                sortOrder: 1, // should be 'index' instead of '1' - how to get it?
            })),
        },
        {
            // this is workaround I came up with
            lineItems: lineItems =>
                setValue(
                    lineItems.value.map((lineItem, index) => ({
                        ...lineItem,
                        sortOrder: index,
                    })),
                    lineItems,
                ),
        },
    );

SetUserDefinedPropertyAction not working

Hi,

I'm dispatching a SetUserDefinedPropertyAction like this:

store.dispatch(new SetUserDefinedPropertyAction('formId.controlId', 'prop', 'value'));

However, no changes were applied. While debugging, I noticed that group/reducer/set-user-defined-property.ts is called but returns the state unchanged in Line 13. group/reducer/set-value.ts has a different implementation as it delegates to childReducer(...) in Line 14 (as do all the other reducers in the same directory). Is this behavior intentional? And if yes, how can I change a user-defined property via an action?

Updating array inside FormGroupState in reducer

As per title assume I have this reducer:

export class TemplateItemViewModel {
    public foo: string;
    public bar: string:
}

export class TemplateViewModel {
    public name: string;
    public description: string;
    public items: TemplateItemViewModel[];
}

export const initialState = createFormGroupState<TemplateViewModel>(
    'template',
    new TemplateViewModel()
);

export function reducer(state = initialState, action: editTemplateActions.Actions): FormGroupState<TemplateViewModel> {
    const myForm = formGroupReducer(state, action);
    if (myForm !== state) {
        state = { ...state, ...myForm };
    }

    switch (action.type) {

        case editTemplateActions.FETCH_COMPLETE:
            return createFormGroupState<TemplateViewModel>(
                'template',
                action.payload
            );

        case editTemplateActions.ADD_ROLE:
            // ???
            return state;

        default:
            return state;
    }
}

The problem becomes the documentation (https://github.com/MrWolfZ/ngrx-forms/blob/master/docs/FORM_ARRAYS.md) talks about updating a FormArrayState<T> not an array part of FormGroupState<T>.

I've tried using setValue but it triggers ngrx freeze to throw an error.

Do you have any examples? I've read the documentation and the example app, perhaps I'm missing something obvious!

Thanks, Phil

Accessing the error state in another directive

I have a custom input-container component which implements error class toggling in a similar way that the material form field component does -- by accessing a directive attached to the input inside, which in turn gets the error state from NgControl.

I wanted to do a similar thing for ngrx-forms via the NgrxFormControlDirective but the state and stateSubject properties and the state$ getter are both private, meaning I can't get the state of the input from the container component.

Is there a workaround for this? Or do I have to add an input to my input directive taking the form control state object and use that instead?

Reducer action firing multiple times-please close, I resolved it. I have to add a sender on the payload and then filter the owner of the action

Hi,

I have an issue where my update action is firing unintentionally inside of the reducer:

Here is what my model looks like:

export interface EntityVM {
EntityDescription: string;
EntityLocations: { [ id: string]: EntityLocationVM };
}

export interface EntityLocationVM {
_id: string;
ParentEntity: string;
City: string;
}

If I have this:

const data = {
EntityDescription: 'test',
EntityLocations: {
['100']: {
_id: 1000,
ParentEntity: '200',
City: 'test100'
},
['200']: {
_id: 2000,
ParentEntity: '200',
City: 'test200'
}
}
};

If just trigger an update on _id = 2000 with City='test123', 2 reducer actions gets fired making the values change to both _id=1000 and _id=2000 City to 'test123', should only update the City on _id=2000 but instead it updated all the data because it fired 2 times, it should have only fired the reducer action only once.

case UpdateCityAction.TYPE:

  return updateGroup<EntityVM>({

    EntityLocations: updateGroup<{ [id: string]: EntityLocationVM }> ({

      [a.payload.id]: updateGroup<any>({

        City: city => {

          const newVal = setValue(a.payload.data, cast(city));

          return newVal;
        },
      }),
    }),
  })(s);

Is there a way around this issue?

Thanks

Howto - validation of dependent state fields without group

May be I'm not clever enough, but how can one do validation of dependent fields in state without
a group?
The examples are handling groups only (like the nested stuff from user guide or password stuff from the example app).

I do not want to nest the fields and introduce a over-engineered handling on backend side.
Use case is that two fields in the state have to have different values.

Missing support of Date types: "Date values are not supported. Please used serialized strings instead."

Hello

First, thank you very much for bringing ngrx-forms to the community. It seems for me to be the right way in combination of using a store.

We created some forms and they seem to work pretty well - except for date values. If the form control value is of type Date, the data will not be set but the reducer throws an error: Date values are not supported. Please used serialized strings instead.

If I comment out this line of code in the setValueReducer function of ngrx/forms, everything seems to work normally:

/if (action.payload.value instanceof Date) {
throw new Error('Date values are not supported. Please used serialized strings instead.');
}
/

What is the reason for this restriction? Are there any side effects by using date types?

Thank you very much
Kind regards
Martin Cabalzar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.