Angular2 FormControl - forms

I'm building a form using model-driven forms and building my form elements with formcontrol. I have a service where I put some functions and I send the formgroup to this function to access the formcontrols. I would like to know how could I change the formcontrols properties to do things like hide the element. I can't find a way to do this on the internet.
Thanks for all
edit:
Here is the example code:
My app begins with app.component.html
<h1>
Orçamento
</h1>
<app-form [campos]="cpos"></app-form>
and here my app.component.ts
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
cpos: any[] = [];
constructor() { }
ngOnInit() {
}
}
I have a class called campo-base.ts it's the base for all my fields:
export class CampoBase<T> {
nome: string;
valor: T;
label: string;
obrigatorio: boolean;
ordem: string;
tamanho: number;
tipoCampo: string;
combo: {chave: string, valor: string}[] = [];
tipo: string;
disabled: boolean;
tamcol: string;
espcol: string;
classe: string;
rowcol: number;
f3: string;
change: string;
visivel:boolean;
constructor (opcoes: {
valor?: T,
nome?: string,
label?: string,
obrigatorio?: boolean,
ordem?: string,
tamanho?: number,
tipoCampo?: string,
combo?: {chave: string, valor: string}[],
tipo?: string,
disabled?: boolean,
tamcol?: string,
espcol?: string,
classe?: string,
rowcol?: number,
f3?: string,
change?: string,
visivel?: boolean
} = {}) {
this.valor = opcoes.valor;
this.nome = opcoes.nome || '';
this.label = opcoes.label || '';
this.obrigatorio = !!opcoes.obrigatorio;
this.ordem = opcoes.ordem || '';
this.tamanho = opcoes.tamanho === undefined ? 1 : opcoes.tamanho;
this.tipoCampo = opcoes.tipoCampo || '';
this.combo = opcoes.combo || [];
this.tipo = opcoes.tipo || '';
this.disabled = !!opcoes.disabled;
this.tamcol = opcoes.tamcol || '';
this.espcol = opcoes.espcol || '';
this.classe = opcoes.classe || '';
this.rowcol = opcoes.rowcol === undefined ? 0 : opcoes.rowcol;
this.f3 = opcoes.f3 || '';
this.change = opcoes.change || '';
this.visivel = (!!opcoes.visivel ? true : opcoes.visivel);
}
}
and I use it in form.component.ts which received data from a service containing a list of fields with their properties.
import { Component, Input, OnInit } from '#angular/core';
import { FormGroup } from '#angular/forms';
import { CampoBase } from './campo-base';
import { FormService } from "./form.service";
import { FormDadosService } from './form-dados.service';
#Component({
selector: 'app-form',
templateUrl: './form.component.html',
styleUrls: ['./form.component.css'],
providers:[FormService,FormDadosService]
})
export class FormComponent implements OnInit {
#Input() campos: CampoBase<any>[] = [];
formulario: FormGroup = new FormGroup({});
payLoad = '';
coluna: number = 0;
constructor(private formService: FormService, private servico: FormDadosService) { }
ngOnInit() {
this.servico.getCampos().subscribe((data) => {
let classe: string = '';
let combobox: {chave: string, valor:string}[] = [];
data.forEach(campo => {
classe = '';
//Ajusta os tamanhos dos campos no form
if (campo.ZI2_TIPO != '4') {
classe += 'form-group ';
if (campo.ZI2_ESPCOL !== '00') {
classe += 'col-md-' + parseInt(campo.ZI2_TAMCOL).toString() + ' col-md-pull-' + parseInt(campo.ZI2_ESPCOL).toString() + ' col-md-offset-' + parseInt(campo.ZI2_ESPCOL).toString();
classe += ' col-lg-' + parseInt(campo.ZI2_TAMCOL).toString() + ' col-lg-pull-' + parseInt(campo.ZI2_ESPCOL).toString() + ' col-lg-offset-' + parseInt(campo.ZI2_ESPCOL).toString();
} else {
classe += 'col-md-' + parseInt(campo.ZI2_TAMCOL).toString() + ' col-lg-' + parseInt(campo.ZI2_TAMCOL).toString();
}
}
//Calcula o tamanho dos campos na linha para adicionar um novo row
if (this.coluna >= 12) {
this.coluna = 0;
}
this.coluna += parseInt(campo.ZI2_TAMCOL) + parseInt(campo.ZI2_ESPCOL);
//Trata os campos combobox
combobox.length = 0;
if (campo.ZI2_CBOX !== null) {
for (let x in campo.ZI2_CBOX) {
if (campo.ZI2_CBOX.hasOwnProperty(x)) {
combobox.push({chave: x, valor: campo.ZI2_CBOX[x]});
}
}
}
//Instancia o campo e adiciona na lista de campos
this.campos.push(new CampoBase({
valor: '',
nome: campo.ZI2_CAMPO,
label: campo.ZI2_DESC,
tipoCampo: campo.ZI2_TIPO,
tamanho: campo.ZI2_TAM,
ordem: campo.ZI2_SEQ,
obrigatorio: campo.ZI2_OBRIGA === '1',
disabled: campo.ZI2_VISUAL !== "A",
classe: classe,
tamcol: campo.ZI2_TAMCOL,
espcol: campo.ZI2_ESPCOL,
rowcol: this.coluna,
f3: campo.ZI2_F3,
combo: combobox.slice(),
change: campo.ZI2_CHANGE,
visivel: true
}));
});
this.formulario = this.formService.toFormGroup(this.campos);
});
}
onSubmit() {
this.payLoad = JSON.stringify(this.formulario.value);
}
}
I build my form using form-campo.component.ts:
import { Component, Input, OnInit } from '#angular/core';
import { FormGroup } from '#angular/forms';
import { CampoBase } from './campo-base';
import { OrcamentoService } from '../orcamento.service';
#Component({
selector: 'formcampos',
templateUrl: './form-campo.component.html',
styleUrls: ['./form-campo.component.css']
})
export class FormCampoComponent implements OnInit {
#Input() campo: CampoBase<any>;
#Input() form: FormGroup;
isValid() {
return this.form.controls[this.campo.nome].pristine || this.form.controls[this.campo.nome].valid;
}
constructor(private orcamentoService: OrcamentoService){}
ngOnInit() {
if (this.campo.change)
this.form.controls[this.campo.nome].valueChanges.subscribe(valor => {
let aChanges = this.campo.change.split(";");
let expression = "";
for (let i = 0; i < aChanges.length; i++) {
if (aChanges[i].length > 0) {
expression = "_this.orcamentoService." + aChanges[i].replace(/\(\)/g,"") + '(_this.form, valor)';
eval(expression);
}
}
});
}
}
with this template:
<div [formGroup]="form" [class]="campo.classe" *ngIf="campo.visivel">
<label [attr.for]="campo.nome" [hidden]="campo.tipoCampo === '4'">{{campo.label}}</label>
<div [class.input-group]="campo.f3">
<select *ngIf="(campo.tipoCampo == '1' || campo.tipoCampo == '3') && campo.combo.length > 0" [formControlName]="campo.nome" class="form-control" [id]="campo.nome">
<option *ngFor="let opt of campo.combo" [value]="opt.chave">{{opt.valor}}</option>
</select>
<input *ngIf="(campo.tipoCampo == '1' || campo.tipoCampo == '3') && campo.combo.length == 0" [formControlName]="campo.nome" class="form-control"
[id]="campo.nome" [type]="'text'" [maxlength]="campo.tamanho" [placeholder]="campo.label">
<input *ngIf="campo.tipoCampo == '2'" [formControlName]="campo.nome" class="form-control"
[id]="campo.nome" [type]="'email'" [maxlength]="campo.tamanho" [placeholder]="campo.label">
<input *ngIf="campo.tipoCampo == '4'" [formControlName]="campo.nome"
[id]="campo.nome" [type]="'hidden'">
<textarea *ngIf="campo.tipoCampo == '5'" [formControlName]="campo.nome" class="form-control"
[id]="campo.nome" [placeholder]="campo.label"></textarea>
<span class="input-group-btn" *ngIf="campo.f3">
<button class="btn btn-primary" type="button" id="f3{{campo.nome}}"><span class="glyphicon glyphicon-search"></span></button>
</span>
</div>
</div>
and finally in orcamento.service I tried to manipulate the visibility of some fields like below:
import { Injectable } from '#angular/core';
#Injectable()
export class OrcamentoService {
constructor() { }
gatTipoOrc(form, valor) {
if (valor == "N") {
form.controls['ZPO_UM'].enable();
} else {
form.controls['ZPO_UM'].disable();
form.controls['ZPO_UM'].reset();
}
}
gatUM(form, valor) {
form.controls['ZPO_QTDPCX'].visivel = false;
}
habEntrega(form, valor) {
}
}

