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 string
s, 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;
...
}
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
.
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.