What is the correct method to obtain a reference to the host directive within a ControlValueAccessor implementation?

Is there a proper way to connect two directives, or a directive to a component (which is a directive as well) in angular2 following the "angular way of writing code"?

Given the limited documentation on angular2, any insights or references on this topic would be greatly appreciated.


Every angular2 example typically demonstrates binding to a string using ngModel:

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

Now, suppose I want to use ngModel on a custom component in angular2, where the custom component does not represent strings but other values like number, classes, or interfaces:

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;
}

But why use ngModel when there are simpler data binding options available? In this case, it's for implementing a design shim for angular2, where components are used similar to native <input> elements:

<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>

In this case, user.country would be an object instead of a string:

interface Country {
    countryCode: string,
    countryName: string
}

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

Current Solution, but with Some Concerns:

GitHub repository for this example

To establish the connection between the reference supplied to the ngModel directive and my CustomerEditor component, I'm currently utilizing my custom 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); }
    }
}

However, my main concern with this ControlValueAccessor is the way I retrieve a reference to the host component:

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

This approach not only involves 3 dependencies when one should suffice (ElementRef, AppViewManager, CustomerEditor), but it also feels incorrect to perform type-checking during runtime.

What is the proper method to obtain a reference to the host component in angular2?


Other Approaches Attempted, but Unsuccessful:

  • This answer by Thierry Templier suggests including the component class in the constructor of the ControlValueAccessor for automatic injection by angular:

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

    Unfortunately, this method did not work for me and resulted in 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) { }
    }
    

    This approach also led to the same exception as the previous one.

  • Attempting @Optional:

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

    While this did not throw an exception, CustomerEditor remained uninitialized and null.


Given the frequent changes in angular, the specific versions being used may be relevant, which is

<span class="__cf_email__" data-cfemail="4e2f20293b222f3c7c0e7c607e607e632c2b3a2f6078">[email protected]</span>
.

Answer №1

Thanks to the valuable input from Günter Zöchbauer, I was able to find the right solution.

In order to bind a value on a component using ngModel, it is necessary for the component itself to implement the ControlValueAccessor interface and provide a forwardRef pointing to itself in the providers: section 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;
    }
}

Here is an example of how this can be used in 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();
    }
}

Most examples of ControlValueAccessor demonstrate using it with a separate class or directive, rather than implementing it directly within the host component class.

Answer №2

Upon review of your example, it appears that your CustomerValueAccessor directive is linked to the CustomerComponent component (with the selector customer-editor), and not a component of type CustomerEditor. This could be the reason why you are encountering issues with injection.

Can you clarify the purpose of CustomEditor in your implementation?

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Is there a function return type that corresponds to the parameter types when the spread operator is used?

