0

I'm trying to make a custom counter input component, and I'm struggling to have the input value controlled by the custom increment/decrement buttons.

This is what I'm trying to make:

CounterInputComponent

I want to use content projection and have the input exposed in order to use it in forms and add properties as I would on a regular number input. So a directive on an input seems like the way to go:

<my-counter-input>
  <input [(ngModel)]="count" myInputRef />
</my-counter-input>

The component itself would have two buttons and a ng-content-slot:

<button (click)="decrement()">-</button>
<ng-content select="input"></ng-content>
<button (click)="increment()">+</button>

So far everything seems alright. Now I can interact with the directive by using the @ContentChild decorator.

@Component({
  selector: 'my-counter-input',
  templateUrl: './counter-input.component.html',
  styleUrls: ['./counter-input.component.scss'],
})
export class CounterInputComponent {
  @ContentChild(InputRefDirective)
  public input: InputRefDirective;

  constructor() {}

  public increment() {
    this.input.increment();
  }
  public decrement() {
    this.input.decrement();
  }
}

I guess this is where the problem arises. I'm binding the value of the input element to the directive, but neither increment/decrement method has any effect on the native input value. I'd like to have a two-way binding on the native input value, but it seems like this is not the case.

@Directive({
  selector: 'input [myInputRef]',
})
export class InputRefDirective {
  @HostBinding('type') readonly type = 'number';

  @HostBinding('value') value = 5;

  public increment() {
    this.value++;
  }
  public decrement() {
    this.value--;
  }
}

I'm not sure how I should go about this. How can I update the value of the native input and also trigger the native change event?

3 Answers 3

3

Use NgControl

A base class that all FormControl-based directives extend. It binds a FormControl object to a DOM element.

We can Inject the NgControl in directive and the Angular DI framework will provide us the closest form control directive. Then we can set the value to formControl dynamically.

import { Directive, HostBinding, AfterViewInit } from "@angular/core";
import { NgControl } from "@angular/forms";

@Directive({
  selector: "[appInput]"
})
export class InputDirective implements AfterViewInit {
  @HostBinding("type") readonly type = "number";

   value = 5;
 

  ngAfterViewInit(){
     setTimeout(()=>{this.control.control.setValue(this.value)})
  }

  constructor(private control: NgControl) {
    
  }

  public increment() {
    this.control.control.setValue(this.value++);
  }
  public decrement() {
    this.control.control.setValue(this.value--);
  }
}

Forked Working Example

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks! I ended up combining this with another answer and have an optional NgControl and optional NgModel dependency.
1

You can tell to the ngModel to update on initialization and on each change

public increment() {
  this.value++;
  this.updateNgModel(this.value);
}

public decrement() {
  this.value--;
  this.updateNgModel(this.value);
}

private updateNgModel(value: number) {
  this.ngModel.update.emit(value);
}

https://stackblitz.com/edit/angular-ivy-mw2ltg?file=src/app/input-ref.directive.ts

1 Comment

Thanks! I ended up combining this with another answer and have an optional NgControl and optional NgModel dependency.
0

Unless you have to use content projection and let the parent provide the input control/template, you can just create your reusable component as a standalone component that handles everything. It's a little simpler that way. Here is a forked example.

But the other two examples already given do fix the update issue you were facing.

4 Comments

Thanks! I'm considering this, but there are some issues, I find, in hiding the input within the component.
I'd love to understand more about these issues. We have been using this pattern for shareable components pretty well. You can also use the ControlValueAccessor pattern to make the input work well with reactive forms (instead of doing the output event as shown). That strategy might help?
Here is another forked version that for simplicity accepts a FormControl as an input (instead of doing controlValueAccessor) and is easy to reuse in forms - stackblitz.com/edit/… . This way, you can use reactive forms and encapsulate the control behavior in reusable components. Maybe this is helpful :)
I'm mostly referring to the issues raised in this article

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.