mrwolfz / ngrx-forms Goto Github PK
View Code? Open in Web Editor NEWEnhance your forms in Angular applications with the power of ngrx
License: MIT License
Enhance your forms in Angular applications with the power of ngrx
License: MIT License
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
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!
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?
Because of the extended value accessor, this._select
is null meaning that ngValue
cannot be used.
Is there a way around this so we can use select elements with non string 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?
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?
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;
}
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?
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?
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
Get warnings when installing to an Angular 5 project
warning "[email protected]" has incorrect peer dependency "@angular/core@^4.0.0".
warning "[email protected]" has incorrect peer dependency "@angular/forms@^4.1.0".
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
.
Then the vlaue of the "USD" option is set to "0"
via the ' NgrxSelectOption. This fixes the
optionMap` problem above from what I understand.
Then setViewValue
is called again with "USD" and because the optionMap
now has the correct value, the select
is rendered with the correct value
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?
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"
In the result, the select loses its value.
Any ideas?
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!
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?
// 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>
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?
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
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?
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,
),
},
);
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.
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>
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?
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
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>;
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
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?
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
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?
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
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:
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)
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
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. :-)
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?
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[];
}
One more issue I've encountered -- when traveling back in time using the dev tools, the <select>
element only updates the selected option once, despite the value changing correctly in the store if I output the control value to the template directly.
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?
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.
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:
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.
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.
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:
controlId
to: FORM_ID.controlId using a flat error object.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?
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?
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
.
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:
selectedUser: User
? That will need to be cleared/reset/updated when another user is selected.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
Hey,
I'm having an issue using the library with <select>
elements.
Here are the two problems I've encountered so far:
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.
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.
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
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
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?
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.
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?
It was Webstorm, it did "import {combineReducers} from '@ngrx/store/store';"
It should be "import {combineReducers} from '@ngrx/store';"
Webstorm auto imports it once its included on the code.
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.
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));
});
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?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.