Angular6 Material - using Stepper with Input with a custom ErrorStateMatcher - input

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!

Related

Vue class components dynamically add component depending on answer from backend

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 "";
}
}
}
};

Aurelia - bound control's backing property setter called twice

I implemented a custom control which is basically a styled file uploader specifically for images, which allows the preview of images. The problem is that I experience a weird behavior: the setter of the property to which the regular HTML <input type="file"> is bound gets called twice in FF 53, IE 11, Edge 38, but not in Chrome 57. I have no idea why this happens, perhaps some of you have experienced similar behavior and know a solution.
The markup is the following:
<template>
<!-- markup for preview functionality, irrelevant here -->
<input type="file" class="sr-only" id="${id}" aria-describedby="${id}-help" multiple accept="image/*" files.bind="imageGroup" />
<label class="btn btn-default btn-sm" aria-hidden="true" for="${id}" role="button">
<i class="fa fa-plus" aria-hidden="true"></i> Add files
</label>
</template>
The backing TypeScript code:
import { bindable, bindingMode, autoinject } from 'aurelia-framework';
import { DialogService } from 'aurelia-dialog';
import { Confirm } from '../confirm';
#autoinject
export class ImageUploader {
#bindable({ defaultBindingMode: bindingMode.twoWay }) imageArray: Array<File> = null;
#bindable id: string = null;
#bindable maxImageCount: number = 10;
#bindable maxFileSizeKiloBytes: number = 2048;
constructor(private dialogService: DialogService) { }
idChanged(newValue: string) {
if (!newValue) {
throw Error('The id parameter needs a value in order to make the file uploader work.');
}
}
set imageGroup(fileList: FileList) {
console.log('imageGroup');
if (!fileList || !fileList.length) return;
if (!this.imageArray) return;
for (let i = 0; i < fileList.length; ++i) {
let file = fileList.item(i);
if (this.imageArray.length >= this.maxImageCount) {
// TODO: Alert: maximum number of images reached
} else {
if (file && file.type.startsWith('image/')) {
// Size is in bytes. Div by 1024 to get kilobytes.
if (file.size / (1024) > this.maxFileSizeKiloBytes) {
// TODO: Alert: Image file too large.
} else {
this.imageArray.push(file);
}
} else {
// TODO: Alert: unsupported media type.
}
}
}
}
}
As you can see, I use the "hide the default file uploader and style a bound label instead" trick to style the uploader itself - I only say this to point out that I've checked if maybe this is the cause, but it's not, the same behavior can be experienced if I show the default file uploader and use that.
You can also see that I've bound the <input type="file"> to a setter-only property. I've done that because this property is basically just a proxy which populates another array (which is not in the control, this is what is bound to the control) which is necessary because I need to allow the user to upload files in multiple turns so that (s)he can select files from different folders.
The rest of the code is irrelevant, because the console.log line runs twice whenever I select some files, so the error is located somewhere here - I am just unable to figure out exactly what is causing this to happen.
Any help is appreciated.
I confirmed that using setter on that case causes to call your setter two times in FF, aurelia created another setter for every property that needs to observe. However you can achieve same behavior using observable with minimal changes and also provided encapsulation in this case. See those gist.run link. More info here.
UPDATE
For Firefox this is still open as a bug. But a workaround using change.delegate as elefantino and abemeister said works on FF, see gist here.

Aurelia Validation on dynamically built form