Is it possible to specify a return type for the Mixin() function below that would result in an intersection type based on the parameter types? function Mixin(...classRefs: any[]) { return merge(class {}, ...classRefs); } function merge(derived: any, ... ...

Can you explain the distinction between ng test and ng e2e?

Concerned that someone might close my question, I struggled to find a satisfactory answer. Perhaps it's due to my limited knowledge in the Angular 2+ realm or maybe I misunderstood something. From what I gathered after dabbling in Hello World project ...

Tips for getting Atom cucumber step jump package to function properly on a Windows operating system

Just recently, I installed the Atom text editor along with the cucumber-step package available at this link. However, after pressing CTRL+ALT+j, it failed to jump to the step implementation/definition. My operating system is Windows 10 and I am utilizing ...

How to detach functions in JavaScript while preserving their context?

Can functions in JavaScript be detached while still retaining access to their context? For instance, let's say we have an instance of ViewportScroller called vc. We can retrieve the current scroll position with the following method: vc.getScrollPosi ...

The custom validation feature in Angular 4 is failing to function as expected

Currently, my focus is on Angular 4 where I have developed a custom validator for checking CGPA values (to ensure it is between 2.0 and 4.0). Although the predefined `Validators.required` works fine, my custom validator seems to be not triggering as expect ...

Error: The render view is unable to read the 'vote' property of a null object

Within this component, I am receiving a Promise object in the properties. I attempt to store it in state, but upon rendering the view, I encounter the error message "TypeError: Cannot read property 'vote' of null." Seeking a solution to my predic ...

Using React Testing Library with TypeScript revealed issues with ES6 modules causing test failures

I am currently working on a small project that involves React, Typescript, and Mui v5. The application is relatively small and uses the default Create React App setup. Although I am new to unit and integration testing, I am eager to make use of the tools ...

Extract Method Parameter Types in Typescript from a Generic Function

Can we retrieve the type of parameters of methods from a generic interface? For instance, if we have: interface Keys { create: any; ... } type MethodNames<T> = { [P in keyof Keys]: keyof T; } Then, is it feasible to obtain the type of paramete ...

How can RxJS be used to handle only the first value returned when calling multiple URLs?

I am faced with the challenge of having multiple URLs containing crucial information. My goal is to find a specific ID within these URLs, but I do not know which URL holds the necessary details. The approach I'm taking involves calling each URL and us ...

What is the process for defining a literal type in React component parameters?

I introduced a brand new interface called SelectProps! export interface SelectProps { value: string options: string[] onChange: (value: any) => void } Behold, my latest creation - a react component! <Select value="red" options={[ ...

Using typescript with Ramda's filter and prop functions can lead to unexpected errors

I'm new to TypeScript and currently facing the challenge of converting JavaScript functions that use Ramda library into TypeScript functions. The lack of clear TypeScript usage in the Ramda documentation is making this task quite difficult for me. Sp ...

Categorize items based on their defined attributes using Typescript

[origin object array and expect object array ][1] origin object array: 0: amount: 100000000000000000000 feeTier: 0.3 price: 00000 priceDecimal: 0000 status: "unknown" tokenXAddr: "0x*********" tokenXSymbol: "USDC" tokenYAddr: ...

What exactly does "context" refer to in the context of TypeScript and React when using getServerSideProps?

As a beginner in coding, I have a question regarding a specific syntax I recently encountered. I am confused about this particular line of code and couldn't find any helpful explanations online: export async function getServerSideProps(context: GetSer ...

Learn how to implement Angular 8 to listen for changes in an observable within an interceptor

Currently, I am in the process of developing an HTTP interceptor that aims to include an 'access level' parameter in the request. The challenge arises when attempting to extract this value from an observable named currentAccessLevel$. Unfortunate ...

Sacrificing type safety versus retaining type safety

I'm curious to know what sets apart these two approaches when declaring the status property. I understand that the second version maintains type safety, but how exactly does it achieve this? export type OwnProps = { id: number; name: string; sta ...

How can I stop TypeScript from causing my builds to fail in Next.js?

Encountering numerous type errors when executing yarn next build, such as: Type error: Property 'href' does not exist on type '{ name: string; }'. This issue leads to the failure of my build process. Is there a specific command I can ...

What is the best way to first identify and listen for changes in a form

In Angular, there are reactive forms that allow you to track changes in both the complete form and specific fields: this.filterForm.valueChanges.subscribe(() => { }); this.filterForm.controls["name"].valueChanges.subscribe(selectedValue => { }); ...

How can I confirm if a class is an instance of a function-defined class?

I have been attempting to export a class that is defined within a function. In my attempts, I decided to declare the class export in the following way: export declare class GameCameraComponent extends GameObject { isMainCamera: boolean; } export abstra ...

Issue encountered while managing login error messages: http://localhost:3000/auth/login net::ERR_ABORTED 405 (Method Not Allowed)

I am working on the /app/auth/login/route.ts file. import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' import { cookies } from 'next/headers' import { NextResponse } from 'next/server' export async functi ...

Leveraging the power of both IntelliJ and AngularCLI 6 to effortlessly import libraries using their package names

My Angular CLI 6 project consists of two components: A library containing services and components A project that utilizes this library When integrating the library into the frontend project, I typically use: import { SomeLibModule } from "some-lib"; H ...