If I understand you correctly, you want to do things like hide an element - <div hidden></div> or in angular1 <div ng-hide="isHidden"></div>
In angular2 you should bind to the element properties similar to the traditional way:
<div [hidden]="true"></div>.
Of course you can use a variable, <div [hidden]="isHidden"></div>
Also note that using hidden is not recomended: https://stackoverflow.com/questions/35578083
EDIT:
As J. Adam Connor mentioned you can also use *ngIf instead of [hidden]. This will cause your element to be completely removed from the DOM which is a very good approach in general but should be carefully used with Forms because some validations and bindings require your form to match your model. That can be messy if it is missing from your actual Form in the DOM.

Well, I did it using *ngIf and interacting with another sibling components through a shared service.
Thanks for all for the help.

Related

Ionic 4 swipeable tabs give error when swiping

In my angular app want to make tabs swipable .I am using directive and module in directive i am defined onTabInitialized method and calling in tabs.page.ts but it not working it gives error-> ERROR TypeError: Cannot read property 'onTabInitialized' of undefined. How to solve this problem plese help me..
swipe-tab.directive.ts
import { Directive, ElementRef, Output, EventEmitter, OnInit, Renderer2, OnDestroy } from '#angular/core';
import 'hammerjs';
#Directive({
selector: '[appSwipeTab]'
})
export class SwipeTabDirective implements OnInit, OnDestroy {
#Output() tabChange = new EventEmitter();
private currentTabIndex = 0;
private tabCount = 0;
private swipeCoords: [number, number];
private swipeDuration: number;
private browserSwipeGesture: HammerManager;
private touchListenersFns = [];
tabNames: String[] = [];
constructor(
public _el: ElementRef,
private _renderer: Renderer2
) {
console.log('[SwipeTabDirective] constructor');
}
ngOnInit() {
const tabsList = this._el.nativeElement.querySelectorAll('ion-tab-button');
for (let i = 0, len = tabsList.length; i < len; i += 1) {
this.tabNames.push(tabsList[i].tab);
}
this.tabCount = this.tabNames.length - 1;
console.log('[SwipeTabDirective] ngOnInit, tabNames: ', this.tabNames);
}
onTabInitialized(tabName: string): void {
console.log('[SwipeTabDirective] onTabInitialized, tabName: ', tabName);
this.currentTabIndex = this.tabNames.indexOf(tabName);
const currentTabName = `app-${tabName}`;
const elem = this._el.nativeElement.querySelectorAll(currentTabName)[0];
if (!elem) {
throw new Error('Make sure tab selector has app prefix');
} else {
const content = elem.getElementsByTagName('ion-content')[0];
if (content.querySelector('.swipe-area') === null) {
console.log('[SwipeTabDirective] adding swipe area');
this.createWrapperDiv(content);
}
}
}
createWrapperDiv(content: HTMLElement): void {
const divElement = this._renderer.createElement('div');
this._renderer.addClass(divElement, 'swipe-area');
this._renderer.insertBefore(content, divElement, null);
while (content.children.length > 1) {
const child = content.children[0];
this._renderer.removeChild(content, child);
this._renderer.appendChild(divElement, child);
}
this.addEventListeners(divElement);
}
addEventListeners(divElement: HTMLElement) {
if ('ontouchstart' in document.documentElement) {
this.touchListenersFns.push(
this._renderer.listen(divElement, 'touchstart', ($event) => {
this.deviceSwipeHandler($event, 'start');
}),
this._renderer.listen(divElement, 'touchend', ($event) => {
this.deviceSwipeHandler($event, 'end');
})
);
} else {
this.browserSwipeGesture = new Hammer(divElement);
this.browserSwipeGesture.on('swipe', (event) => {
this.browserSwipeHandler(event);
});
}
}
deviceSwipeHandler(event: TouchEvent, status: string): void {
console.log('[SwipeTabDirective] deviceSwipeHandler, status: ', status);
const coords: [number, number] = [event.changedTouches[0].pageX, event.changedTouches[0].pageY];
const time = new Date().getTime();
if (status === 'start') {
this.swipeCoords = coords;
this.swipeDuration = time;
} else if (status === 'end') {
const direction = [coords[0] - this.swipeCoords[0], coords[1] - this.swipeCoords[1]];
const duration = time - this.swipeDuration;
if (duration < 1000 && Math.abs(direction[0]) > 50
&& Math.abs(direction[0]) > Math.abs(direction[1] * 3)) {
if (direction[0] > 0) {
this.moveBackward();
} else {
this.moveForward();
}
}
}
}
browserSwipeHandler(event) {
console.log('[SwipeTabDirective] browserSwipeHandler, direction: ', event.direction);
switch (event.direction) {
case 2:
this.moveForward();
break;
case 4:
this.moveBackward();
break;
default:
break;
}
}
moveForward(): void {
console.log('[SwipeTabDirective] moveForward');
if (this.currentTabIndex < this.tabCount) {
this.currentTabIndex++;
this.tabChange.emit(this.tabNames[this.currentTabIndex]);
}
}
moveBackward(): void {
console.log('[SwipeTabDirective] moveBackward');
if (this.currentTabIndex > 0) {
this.currentTabIndex--;
this.tabChange.emit(this.tabNames[this.currentTabIndex]);
}
}
ngOnDestroy() {
if (this.touchListenersFns.length > 0) {
this.touchListenersFns.forEach(fn => fn());
} else if (this.browserSwipeGesture) {
this.browserSwipeGesture.off('swipe');
}
}
}
also make a module for export the directives which code show in below..
directives.module.ts
import { NgModule } from '#angular/core';
import { CommonModule } from '#angular/common';
import { SwipeTabDirective } from './swipe-tab.directive';
#NgModule({
declarations: [SwipeTabDirective],
imports: [
CommonModule
],
exports: [
SwipeTabDirective
]
})
export class DirectivesModule { }
The directive function use in tabs.page.ts and import directives module in tabs.page.ts
tabs.page.ts
import { Component, ViewChild } from '#angular/core';
import { IonTabs } from '#ionic/angular';
import { SwipeTabDirective } from '../directives/swipe-tab.directive';
#Component({
selector: 'app-tabs',
templateUrl: 'tabs.page.html',
styleUrls: ['tabs.page.scss']
})
export class TabsPage {
#ViewChild(SwipeTabDirective) swipeTabDirective: SwipeTabDirective;
#ViewChild('myTabs') tabRef: IonTabs;
constructor() { }
ionTabsDidChange($event) {
console.log('[TabsPage] ionTabsDidChange, $event: ', $event);
this.swipeTabDirective.onTabInitialized($event.tab);
}
onTabChange($event) {
console.log('[TabsPage] onTabChange, $event: ', $event);
this.tabRef.select($event);
}
}
In tabs.page.html i am define two function ionTabsDidChange and onTabChange ...
tabs.page.html
<ion-tabs appSwipetab (ionTabsDidChange)="ionTabsDidChange($event)" (tabChange)="onTabChange($event)" #myTabs>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="tab1">
<ion-icon name="flash"></ion-icon>
<ion-label>Tab One</ion-label>
</ion-tab-button>
<ion-tab-button tab="tab2">
<ion-icon name="apps"></ion-icon>
<ion-label>Tab Two</ion-label>
</ion-tab-button>
<ion-tab-button tab="tab3">
<ion-icon name="send"></ion-icon>
<ion-label>Tab Three</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
I did some change in swipe-tab.directive.ts
from
const currentTabName = `app-${tabName}`;
to
const currentTabName = `${tabName}`;
and the error has gone.

