Angular Material mat-table data not refreshed from parent changes - angular-material2

I am using in a component a mat-table with a MatTableDataSource and I want to set up the data from its parent component but data are not refreshed at screen whereas child component well detects data changes...
Child component
#Component({
selector: 'app-person-list',
template: `
<mat-table #table [dataSource]="dataSource">
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef> Name </mat-header-cell>
<mat-cell *matCellDef="let person">{{ person.name }}</mat-cell>
</ng-container>
<ng-container matColumnDef="age">
<mat-header-cell *matHeaderCellDef> Age </mat-header-cell>
<mat-cell *matCellDef="let person">{{ person.age }}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
`
})
export class PersonListComponent implements OnInit, OnChanges {
#Input() data: Person[];
displayedColumns = ['name', 'age'];
dataSource = new MatTableDataSource<Person>();
ngOnInit() {
this.dataSource.data = this.data;
}
ngOnChanges(changes) {
console.log('Data changed', this.data);
}
}
Parent component
#Component({
selector: 'app',
template: `<app-person-list [data]="personList"></app-person-list>`
})
export class AppComponent implements OnInit {
personList: Person[] = [];
constructor(private service: PersonService) {
}
ngOnInit() {
this.service.getPersonList().subscribe(res => this.personList.push(...res));
}
}
Plunker
Does someone knows why the table is not updated when data changes ? Thanks in advance!

The problem is that you're calling this.dataSource.data = this.data; only on ngOnInit, which means that you aren't doing anything with the updated data.
I created a pipe just for this, and so that I wouldn't have to use MatTableDataSource everywhere -
#Pipe({
name: 'dataSource'
})
export class DataSourcePipe implements PipeTransform {
transform(array: any[]): any {
return array ? new MatTableDataSource(array) : new MatTableDataSource([]);
}
}
Using it like this $data | dataSource in the template
That way your data is always up to date and you don't have to use MatTableDataSource manually.
EDIT:
You can still use filter and sort functionalities when using this.
You're giving data to the component and then using MatTableDataSource to wrap it. Instead you can do it like this - <component [data]="$array | async | dataSource"></component> where $array is an observable.
And then inside the component - <mat-table [dataSource]="data"></mat-table>
This way you can just manipulate the data object, without having to wrap it in the MatTableDataSource inside the component.

The only way I found is to change the reference of the array and reassign it...
Child component
#Component( [...] )
export class PersonListComponent implements OnChanges {
#Input() data: Person[];
displayedColumns = ['name', 'age'];
dataSource = new MatTableDataSource<Person>();
ngOnChanges(changes) {
this.dataSource.data = this.data;
}
}
Parent component
#Component( [...] )
export class AppComponent implements OnInit {
personList: Person[] = [];
constructor(private service: PersonService) {
}
ngOnInit() {
this.service.getPersonList().subscribe(res => {
if (this.personList.length > 0) {
res.unshift(this.personList);
}
this.personList = res;
});
}
}

Related

Custom validator in angular reactive forms is never firing

I have created a very simple custom validator for a simple form control, e.g. MatInput, which would always return non-null e.g. invalid. I hav also added one of the pre-built validators e.g. required. When I start my app I can see that status = INVALID and errors.required = true.
Once I start typing, I expected that status will remain INVALID and errors.myError = true, but this does not happen. What am I doing wrong? I have built my example on StackBlitz. I have also added the contents on my TS & HTML files below
TS
import { Component } from '#angular/core';
import { AbstractControl, FormControl, ValidationErrors, ValidatorFn, Validators} from '#angular/forms';
export function myValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
return { "myError": true };
};
}
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
name = new FormControl('', [Validators.required, myValidator]);
}
HTML
<label>
Name:
<input type="text" [formControl]="name">
</label>
{{ name | json }}
I am quite new to Angular and I am not sure how to debug this. What can I try next?
TL;DR:
export class AppComponent {
name = new FormControl('', [Validators.required, myValidator()]);
}
Explanation:
myValidator is not being called, so you are not getting the ValidatorFn

Angular2/4 - model driven form - asynchronous custom validator not working

