ng2-select2 icon indicating copy to clipboard operation
ng2-select2 copied to clipboard

Reactive Forms - value not updating in view when using with Observable

Open santoshkt opened this issue 8 years ago • 3 comments

I modified the select2 component to use with Reactive Forms by adding a ControlValueAccessor. When I try to patch the select value using an Observable, the view is not updated. However, when I return a mock observable, view is updated. Not sure if I need to add any propagation or handle anything in the plug. Will appreciate inputs.

import {
    AfterViewInit,
    ChangeDetectionStrategy,
    Component,
    ElementRef,
    EventEmitter,
    Input,
    OnChanges,
    DoCheck,
    OnDestroy,
    Output,
    SimpleChanges,
    ViewChild,
    ViewEncapsulation,
    Renderer,
    OnInit,
    forwardRef
} from "@angular/core";
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from "@angular/forms";
import {Select2OptionData} from "./select2.interface";

@Component({
    selector: 'wp-select2',
    template: `
        <select #selector>
            <ng-content select="option, optgroup">
            </ng-content>
        </select>`,
    encapsulation: ViewEncapsulation.None,
    changeDetection: ChangeDetectionStrategy.OnPush,
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => Select2Component),
            multi: true
        }
    ]
})
export class Select2Component implements AfterViewInit, OnChanges, OnDestroy, OnInit, DoCheck, ControlValueAccessor {
    @ViewChild('selector') selector: ElementRef;

    // data for select2 drop down
    @Input() data: Array<Select2OptionData>;

    // value for select2
    @Input() value: string | string[];

    // enable / disable default style for select2
    @Input() cssImport: boolean = false;

    // width of select2 input
    @Input() width: string;

    // enable / disable select2
    @Input() disabled: boolean = false;

    // all additional options
    @Input() options: Select2Options;

    // emitter when value is changed
    @Output() valueChanged = new EventEmitter();

    private element: JQuery = undefined;
    private check: boolean = false;

    constructor(private renderer: Renderer) {
    }

    ngDoCheck() {
        if (!this.element) {
            return;
        }
    }

    ngOnInit() {
        if (this.cssImport) {
            const head = document.getElementsByTagName('head')[0];
            const link: any = head.children[head.children.length - 1];

            if (!link.version) {
                const newLink = this.renderer.createElement(head, 'style');
                this.renderer.setElementProperty(newLink, 'type', 'text/css');
                this.renderer.setElementProperty(newLink, 'version', 'select2');
                this.renderer.setElementProperty(newLink, 'innerHTML', this.style);
            }
        }
    }

    ngOnChanges(changes: SimpleChanges) {
        if (!this.element) {
            return;
        }

        if (changes['data'] && JSON.stringify(changes['data'].previousValue) !== JSON.stringify(changes['data'].currentValue)) {
            this.initPlugin();

            const newValue: string = this.element.val();
            this.valueChanged.emit({
                value: newValue,
                data: this.element.select2('data')
            });
            this.propagateChange(newValue);
        }

        if (changes['value'] && changes['value'].previousValue !== changes['value'].currentValue) {

            console.log('value changed.. ')

            const newValue: string = changes['value'].currentValue;

            this.setElementValue(newValue);
            this.valueChanged.emit({
                value: newValue,
                data: this.element.select2('data')
            });
            this.propagateChange(newValue);
        }

        if (changes['disabled'] && changes['disabled'].previousValue !== changes['disabled'].currentValue) {
            this.renderer.setElementProperty(this.selector.nativeElement, 'disabled', this.disabled);
        }
    }

    ngAfterViewInit() {
        this.element = jQuery(this.selector.nativeElement);
        this.initPlugin();

        if (typeof this.value !== 'undefined') {
            this.setElementValue(this.value);
        }

        this.element.on('select2:select select2:unselect', () => {
            const newValue: string = this.element.val();
            this.valueChanged.emit({
                value: newValue,
                data: this.element.select2('data')
            });
            this.propagateChange(newValue);
        });
    }

    ngOnDestroy() {
        this.element.off("select2:select");
    }

    private initPlugin() {
        if (!this.element.select2) {
            if (!this.check) {
                this.check = true;
                console.log("Please add Select2 library (js file) to the project. You can download it from https://github.com/select2/select2/tree/master/dist/js.");
            }

            return;
        }

        // If select2 already initialized remove him and remove all tags inside
        if (this.element.hasClass('select2-hidden-accessible') == true) {
            this.element.select2('destroy');
            this.renderer.setElementProperty(this.selector.nativeElement, 'innerHTML', '');
        }

        let options: Select2Options = {
            data: this.data,
            width: (this.width) ? this.width : 'resolve'
        };

        Object.assign(options, this.options);

        if (options.matcher) {
            jQuery.fn.select2.amd.require(['select2/compat/matcher'], (oldMatcher: any) => {
                options.matcher = oldMatcher(options.matcher);
                this.element.select2(options);

                if (typeof this.value !== 'undefined') {
                    this.setElementValue(this.value);
                }
            });
        } else {
            this.element.select2(options);
        }

        if (this.disabled) {
            this.renderer.setElementProperty(this.selector.nativeElement, 'disabled', this.disabled);
        }
    }

