I have the following requirements:
I got two FormControl objects for select-elements mainSelect and subSelect that are required.
subSelect changes depending on the value from mainSelect.
When mainSelect changes to a value in which the value from subSelect isn't included subSelect needs to become invalid so the FormGroup both of the FormControl's are part of becomes invalid, too.
But if the value from subSelect is included subSelect needs to hold his actual value.
(A concrete example is described after the StackBlitz link.)
My problem solving this requirement:
If the value of mainSelect changes and the value of subSelect isn't included subSelect takes the first value of the list instead of becoming null/invalid.
So the SOLUTION would be if the selected value of 'subSelect' becomes null and no value is selected in the browser.
What I tried so far:
I tried to create a component and implement the ControlValueAccessor interface. Seems like here lies my problem. I think I don't really understand how that works.
I watched the following video on YouTube and read articles (1, 2) related to ControlValueAccessor, but still couldn't solve my problem.
This is part of my code:
Also you can find it on StackBlitz
Example
If in the browser MainSelect has the value thirdMainSelect and SubSelect has the value fifthSubSelect and MainSelect changes his value to firstMainSelect SubSelect should have no selected value.
select.component.ts
export class SomeObject {
value: string;
parameters: {[parameterName: string]: string} = {};
}
#Component({
selector: "app-select",
templateUrl: "./select.component.html",
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: SelectComponent,
multi: true
}]
})
export class SelectComponent implements ControlValueAccessor, OnChanges {
#ViewChild("select", {static: true}) select: ElementRef;
#Input() tableId: string;
#Input() filter: { [parameterName: string]: string};
returnedTable: SomeObject[];
onChange: (_: any) => void;
onTouched: () => void;
selected: string;
constructor(private tableService: TableService) { }
loadTable(): void {
this.tableService.getTable(this.tableId, this.filter)
.subscribe(table => {
this.returnedTable = table;
});
}
ngOnChanges(): void {
this.loadTable();
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
writeValue(value: string): void {
this.selected = value;
}
}
select.component.html
<select class="form-control" #select (change)="onChange($event.target.value)">
<option *ngFor="let item of returnedTable" [value]="item.value" [selected]="selected === item.value">{{item.value}}</option>
</select>
app.component.ts
#Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"]
})
export class AppComponent implements OnInit {
form: FormGroup;
containerObject: ContainerObject;
selectedMainValue: string;
constructor(private tableService: TableService,
private formBuilder: FormBuilder) {
}
ngOnInit(): void {
this.tableService.getContainerObject()
.subscribe(containerObject => {
this.containerObject = containerObject;
this.selectedMainValue = containerObject.mainSelect;
this.initForm();
});
}
private initForm(): void {
this.form = this.formBuilder.group({
mainSelect: [this.containerObject.mainSelect, Validators.required],
subSelect: [this.containerObject.subSelect, Validators.required]
});
this.subscribeToMainSelectChanged();
this.subscribeToSubSelectChanged();
}
onSubmit(): void {
if (this.form.valid) {
this.containerObject.mainSelect = this.form.get("mainSelect").value;
this.containerObject.subSelect = this.form.get("subSelect").value;
this.tableService.saveContainerObject(this.containerObject);
}
}
private subscribeToMainSelectChanged() {
this.form.get("mainSelect").valueChanges
.subscribe(mainSelect => {
this.selectedMainValue = mainSelect;
console.log(this.form.status);
});
}
private subscribeToSubSelectChanged() {
this.form.get("subSelect").valueChanges
.subscribe(() => {
console.log(this.form.status);
});
}
}
app.component.html
<div>
<form id="wrapper" [formGroup]="form" (ngSubmit)="onSubmit()">
<div id="left" class="form-group row">
<label for="mainSelect" class="col-form-label col-sm-2">MainSelect</label>
<div class="col-sm-6">
<app-select
id="mainSelect"
formControlName="mainSelect"
[tableId]="'mainSelectTable'"
[filter]="{firstType: 'firstParameter'}"
></app-select>
</div>
</div>
<div id="right" class="form-group row">
<label for="subSelect" class="col-form-label col-sm-2">SubSelect</label>
<div class="col-sm-6">
<app-select
id="subSelect"
formControlName="subSelect"
[tableId]="'firstParameter'"
[filter]="{firstType: 'firstParameter', secondType: selectedMainValue}"></app-select>
</div>
</div>
<p></p>
<button id="button" type="submit">Submit</button>
</form>
</div>
I think that it's only use valuesChange. If you has two arrays data and subdata and a form like
form = new FormGroup({
prop1: new FormControl(),
prop2: new FormControl()
});
A simple
this.form.get("prop1").valueChanges.subscribe(res => {
this.dataService.getData(res).subscribe(data=>{
this.subdata=data;
if (!this.subdata.find(x=>x.value==this.form.get("prop2").value))
this.form.get("prop2").setValue(null);
}).unsubscribe()
});
must be enough, see stackblitz
After a while trying to solve this problem only using the SelectComponent and especially writeValue, the following code did the job:
I changed the select.component.ts as following:
export class SomeObject {
value: string;
parameters: {[parameterName: string]: string} = {};
}
#Component({
selector: "app-select",
templateUrl: "./select.component.html",
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: SelectComponent,
multi: true
}]
})
export class SelectComponent implements ControlValueAccessor, OnChanges {
#ViewChild("select", {static: true}) select: ElementRef;
#Input() tableId: string;
#Input() filter: { [parameterName: string]: string};
returnedTable: SomeObject[];
onChange: (_: any) => void;
onTouched: () => void;
selected: string;
constructor(private tableService: TableService) { }
loadTable(): void {
this.tableService.getTable(this.tableId, this.filter)
.subscribe(table => {
this.returnedTable = table;
if (!!this.select && !!this.select.nativeElement.value) {
this.writeValue(this.select.nativeElement.value);
this.onChange(this.selected);
}
});
}
ngOnChanges(): void {
this.loadTable();
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
writeValue(value: string): void {
if (!!this.returnedTable && !this.returnedTable.some(item => item.value === value)) {
this.selected = null;
} else {
this.selected = value;
}
}
}
And the select.component.html like this:
<select class="form-control" #select (change)="onChange($event.target.value)">
<option hidden *ngIf="!selected" value=""></option>
<option *ngFor="let item of returnedTable" [value]="item.value" [selected]="selected === item.value">{{item.value}}</option>
</select>
To deselect an option:— option.selected = false.
Angular library with function decompilation at its core for $scope and Nested Forms, an Angular feature that indicates that Angular team doesn't know how to effectively use HTML. HTML doesn't allow nested forms so why the hell would you try to shoehorn the language to do that? More trouble than it's worth. Of course, you can't expect much better from guys like Bradley Green, former Angular JS manager.
Related
I'm using Angular 10, and i'm havin issues to create a Input Component with ControlValueAccessor.
I'm creating public vars and public arrow functions, and when I call the arrow function is returning undefined.
Here is my .ts code:
import { Component, forwardRef, Input } from '#angular/core';
import { ControlValueAccessor, FormControl, FormGroup, NG_VALUE_ACCESSOR } from '#angular/forms';
#Component({
selector: 'app-input',
templateUrl: './input.component.html',
styleUrls: ['./input.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(
() => InputComponent
),
multi: true
}
]
})
export class InputComponent implements ControlValueAccessor {
#Input() public parentForm: FormGroup;
#Input() public fieldName: string;
#Input() public label: string;
public value: string;
public changed: ( value: string ) => void;
public touched: () => void;
public isDisabled: boolean;
get formField (): FormControl {
return this.parentForm?.get( this.fieldName ) as FormControl;
}
constructor () { }
public writeValue ( value: string ): void {
this.value = value;
}
public onChange ( event: Event ): void {
const value: string = (<HTMLInputElement>event.target).value;
this.changed( value );
}
public registerOnChange ( fn: any ): void {
this.changed = fn;
}
public registerOnTouched ( fn: any ): void {
this.touched = fn;
}
public setDisabledState ( isDisabled: boolean ): void {
this.isDisabled = isDisabled;
}
}
And my .html file:
<div class="form-group">
<label class="form-label" [for]="fieldName">
{{ label }}
</label>
<input
type="text"
class="form-control"
[ngClass]="{ 'has-error': formField?.invalid && formField?.dirty }"
[id]="fieldName"
[name]="fieldName"
[value]="value"
[disabled]="isDisabled"
(input)="onChange( $event )"
(blur)="touched()"
/>
<app-field-errors [formField]="formField">
</app-field-errors>
</div>
When I interact with the Input (change/input or blur) I get this errors:
ERROR TypeError: this.changed is not a function
ERROR TypeError: ctx.touched is not a function
I believe the this.changed error is because I'm calling on onChange function, and ctx.touched is because i'm calling on HTML file.
I can access normally the Input() vars, like parentForm, fieldName and label.
Thanks for you help.
Change these lines
public changed: ( value: string ) => void;
public touched: () => void;
to
public changed: any = ( value: string ) => void; // personally I prefer {} rather than void
public touched: any = () => void;
I'm not sure if there's a better way to do this, I assume there should be. Basically I have a component I want to treat as a standalone form control. This control will always have some sort of special validation attached and I would like it to bubble up to the form whenever the component is used.
I've attached a plunker. Is there a way to for the form to be marked invalid if the component/formControl is invalid? I know I could add the validator to the form itself, but I would like to make things easy and more predictable for future use of this component. I'm also open to better ideas of doing this.
#Component({
selector: 'my-app',
template: `
<form [formGroup]="form">
<my-component-control formControlName="myComponent"></my-component-control>
</form>
<div>Form Value: {{form.value | json}}</div>
<div>Form Valid: {{form.valid}}</div>
`,
})
export class App {
constructor(fb: FormBuilder) {
this.form = fb.group({
myComponent: ''
});
}
}
#Component({
selector: 'my-component-control',
template: `
<div>Control Errors: {{control.errors | json}}</div>
<input [formControl]="control">
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => MyComponentComponent),
}
]
})
export class MyComponentComponent implements ControlValueAccessor {
control: FormControl;
onChange: any = () => { };
onTouched: any = () => { };
constructor() {
this.control = new FormControl('', (control) => {
return control.value ? null : {shouldHaveSomething: true};
});
this.control.valueChanges.subscribe((value) => {
this.onChange(value);
this.onTouched();
});
}
writeValue (obj: any): void {
this.control.setValue(obj);
}
registerOnChange (fn: any): void {
this.onChange = fn;
}
registerOnTouched (fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
throw new Error('Method not implemented.');
}
}
One solution could be using setValidators method on host AbstractControl
To do so I'm going to get reference to AbstractControl through NgControl. We can't just inject NgControl in constructor because we'll get issue with instantiating cyclic dependency.
constructor(private injector: Injector) {
...
}
ngOnInit() {
Promise.resolve().then(() => {
const hostControl = this.injector.get(NgControl, null);
if (hostControl) {
hostControl.control.setValidators(this.control.validator);
hostControl.control.updateValueAndValidity();
}
});
}
Ng-run Example
HTML Template
<tr *ngFor="let wi of page.items" (click)="selectWorkItem(wi)">
<td><input [ngModel]="wi.checked" type="checkbox"></td>
<td [textContent]="wi.someText"></td>
<td>
<input
class="form-control"
tabindex="-1"
[typeahead]="WHAT THE ?" <!-- tried propertyManagers below -->
[ngModel]="wi.propertyManager" />
Component
export class WorkItemListComponent implements OnInit {
selectedPropertyManager: any;
propertyManagers = Observable.create((observer: any) => {
observer.next(this.selectedPropertyManager);
}).mergeMap((token: string) =>
this.partyService.loadPartyHints(token).map(_ => _.displayName));
page.items contains a list of items that correspond to each row in the table.
What I am observing, is that in my case the observer.next is meant to be bound to the [ngModel] of the page.items[?].propertyManager, but the observable is actually bound to selectedPropertyManager (which is NOT the same as the [ngModel]).
Is there any way to create a typeahead that observes the current model value, and passes that to the loadPartyHints function.
<input
class="form-control"
tabindex="-1"
[typeahead]="CREATE_OBSERVABLE_OVER_MODEL(wi.propertyManager)"
[ngModel]="wi.propertyManger"
<!-- THIS is the model value, not selectedPropertyManager -->
Edit
I have tried this...
With a template like this...
<input
#inp
class="form-control"
tabindex="-1"
[typeahead]="propertyManagers"
[(ngModel)]="wi.propertyManager"
(ngModelChange)="valueChanged(inp.value)"/>
and the following Subject
hints = new Subject();
propertyManagers = this.hints.mergeMap((token: string) => this.partyService.loadPartyHints(token).map(_ => _.map(x => x.displayName)));
valueChanged(value: string) {
this.logger.info(`input changed :: ${value}`);
this.hints.next(value);
}
This get's me closer, but now all the ngx-bootstrap typeahead(s) are interfering with each other and getting each other's data.
I need to someone create a Subject / Observable factory that wires up each row independently?
import { Component, Provider, forwardRef, Input, Output, EventEmitter } from "#angular/core";
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "#angular/forms";
import { NGXLogger } from "ngx-logger";
import { Subject } from "rxjs/Subject";
#Component({
selector: "typeahead-input",
template: `<input
#inp
class="form-control"
tabindex="-1"
[typeahead]="observable"
[(ngModel)]="value"
(typeaheadOnSelect)="select($event)"
(ngModelChange)="hintValueChanged(inp.value)"/>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TypeaheadInputComponent),
multi: true
}]
})
export class TypeaheadInputComponent implements ControlValueAccessor {
private _value: any = "";
hints = new Subject();
observable = this.hints.mergeMap((token: string) => this.loadHints(token));
#Input() hint: Function;
#Output() itemSelected = new EventEmitter<string>();
constructor(
private readonly logger: NGXLogger) {
}
get value(): any { return this._value; };
set value(v: any) {
if (v !== this._value) {
this._value = v;
this.onChange(v);
}
}
loadHints(token: string) {
this.logger.info(`loading hints ${token}`);
return this.hint(token);
}
hintValueChanged(hint: string) {
this.logger.info(`typehead :: hint value (${hint})`);
this.hints.next(hint);
}
select(selectedItem: any) {
this.itemSelected.emit(selectedItem.value);
}
writeValue(value: any) {
this._value = value;
this.onChange(value);
}
onChange = (_) => { };
onTouched = () => { };
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}
I'd like to create a custom form input that is just a wrapper around the normal input and that displays an error message in case the formControl isn't valid.
So I made a component implementing ControlValueAccessor but now that I want to display the error message I realize I have no hold of the FormControl, thus I need to pass it as #Input(). The problem is... Why implement the ControlValueAccessor in the first place if I can just pass the FormControl as input ? I'm confused.
html
<input
[placeholder]="placeholder"
[type]="type"
[(ngModel)]="value"
(keyup)="onChange()"
(change)="onChange()"
(blur)="onTouched()"
[disabled]="disabled"/>
ts
#Component({
selector: 'input-app',
templateUrl: './input.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputComponent),
multi: true
}
]
})
export class InputComponent implements ControlValueAccessor {
#Input() type = 'text';
#Input() placeholder = 'placeholder';
#Input() errorMsg = '';
value: string;
disabled: boolean;
private onChangeFn;
private onTouchedFn;
writeValue(val: string | number): void {
this.value = val;
}
registerOnChange(fn: any): void {
this.onChangeFn = fn;
}
registerOnTouched(fn: any): void {
this.onTouchedFn = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
onChange(){
this.onChangeFn(this.value);
}
onTouched(){
this.onTouchedFn();
}
}
The task is simple, it is necessary that the input was entered only numbers below a certain number. I did so:
export class MaxNumber implements PipeTransform{
transform(value, [maxNumber]) {
value = value.replace(/[^\d]+/g,'');
value = value > maxNumber?maxNumber:value;
return value;
}
}
and then in the template called something like:
<input type="text" [ngModel]="obj.count | maxNumber:1000" (ngModelChange)="obj.count=$event" />
But it works very strange click.
I probably misunderstand something. I would be grateful if someone will explain that behavior.
I think that you need rather a custom value accessor. This way you will be able to check the value before setting it in the ngModel. This way you obj.count won't be upper than 1000.
Here is a sample implementation:
const CUSTOM_VALUE_ACCESSOR = new Provider(
NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => MaxNumberAccessor), multi: true});
#Directive({
selector: 'input',
host: {'(input)': 'customOnChange($event.target.value)'},
providers: [ CUSTOM_VALUE_ACCESSOR ]
})
export class MaxNumberAccessor implements ControlValueAccessor {
onChange = (_) => {};
onTouched = () => {};
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
writeValue(value: any): void {
var normalizedValue = (value == null) ? '' : value;
this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', normalizedValue);
}
customOnChange(val) {
var maxNumber = 1000;
val = val.replace(/[^\d]+/g,'');
val = val > maxNumber?maxNumber:val;
this.onChange(val);
}
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}
There is nothing to do in your component to use it than setting this directive into its directives attribute:
#Component({
selector: 'my-app',
template: `
<div>
<h2>Hello {{name}}</h2>
Number: <input type="text" [(ngModel)]="obj.count" />
<p>Actual model value: {{obj.count}}</p>
</div>
`,
directives: [MaxNumberAccessor]
})
export class App {
(...)
}
Corresponding plunkr: https://plnkr.co/edit/7e87xZoEHnnm82OYP84o?p=preview.