I've a problem getting a custom async validator to work. My approach is as follows.
Component:
import {Component, OnInit} from "#angular/core";
import {FormGroup, FormBuilder, Validators} from "#angular/forms";
import {User} from "./user";
import {EmailValidators} from "./emailValidators";
#Component({
selector: '<userform></userform>',
templateUrl: 'app/users/userform.component.html',
})
export class UserFormComponent implements OnInit{
public myForm: FormGroup;
constructor(private _fb: FormBuilder) {}
ngOnInit() {
this.myForm = this._fb.group({
name: ['', [<any>Validators.required]],
email: ['', Validators.compose([
<any>EmailValidators.emailFormat,
<any>Validators.required
]), EmailValidators.shouldBeUnique], // Async validator
address: this._fb.group({
street: ['', <any>Validators.required],
zipCode: ['']
})
});
}
// save() and other methods here...
}
Validator Class:
import {FormControl} from "#angular/forms";
export class EmailValidators {
static shouldBeUnique(control: FormControl) {
console.log("shouldBeUnique called");
return new Promise((resolve, reject) => {
setTimeout(function () {
if (control.value == "m#m.de") {
console.log("shouldBeUnique TRUE");
resolve({shouldBeUnique: true});
} else {
console.log("shouldBeUnique NULL");
resolve(null);
}
}, 1000);
});
}
static emailFormat(control: FormControl) {
var value = <string> control.value;
var regX = /^(([^<>()\[\]\\.,;:\s#"]+(\.[^<>()\[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (!new RegExp(regX).test(value)) {
console.log("emailFormat: True", value)
return {emailFormat: true};
}
return {emailFormat: null}; // if validation passes return should be null
}
}
HTML:
<div class="form-group">
<label for="email">Email</label>
<input
id="email"
type="text"
class="form-control"
formControlName="email"/>
<div *ngIf="myForm.controls.email.errors.required"
class="alert alert-danger">Email is required {{ myForm.controls.email.errors | json }}</div>
<div *ngIf="myForm.controls.email.errors.emailFormat"
class="alert alert-danger">Email Format should be something like 'yourname#some-domain.com' {{ myForm.controls.email.errors | json }}</div>
<div *ngIf="myForm.controls.email.errors.shouldBeUnique"
class="alert alert-danger">shouldBeUnique {{ myForm.controls.email.errors | json }}</div>
</div>
Required validator works fine, the custom emailFormat validator works fine as well, the divs are kicking in and out where they should and even the shouldBeUnique validator works if its implemented as a standard validator.
But it does not as implemented above. The static method shouldBeUnique from the validator class is not even called. So - I assume the problem is somewhere in the components formGroup/formControl part. Please help.
Update
After testing various Angular-Versions (currently CLI/ 4.1.3) I found the following Error in the console:
email: FormControl
asyncValidator: function (control)
arguments: [Exception: TypeError: 'caller' and 'arguments' are restricted function properties and cannot be accessed in this context. at function.remoteFunction (<anonymous>:2:14)]
caller: (...)
Update
...actually the same error appears for sync-validators but these are working fine. While many tutorials give the impression that async validation in NG is an easy task I also read this: https://kahlillechelt.com/asynchronous-validation-with-angular-reactive-forms-1a392971c062
Confusing.

In Angular 2, how to setup an asynchronous validator when using template-driven forms?

I've defined a directive for my asynchronous validator:
#Directive({
selector: '[validatorUsername]',
providers: [{ provide: NG_ASYNC_VALIDATORS, useExisting: ValidatorUsernameDirective, multi: true }]
})
export class ValidatorUsernameDirective implements Validator {
validate(c: AbstractControl) {
let username = c.value;
return new Promise(resolve => {
setTimeout(() => {
if( username === "nemo" ) {
resolve({
'taken': true
})
} else {
resolve(null);
}
}, 1000);
});
}
}
In template, I've applied it as follows:
<input type="text" [(ngModel)]="username" name="username" required validatorUsername>
Then I've applied validation messages from code (not from template), as described in Angular's Cookbook, chapter Form Validation:
export class App implements OnInit {
#ViewChild('myForm') myForm: NgForm;
name: string;
username: string;
ngOnInit() {
this.myForm.valueChanges.subscribe(_ => this.onValueChanged());
}
onValueChanged() {
// fill 'formErrors' object
}
formErrors = {
'name': '',
'username': ''
};
}
The problem is that onValueChanged() doesn't get called when validator's promise is resolved, thus the validation message for username does not appear. It appears though if you try to edit the name field. What should I do to trigger the update on UI?
Here is the plunker for my code.
References:
Angular2 template driven async validator
https://angular.io/docs/ts/latest/cookbook/form-validation.html
https://netbasal.com/angular-2-forms-create-async-validator-directive-dd3fd026cb45
You can subscribe to statusChanges event that is fired after calling async validator
this.myForm.statusChanges.subscribe(_=> this.onValueChanged());
Modified Plunker

