Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How To Use A Directive In A Custom Input With ControlValueAccessor Angular

I've been create a simple custom input component in my angular app use ControlValueAccessor. So, when I want to create a form input element, I don't have to call <input />, only call <my-input>.

I have a problem, when I use <input />, I can use myDirective. For example:

<input type="text" class="form-control" formControlName="name" myDirective />

But, when I use my-input, then I can't use myDirective. For example:

<my-input formControlName="name" myDirective></my-input>

myDirective dosn't work in my-input

This is my-input component use ControlValueAccessor code:

import { Component, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';

@Component({
  selector: 'my-input',
  templateUrl: './my-input.component.html',
  styleUrls: ['./my-input.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(()  => MyInputComponent ),
      multi: true
    }
  ]
})

export class MyInputComponent implements ControlValueAccessor {

  onChange: () => void;
  onTouched: () => void;

  value: string;

  writeValue(value: string): void {
    this.value = value ? value : '';
  }

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

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

Updated: myDirective code:

import { Directive, HostListener } from '@angular/core';
import { FormControlName } from '@angular/forms';

@Directive({
    selector: '[myDirective]'
})

export class MyDirective{
    constructor(private formControlName: FormControlName) { }

    @HostListener('input', ['$event']) 
    onInputChange() {
        this.formControlName.control.setValue(this.formControlName.value.replace(/[^0-9]/g, ''));
    }
}

Is there a way for myDirective to be used in themy-input component?

Thanks in advance.

like image 364
Titus Sutio Fanpula Avatar asked Feb 19 '20 08:02

Titus Sutio Fanpula


People also ask

Can a directive have input?

If we also specify an input property in our directive's class using the same value as the selector property value we can input data into our directive using the attribute on the host element. This instructs Angular that the appBtnGrow class member property is now an input that can receive a value from the host element.

Can we import directives in Angular?

directive. ts file in src/app folder and add the code snippet below. Here, we are importing Directive and ElementRef from Angular core. The Directive provides the functionality of @Directive decorator in which we provide its property selector to appHighLight so that we can use this selector anywhere in the application.

How can we create a custom directive in Angular?

Creating a custom directive is easy. Just create a new class and decorate it with the @Directive decorator. We need to make sure that the directive is declared in the corresponding (app-) module before we can use it. If you are using the angular-cli this should be done automatically.

How do you use directives in Angular 10?

Use the @Directive decorator in the class. Set the value of the selector property in the @directive decorator function. The directive will be used, using the selector value on the elements. In the constructor of the class, inject ElementRef and the renderer object.


2 Answers

There're a problem with your directive. Inject a NgControl and control this ngControl

export class MyDirective{
    constructor(private control: NgControl) { } //<--inject NgControl

    @HostListener('input', ['$event']) 
    onInputChange() {
        this.control.control.setValue(this.control.value.replace(/[^0-9]/g, ''));
    }
}

You can see in stackblitz

NOTE: Don't forget include in the module declarations

@NgModule({
  imports:      [ BrowserModule, FormsModule,ReactiveFormsModule ],
  declarations: [ AppComponent, MyInputComponent,MyDirective ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }
like image 104
Eliseo Avatar answered Nov 15 '22 04:11

Eliseo


You can't directly but you can do some Hack with directives queries

According to the Angular Directives documentation:

Decorator that marks a class as an Angular directive. You can define your own directives to attach custom behaviour to elements in the DOM.

a decorator is a behaviour attached to an element to the DOM, so it will affect the element wich use the directive.

BUT (and is a big BUT for a reason - check notes), you can hack this behaviour using directive queries.

BUT remember you must define specific directives which will work only with specific queries and cannot be used for any DOM element.

The solution

The solution is based on the following:

I want to define a directive which works on an element children, and not on the element itself.

You can use @ContentChildren and QueryList to check if you have some children with a specific directive.

For example, I have a background directive applied to input parent and a CustomFormControlDirective to query the children:

import {
  Directive,
  ElementRef,
  Input,
  ContentChildren,
  ViewChildren,
  QueryList
} from "@angular/core";

@Directive({
  selector: "[customformcontrol]"
})
export class CustomFormControlDirective {
  constructor(public elementRef: ElementRef) {}
}

@Directive({
  selector: "[backgroundColor]",
  queries: {
    contentChildren: new ContentChildren(CustomFormControlDirective),
    viewChildren: new ViewChildren(CustomFormControlDirective)
  }
})
export class BackgroundColorDirective {
  @Input()
  set backgroundColor(color: string) {
    this.elementRef.nativeElement.style.backgroundColor = color;
  }

  @ContentChildren(CustomFormControlDirective, { descendants: true })
  contentChildren: QueryList<CustomFormControlDirective>;
  viewChildren: QueryList<CustomFormControlDirective>;

  constructor(private elementRef: ElementRef) {}

  ngAfterContentInit() {
    // contentChildren is set
    console.log("%o", this.contentChildren);
  }
}
[...]
<div backgroundColor="red">
  <input customformcontrol />
</div>

Of course this directive will apply the bg color to the parent div:

Div with red bg]

So how can we set this property to the children?

We have the children inside our contentChildren variable:

Console log

So we need to apply the desired background to our children element, we can loop trough the query results and try to apply the style:

  [...]
  _backgroundColor = null;
  @Input()
  set backgroundColor(color: string) {
    /// save the bg color instead of apply style
    this._backgroundColor = color;
  }
  ngAfterContentInit() {
    if (this.contentChildren.length) {
      /// then loop through childrens to apply style
      this.contentChildren.forEach(customFormControl => {
        customFormControl.elementRef.nativeElement.style.backgroundColor = this._backgroundColor;
      });
    }
  }
  [...]

And it will apply the style to childrens: Children with red bg

also if there are more than 1 children: More fancy red bgs

Notes

  • this is an example, don't take this implementation as-is, you might define your own method or use a better one, but try to understand the QueryList selectors and ContentChildren.
  • using a custom directive to fetch childrens might not be necessary, you might use ReactiveForm directives directly (AbstractControlDirective / FormControlDirective) but I don't think they will let you access the DOM as it is private.
  • these directives will work only with children, so you might choose a better naming convention (e.g. ApplyToControlBackgroundDirective)
like image 39
nicola_castellani Avatar answered Nov 15 '22 04:11

nicola_castellani