    private setElementValue(newValue: string | string[]) {

        if (Array.isArray(newValue)) {
            for (let option of this.selector.nativeElement.options) {
                if (newValue.indexOf(option.value) > -1) {
                    this.renderer.setElementProperty(option, 'selected', 'true');
                }
            }
        } else {
            this.renderer.setElementProperty(this.selector.nativeElement, 'value', newValue);
        }

        this.element.trigger('change.select2');
    }

    private style: string = `CSS`;

    writeValue(value: any) {
        if (value !== undefined) {
            this.value = value;
        }
    }

    propagateChange = (_: any) => {
    };

    registerOnChange(fn) {
        this.propagateChange = fn;
    }

    registerOnTouched() {
    }
}

santoshkt avatar Apr 06 '17 00:04 santoshkt

hi santoshkt, this bellow code is working for me fine. it may solve your issue. & it also contain option for placeholder & allowClear.

import {
    AfterViewInit,
    ChangeDetectionStrategy,
    Component,
    ElementRef,
    EventEmitter,
    Input,
    OnChanges,
    DoCheck,
    OnDestroy,
    Output,
    SimpleChanges,
    ViewChild,
    ViewEncapsulation,
    Renderer,
    OnInit,
    forwardRef,
    NgZone,
    Self
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
import { Select2OptionData } from './Guru-select.interface';

import { Select2Options } from './Select2';
declare var jQuery: any;
@Component({
    selector: 'select2',
    template: `
        <select #selector>
            <ng-content select="option, optgroup">
            </ng-content>
        </select>`,
    encapsulation: ViewEncapsulation.None,
    changeDetection: ChangeDetectionStrategy.OnPush,
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => Select2Component),
            multi: true
        }
    ]
})
export class Select2Component implements AfterViewInit, OnChanges, OnDestroy, OnInit, DoCheck, ControlValueAccessor {
    @ViewChild('selector') selector: ElementRef;

    // data for select2 drop down
    @Input() data: Array<Select2OptionData>;

    // value for placeholder
    @Input() placeholder = '';
    // value for allowClear
    @Input() allowClear = false;

    // value for select2
    @Input() value: string | string[];

    // enable / disable default style for select2
    @Input() cssImport = false;

    // width of select2 input
    @Input() width: string;

    // enable / disable select2
    @Input() disabled = false;

    // all additional options
    @Input() options: Select2Options;

    // emitter when value is changed
    @Output() valueChanged = new EventEmitter();

    private element: any = undefined;
    private check = false;
    private style = `CSS`;

    constructor(private renderer: Renderer, public zone: NgZone, public _element: ElementRef) {
    }

    ngDoCheck() {
        if (!this.element) {
            return;
        }
    }

    ngOnInit() {
        if (this.cssImport) {
            const head = document.getElementsByTagName('head')[0];
            const link: any = head.children[head.children.length - 1];

            if (!link.version) {
                const newLink = this.renderer.createElement(head, 'style');
                this.renderer.setElementProperty(newLink, 'type', 'text/css');
                this.renderer.setElementProperty(newLink, 'version', 'select2');
                this.renderer.setElementProperty(newLink, 'innerHTML', this.style);
            }
        }
    }

    ngOnChanges(changes: SimpleChanges) {
        if (!this.element) {
            return;
        }
        if (changes['data'] && JSON.stringify(changes['data'].previousValue) !== JSON.stringify(changes['data'].currentValue)) {
            this.initPlugin();

            const newValue: string = this.element.val();
            this.valueChanged.emit({
                value: newValue,
                data: this.element.select2('data')
            });
            this.propagateChange(newValue);
        }
        if (changes['value'] && changes['value'].previousValue !== changes['value'].currentValue) {

            console.log('value changed.. ');

            const newValue: string = changes['value'].currentValue;

            this.setElementValue(newValue);
            this.valueChanged.emit({
                value: newValue,
                data: this.element.select2('data')
            });
            this.propagateChange(newValue);
        }
        if (changes['disabled'] && changes['disabled'].previousValue !== changes['disabled'].currentValue) {
            this.renderer.setElementProperty(this.selector.nativeElement, 'disabled', this.disabled);
        }
        if (changes['placeholder'] && changes['placeholder'].previousValue !== changes['placeholder'].currentValue) {
            this.renderer.setElementAttribute(this.selector.nativeElement, 'data-placeholder', this.placeholder);
        }
        if (changes['allowClear'] && changes['allowClear'].previousValue !== changes['allowClear'].currentValue) {
            this.renderer.setElementAttribute(this.selector.nativeElement, 'data-allow-clear', this.allowClear.toString());
        }
    }

