Reactive Forms - value not updating in view when using with Observable
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() {
}
}
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, 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.
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; } }