I'm creating a custom element that dynamically generates a user input form. So far it's working quite well, but I'm having problems setting up Aurelia-Validation on it and need some help.
my-form.html
<div class="form-group" repeat.for="control of controls">
<label>${control.label}</label>
<div>
<compose containerless
view-model="resources/elements/${control.type}/${control.type}"
model.bind="{'control': control, 'model': model, 'readonly': readonly}"
class="form-control"></compose>
</div>
</div>
I have several different control.type's available and working (my-textbox, my-dropdown, my-datepicker) -- each of them is a custom element. For instance:
Example control (my-textbox.html)
<template>
<input type="text"
class="form-control"
value.bind="model[control.bind] & validate">
</template>
Parent view:
<my-form controls.bind="controls" model.bind="model" if.bind="controls"></my-form>
Parent view-model:
controls = [
{label: 'Username', type: 'my-textbox', bind: 'user_username'},
{label: 'Status', type: 'my-dropdown', bind: 'user_status', enum: 'ActiveInactive', default: '(Choose)'},
{label: 'Last_login', type: 'my-datepicker', bind: 'user_lastlogin', vc: 'date'}];
ValidationRules
.ensure('user_username').required().minLength(1).maxLength(10)
.ensure('user_status').required()
.ensure('user_lastlogin').required()
.on(this.model);
I'm getting the error Error: A ValidationController has not been registered. at ValidateBindingBehaviorBase.bind.... However, I only want one validator for the whole dynamically-built form, so I don't want to import validation in each of the controls. What do I do?
It actually works to import the Validation Controller and related resources in the parent form, even though the & validate is attached to the child controls.
Parent view-model
import {inject} from 'aurelia-framework';
import {ValidationControllerFactory, ValidationRules} from 'aurelia-validation';
import {BootstrapFormRenderer} from 'common/bootstrap-form-renderer';
#inject(Core, ValidationControllerFactory)
export class MyForm {
constructor(validationControllerFactory) {
this.validationCtrl = validationControllerFactory.createForCurrentScope();
this.validationCtrl.addRenderer(new BootstrapFormRenderer());
}
setupValidator() {
let rules = [];
this.controls.map(control => {
if (control.validation) {
rules.push(ValidationRules.ensure(control.bind).required().rules[0]);
}
this.validationCtrl.addObject(this.modelEdit, rules);
}
}

How do you properly clear Aurelia binding of a complex object?

Background: I'm trying to create a form using Aurelia. I have a person object that I would like to be able to fill in data for. If the user knows some identifying information about the person's family, they can enter it in an input and a select box will be displayed to allow the user to select the individual from that family for this particular form. The form will then fill in any information it knows about that individual into input fields allowing the user to overwrite any of the information if necessary. The form also allows them to clear the selected person if they want to choose another one.
Most of the functionality seems to work as expected, but when I try to allow the user to clear out the selected person, I'm seeing some behavior that I wouldn't have expected.
I have created a GistRun. The bottom pane is working as I would expect, after the user gets data, selects a person and then clears their selection, they are provided with the select element again. If you uncomment the input element, you will see that the user now has to click the clear action twice before they see the select element again. Why?
How can I update the application so that the user will only need to clear out the person once and the select box will appear again to allow the user to make another selection?
If you have an Aurelia application, you should be able to reproduce this by replacing the app.html with the following:
<template>
<select value.bind="val2" if.bind="opts2 && !val2">
<option repeat.for="opt of opts2" model.bind="opt">${opt.firstName}</option>
</select>
<div if.bind="!opts2 || val2">
<span>${val2.firstName}</span>
<button click.delegate="clearVal2()" if.bind="val2">Clear</button>
</div>
<button click.delegate="getOpts2()">Get</button>
<div>${val2.blah}</div>
<!--<input type="text" value.bind="val2.blah"/>-->
</template>
An the app.js with this:
export class App {
opts2;
val2;
getOpts2(){
this.opts2 = [
undefined,
{
blah: 1,
firstName: 'foo',
address: {
line1: '123 Main St.'
}
},
{
blah: 2,
firstName: 'bar',
address: {
line1: '456 Other Wy.'
}
}
];
}
clearVal2(){
this.val2 = null;
}
}
Any help would be greatly appreciated. Thanks!
UPDATE
If I put the input in a custom element and bind to that, things seem to work as expected. The values that I'm putting into my form though aren't in one location that I could utilize a custom element for. I have updated the Gist with an example.
How can I achieve the same functionality without the need for a custom element?
In all honesty I'm not sure why, but if you add if.bind="val2"on the input element, it clears the value and the select button returns.
<input type="text" if.bind="val2" value.bind="val2.blah"/>
Hope this (slightly) helps
Give that you are allowing the user to either select a value from the list or create a completely new entry, I would tend towards separating the value selected in the list and the data backing up the text boxes. Whenever the value of the select changes, I would set the value of the object backing the text boxes to the value of the select. The way I chose to do this in my sample code is to use the observable decorator on the value the select is bound to.
Here's an example: https://gist.run?id=e4b594eaa452b47d9b3984e7f9b04109
app.html
<template>
<div>
<select value.bind="val" if.bind="opts && !val">
<option repeat.for="opt of opts" model.bind="opt">${opt.firstName}</option>
</select>
<button click.delegate="getOpts()">Get</button>
</div>
<div if.bind="!opts || person">
<span>First Name: ${person.firstName}</span>
<button click.delegate="resetForm()" if.bind="val">Clear Selection</button>
</div>
Address: <input type="text" value.bind="person.address.line1" />
<hr />
val
<pre><code>
${toJSON(val)}
</code></pre>
person
<pre><code>
${toJSON(person)}
</code></pre>
</template>
app.js
import {observable} from 'aurelia-framework';
export class App {
#observable val = null;
person = {};
getOpts(){
this.opts = [
null,
{
blah: 1,
firstName: 'foo',
address: {
line1: '123 Main St.'
}
},
{
blah: 2,
firstName: 'bar',
address: {
line1: '456 Other Wy.'
}
}
];
}
valChanged() {
this.person = this.val;
console.log("set person");
}
resetForm(){
this.val = null;
console.log("reset val");
}
toJSON(value) {
if(!(value === false) && !value) {
return '';
}
return JSON.stringify(value);
}
}
You can see something interesting is happening when I reset the form. Aurelia is creating the properties necessary for bindings to person (namely person.address.line1 when we set person = null. But it doesn't create a firstName property, b/c that property isn't being bound until person stops being falsey.
Another option here is to simply use the with attribute to scope the input.
https://gist.run/?id=7b9d230f7d3c6dc8c13cefdd7be50c7f
<template>
<template with.bind="val.address">
<input value.bind="line1" />
</template>
</template>
Although I agree that mixing the logic of selections and inputs like that is probably not the best idea :)

Aurelia: validating form with reusable validatable custom element

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!