BeniaminoBaggins
BeniaminoBaggins

Reputation: 12433

Angular 2 form.value property is undefined

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

Answers (1)

silentsod
silentsod

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

Related Questions