Reputation: 12433
I'm trying to use the name
and [(ngModel)]
template driven forms syntax for the first time, on a custom control which uses a controlValueAccessor
which I am also using for the first time.
When I enter some words into my <input>
then log my form.value
to the console, I see the name of the form field that I added but it is still undefined:
Object {keywords: undefined}
If I programmatically set a value for result.keywords, then when I log the form.value to the console, the keywords property is populated. The binding from the model to the form.value is working. The binding from the view (html input control) to the model is not working.
ngOnInit() {
this.result = new Result();
this.result.keywords = ["aaa"]; <----works
}
The above will show ["aaa"] in the console but it will not show anything in the view. How can I correctly get the keywords property of the form to populate?
My code:
My form:
<form class="text-uppercase" (ngSubmit)="onSubmit(findForm.value, findForm.valid)" #findForm="ngForm">
<vepo-input
[placeholder]='"keywords (optional)"'
[id]='"keywordsInput"'
name="keywords"
[(ngModel)]="result.keywords">
</vepo-input>
</form>
input-component.ts:
import { Component, ViewChild, ElementRef, Input, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
const noop = () => {
};
export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputComponent),
multi: true
};
@Component({
selector: 'vepo-input',
templateUrl: 'app/shared/subcomponents/input.component.html',
styleUrls: ['app/shared/subcomponents/input.component.css'],
providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR]
})
export class InputComponent implements ControlValueAccessor {
@Input() private placeholder: string;
@Input() private id: string;
//The internal data model
private innerValue: any = '';
//Placeholders for the callbacks which are later providesd
//by the Control Value Accessor
private onTouchedCallback: () => void = noop;
private onChangeCallback: (_: any) => void = noop;
//get accessor
get value(): any {
return this.innerValue;
};
//set accessor including call the onchange callback
set value(v: any) {
if (v !== this.innerValue) {
this.innerValue = v;
this.onChangeCallback(v);
}
}
//Set touched on blur
onBlur() {
this.onTouchedCallback();
}
//From ControlValueAccessor interface
writeValue(value: any) {
if (value !== this.innerValue) {
this.innerValue = value;
}
}
//From ControlValueAccessor interface
registerOnChange(fn: any) {
this.onChangeCallback = fn;
}
//From ControlValueAccessor interface
registerOnTouched(fn: any) {
this.onTouchedCallback = fn;
}
}
input.component.html:
<input class="form-item-input"
placeholder={{placeholder}}
id={{id}} />
<label attr.for="{{id}}"
class="form-item-right-icon input-icon">
</label>
My form is actually a lot bigger than what I posted but I don't wanna overload everyone with irrelevant code. For the sake of completeness here is the full form.ts file:
import {
Component,
ViewChild,
ElementRef,
EventEmitter,
Output,
OnInit
} from '@angular/core';
import {
FormGroup,
FormBuilder,
Validators,
ControlValueAccessor
} from '@angular/forms';
import { ResultService } from '../../../services/result.service';
import { Result } from '../../../models/all-models';
import { HighlightDirective } from '../../../directives/highlight.directive';
import { DistanceUnitsComponent } from './distance-units.component';
import { MultiselectComponent } from './multiselect-find-category.component';
import { MultiselectFindMealTypeComponent } from
'./multiselect-find-meal-type.component';
import { AreaComponent } from './area-picker.component';
import { NumberPickerComponent } from './number-picker.component';
import { InputComponent } from '../../../shared/subcomponents/input.component';
@Component({
selector: 'find-form',
templateUrl: 'app/find-page/subcomponents/find-page/find-form.component.html',
styleUrls: ['app/find-page/subcomponents/find-page/find-form.component.css'],
providers: [ResultService]
})
export class FindFormComponent implements OnInit {
@ViewChild('multiselectFindCategory')
private multiselectFindCategory: MultiselectComponent;
@ViewChild('multiselectFindMealType')
private multiselectFindMealType: MultiselectFindMealTypeComponent;
@ViewChild('distanceUnits') private distanceUnits: DistanceUnitsComponent;
@ViewChild('numberPicker') private numberPicker: NumberPickerComponent;
@ViewChild('areaInput') private areaInput: AreaComponent;
@ViewChild('keywordsInput') private keywordsInput: InputComponent;
@Output() private onResultsRecieved:
EventEmitter<Object> = new EventEmitter<Object>();
@Output() private onSubmitted: EventEmitter<boolean> =
new EventEmitter<boolean>();
private categoryError: string = 'hidden';
private mealTypeError: string = 'hidden';
private areaError: string = 'hidden';
private findForm: FormGroup;
private submitted: boolean = false;
private result: Result;
private displayMealCategories: boolean = false;
private mealSelected: boolean = false;
private place: google.maps.Place;
constructor(private resultService: ResultService,
private formBuilder: FormBuilder,
el: ElementRef) { }
ngOnInit() {
this.result = new Result();
}
private setCategoryErrorVisibility(
multiselectFindCategory: MultiselectComponent
): void {
if (multiselectFindCategory.selectedCategories.length < 1 &&
!multiselectFindCategory.allSelected &&
this.submitted) {
this.categoryError = 'visible';
} else {
this.categoryError = 'hidden';
}
}
private setMealTypeErrorVisibility(
multiselectFindMealType: MultiselectFindMealTypeComponent
): void {
if (multiselectFindMealType) {
if (multiselectFindMealType.selectedCategories.length < 1 &&
!multiselectFindMealType.allSelected &&
this.submitted) {
this.mealTypeError = 'visible';
} else {
this.mealTypeError = 'hidden';
}
}
}
private setAreaErrorVisibility(): void {
if (this.areaInput.areaInput.nativeElement.value) {
if (!this.areaInput.address) {
this.areaError = 'visible';
this.areaInput.areaInput.nativeElement.setCustomValidity("Please select from dropdown or leave blank.");
} else {
this.areaError = 'hidden';
this.areaInput.areaInput.nativeElement.setCustomValidity("");
}
} else {
this.areaError = 'hidden';
this.areaInput.areaInput.nativeElement.setCustomValidity("");
}
}
private onCategoriesChanged(): void {
this.setCategoryErrorVisibility(this.multiselectFindCategory);
this.mealSelected = this.multiselectFindCategory.mealSelected;
if (!this.mealSelected) {
this.mealTypeError = 'hidden';
}
}
private onMealTypesChanged(): void {
this.setMealTypeErrorVisibility(this.multiselectFindMealType);
}
private onAreaChanged(areaEntered: any): void {
this.setStateOfDistanceControls(areaEntered.areaEntered);
this.areaError = "hidden";
this.areaInput.areaInput.nativeElement.setCustomValidity("");
if (areaEntered.place) {
this.place = areaEntered.place;
}
}
private setStateOfDistanceControls(areaEntered: any): void {
if (areaEntered.areaEntered) {
this.distanceUnits.isEnabled = true;
this.numberPicker.isEnabled = true;
} else {
this.distanceUnits.isEnabled = false;
this.numberPicker.isEnabled = false;
}
this.distanceUnits.setImage();
}
private getResults(): void {
var results: Result[] = [];
results = this.resultService.getResults();
if (results) {
this.onResultsRecieved.emit({
recieved: true,
results: results,
place: this.place
});
}
}
private onSubmit(model: any, isValid: boolean): void {
console.log(model, isValid);
// this.submitted = true;
// this.setCategoryErrorVisibility(this.multiselectFindCategory);
// this.setMealTypeErrorVisibility(this.multiselectFindMealType);
// this.setAreaErrorVisibility();
// if (this.areaError === "hidden" &&
// this.categoryError === "hidden" &&
// this.mealTypeError === "hidden") {
// this.onSubmitted.emit(true);
// this.getResults();
// }
}
}
Upvotes: 0
Views: 3703
Reputation: 8335
Alright, here's my stab at your intent with a working plunker to back up the work:
In your input.component.html you need to make sure you have the bindings set up into ngModel
<input class="form-item-input" [(ngModel)]="value" [placeholder]="placeholder" id="{{id}}" />
Aside from that, there's really nothing else to do.
Here's the plunker: http://plnkr.co/edit/HleTVBnvd8ePgMClAZS2?p=preview
Upvotes: 2