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.
Related
I created the form for multiple inputs, where the specific input data shall be validated at the time of data entry and once again for all data just before the submission of the form to the backend.
The conditions to submit: all fields are mandatory and the data is valid.
My program works, but I don't like that I'm repeating the validation code in 2 places: in ErrorOutput and hadleSubmit.
In ErrorOutput I check the data and, if necessary, display an error message.
In handleSubmit I just check the data without displaying of error message and if all data is valid, I confirm submitting.
How can I improve my example to prevent the repetition of this code, but the data validation was also at the time of data entry and before submission?
import React from 'react'
import { render } from 'react-dom'
const ErrorOutput = props => {
let name = props.name
let inputValue = props.case
let submit = props.submit
// Data validation
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
let error = true
const { firstName, telNo } = this.state
this.setState ({submit: submit})
// Repeat the data validation before submission
if (firstName === '' || !firstName.match(/^[a-zA-Z]+$/)) {
error = true
} else if (telNo === '' || !telNo.match(/^[0-9]+$/)) {
error = true
} else {
error = false
}
// Submited if all data is valid
if (!error) {
// send data
return alert('Success!')
}
}
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')
)
You could extract a FormItem component:
class FormItem extends React.Component {
render() {
return (
<div>
<label>
{this.props.label}
</label>
<input
{...this.props.input}
/>
<ErrorOutput
case={this.props.input.value}
name={this.props.input.name}
submit={this.props.onSubmit}
/>
</div>
);
}
}
and use it in your App:
render() {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<FormItem label='First name:' input={{
type: 'text'
name: 'firstName'
value: this.state.firstName,
onChange: this.handleValidation.bind(this)
}}
onSubmit={this.state.submit}
/>
<FormItem label='Phone number:' input={{
type:'tel'
name :'telNo'
value : {this.state.telNo}
onChange : {this.handleValidation.bind(this)}
}}
onSubmit={this.state.submit}
/>
<button>
Submit
</button>
</form>
)
}
this is where libraries like react-final-form and redux-form become handy.
UPD
ErrorOutput component should not validate anything, it is not a responsibility of a component. Instead, you could validate your values on inputs blur event and before submit:
class App extends React.Component {
constructor(props){
super(props)
this.state = {
firstName: '',
telNo: '',
submit: false,
errors: {},
invalid: false,
}
}
handleSubmit(e){
e.preventDefault()
if (this.validate()) {
// handle error
} else {
// submit
}
}
validate = () => {
const { firstName, telNo } = this.state
const errors = {}
let invalid = false;
if (firstName === '' || !firstName.match(/^[a-zA-Z]+$/)) {
errors.firstName = 'first name is required'
invalid = true;
} else if (telNo === '' || !telNo.match(/^[0-9]+$/)) {
telNo.telNo = 'telNo is required'
invalid = true;
}
this.setState({
invalid,
errors,
})
return invalid;
}
render() {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<FormItem label='First name:' input={{
type: 'text',
name: 'firstName',
value: this.state.firstName,
onChange: e => this.setState({ firstName: e.target.value }),
onBlur: () => this.validate(),
}}
/>
<FormItem label='Phone number:' input={{
type: 'tel',
name: 'telNo',
value: this.state.telNo,
onChange: e => this.setState({ telNo: e.target.value }),
onBlur: () => this.validate(),
}}
/>
<button>
Submit
</button>
</form>
)
}
}
and FormItem and ErrorOutput:
const ErrorOutput = ({ error }) => <span>{error}</span>
class FormItem extends React.Component {
render() {
return (
<div>
<label>
{this.props.label}
</label>
<input
{...this.props.input}
/>
{this.props.error && <ErrorOutput error={this.props.error} />}
</div>
);
}
}
const ErrorOutput = ({ errorText }) => <span>{errorText}</span>;
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
firstName: "",
telNo: "",
submit: false,
errors: {} //Add errors object to the state.
};
}
handleSubmit(e) {
e.preventDefault();
const errors = this.validateData();
if (Object.keys(errors).length === 0) {
alert("Success");
}
//else errors exist
this.setState({ errors });
}
validateData = () => {
let errors = {};
const { firstName, telNo } = this.state; // read the values to validate
if (firstName.length === 0) {
errors.firstName = "Required";
} else if (firstName.length > 0 && !firstName.match(/^[a-zA-Z]+$/)) {
errors.firstName = "Letters only";
}
if (telNo.length === 0) {
errors.telNo = "Required";
} else if (telNo.length > 0 && !telNo.match(/^[0-9]+$/)) {
errors.telNo = "Numbers only";
}
return errors;
};
handleValidation(e) {
this.setState({
[e.target.name]: e.target.value
});
}
render() {
const { errors } = this.state; // read errors from the state
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)}
/>
{errors.firstName && <ErrorOutput errorText={errors.firstName} />}
</div>
<div>
<label>Phone number:</label>
<input
type="tel"
name="telNo"
value={this.state.telNo}
onChange={this.handleValidation.bind(this)}
/>
{errors.telNo && <ErrorOutput errorText={errors.telNo} />}
</div>
<button>Submit</button>
</form>
);
}
}
render(<App />, document.getElementById("root"));
First I show a list of transactions, when a user selects a single transaction a new page is opened with the transaction ID in the URL. On this page are details of the transaction displayed.
The code below is just the details page. It shows all the right details.
One of the details is a list of 0 or more tags, I'd like to be able to edit the list of tags and save the result.
At this point, I always end up with a clean Input field and I do not understand how to populate this field with the existing transaction['tags'] data.
It seems that the transaction['tags'] is not initialized until the page is rendered, I cannot use it in the constructor or in the componentDidMount.
What I expect is that the transaction object as stated in the mapStateToProps is available and I can change the line in the constructor from: this.state = {value: ''}; to this.state = {value: transaction['tags']}
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { fetchTransaction } from '../actions';
class TransactionsIndex extends Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount() {
const { _key } = this.props.match.params;
this.props.fetchTransaction(_key);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('A name was submitted: ' + this.state.value);
event.preventDefault();
}
render() {
const { transaction } = this.props;
if (!transaction) {
return <div>Loading...</div>;
}
let tags = null;
tags =
<div>
<form onSubmit={this.handleSaveTagsclick}>
<input type="text" value={this.state.value} onChange={this.handleChange} />
<input type="submit" value="Submit" />
</form>
</div>
// console.log(transaction['tags']);
return (
<div className="container">
<div className="well">
<div>Transactiedatum: { transaction["Transactiedatum"] }</div>
<div>Valutacode: { transaction["Valutacode"] }</div>
<div>CreditDebet: { transaction["CreditDebet"] }</div>
<div>Bedrag: { transaction["Bedrag"] }</div>
<div>Tegenrekeningnummer: { transaction["Tegenrekeningnummer"] }</div>
<div>Tegenrekeninghouder: { transaction["Tegenrekeninghouder"] }</div>
<div>Valutadatum: { transaction["Valutadatum"] }</div>
<div>Betaalwijze: { transaction["Betaalwijze"] }</div>
<div>Omschrijving: { transaction["Omschrijving"] }</div>
<div>Type betaling: { transaction["Type betaling"] }</div>
<div>Machtigingsnummer: { transaction["Machtigingsnummer"] }</div>
<div>Incassant ID: { transaction["Incassant ID"] }</div>
<div>Adres: { transaction["Adres"] }</div>
<div>Status: { transaction["status"] }</div>
<div>Created: { transaction["created"] }</div>
{tags}
</div>
<Link to="/"><button type="button" className="btn btn-default">Back</button></Link>
</div>
);
};
}
function mapStateToProps({ transactions }) {
// console.log('transactions_selectedTransaction: ' + transactions['selectedTransaction']);
return { transaction: transactions['selectedTransaction'] };
}
export default connect(mapStateToProps, { fetchTransaction })(TransactionsIndex);
I found this but it did not help me: Redux-form: Set form values from state
and this: How to get state / value from form component?
I am trying to implement a custom validator that checks if the keyed username exists. However, I am running into a problem where the control is always invalid. I have confirmed the webApi is returning the correct values and the validator function is stepping into the proper return statements.
My custom validator is as follows:
import { Directive, forwardRef } from '#angular/core';
import { AbstractControl, ValidatorFn, NG_VALIDATORS, Validator, FormControl } from '#angular/forms';
import { UsersService } from '../_services/index';
import { Users } from '../admin/models/users';
function validateUserNameAvailableFactory(userService: UsersService): ValidatorFn {
return (async (c: AbstractControl) => {
var user: Users;
userService.getUserByName(c.value)
.do(u => user = u)
.subscribe(
data => {
console.log("User " + user)
if (user) {
console.log("Username was found");
return {
usernameAvailable: {
valid: false
}
}
}
else {
console.log("Username was not found");
return null;
}
},
error => {
console.log("Username was not found");
return null;
})
})
}
#Directive({
selector: '[usernameAvailable][ngModel]',
providers: [
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => UserNameAvailableValidator), multi: true }
]
})
export class UserNameAvailableValidator implements Validator {
validator: ValidatorFn;
constructor(private usersService: UsersService) {
}
ngOnInit() {
this.validator = validateUserNameAvailableFactory(this.usersService);
}
validate(c: FormControl) {
console.log(this.validator(c));
return this.validator(c);
}
}
And the form looks like:
<form #userForm="ngForm" (ngSubmit)="onSubmit()" novalidate>
<div class="form form-group col-md-12">
<label for="UserName">User Name</label>
<input type="text" class="form-control" id="UserName"
required usernameAvailable maxlength="50"
name="UserName" [(ngModel)]="user.userName"
#UserName="ngModel" />
<div [hidden]="UserName.valid || UserName.pristine" class="alert alert-danger">
<p *ngIf="UserName.errors.required">Username is required</p>
<p *ngIf="UserName.errors.usernameAvailable">Username is not available</p>
<p *ngIf="UserName.errors.maxlength">Username is too long</p>
</div>
<!--<div [hidden]="UserName.valid || UserName.pristine" class="alert alert-danger">Username is Required</div>-->
</div>
</form>
I have followed several tutorials proving this structure works, so I am assuming that I am possibly messing up the return from within the validator function.
Also, is there a way I can present the list of errors (I have tried using li with ngFor, but I get nothing from that)?
So, there was something wrong with using .subscribe the way I was. I found a solution by using the following 2 links:
https://netbasal.com/angular-2-forms-create-async-validator-directive-dd3fd026cb45
http://cuppalabs.github.io/tutorials/how-to-implement-angular2-form-validations/
Now, my validator looks like this:
import { Directive, forwardRef } from '#angular/core';
import { AbstractControl, AsyncValidatorFn, ValidatorFn, NG_VALIDATORS, NG_ASYNC_VALIDATORS, Validator, FormControl } from '#angular/forms';
import { Observable } from "rxjs/Rx";
import { UsersService } from '../_services/index';
import { Users } from '../admin/models/users';
#Directive({
selector: "[usernameAvailable][ngModel], [usernameAvailable][formControlName]",
providers: [
{
provide: NG_ASYNC_VALIDATORS,
useExisting: forwardRef(() => UserNameAvailableValidator), multi: true
}
]
})
export class UserNameAvailableValidator implements Validator {
constructor(private usersService: UsersService) {
}
validate(c: AbstractControl): Promise<{ [key: string]: any }> | Observable<{ [key: string]: any }> {
return this.validateUserNameAvailableFactory(c.value);
}
validateUserNameAvailableFactory(username: string) {
return new Promise(resolve => {
this.usersService.getUserByName(username)
.subscribe(
data => {
resolve({
usernameAvailable: true
})
},
error => {
resolve(null);
})
})
}
}
This is now working. However, I now have an issue where the the control temporarily invalidates itself while the async validator is running.
Return for true should be:
{usernameAvailable: true}
or null for false.
I am using React-Validation-Mixin together with Joi and Joi-Validation-Strategy to do some validations on a React Step/Wizard Form.
I have a parent FormStart Element that receives the state of its FormStep children through props.
The validation correctly signals that the input is required, but when I write a correct number in the browser (5 numbers as in PLZ/ZIP-Code), it will still signal that the input is invalid, even though the zip state shows a correct 5-digit number, so the next button never takes me to the next Form step.
class FormStart extends Component {
constructor(props) {
super(props);
this.state = {
step: 1,
zip: ""
}
this.goToNext = this.goToNext.bind(this);
}
goToNext() {
const { step } = this.state;
if (step !== 10) {
this.setState({ step: step + 1 });
if (step == 9) {
const values = {
zip: this.state.zip,
};
console.log(values);
// submit `values` to the server here.
}
}
}
handleChange(field) {
return (evt) => this.setState({ [field]: evt.target.value });
}
render(){
switch (this.state.step) {
case 1:
return <FormButton
onSubmit={this.goToNext}
/>;
//omitting the other 8 cases
case 9:
return <FormStep7
onSubmit={this.goToNext}
zip={this.state.zip}
onZipChange={this.handleChange('zip')}
/>;
case 10:
return <FormSuccess/>;
}
}
}
export default FormStart;
The React console shows that the zip state is correctly changed, and the Validation object also receives the same correct 5-digit zip and still holds the correct value onBlur.
class FormStep7 extends Component {
constructor(props) {
super(props);
this.validatorTypes = {
PLZ: Joi.number().min(5).max(5).required().label('PLZ').options({
language: {
number: {
base: 'wird benötigt',
min: 'muss {{limit}} Nummern enthalten',
max: 'muss {{limit}} Nummern enthalten'
}
}
})
};
this.getValidatorData = this.getValidatorData.bind(this);
this.getClasses = this.getClasses.bind(this);
this.renderHelpText = this.renderHelpText.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
getValidatorData() {
return {
PLZ: this.props.zip
};
}
getClasses(field) {
return classnames({
'form-control': true,
'has-error': !this.props.isValid(field)
});
}
renderHelpText(message) {
return (
<span className='help-block'>{message}</span>
);
}
handleSubmit(evt) {
evt.preventDefault();
const onValidate = (error) => {
if (error) {
//form has errors; do not submit
} else {
this.props.onSubmit();
}
};
this.props.validate(onValidate);
}
render() {
return (
<form role="form" onSubmit={this.handleSubmit}>
<div className='row'>
<div className="col-md-10 col-md-offset-1">
<div className='form-group'>
<label htmlFor="zip">
Einsatzort
</label>
<br />
<input className={this.getClasses('PLZ')} id="PLZ" placeholder="Meine PLZ" type="text" onChange={this.props.onZipChange} onBlur={this.props.handleValidation('PLZ')} value={this.props.zip} />
{this.renderHelpText(this.props.getValidationMessages('PLZ'))}
</div>
</div>
</div>
<div className='row'>
<div className="col-md-10 col-md-offset-1">
<button className="btn btn-green btn-block">Next</button>
</div>
</div>
</div>
</form>
);
}
}
FormStep7.propTypes = {
errors: PropTypes.object,
validate: PropTypes.func,
isValid: PropTypes.func,
handleValidation: PropTypes.func,
getValidationMessages: PropTypes.func,
clearValidations: PropTypes.func
};
export default validation(strategy)(FormStep7);
What am I doing wrong?
I found out that the issue was on Joi.number(). I changed the validation to match a Regex String pattern and then it worked.
this.validatorTypes = {
PLZ: Joi.string().regex(/^[0-9]{5}$/).label('PLZ').options({
language: {
string: {
regex: {
base: "mit 5 Nummern wird benötigt"
}
}
}
})
};
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.