Aurelia - disable reset on router-view

My forms keeps resetting if I change tabs on router-view, is there a way to prevent this (if not what's the alternative)
export class App {
configureRouter(config, router) {
config.title = "Aurelia";
config.map([
{ route: ["", "dashboard"], name: "dashboard", moduleId: "HTML/viewmodels/dashboard", nav: true, title: "Dashboard", settings: "icon-home" },
{ route: "addons", name: "addons", moduleId: "HTML/viewmodels/addons", nav: true, title: "Addons", settings: "icon-file" },
{ route: "error", name: "error", moduleId: "HTML/viewmodels/error", nav: true, title: "Error", settings: "icon-info-sign" }
]);
this.router = router;
console.log(router);
}
}
This is a state persisting issue. The easiest solution is to store state in:
The parent view
A non-view related singleton class (or static variables)
Localstorage persisting (possibly coinciding with the previous option)
The first way to do it: save it in the parent view.
For example, you could create a cache inside the App class:
export class App {
formData = {};
configureRouter(config, router) {
{ route: ["", "dashboard"], [..snip..] settings: { formData: this.formData, icon: 'icon-home' } }
}
}
Inside your view, you can then do:
activate(params, routeConfig) {
this.formData = routeConfig.settings.formData;
this.icon = routeConfig.settings.icon;
}
And in the HTML, just bind it to formData:
<input type="text" value.bind="formData.text" />
This kind of assumes, that the whole App revolves around that form. Otherwise, it wouldn't really make much sense to store it in there.
That's one way to do it, anyway (the first one I pointed out in the list).
The other way to do it: singleton.
Create a new class called AppState or something along those lines.
export class AppState {
formData = {};
}
Then in your view, you should import and inject it:
import { AppState } from './app-state';
export class YourView {
static inject = [AppState];
constructor(appState) {
this.appState = appState;
}
}
And then in your HTML, you can bind it like so:
<input type="text" value.bind="appState.formData.text" />
Or third way: a static class.
Create a new class called AppState or something along those lines.
export class AppState {
static formData = {};
}
Then in your view, you should import and inject it:
import { AppState } from './app-state';
export class YourView {
AppState = AppState;
}
And then in your HTML, you can bind it like so:
<input type="text" value.bind="AppState.formData.text" />

Fields not being passed correctly to Props using ReduxForm

I'm using Redux Form in one of my projects (pretty much just copying the dynamic one from Rally Coding), but whenever I access this.props.fields, it simply gives me an array of the names of my fields as opposed to an object. What's even weirder is that I'm copying and pasting this code into another one of my projects that uses RF and it's giving me what I want from this.props.fields. Part of me thinks that I set RF up incorrectly, but I did import the formReducer into App.js and combined it with my other reducers.
When I hit the debugger, this.props.fields = ['query', 'numberOfResults'] which is messing everything up.
Here's my code:
import _ from 'lodash';
import React, { Component, PropTypes } from 'react';
import { Field, reduxForm } from 'redux-form';
const FIELDS = {
query: {
type: 'input',
label: 'What are you looking for?'
},
numberOfResults: {
type: 'input',
label: 'Number of Results'
}
};
class YelpForm extends Component {
onSubmit(props) {
console.log('hey cutie')
}
renderField(fieldConfig, field) {
debugger
const fieldHelper = this.props.fields[field]
return (
<div className={`form-group ${fieldHelper.touched && fieldHelper.invalid ? 'has-danger' : '' }`} >
<label>{fieldConfig.label}</label>
<fieldConfig.type type="text" className="form-control" {...fieldHelper} />
<div className="text-help">
{fieldHelper.touched ? fieldHelper.error : ''}
</div>
</div>
);
}
render() {
const { handleSubmit } = this.props;
return (
<form onSubmit={handleSubmit(props => this.onSubmit(props))} >
{_.map(FIELDS, this.renderField.bind(this))}
<input type="submit">Submit</input>
</form>
);
}
}
function validate(values) {
const errors = {};
_.each(FIELDS, (type, field) => {
if (!values[field]) {
errors[field] = `Enter a ${field}`;
}
});
return errors;
}
export default reduxForm({
form: 'Yelp Form',
fields: _.keys(FIELDS),
validate
})(YelpForm);
This is my first question on StackOverflow; thanks for the help in advance!
Try downgrading to redux-form version 5.2.3. It seems version 6.0.2 is either buggy, or not documented correctly.

Resources