How do I properly get a reference to the host directive in a ControlValueAccessor?

大城市里の小女人 提交于 2019-12-23 03:38:35

问题


How do I properly connect two directives, or a directive to a component (which is a directive too) in angular2 in the "angular way of writing code"?

Since the documentation on angular2 is still quite scarce, any insight or reference is greatly appreciated.


This is what every angular2 example shows - binding to a string via ngModel:

@Component({
    template: 'Hello <input type="text" [(ngModel)]="myVariable">!'
})
class ExampleComponent() {
    myVariable: string = 'World';
}

Suppose I want to use ngModel on a custom component which does not represent strings, but any other value, for example a number or a class or interface:

interface Customer {
    name: string;
    company: string;
    phoneNumbers: string[];
    addresses: Address[];
}
@Component({
    selector: 'customer-editor',
    template: `
        <p>Customer editor for {{customer.name}}</p>
        <div><input [(ngModel)]="customer.name"></div>`
})
class CustomerEditor {
    customer: Customer;
}

Why do I use ngModel, you may ask, when any other attribute would make data binding a lot easier? Well, I am implementing a design shim for angular2, and the components would be used like (or alongside) native <input>s:

<input name="name" [(ngModel)]="user.name">
<pretty-select name="country" [(ngModel)]="user.country" selectBy="countryCode">
    <option value="us">United States of America</option>
    <option value="uk">United Kingdom</option>
    ...
</pretty-select>

user.country would be an object, not a string:

interface Country {
    countryCode: string,
    countryName: string
}

class User {
    name: string;
    country: Country;
    ...
}

What I have got working so far, but "feels wrong":

GitHub repository for this example

To link up the reference supplied to the ngModel directive with my CustomerEditor component, currently I am using my own ControlValueAccessor: (simplified)

const CUSTOMER_VALUE_ACCESSOR: Provider = CONST_EXPR(
    new Provider(NG_VALUE_ACCESSOR, {
        useExisting: forwardRef(() => CustomerValueAccessor)
    })
);

@Directive({
    selector: 'customer-editor[ngModel]',
    providers: [CUSTOMER_VALUE_ACCESSOR]
})
@Injectable()
class CustomerValueAccessor implements ControlValueAccessor {
    private host: CustomerEditor;

    constructor(element: ElementRef, viewManager: AppViewManager) {
        let hostComponent: any = viewManager.getComponent(element);
        if (hostComponent instanceof CustomerEditor) {
            this.host = hostComponent;
        }
    }

    writeValue(value: any): void {
        if (this.host) { this.host.setCustomer(value); }
    }
}

Now, what disturbs me about that ControlValueAccessor is the way I get a reference to my host component:

        if (hostComponent instanceof CustomerEditor) {
            this.host = hostComponent;
        }

This not only requires 3 dependencies where one should suffice (ElementRef, AppViewManager, CustomerEditor), it also feels very wrong to do type-checking during runtime.

How is the "proper" way to get a reference to the host component in angular2?


Other things I have tried, but not have gotten to work:

  • This answer by Thierry Templier notes the component class in the constructor of the ControlValueAccessor for it to be injected by angular:

    class CustomerValueAccessor implements ControlValueAccessor {
        constructor(private host: CustomerEditor) { }
    }
    

    Unfortunately, that does not work for me, and gives me an exception:

    Cannot resolve all parameters for 'CustomerValueAccessor'(undefined). Make sure that all the parameters are decorated with Inject or have valid type annotations and that 'CustomerValueAccessor' is decorated with Injectable.

  • Using @Host:

    class CustomerValueAccessor implements ControlValueAccessor {
        constructor(@Host() private editor: CustomerEditor) { }
    }
    

    Throws the same exception as the solution above.

  • Using @Optional:

    class CustomerValueAccessor implements ControlValueAccessor {
        constructor(@Optional() private editor: CustomerEditor) { }
    }
    

    Does not throw an exception, but CustomerEditor is not injected and stays null.


Since angular changed/changes very frequently, the specific versions I am working with might be relevant, which is angular2@2.0.0-beta.6.


回答1:


The comment by Günter Zöchbauer pointed me into the right direction.

To bind a value on a component with ngModel, the component itself needs to implement the ControlValueAccessor interface and provide a forwardRef to itself in the providers: key of the component configuration:

const CUSTOMER_VALUE_ACCESSOR: Provider = CONST_EXPR(
    new Provider(NG_VALUE_ACCESSOR, {
        useExisting: forwardRef(() => CustomerEditor),
        multi: true
    })
);

@Component({
    selector: 'customer-editor',
    template: `template for our customer editor`,
    providers: [CUSTOMER_VALUE_ACCESSOR]
})
class CustomerEditor implements ControlValueAccessor {
    customer: Customer;
    onChange: Function = () => {};
    onTouched: Function = () => {};

    writeValue(customer: Customer): void {
        this.customer = customer;
    }

    registerOnChange(fn: Function): void {
        this.onChange = fn;
    }

    registerOnTouched(fn: Function): void {
        this.onTouched = fn;
    }
}

Usage from a parent component:

@Component({
    selector: 'customer-list',
    template: `
        <h2>Customers:</h2>
        <p *ngFor="#c of customers">
            <a (click)="editedCustomer = c">Edit {{c.name}}</a>
        </p>
        <hr>
        <customer-editor *ngIf="editedCustomer" [(ngModel)]="editedCustomer">
        </customer-editor>`,
    directives: [CustomerEditor]
})
export class CustomerList {
    private customers: Customer[];
    private editedCustomer: Customer = 0;

    constructor(testData: TestDataProvider) {
         this.customers = testData.getCustomers();
    }
}

Every example for ControlValueAccessor always show how to use it with a separate class or a directive on a host component, never implemented on the host component class itself.




回答2:


In your sample, it seems that your CustomerValueAccessor directive is attached on the CustomerComponent component (the one is the selector customer-editor) and not one of type CustomerEditor. I think that it's the reason why you can't inject it.

What does CustomEditor correspond to?



来源:https://stackoverflow.com/questions/35775583/how-do-i-properly-get-a-reference-to-the-host-directive-in-a-controlvalueaccesso

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!