0
votes

I am trying to create a Material dropdown wrapper, (mat-select dropdown), which will work with formControlName. Can someone post their Stackblitz if they have on their library? Feel free to start from scratch, and create own answer, whatever meets requirements.

Requirements:

1) Needs to work with formControlName. We have parent component form with formBuilder/and its validators which is trying to refer to this child wrapper. The Parent Component formbuilder has many other form fields also, as typical scenarios.

2) Needs to show error red invalid, if data does not meet requirements of parent FormBuilder validators.

3) a) Needs to work not only with formControlName/patchValue (patchValue should work with whole class); b) optionally also if someone places data into @Input() SelectedValueId Id number. can work with the two

Attempting to get this working, but not successful yet, Does anyone have any code to fix this?

Need working stackblitz, for successful answer,

In this case, Id is sourceOfAddressId

export class SourceOfAddressDto implements ISourceOfAddressDto {
    sourceOfAddressId: number | undefined;  // should work with this Id
    sourceOfAddressCode: string | undefined;
    sourceOfAddressDescription: string | undefined;

Typescript:

@Component({
    selector: 'app-address-source-dropdown',
    templateUrl: './address-source-dropdown.component.html',
    styleUrls: ['./address-source-dropdown.component.scss'],
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => AddressSourceDropdownComponent),
            multi: true
        }
    ]
})
export class AddressSourceDropdownComponent implements OnInit, OnChanges {

    dataList: any[] = []; 
    @Input() Label = 'Address Source';
    @Input() sourceOfAddressDefaultItem: SourceOfAddressDto = SourceOfAddressDefault;
    @Input() selectedSourceOfAddress: any;
    @Input() TxtValue = 'sourceOfAddressId';
    @Input() TxtField = 'sourceOfAddressDescription';
    @Input() Disabled: boolean;
    @Input() valuesToExclude: number[] = [];
    @Input() Hint = '';
    @Input() styles: string;
    @Input() defaultSourceOfAddressCode: any;
    @Output() addressSourceChange = new EventEmitter<any>();

    private _selectedValueId: number;

    @Input() set selectedValueId(value: number) {
        this._selectedValueId = value;

        let outputData: any;
        if (this.selectedValueId == this.sourceOfAddressDefaultItem[this.TxtValue]) {
            outputData = null;
        } else {
            outputData = this.dataList.find(x => x[this.TxtValue] == this.selectedValueId);
        }

        this.onChange(outputData);
    }
    get selectedValueId(): any {
        return this._selectedValueId;
    }
    @Input() errors: any = null;
    disabled: boolean;
    control: FormControl;
    writeValue(value: any) {
        this.selectedValueId = value ? value : '';
    }
    onChange = (_: any) => { };
    onTouched: any = () => { };
    registerOnChange(fn: any) { this.onChange = fn; }
    registerOnTouched(fn: any) { this.onTouched = fn; }
    setDisabledState(isDisabled) { this.disabled = isDisabled; }

    constructor(
        public injector: Injector,
        private AddressService: AddressServiceProxy,
    ) { }

    ngOnInit() {
        this.loadDataList();
    }

    ngOnChanges() { }

    loadDataList() {
        this.AddressService.getSourceOfAddressAll().subscribe(res => {
            this.dataList = res.body.filter(q => q.sourceOfAddressId !== -1);
        });
    }

}

HTML:

<div class="dropdown-cont">
  <mat-form-field appearance="outline">
    <mat-label>{{Label}}</mat-label>
    <mat-select 
      disableOptionCentering 
      [disabled]="Disabled" 
      [ngStyle]="styles" 

      (ngModelChange)="selectedValueId=$event"
        required>
      <mat-option [value]="sourceOfAddressDefaultItem[TxtValue]">{{sourceOfAddressDefaultItem[TxtField]}}</mat-option>
      <mat-option *ngFor="let item of dataList" [value]="item[TxtValue]">
        {{item[TxtField]}}
      </mat-option>
    </mat-select>
    <mat-hint>{{Hint}}</mat-hint>
  </mat-form-field>