Form validation before the submission and at time of the data entry

I started to study React and wanted to create the form for multiple inputs, where I can check the validation of the data at the time of input and again before submitting of the form.
The conditions to submit: all fields are mandatory and the data is valid.
Currently, if user enters invalid data in input field, error text is displayed near the same field. And if user clicked button "submit" on the form with empty fields, error text is also displayed.
But I can't really work it out, how should I do the validation before the submission of the form in my example: : the form has the input field with an error or not.
import React from 'react'
import { render } from 'react-dom'
const ErrorOutput = props => {
let name = props.name
let inputValue = props.case
let submit = props.submit
console.log(props.submit)
if (name === 'firstName') {
if (!inputValue.match(/^[a-zA-Z]+$/) && inputValue.length > 0) {
return <span>Letters only</span>
} else if (submit && inputValue.length === 0) {
return <span>Required</span>
}
return <span></span>
}
if (name === 'telNo') {
if(!inputValue.match(/^[0-9]+$/) && inputValue.length > 0) {
return <span>Numbers only</span>
} else if (submit && inputValue.length === 0) {
return <span>Required</span>
}
return <span></span>
}
}
class App extends React.Component {
constructor(props){
super(props)
this.state = {
firstName: '',
telNo: '',
submit: false
}
}
handleSubmit(e){
e.preventDefault()
let submit = true
this.setState ({submit: submit})
// ... Validation
}
handleValidation(e) {
this.setState({
[e.target.name]: e.target.value
})
}
render() {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<div>
<label>
First name:
</label>
<input
type='text'
name ='firstName'
value = {this.state.firstName}
onChange = {this.handleValidation.bind(this)}
/>
<ErrorOutput case={this.state.firstName} name={'firstName'} submit = {this.state.submit} />
</div>
<div>
<label>
Phone number:
</label>
<input
type='tel'
name ='telNo'
value = {this.state.telNo}
onChange = {this.handleValidation.bind(this)}
/>
<ErrorOutput case={this.state.telNo} name={'telNo'} submit = {this.state.submit} />
</div>
<button>
Submit
</button>
</form>
)
}
}
render(
<App />,
document.getElementById('root')
)
the following code is an example of adding data via form and form validation prior to the submit. More validations can be added.
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
name: '',
age: '',
email: '',
errorName: '',
errorAge: '',
errroMail: '',
dataValue: false
};
this.getName = this.getName.bind(this);
this.getAge = this.getAge.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.postDatainDisplay = this.postDatainDisplay.bind(this);
}
componentWillReceiveProps(nextProps) {
if (this.props.name !== nextProps.name) {
this.setState({ dataValue: true });
}
}
postDatainDisplay(dataObj) {
this.props.postData(dataObj);
}
getName(event) {
const { name, age } = this.state;
this.setState({ errorName: '' });
this.setState({ name: event });
}
getAge(event) {
const { age } = this.state;
this.setState({ errorAge: '' });
this.setState({ age: event });
}
handleSubmit() {
const { name, age } = this.state;
//add more validation here
if (name === '') {
this.setState({ errorName: 'name cannot be blank', dataValue: false
});
} else if (age === '') {
this.setState({ errorAge: 'Age cannot be blank', dataValue: false });
} else
{ this.setState({ data: { name, age } }, () => {
this.props.sendData(this.state.data);
}
render() {
const { name, age } = this.props;
return (
<div className="container">
<form>
name:<input
type="text"
onChange={event => {
this.getName(event.target.value);
}}
/>
{this.state.errorName}
<br />
<br />
age:{' '}
<input
type="text"
onChange={event => {
this.getAge(event.target.value);
}}
/>
{this.state.errorAge}
<br />
<br />
<input type="button" onClick={this.handleSubmit} value="Submit"
/>
</form>
</div>
class App extends React.Component {
constructor(props){
super(props)
this.state = {
form:{
firstName: {
value: '',
validation: {
required: true
},
valid: false,
touched: false
},
telNo: {
value: '',
validation: {
required: true
},
valid: false,
touched: false
}
},
formIsValid:false
}
}
checkValidity(value, rules) {
let isValid = true;
if (rules.required) {
isValid = value.trim() !== '' && isValid;
}
return isValid;
}
handleValidation = (event) => {
let fieldName = event.target.name;
let fieldValue = event.target.value;
const updatedCategoryForm = {
...this.state.form
};
const updatedFormElement = {
...updatedCategoryForm[fieldName]
};
updatedFormElement.touched = true;
updatedFormElement.value = fieldValue;
updatedFormElement.valid = this.checkValidity(updatedFormElement.value, updatedFormElement.validation);
if (!updatedFormElement.valid && updatedFormElement.validation ) {
updatedFormElement.elementValidation = "Invalid";
} else {
updatedFormElement.elementValidation = "";
}
updatedCategoryForm[fieldName] = updatedFormElement;
let formIsValid = true;
for (let inputIdentifier in updatedCategoryForm) {
formIsValid = updatedCategoryForm[inputIdentifier].valid && formIsValid;
}
this.setState({ form: updatedCategoryForm, formIsValid: true });
}
Based on the value of formIsValid field disable submit button

Adding Validation to a Dynamically constructed form

Scenario
I am developing a savedialog which accepts any simple object with string, boolean and date fields. I've constructed a component which handels the construction of this, now I would like to dynamically add validation to the input fields which have been created.
I'm programming in TypeScript, using Angular2 and Also a framework called PrimeNG which is the Angular2 version of PrimeFaces.
My code at the moment
Html
<p-dialog #dialog header="{{dialogTitle}}" [(visible)]="isDisplayed" [responsive]="true" showEffect="fade"
[modal]="true" [closable]="false" [closeOnEscape]="false" [modal]="true" [width]="1000" [resizable]="false">
<form novalidate #form (ngSubmit)="save()">
<div class="ui-grid ui-grid-responsive ui-fluid" *ngIf="saveObject">
<div *ngFor="let i of rows" class="ui-grid-row">
<div class="ui-grid-col-6" *ngFor="let field of fields | slice:(i*itemsPerRow):(i+1)*itemsPerRow">
<div class="ui-grid-col-5 field-label">
<label for="attribute">{{field.key}}</label>
</div>
<div class="ui-grid-col-5">
<p-checkbox *ngIf="booleanFields.indexOf(field.key) !== -1" binary="true"
[(ngModel)]="saveObject[field.key]" name="{{field.key}}"
[ngClass]="{'error-border-class': field.key.errors && (field.key.dirty || field.key.touched)}"
[valueValidator]="field.key" [validateObject]="saveObject"></p-checkbox>
<p-calendar *ngIf="dateFields.indexOf(field.key) !== -1" [(ngModel)]="saveObject[field.key]"
name="{{field.key}}"
[ngClass]="{'error-border-class': field.key.errors && (field.key.dirty || field.key.touched)}"
dateFormat="dd/mm/yy" [showIcon]="true" [appendTo]="dialog"
[monthNavigator]="true"
[ngClass]="{'error-border-class': field.key.errors && (field.key.dirty || field.key.touched)}"
[valueValidator]="field.key"
[validateObject]="saveObject"></p-calendar>
<input *ngIf="(booleanFields.indexOf(field.key) === -1) && (dateFields.indexOf(field.key) === -1)"
pInputText id="attribute" [(ngModel)]="saveObject[field.key]" name="{{field.key}}"
[ngClass]="{'error-border-class': field.key.errors && (field.key.dirty || field.key.touched)}"
[valueValidator]="field.key" [validateObject]="saveObject"/>
</div>
</div>
</div>
</div>
</form>
<p-footer>
<div class="ui-dialog-buttonpane ui-widget-content ui-helper-clearfix">
<button label=" " type="button" pButton (click)="hideDialog(true)">
<i class="fa fa-times fa-fw" aria-hidden="true"></i>Sluit
</button>
<button label=" " type="submit" pButton (click)="hideDialog(false)" [disabled]="!form.valid">
<i class="fa fa-check fa-fw" aria-hidden="true"></i>Opslaan
</button>
</div>
</p-footer>
SaveDialogComponent
import { Component, Input, Output, OnChanges, EventEmitter } from '#angular/core';
import { FieldUtils } from '../utils/fieldUtils';
import { ValueListService } from '../value-list/value-list.service';
#Component({
moduleId: module.id,
selector: 'save-dialog',
templateUrl: 'savedialog.component.html',
styleUrls: ['savedialog.component.css']
})
export class SaveDialogComponent implements OnChanges {
#Input() dialogTitle:string;
#Input() isDisplayed:boolean;
#Input() saveObject:any;
#Input() unwantedFields:string[] = [];
#Input() booleanFields:string[] = [];
#Input() dateFields:string[] = [];
#Output() hide:EventEmitter<boolean> = new EventEmitter<boolean>();
#Output() result:EventEmitter<Object> = new EventEmitter<any>();
private fields:any[];
private itemsPerRow:number = 2;
private rows:any[];
ngOnChanges() {
this.fields = FieldUtils.getFieldsWithValues(this.saveObject, this.unwantedFields);
this.rows = Array.from(Array(Math.ceil(this.fields.length / this.itemsPerRow)).keys());
}
hideDialog(clearChanges:boolean) {
if(clearChanges){
this.resetObject()
}
this.isDisplayed = false;
this.hide.emit(this.isDisplayed);
}
save() {
this.result.emit(this.saveObject);
}
private resetObject() {
for(let field of this.fields) {
this.saveObject[field.key] = field.value;
}
}
private getDate(date:string):Date {
return new Date(date);
}
}
I'm Not including field utils since this it isn't that important, basically It extracts all the fields from an object, uses the booleanFields and dateFields to order the fields inorder to place all the fields of a same type next to one another.
ValidatorDirective
import { Directive, Input } from '#angular/core';
import { AbstractControl, ValidatorFn, Validator, NG_VALIDATORS, FormControl } from '#angular/forms';
#Directive({
selector: '[valueValidator][ngModel]',
providers: [
{provide: NG_VALIDATORS, useExisting: ValueValidatorDirective, multi: true}
]
})
export class ValueValidatorDirective implements Validator {
#Input('valueValidator') fieldName:string;
#Input() validateObject:any;
#Input() values:any[];
datumStartValidator:ValidatorFn;
datumEindValidator:ValidatorFn;
codeValidator:ValidatorFn;
naamValidator:ValidatorFn;
volgNrValidator:ValidatorFn;
validate(c:FormControl) {
this.instantiateFields();
switch (this.fieldName) {
case 'datumStart':
return this.datumStartValidator(c);
case 'datumEind':
return this.datumEindValidator(c);
case 'code':
return this.codeValidator(c);
case 'naam':
return this.naamValidator(c);
case 'volgnr':
return this.volgNrValidator(c);
default :
return {
error: {
valid: false
}
}
}
}
instantiateFields() {
this.datumStartValidator = validateStartDatum(this.validateObject['datumEind']);
this.datumEindValidator = validateEindDatum(this.validateObject['datumStart']);
this.codeValidator = validateCode();
this.naamValidator = validateNaam();
this.volgNrValidator = validateVolgNr(null);
}
}
//We'll need multiple validator-functions here. one for each field.
function validateStartDatum(datumEind:Date):ValidatorFn {
return (c:AbstractControl) => {
let startDatum:Date = c.value;
let isNotNull:boolean = startDatum !== null;
let isBeforeEind:boolean = false;
if (isNotNull) {
isBeforeEind = datumEind.getTime() > startDatum.getTime();
}
if (isNotNull && isBeforeEind) {
return null
} else {
return returnError();
}
}
}
function validateEindDatum(startDatum:Date):ValidatorFn {
return (c:AbstractControl) => {
let eindDatum:Date = c.value;
let isNotNull:boolean = eindDatum !== null;
let isBeforeEind:boolean = false;
if (isNotNull) {
isBeforeEind = eindDatum.getTime() > startDatum.getTime();
}
if (isNotNull && isBeforeEind) {
return null
} else {
return returnError();
}
}
}
function validateCode():ValidatorFn {
return (c:AbstractControl) => {
let code:string = c.value;
if (code !== null) {
return null;
} else {
return returnError();
}
}
}
function validateNaam():ValidatorFn {
return (c:AbstractControl) => {
let naam:string = c.value;
if (naam !== null) {
return null;
} else {
return returnError();
}
}
}
function validateVolgNr(volgnummers:string[]):ValidatorFn {
return (c:AbstractControl) => {
return returnError();
}
}
function returnError():any {
return {
error: {
valid: false
}
}
}
My problem
First of all I want to say thank you for reading this all the way, you're a champ! I know my code is not the most well writen beautiful code you've ever seen but I'm just trying to get it to work at the moment. The problem I have at the moment is that Validation happens but I can't seem to set the class using
[ngClass]="{'error-border-class': field.key.errors && (field.key.dirty || field.key.touched)}"
field.key is found no problem But I don't seem to be able to use that as reference to the dynamic name given to the input fields. field.key.errors is undefined which I guess is to be expected. But I haven't found how to fix this yet.
Secondly I think that the way I have It now I'm going to end up having trouble with the validation of the entire form (to disable the submit button)
<button label=" " type="submit" pButton (click)="hideDialog(false)" [disabled]="!form.valid">
Any Ideas on how to fix this are welcome, I'm open to all suggestions.

Angular2 - access the AbstractControl instance in validation directive

I have to trigger validation from the inside of a validator directive.
Here is the directive I have. It works as expected. However I want it to trigger the validation process when the validator function changes. I.e. when its input variable maxDate changes.
How could I do this ?
If I could access the AbstractControl instance in the constructor I could easily do this. I can't think of a way to do it, however.
import { AbstractControl, FormGroup, ValidatorFn, Validator, NG_VALIDATORS, Validators } from '#angular/forms';
import { Directive, Input, OnChanges, SimpleChanges, EventEmitter } from '#angular/core';
function parseDate(date: string):any {
var pattern = /(\d{2})-(\d{2})-(\d{4})/;
if (date) {
var replaced = date.search(pattern) >= 0;
return replaced ? new Date(date.replace(pattern,'$3-$1-$2')) : null;
}
return date;
}
export function maxDateValidator(maxDateObj): ValidatorFn {
return (control:AbstractControl): {[key: string]: any} => {
const val = control.value;
let date = parseDate(val);
let maxDate = parseDate(maxDateObj.max);
if (date && maxDate && date > maxDate) {
return {
maxDateExceeded: true
};
}
return null;
};
}
...
#Directive({
selector: '[maxDate]',
providers: [{provide: NG_VALIDATORS, useExisting: maxDateDirective, multi: true}]
})
export class maxDateDirective implements Validator, OnChanges {
#Input() maxDate: string;
private valFn = Validators.nullValidator;
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
const change = changes['maxDate'];
if (change) {
const val: string = change.currentValue;
this.valFn = maxDateValidator(val);
}
else {
this.valFn = Validators.nullValidator;
}
//This is where I want to trigger the validation again.
}
validate(control): {[key: string]: any} {
return this.valFn(control);
}
}
Usage:
<input type="text" [(ngModel)]="deathDateVal">
<input class="form-control"
type="text"
tabindex="1"
[maxDate]="deathDateVal"
name="will_date"
[textMask]="{pipe: datePipe, mask: dateMask, keepCharPositions: true}"
ngModel
#willDate="ngModel">
Here is what I've just come up with:
#Directive({
selector: '[maxDate]',
providers: [{provide: NG_VALIDATORS, useExisting: maxDateDirective, multi: true}]
})
export class maxDateDirective implements Validator, OnChanges {
#Input() maxDate: string;
private valFn = Validators.nullValidator;
private control:AbstractControl;
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
const change = changes['maxDate'];
if (change) {
const val: string = change.currentValue;
this.valFn = maxDateValidator(val);
}
else {
this.valFn = Validators.nullValidator;
}
if (this.control) {
this.control.updateValueAndValidity(this.control);
}
}
validate(_control:AbstractControl): {[key: string]: any} {
this.control = _control;
return this.valFn(_control);
}
}
It works. Validate is called on initialization so I just store its parameter.
It is fuckin' ugly but it works.
To get your hands on the abstractControl of the input you can do something like this:
#Directive({
// tslint:disable-next-line:directive-selector
selector: 'input[type=date][maxDate]'
})
export class InputFullWithDirective implements Validator, OnChanges {
constructor(#Self() private control: NgControl) {}
/** the rest is mostly unchanged from the question */
}

Angular 2 Reactive form + directive validation

I'm trying to wrap my head around the following problem:
I have a 'google-place-autocomplete' directive that adds the autocomplete functionality to an input field.
Now I also wanted it to be able to force a google place selection and only be 'valid' if the user has selected a place.
E.g:
#Directive({
selector: '[googlePlace][formControlName], [googlePlace][ngModel]',
providers: [{provide: NG_VALIDATORS, useExisting: GooglePlaceDirective, multi: true}]
})
export class GooglePlaceDirective implements Validator, OnChanges {
valid = false;
#Output() googlePlaceAddressChange: any = new EventEmitter();
#Input() googlePlaceAddress: any;
#Output() ngModelChange: any = new EventEmitter();
private autocomplete: any;
constructor(private googleMapService: GoogleMapsService,
private element: ElementRef,
private zone: NgZone) {
}
ngOnInit() {
let self = this;
this.googleMapService
.load()
.subscribe(
() => {
this.autocomplete = new google.maps.places.Autocomplete(this.element.nativeElement);
this.autocomplete.addListener('place_changed', function () {
self.placeChanged(this.getPlace());
});
}
);
}
private placeChanged(place) {
this.zone.run(() => {
this.googlePlaceAddress = {
address: this.element.nativeElement.value,
formattedAddress: place.formatted_address,
latitude: place.geometry.location.lat(),
longitude: place.geometry.location.lng()
};
this.valid = true;
this.googlePlaceAddressChange.emit(this.googlePlaceAddress);
this.ngModelChange.emit(this.element.nativeElement.value);
});
}
ngOnChanges(changes): void {
let googlePlaceDefined = typeof (changes.googlePlaceAddress) !== 'undefined';
let modelDefined = typeof (changes.ngModel) !== 'undefined';
if(modelDefined && !googlePlaceDefined) {
this.valid = false;
} else if(googlePlaceDefined && !modelDefined) {
this.valid = true;
}
}
validate(control: AbstractControl) {
return this.valid === false ? {'googlePlaceAddress': true} : null;
}
}
If I use this directive in an template driven form:
...
<input name="addr" type="text" [(ngModel)]="textValue" [(googlePlaceAddress)]="googleAddress" required>
<p *ngIf="addr.errors.googlePlaceAddress">Please select a proposed address</p>
...
it works fine.
Now I need to use this in an Reactive Form using FormGroup
let groups = [
new FormControl('', [Validators.required])
];
/** HTML **/
...
<input [id]="addr"
[formControlName]="address"
class="form-control"
type="text"
googlePlace
[placeholder]="question.label"
[(googlePlaceAddress)]="googleAddress">
...
However in this case the validation from the directive is never triggered.
I suppose angular2 expects it to be given through, when using Reactive Forms:
new FormControl('', [Validators.required, ???])
I must have taken a wrong path somewhere.
For future reference:
I solved my problem creating a component out of it together with a Value accessor:
#Component({
selector: 'app-google-place',
templateUrl: './google-place.component.html',
styleUrls: ['./google-place.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GooglePlaceComponent),
multi: true
}
]
})
export class GooglePlaceComponent implements OnInit, ControlValueAccessor {
#ViewChild('inputElement') inputElement: ElementRef;
#Input() public placeholder: string = "Address";
#Input() public textValue: string = "";
private autocomplete: any;
private _place = null;
constructor(
private googleMapService: GoogleMapsService,
private zone: NgZone
) {
}
ngOnInit() {
this.googleMapService
.load()
.subscribe(
() => {
this.autocomplete = new google.maps.places.Autocomplete(this.inputElement.nativeElement);
this.autocomplete.addListener('place_changed', () => this.placeChanged());
}
);
}
placeChanged() {
this.zone.run(() => {
let place = this.autocomplete.getPlace();
this._place = {
address: this.inputElement.nativeElement.value,
formattedAddress: place.formatted_address,
latitude: place.geometry.location.lat(),
longitude: place.geometry.location.lng()
};
this.propagateChange(this._place);
});
}
onNgModelChange($event) {
if(this._place !== null) {
if(this._place.address !== $event) {
this._place = null;
this.propagateChange(this._place);
}
}
}
onBlur() {
this.propagateTouched();
}
writeValue(obj: any): void {
if(obj !== undefined) {
this._place = obj;
}
}
propagateChange = (_: any) => {};
registerOnChange(fn) {
this.propagateChange = fn;
}
propagateTouched = () => {};
registerOnTouched(fn: any): void {
this.propagateTouched = fn;
}
}
Using this I can use it in a FormGroup with the Validators.required and it will only be valid if a user has selected a google place.
EDIT
The html:
<input type="text"
(blur)="onBlur()"
#inputElement
class="form-control"
[(ngModel)]="textValue"
(ngModelChange)="onNgModelChange($event)">
The service:
import {Injectable} from '#angular/core';
import {Subject} from 'rxjs/Subject';
import {Observable} from 'rxjs/Observable';
#Injectable()
export class GoogleMapsService {
private key = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
private loaded = false;
private currentRequest = null;
constructor() {
}
load() {
if (this.loaded) {
return Observable.create((observer) => {
observer.next();
observer.complete();
});
}
if (this.currentRequest === null) {
//http://reactivex.io/rxjs/manual/overview.html#multicasted-observables
const source = Observable.create((observer) => {
this.loadMaps(observer);
});
const subject = new Subject();
this.currentRequest = source.multicast(subject);
this.currentRequest.connect();
}
return this.currentRequest;
}
private loadMaps(observer: any) {
const script: any = document.createElement('script');
script.src = 'https://maps.googleapis.com/maps/api/js?key=' + this.key + '&libraries=places';
if (script.readyState) { // IE, incl. IE9
script.onreadystatechange = () => {
if (script.readyState == 'loaded' || script.readyState == 'complete') {
script.onreadystatechange = null;
this.loaded = true;
observer.next();
observer.complete();
this.currentRequest = null;
}
};
} else {
script.onload = () => { // Other browsers
this.loaded = true;
observer.next();
observer.complete();
this.currentRequest = null;
};
}
script.onerror = () => {
observer.error('Unable to load');
this.currentRequest = null;
};
document.getElementsByTagName('head')[0].appendChild(script);
}
}
The 'usage':
With template ngModel
<app-google-place ([ngModel)]="place"></app-google-place>