I want to define a Form which consists of FormControls and FormGroups.
But i receive the following Error if i add the optionalInputGroup:
TypeError: this.form._updateTreeValidity is not a function
at FormGroupDirective._updateDomValue (http://localhost:8100/build/main.js:30206:19)
at FormGroupDirective.ngOnChanges (http://localhost:8100/build/main.js:30065:18)
at Wrapper_FormGroupDirective.ngDoCheck (/ReactiveFormsModule/FormGroupDirective/wrapper.ngfactory.js:30:18)
at CompiledTemplate.proxyViewClass.View_ConfiguratorOptionalGroupInputComponent0.detectChangesInternal (/AppModule/ConfiguratorOptionalGroupInputComponent/component.ngfactory.js:397:32)
at CompiledTemplate.proxyViewClass.AppView.detectChanges (http://localhost:8100/build/main.js:134498:14)
at CompiledTemplate.proxyViewClass.DebugAppView.detectChanges (http://localhost:8100/build/main.js:134693:44)
at CompiledTemplate.proxyViewClass.AppView.internalDetectChanges (http://localhost:8100/build/main.js:134483:18)
at View_ConfiguratorPage5.detectChangesInternal (/AppModule/ConfiguratorPage/component.ngfactory.js:245:19)
at View_ConfiguratorPage5.AppView.detectChanges (http://localhost:8100/build/main.js:134498:14)
at View_ConfiguratorPage5.DebugAppView.detectChanges (http://localhost:8100/build/main.js:134693:44)
at ViewContainer.detectChangesInNestedViews (http://localhost:8100/build/main.js:134830:37)
at View_ConfiguratorPage1.detectChangesInternal (/AppModule/ConfiguratorPage/component.ngfactory.js:369:14)
at View_ConfiguratorPage1.AppView.detectChanges (http://localhost:8100/build/main.js:134498:14)
at View_ConfiguratorPage1.DebugAppView.detectChanges (http://localhost:8100/build/main.js:134693:44)
at ViewContainer.detectChangesInNestedViews (http://localhost:8100/build/main.js:134830:37)
I create my FormConfig with the following Method:
flattenConfigForForm(inputs: [AbstractConfiguratorControl], returnObject = {}): Object {
inputs.map(input => {
switch (input.type) {
case ConfigurationInputType.Group:
let groupInput: ConfiguratorInputGroup = <ConfiguratorInputGroup> input;
returnObject[groupInput.key] = groupInput;
break;
default:
let basicInput: ConfiguratorInput = <ConfiguratorInput> input;
returnObject[basicInput.key] = basicInput;
returnObject[basicInput.key + ".ion"] = basicInput.value;
}
});
return returnObject;
}
where ConfigurationInputType.Group is an instance of FormGroup with an inputs-Attribute which itself contains FormControls and default is an instance of FormControl.
This is how i build my form in html, where values is the FormGroup which was created by flattenConfigForForm.
<form [formGroup]="values" (ngSubmit)="logForm()">
<div class="requiredInputs">
<div *ngFor="let input of config.inputs">
<configurator-number-input [passedFormGroup]="values" *ngIf="isEqualType(input.type,configInputType.Number)"
formControlName="{{input.key}}"></configurator-number-input>
<configurator-toggle-input [passedFormGroup]="values" *ngIf="isEqualType(input.type,configInputType.Toggle)"
formControlName="{{input.key}}"></configurator-toggle-input>
<configurator-select-input [passedFormGroup]="values" *ngIf="isEqualType(input.type,configInputType.Select)"
formControlName="{{input.key}}"></configurator-select-input>
<configurator-optional-group-input formGroupName="{{input.key}}" [passedFormGroup]="input"
formControlName="{{input.key}}"
*ngIf="isEqualType(input.type,configInputType.Group)"></configurator-optional-group-input>
</div>
</div>
</form>
this is my formGroupInputComponent.html
<div [formGroup]="passedFormGroup">
<ion-input>
<ion-checkbox [(ngModel)]="input.enabled"></ion-checkbox>
</ion-input>
<div *ngFor="let subinput of input.inputs">
<configurator-number-input [passedFormGroup]="input" *ngIf="isEqualType(subinput.type,configInputType.Number)"
formControlName="{{subinput.key}}"></configurator-number-input>
<configurator-toggle-input [passedFormGroup]="input" *ngIf="isEqualType(subinput.type,configInputType.Toggle)"
formControlName="{{subinput.key}}"></configurator-toggle-input>
<configurator-select-input [passedFormGroup]="input" *ngIf="isEqualType(subinput.type,configInputType.Select)"
formControlName="{{subinput.key}}"></configurator-select-input>
</div>
</div>
and this is my formGroupInput.ts
#Component({
selector: 'configurator-optional-group-input',
templateUrl: 'configurator-optional-group-input.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ConfiguratorOptionalGroupInputComponent),
multi: true,
}
]
})
export class ConfiguratorOptionalGroupInputComponent implements ControlValueAccessor {
input: ConfiguratorInputGroup;
#Input() public passedFormGroup: FormGroup;
// the method set in registerOnChange, it is just
// a placeholder for a method that takes one parameter,
// we use it to emit changes back to the form
private propagateChange = (_: any) => {
};
constructor() {
}
//ControlValueAccessor
writeValue(obj: any): void {
if (obj) {
console.log(this.passedFormGroup);
this.input = <ConfiguratorInputGroup> obj;
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
// not used, used for touch input
registerOnTouched(fn: any): void {
}
}
Thanks in advance!
Related
I am trying to write an accessor component, for an input that has a label associated with it, but I am not sure if I am doing this correctly. When I attempt to do two-way-binding, the value isn't set in the input field even though it is set within the class MyComponent.
Why isn't my two-way-binding working? I would expect to see Something Special inside the <input> that is within <third-party-input>. However, the input is empty.
When using <third-party-input> on its own, two-way-binding works there, just not when I wrap my component around it.
Main Component
<ui-input [(ngModel)]="myValue"></ui-input>
export class MyComponent {
public myValue = 'Something Special';
}
Input With Label Component
<label>{{label}}</label>
<third-party-input [(ngModel)]="value"></third-party-input>
#Component({
selector: 'ui-input',
templateUrl: './input.component.html',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputComponent),
multi: true
}]
})
export class InputComponent extends AccessorComponent {}
#Component({
selector: 'app-accessor',
template: ''
})
export abstract class AccessorComponent implements ControlValueAccessor {
protected _value: any;
public get value() {
return this._value;
}
public set value(val) {
this._value = val;
this.onChange(val);
this.onTouched();
}
protected onChange = (_: any) => { };
protected onTouched: any = () => { };
public registerOnChange(fn: (_: any) => void): void {
this.onChange = fn;
}
public registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
public writeValue(value: any): void {
if (value !== undefined) {
this._value = value;
}
}
}
I've a simple angular2 reactive form:
formangular.component.html
<form novalidate (ngSubmit)="onSubmit(formDir.value)" [formGroup]="complexForm" #formDir="ngForm">
<div class="form-group">
<custom-input id="idCausaleRitenuta"
class="form-control"
label="Codice:"
formControlName="idCausaleRitenuta"
[(ngModel)]="idCausaleRitenuta"></custom-input>
<div *ngIf="idCausaleRitenuta.invalid && (idCausaleRitenuta.dirty || idCausaleRitenuta.touched)"
class="alert alert-danger">
........
the relative .ts file:
formangular.component.ts
export class FormAngularComponent {
get idCausaleRitenuta() { return this.complexForm.get('idCausaleRitenuta');}
constructor(private fb: FormBuilder, private ritenuteService: RitenuteService, private sharedService: SharedService) {
this.createForm();}
createForm() {
this.complexForm = this.fb.group({
idCausaleRitenuta: ['', Validators.compose([Validators.required, Validators.maxLength(4)])],
........
...And the custom-input control:
custominput.component.ts
.................
#Component({
selector: 'custom-input',
template: `<div>
<label *ngIf="label" [attr.for]="identifier">{{label}}</label>
<input [id]="identifier"
[type]="type"
[class]="class"
[placeholder]="placeholder"
[(ngModel)]="value"
(blur)="touch()" />
</div>`,
.......
})
export class CustomInputComponent<T> implements ControlValueAccessor {
.........
identifier = `custom-input-${identifier++}`;
private innerValue: T;
private changed = new Array<(value: T) => void>();
private touched = new Array<() => void>();
get value(): T {
return this.innerValue;
}
set value(value: T) {
if (this.innerValue !== value) {
this.innerValue = value;
this.changed.forEach(f => f(value));
}
}
touch() {
this.touched.forEach(f => f());
}
writeValue(value: T) {
this.innerValue = value;
}
registerOnChange(fn: (value: T) => void) {
this.changed.push(fn);
}
registerOnTouched(fn: () => void) {
this.touched.push(fn);
}
}
let identifier = 0;
When I render the form, the value of the input inside the 'custom control' is '[object Object]', can someone tell me why?
When you make this call:
get idCausaleRitenuta() { return this.complexForm.get('idCausaleRitenuta');}
You are actually getting the FormControl object (not the value of the form control).
You can access the value property of that control like this:
get idCausaleRitenuta() { return this.complexForm.get('idCausaleRitenuta').value;}
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; }
}
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.