</div>
  • also hopefully works with default values, even if API is lagging sometimes, and default value is inserted first in @Input SelectedValueId
4
Which of your requirements are not working? Do you get any errors?Kari F.
using ngx-sub-form might simplify your life a lot :)maxime1992
you should provide a working stackblitz or some exception which you are facing,Saif
cannot get my stackblitz working online, feel free to start from scratch and create your own new answeruser12425844
Is this helpful for you: stackoverflow.com/questions/58236023/…? It includes a stackblitzjgerstle

4 Answers

2
votes

Others have mentioned that you need to explicitly implement the ControlValueAccessor interface. While this is definitely a good practice, it's not required in TypeScript because anything that satisfies an interface also implicitly implements it, which you're doing with your writeValue, registerOnChange, and registerOnTouched methods (as well as setDisabledState but that one is optional).

So the biggest issue then is the specifics of your implementation. Angular relies on this interface's implementation to do the magical 2-way binding of formControlName (and formControl for that matter), where a parent can both listen and set values on a child component.

Your writeValue and registerOnTouched are fine. Your registerOnChange isn't . This is where you take changes local to your component and "register" them, meaning you hook the usual Angular valueChanges event function into your own custom valueChanges.

The typical way to implement that, using a form control:

control = new FormControl('');

registerOnChange(fn: (value: string) => void) {
    this.control.valueChanges
        .subscribe(fn);
}

So, once you do something similar, you'll get changes going up and down from the parent component.

Now, to satisfy all of your requests, you'll need some more custom code. I happened to have implemented something very similar awhile back, and it works great, and I've recreated it in a stackblitz.

Hopefully it's enough to get you going.

1
votes

You should probably also implement the ControlValueAccessor interface to let angular know you want to use reactive forms

export class AddressSourceDropdownComponent implements OnInit, OnChanges, ControlValueAccessor { ...
0
votes

For the control to work in a mat-form-field there is going to an extra step of implementing the MatFormFieldControl interface. The official material documentation has a good walkthrough and code sample of how to do this here: Creating a custom form field control. Note the sample doesn't implement ControlValueAccessor, which you will still need to do as well.

0
votes

I did this one recently for implementing an angular control for ckeditor. You'll need this code for the ControlValueAccessor this will work with FormControl, FormControlName, as well as ngModel and (if you still had old angularJs ng-model). Not much too it, basically when the value is set call your function I put in the DoAnyCodeYouNeedToDoWhenTheValueChanges placeholder for you. I was setting the value to ckeditor when ever that got called. Then when ckeditor would update I would update this._Value and call the this.onChangeCallback so everything else knew about the change.

//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 {
    //console.warn("Get Value", this._Value);
    return this._Value;
};

//set accessor including call the onchange callback
set value(v: any) {
    //console.warn("Set Value", v);
    if (!this.destoryInitiated) { // was running into an issue with executing after it was destroyed
        if (v !== this._Value) {
            this._Value = v || "";
            this.DoAnyCodeYouNeedToDoWhenTheValueChanges(this._Value)
            this.onChangeCallback(v); // make sure other stuff knows about the change
        }
    }
}

//Set touched on blur
onBlur() {
    //console.warn("onBlur");
    this.onTouchedCallback();
}

//From ControlValueAccessor interface
writeValue(value: any) {
    //console.warn("Write Value", value);
    if (!this.destoryInitiated) {
        this._Value = value || "";
        this.DoAnyCodeYouNeedToDoWhenTheValueChanges(this._Value)
    }
}

//From ControlValueAccessor interface
registerOnChange(fn: any) {
    //console.warn("register on change", fn);
    this.onChangeCallback = fn;
}

//From ControlValueAccessor interface
registerOnTouched(fn: any) {
    //console.warn("register on touched", fn);
    this.onTouchedCallback = fn;
}