Search code examples
angularcomponentsngmodel

Angular2 transform model for custom select Component


In my angular2 app i'd like to have a reusable select component which, in a first draft, looks like this:

import {Component, Input, Output, EventEmitter} from "@angular/core";
@Component({
    selector: 'my-select',
    template: `
        <select [(ngModel)]="selectedValue" (ngModelChange)="selectionChanged()">
            <option disabled="disabled" selected="selected" name="choose" value="choose">choose ...</option>
            <option *ngFor="let opt of selectModel" [ngValue]="opt">
                {{opt}}
            </option>
        </select>
    `
})
export class SelectComponent {

    @Output()
    changed: EventEmitter<any> = new EventEmitter();

    @Input()
    selectModel: any[] = [];

    selectedValue: any = 'choose';

    selectionChanged() {
        this.changed.emit(this.selectedValue);
    }
}

Unfortunately, this works only with an Array of strings as Input Parameter, since

{{ opt }}

would only print out [Object object] for other types. And thus, the EventEmitter will only emit strings.
Now, what I'd like to have is a component, that I can use similar like this:

import {Component} from "@angular/core";

export class Foo {
    bar: string;
    id: number;
    userFriendlyString: string = `id=${this.id}|bar=${this.bar}`;

    constructor(bar: string, id: number) {
        this.bar = bar;
        this.id = id;
    }
}

@Component({
    template: `<my-select [selectModel]="model" (changed)="handle($event)"></my-select>`
})
export class AppComponent {
    model: Foo[] = [new Foo('first', 1), new Foo('second', 2)];
    handle(foo: Foo): void {/* ... */}
}

My intentions:

  1. tell the my-select component, that the shown values should be the userFriendlyString property of Foo. I don't want to hardcode that, since other components should be able to use my-select as well with other model classes. I can't imagine how to do that. My first Approach was to have a callback function as @Input() for the my-select component, but that doesn't work and shouldn't be done according to this answer. The second Approach was to override toString in Foo. Doesn't work neither (I assume something like missing dynamic dispatch in any...?!).
  2. get the EventEmitter work as 'expected': it should be possible to have a correct foo: Foo in the handle function.

So, is there hope for me? :)


Solution

  • I think what I would do is create a common interface that components could map their objects to such as:

    export interface SelectListObject{
        key: string;
        value: any;
    }
    

    Then use this as the input type on your select list component:

    @Component({
        selector: 'my-select',
        template: `
            <select [(ngModel)]="selectedValue" (ngModelChange)="selectionChanged()">
                <option disabled="disabled" selected="selected" name="choose" value="choose">choose ...</option>
                <option *ngFor="let opt of selectModel" [ngValue]="opt.value">
                    {{opt.key}}
                </option>
            </select>
        `
    })
    export class SelectComponent {
    
        @Output()
        changed: EventEmitter<any> = new EventEmitter();
    
        @Input()
        selectModel: SelectListObject[] = [];
    
        selectedValue: any = 'choose';
    
        selectionChanged() {
            this.changed.emit(this.selectedValue);
        }
    }
    

    This way there is a common interface all your components can use and using ngValue in the option element lets you emit entire objects as values.

    You know your other data models best, but you might be able to put the map function in your @Input of the select component so each component doesn't have to transform it's data before sending it into the SelectComponent:

    internalSelectModel: SelectListObject[] = [];
    @Input('selectModel')
    set selectModel(value){
        this.internalSelectModel = value.map(x => <SelectListObject>{key: value.name, value: value};
    }
    

    The handle function will be unique to each component that uses the SelectComponent and thus the value can be whatever type you need(sent in).

    Hope that helps.