Short question: How can I validate a parent form when the validation is part of child custom elements?
Long version:
I built a reusable custom element which includes validation which is working like I expect it to do:
validated-input.html:
<template>
<div class="form-group" validate.bind="validation">
<label></label>
<input type="text" value.bind="wert" class="form-control" />
</div>
</template>
validated-input.js:
import { bindable, inject } from 'aurelia-framework';
import { Validation } from 'aurelia-validation';
#inject(Validation)
export class ValidatedInputCustomElement {
#bindable wert;
constructor(validation) {
this.validation = validation.on(this)
.ensure('wert')
.isNotEmpty()
.isGreaterThan(0);
}
}
I will have some forms that will use this custom element more than once in the same view (can be up to 8 or 12 times or even more). A very simplified example could look like this:
<template>
<require from="validated-input"></require>
<form submit.delegate="submit()">
<validated-input wert.two-way="val1"></validated-input>
<validated-input wert.two-way="val2"></validated-input>
<validated-input wert.two-way="val3"></validated-input>
<button type="submit" class="btn btn-default">save</button>
</form>
</template>
In the corresponding viewmodel file I would like to ensure that the data can only be submitted if everything is valid. I would like to do something like
this.validation.validate()
.then(() => ...)
.catch(() => ...);
but I don't understand yet how (or if) I can pull the overall validation into the parent view.
What I came up with up to now is referencing the viewmodel of my validated-input like this:
<validated-input wert.two-way="val1" view-model.ref="vi1"></validated-input>
and then to check it in the parent like this:
this.vi1.validation.validate()
.then(() => ...)
.catch(() => ...);
but this would make me need to call it 8/12/... times.
And I will probably have some additional validation included in the form.
Here is a plunkr with an example:
https://plnkr.co/edit/v3h47GAJw62mlhz8DeLf?p=info
You can define an array of validation objects (as Fabio Luz wrote) at the form level and then register the custom element validations in this array. The validation will be started on the form submit.
The form code looks like:
validationArray = [];
validate() {
var validationResultsArray = [];
// start the validation here
this.validationArray.forEach(v => validationResultsArray.push(v.validate()));
Promise.all(validationResultsArray)
.then(() => this.resulttext = "validated")
.catch(() => this.resulttext = "not validated");
}
validated-input.js gets a new function to register the validation
bind(context) {
context.validationArray.push(this.validation);
}
The plunker example is here https://plnkr.co/edit/X5IpbwCBwDeNxxpn55GZ?p=preview
but this would make me need to call it 8/12/... times.
And I will probably have some additional validation included in the form.
These lines are very important to me. In my opinion (considering that you do not want to call 8/12 times, and you also need an additional validation), you should validate the entire form, instead of each element. In that case, you could inject the validation in the root component (or the component that owns the form), like this:
import { Validation } from 'aurelia-validation';
import { bindable, inject } from 'aurelia-framework';
#inject(Validation)
export class App {
val1 = 0;
val2 = 1;
val3 = 2;
resulttext = "";
constructor(validation) {
this.validation = validation.on(this)
.ensure('val1')
.isNotEmpty()
.isGreaterThan(0)
.ensure('val2')
.isNotEmpty()
.isGreaterThan(0)
.ensure('val3')
.isNotEmpty()
.isGreaterThan(0);
//some additional validation here
}
validate() {
this.validation.validate()
.then(() => this.resulttext = "valid")
.catch(() => this.resulttext = "not valid");
}
}
View:
<template>
<require from="validated-input"></require>
<form submit.delegate="validate()" validation.bind="validation">
<validated-input wert.two-way="val1"></validated-input>
<validated-input wert.two-way="val2"></validated-input>
<validated-input wert.two-way="val3"></validated-input>
<button type="submit" class="btn btn-default">validate</button>
</form>
<div>${resulttext}</div>
</template>
Now, you can re-use the validated-input component in other places. And of course, you probably have to rename it, because the name validated-input does not make sense in this case.
Here is the plunker example https://plnkr.co/edit/gd9S2y?p=preview
Another approach would be pushing all the validation objects into an array and then calling a function to validate all validations objects, but that sounds strange to me.
Hope it helps!
Related
So from the backend I get a array of objects that look kind of like this
ItemsToAdd
{
Page: MemberPage
Feature: Search
Text: "Something to explain said feature"
}
So i match these values to enums in the frontend and then on for example the memberpage i do this check
private get itemsForPageFeatures(): ItemsToAdd[] {
return this.items.filter(
(f) =>
f.page== Pages.MemberPage &&
f.feature != null
);
}
What we get from the backend will change a lot over time and is only the same for weeks at most. So I would like to avoid to have to add the components in the template as it will become dead code fast and will become a huge thing to have to just go around and delete dead code. So preferably i would like to add it using a function and then for example for the search feature i would have a ref on the parent like
<SearchBox :ref="Features.Search" />
and in code just add elements where the ItemsToAdd objects Feature property match the ref
is this possible in Vue? things like appendChild and so on doesn't work in Vue but that is the closest thing i can think of to kind of what I want. This function would basically just loop through the itemsForPageFeatures and add the features belonging to the page it is run on.
For another example how the template looks
<template>
<div class="container-fluid mt-3">
<div
class="d-flex flex-row justify-content-between flex-wrap align-items-center"
>
<div class="d-align-self-end">
<SearchBox :ref="Features.Search" />
</div>
</div>
<MessagesFilter
:ref="Features.MessagesFilter"
/>
<DataChart
:ref="Features.DataChart"
/>
So say we got an answer from backend where it contains an object that has a feature property DataChart and another one with Search so now i would want components to be added under the DataChart component and the SearchBox component but not the messagesFilter one as we didnt get that from the backend. But then next week we change in backend so we no longer want to display the Search feature component under searchbox. so we only get the object with DataChart so then it should only render the DataChart one. So the solution would have to work without having to make changes to the frontend everytime we change what we want to display as the backend will only be database configs that dont require releases.
Closest i can come up with is this function that does not work for Vue as appendChild doesnt work there but to help with kind of what i imagine. So the component to be generated is known and will always be the same type of component. It is where it is to be placed that is the dynamic part.
private showTextBoxes() {
this.itemsForPageFeatures.forEach((element) => {
let el = this.$createElement(NewMinorFeatureTextBox, {
props: {
item: element,
},
});
var ref = `${element.feature}`
this.$refs.ref.appendChild(el);
});
}
You can use dynamic components for it. use it like this:
<component v-for="item in itemsForPageFeatures" :is="getComponent(item.Feature)" :key="item.Feature"/>
also inside your script:
export default {
data() {
return {
items: [
{
Page: "MemberPage",
Feature: "Search",
Text: "Something to explain said feature"
}
]
};
},
computed: {
itemsForPageFeatures() {
return this.items.filter(
f =>
f.Page === "MemberPage" &&
f.Feature != null
);
}
},
methods: {
getComponent(feature) {
switch (feature) {
case "Search":
return "search-box";
default:
return "";
}
}
}
};
Having problems implementing the locator lookup method depending on its parent in POM
Example of DOM (roughly):
<div class="lessons">
<div [data-test="lesson"]>
<div class="lesson__info">
<div ...>
<h2 [data-test="lessonTitle"]>FirstLesson</h2>
<div class"lesson__data">
<div [data-test="lessonDataButton"]>
<div class"lesson__controls">
<div [data-test="lessonStartButton"]>
<div [data-test="lesson"]>
<div class="lesson__info">
<div ...>
<h2 [data-test="lessonTitle"]>SecondLesson</h2>
<div class"lesson__data">
<div [data-test="lessonDataButton"]>
<div class"lesson__controls">
<div [data-test="lessonStartButton"]>
Example of my POM:
import { Selector, t } from 'testcafe'
class Page {
constructor() {
this.lesson = Selector('[data-test="lesson"]')
this.lessonDataBtn = Selector('[data-test="lessonDataButton"]')
this.lessonStartBtn = Selector('[data-test="lessonStartButton"]')
this.lessonTitle = Selector('[data-test="lessonTitle"]')
}
async getLessonButton(title, lessonButton) {
const titleLocator = this.lessonTitle.withText(title);
const currentLesson = this.lesson.filter((node) => {
return node.contains(titleLocator())
}, { titleLocator });
const buttonSelector = currentLesson.find((node) => {
return node === lessonButton();
}, { lessonButton });
return buttonSelector;
}
In my test I'm trying to click "lessonDataButton" in specific lesson filtered by its "title":
await t.click(await schedule.getLessonButton(testData.lesson.data.title, page.lessonDataBtn))
It works correctly only for first occurrence of "lessonDataBtn" on page, but if I try to find the same button in second lesson - it will be an error:
The specified selector does not match any element in the DOM tree.
> | Selector('[data-test="lesson"]')
| .filter([function])
| .find([function])
I created an example using the code samples you provided and got a different error:
1. The specified selector does not match any element in the DOM tree.
| Selector('[data-test="lesson"]')
| .filter([function])
> | .find([function])
But I believe the case is the same: the lessonButton() call in the filter function of the find method of the currentLesson selector will always return the first node of the set. A straightforward solution is to search for the button directly with the css selector: const buttonSelector = currentLesson.find('[data-test="lessonDataButton"]');. You also can get rid of filter functions completely:
getLessonButton (title) {
return this.lessonTitle.withText(title)
.parent('[data-test="lesson"]')
.find('[data-test="lessonDataButton"]');
}
This seems like it should be pretty straight forward... within a stepper, you're collecting info, and you want to make sure an email is an email. But it seems like the shared 'form' tag causes some issues where the error checker gets messed up and doesn't work?
Further clarification... the issue seems to actually be in the following tag element...
formControlName="emailCtrl"
When I remove this line, and remove it's sibling line from the .ts (emailCtrl: ['', Validators.required],) the error check starts working. However, that means that the stepper can't verify that this step is required.
How can I make sure the stepper validates an entry and at the same time make sure that the ErrorStateMatcher works?
Here is my combined HTML...
<mat-step [stepControl]="infoFormGroup">
<form [formGroup]="infoFormGroup">
<ng-template matStepLabel>Profile Information</ng-template>
<div>
<!-- <form class="emailForm"> -->
<mat-form-field class="full-width">
<input matInput placeholder="Username" [formControl]="emailFormControl"
formControlName="emailCtrl"
[errorStateMatcher]="infoMatcher">
<mat-hint>Must be a valid email address</mat-hint>
<mat-error *ngIf="emailFormControl.hasError('email') && !emailFormControl.hasError('required')">
Please enter a valid email address for a username
</mat-error>
<mat-error *ngIf="emailFormControl.hasError('required')">
A username is <strong>required</strong>
</mat-error>
</mat-form-field>
<!-- </form> -->
</div>
<button mat-button matStepperPrevious>Back</button>
<button mat-button matStepperNext>Next</button>
</form>
</mat-step>
As you can see, I have commented out the nested 'form' for the email slot. In testing, I have tried it commented and not commented out. Either way, the error checking doesn't work right.
Here are some of the pertinent .ts snippets...
import { FormControl, FormGroupDirective, NgForm, Validators } from '#angular/forms';
import { FormBuilder, FormGroup } from '#angular/forms';
import { ErrorStateMatcher } from '#angular/material/core';
export class Pg2ErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = form && form.submitted;
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
}
}
...
export class Pg2Dialog {
...
emailFormControl = new FormControl('', [
Validators.required,
Validators.email,
]);
infoMatcher = new Pg2ErrorStateMatcher();
...
this.infoFormGroup = this._formBuilder.group({
emailCtrl: ['', Validators.required],
});
I believe I figured this out. the ErrorStateMatcher requires a named form control. In this case, it's emailFormControl. This is declared as the following...
emailFormControl = new FormControl('', [
Validators.required,
Validators.email,
]);
Also, the stepper requires a named form group, that in itself declares a new form control. In this case, it was emailCtrl. It was declared as the following...
this.infoFormGroup = this._formBuilder.group({
emailCtrl: ['', Validators.required],
});
To have the stepper form control utilize the ErrorStateMatcher form control, simply drop the square brackets inside the .group assignment and assign emailFormControl to the emailCtrl. Like this...
this.infoFormGroup = this._formBuilder.group({
emailCtrl: this.emailFormControl
});
I tested this in a different code section with a similar problem and it worked in both places!
I'm trying to get data in JSON format from this site https://api.chucknorris.io/ (random joke), but i get an error:ERROR Error: Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays. And "DebugContext_ {view: Object, nodeIndex: 11, nodeDef: Object, elDef: Object, elView: Object}"
This is routing.ts
import { Injectable } from '#angular/core';
import { Http } from '#angular/http';
import 'rxjs/add/operator/map';
#Injectable()
export class RandomService {
constructor(private http: Http) {
console.log('working');
}
getRandomJokes() {
return this.http.get('https://api.chucknorris.io/jokes/random')
.map(res => res.json());
}
}
This is the component i'm trying to get data into
<div *ngFor="let joke of jokes">
<h3>{{joke.value}}</h3>
</div>
`,
providers: [RandomService]
})
export class PocetnaComponent {
jokes: Joke[];
constructor(private jokesService: RandomService){
this.jokesService.getRandomJokes().subscribe(jokes => {this.jokes = jokes});
}
}
interface Joke{
id: number;
value: string;
}
Since what you are receiving is not an array, but an object, therefore the error you are getting is because an object cannot be iterated. You might want to rethink the naming, since we are dealing with just one object, joke would be a more suitable name. But here, let's use jokes.
So you should do is remove the *ngFor completely and simply display the value with:
{{jokes?.value}}
Nothing else is needed :) Notice the safe navigation operator here, which safeguards null and undefined values in property paths. Be prepared to use this a lot in the asynchronous world ;)
Here is more info about the safe navigation operator, from the official docs.
You are not binding an array to the *ngFor, you have to make sure that jokes is actually an array and not a random joke object.
To solve it if it is an object you can just do this.jokes = [jokes].
The url you mention only provides a single value in an object, so you cannot iterate over it.
Maybe make multiple calls like this:
getRandomJokes(n = 10){
return Promise.all(Array(n).fill(0)
.map(() => this.http.get('https://api.chucknorris.io/jokes/random').toPromise()
.then(res => res.json()));
}
and use with promise
this.jokesService.getRandomJokes().then(jokes => {this.jokes = jokes});
you can check if it is an array and then go for ngFor or convert the jokes to array like let jokes = [jokes];
<div *ngIf = "jokes.length > 0" ; else check>
<div *ngFor="let joke of jokes">
<h3>{{joke.value}}</h3>
</div>
</div>
<ng-template #check>
<p>print the value of the object </p>
</ng-template>
May be jokes isn't an array.
<div *ngIf="Array.isArray(jokes)>
<div *ngFor="let joke of jokes">
<h3>{{joke.value}}</h3>
</div>
I'm trying to validate an object property of an Aurelia ViewModel.
ViewModel
#autoinject
class AddUserForm {
user: User;
controller: ValidationController;
constructor(controllerFactory: ValidationControllerFactory) {
this.controller = controllerFactory.createForCurrentScope();
}
validate() {
this.controller.validate.then(res => {
console.log(res.valid);
})
}
}
ValidationRules
.ensure((u: User) => u.id).displayName('User').required()
.on(AddUserForm)
ViewModel -> View
<template>
<form click.trigger="validate()">
<input type="text" value.bind="user.id & validate" />
</form>
</template>
User
class User {
id: string
}
The issue I'm having is that the validator is not picking up the nested user object. I'm I missing something to get this working? I read the docs and it seems like this should work. I'm using version ^1.0.0 of the plugin.
The problem is in your ValidationRules:
ValidationRules
.ensure((u: User) => u.id).displayName('User').required()
.on(AddUserForm)
needs to be
ValidationRules
.ensure((u: User) => u.id).displayName('User').required()
.on(User)
Then to get the controller to run this rule you either need to include "& validate" somewhere in your value.bind for that property, like this:
<input value.bind="user.id & validate" />
or before you call controller.validate(), add the entire object to the controller like this:
this.controller.addObject(this.user);
I use .addObject all the time because it causes validation to run on properties that aren't included in your markup, and I find I prefer that.
This caused an error when I tried it:
validate() {
this.controller.validate(res => {
console.log(res.valid);
})
}
.validate() expects a ValidateInstruction, in your example you're giving (res: any) => void. I would try changing to this instead:
this.controller.validate().then(res => {
console.log(res.valid);
});
Leaving .validate() undefined will cause it to validate all objects and bindings, and .then() will execute after that validation has completed.
This worked for me when I tried it in my test project.
If I misunderstood your question and this alone does not solve it however, you could also try assigning the User objects id to a property in AddUserForm like this:
public userId = this.user.id;
And changing your ValidationRules and view accordingly:
ViewModel
ValidationRules
.ensure((u: AddUserForm) => u.userId)
.displayName("User")
.required()
.on(this);
View
<template>
<form click.delegate="validate()">
<input type="text" value.bind="userId & validate" />
</form>
</template>