    ngAfterViewInit() {
        this.element = jQuery(this.selector.nativeElement);
        this.renderer.setElementAttribute(this.selector.nativeElement, 'data-placeholder', this.placeholder);
        this.renderer.setElementAttribute(this.selector.nativeElement, 'data-allow-clear', this.allowClear.toString());
        // console.log(this.selector.nativeElement);

        this.initPlugin();

        if (typeof this.value !== 'undefined') {
            this.setElementValue(this.value);
        }

        this.element.on('select2:select select2:unselect', (e: any) => {
            const newValue: string = (e.type === 'select2:unselect') ? '' : this.element.val();

            console.log(newValue);
            this.valueChanged.emit({
                value: newValue,
                data: this.element.select2('data')
            });
            this.propagateChange(newValue);
            this.setElementValue(newValue);
        });
    }

    ngOnDestroy() {
        this.element.off('select2:select');
    }

    private initPlugin() {
        if (!this.element.select2) {
            if (!this.check) {
                this.check = true;
                console.log('Please add Select2 library (js file) to the project. You can download it from https://github.com/select2/select2/tree/master/dist/js.');
            }

            return;
        }

        // If select2 already initialized remove him and remove all tags inside
        if (this.element.hasClass('select2-hidden-accessible') === true) {
            this.element.select2('destroy');
            this.renderer.setElementProperty(this.selector.nativeElement, 'innerHTML', '');
        }

        const options: Select2Options = {
            data: this.data,
            width: (this.width) ? this.width : 'resolve'
        };
        // this.options.placeholder = '::SELECT::';
        Object.assign(options, this.options);
        if (options.matcher) {
            jQuery.fn.select2.amd.require(['select2/compat/matcher'], (oldMatcher: any) => {
                options.matcher = oldMatcher(options.matcher);
                this.element.select2(options);

                if (typeof this.value !== 'undefined') {
                    this.setElementValue(this.value);
                }
            });
        } else {
            console.log(options);
            this.element.select2(options);
        }
        if (this.disabled) {
            console.log(this.renderer);
            this.renderer.setElementProperty(this.selector.nativeElement, 'disabled', this.disabled);
        }
    }

    private setElementValue(newValue: string | string[]) {
        this.zone.run(() => {
            if (Array.isArray(newValue)) {
                for (const option of this.selector.nativeElement.options) {
                    if (newValue.indexOf(option.value) > -1) {
                        this.renderer.setElementProperty(option, 'selected', 'true');
                    }
                }
            } else {
                this.renderer.setElementProperty(this.selector.nativeElement, 'value', newValue);
            }
            this.element.trigger('change.select2');
        });
    }


    writeValue(value: any) {
        if (value !== undefined) {
            this.value = value;
            this.setElementValue(value);
        }
    }

    propagateChange = (value: any) => { };

    registerOnChange(fn: any) {
        this.propagateChange = fn;
        // this.valueChanged.subscribe(fn);
    }

    registerOnTouched() {
    }
}

vscodeguru avatar May 06 '17 08:05 vscodeguru

@vscodeguru, how exactly do you use it in your app? I tried like this:

<div [formGroup]="myForm">
<select2 [data]="data" [options]="options" formControlName="instruments"></selec2>
</div>

Where:

data = [
{
    id: 1,
    text: 'One'
},
{
    id: 2,
    text: 'Two
}]
options = {
    multiple = true
}

And my form:

ngOnInit() {
    this.myForm = this.formBuilder.group({
        instruments: this.formBuilder.array([])
    }
}

I'm trying to display form's value like that: <p>myForm.value: {{myForm.value | json}}</p> And when I select options in my select2 input, the form is empty all the time, nothing changes. So is your example not compatible with select multiple or am I using your code in some wrong way? One thing to mention - there are No errors in the console or wherever.

Arcanst avatar Sep 09 '17 13:09 Arcanst

I am try to use the Dropdown using reactive form i am unable to get the data can you please assist me on this

4. Demo with a value changing

Stat value {{startValue | json}}

Form value: {{ form.value | json }}

Form status: {{ form.status | json }}



Selected value in select 2: {{selected}}


import { Component, OnInit } from '@angular/core'; import { DataService } from '../../../services/data.service'; import { Select2OptionData } from 'ng2-select2'; import { FormGroup,FormBuilder ,FormControl} from '@angular/forms';

@Component({ selector: 'app-change', templateUrl: './change.component.html', styleUrls: ['./change.component.css'] }) export class ChangeComponent implements OnInit { public exampleData: Array<Select2OptionData>; public startValue: string; public selected: string;

form : FormGroup;

constructor(private service: DataService,private jformBuilder: FormBuilder) { this.form = this.jformBuilder.group({ userStatus : [''] }); }

ngOnInit() { this.exampleData = this.service.getChangeList(); this.startValue = 'car3'; this.selected = ''; }

public changeValue() { this.startValue = 'car2'; }

public changeData() { this.exampleData = this.service.getChangeListAlternative(); }

public changed(e: any): void { this.selected = e.value; } }

dineshguntha avatar Oct 29 '17 09:10 dineshguntha