3

I have a massive headache trying to figure this out. I am wanting to create a custom input element I can use in my Angular/Ionic application. How can I use Angular attributes with these custom elements, like FormControlName for example.

<my-custom-stencil-input type="text" formControlName="firstName"></my-custom-stencil-input>

I have the following code below, but it is showing errors when I attempt to use FormControlName.

import { Component, h, Prop } from '@stencil/core';

@Component({
  tag: 'ba-text-input-one',
  styleUrl: './text-input-1.component.css',
  scoped: true,
})
export class TextInputOne {
  @Prop({reflect: true}) type: string;
  @Prop({reflect: true}) placeholder: string;
  @Prop({reflect: true}) formControlName: string;


  render() {
    return (
      <div id="cmp">
        <div id="icon-area">
          <slot name="icon"></slot>
        </div>
        <input id="input-field" formControlName={this.formControlName} type={this.type} />
      </div>
    );
  }
}

The documentation on Stencil is not very thorough on how to approach this.

Essentially, I am wanting to use my own custom Stencil input element in an Angular reactive form.

Hayden
  • 779
  • 10
  • 18
  • In your example you're actually adding `formControlName` to an `input` element, not `my-custom-stencil-input`. Also, did you generate the [Angular bindings](https://stenciljs.com/docs/angular#bindings)? – Thomas Sep 16 '20 at 10:55
  • How do I go about generating Angular bindings? The ValueAccessorConfigs were not explained very well. – Hayden Sep 16 '20 at 11:51

1 Answers1

4

I had the same problem, but I found a solution:

First, you don't have to create a formControlName in stencil project.

import { Component, Event, EventEmitter, h, Prop } from "@stencil/core";

@Component({
    tag: 'my-custom-stencil-input',
    styleUrl: 'input.css',
    shadow: true
})

export class Input {
    @Prop({reflect: true, mutable: true}) label: string;
    @Prop({reflect: true, mutable: true}) value: string;
    @Prop() type = 'text'; 
    @Event() valueChanged: EventEmitter<string>;
    private onInputChangeValue(event: Event) {
        this.value = (event.target as HTMLInputElement).value;
        this.valueChanged.emit(this.value);
    }
    render() {
        return (
            <div>
                <label> {this.label} </label>
                <input type= {this.type} value= {this.value} onInput= {this.onInputChangeValue.bind(this)}/>
            </div>
        )
    }
}

Then, in the Angular project you have to create a directive because you need to implement the ControlValueAccessor that acts as a bridge between the Angular forms API and a native element in the DOM.

import { Directive, forwardRef, HostBinding, HostListener } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Directive({
  selector: '[appAccessor]',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => FormDirective),
      multi: true,
    },
  ],
})

export class FormDirective implements ControlValueAccessor {
  @HostBinding('value') hostValue: any;

  lastValue: any;
  private onChange = (value: any) => {};
  private onTouched = () => {};

  writeValue(value: any) {
    this.hostValue = this.lastValue = value == null ? '' : value;
  }

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

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


  @HostListener('valueChanged', ['$event.detail'])
  _handleInputEvent(value: any) {
    if (JSON.stringify(value) !== JSON.stringify(this.lastValue)) {
      this.lastValue = value;
      this.onChange(value);
      this.onTouched();
    }
  }
}

Finally, use the custom element (in this case a input) in reactive forms:

<form [formGroup]="myFormGroup">
      <my-custom-stencil-input label="Any label" appAccessor formControlName="control1" ></my-custom-stencil-input>
</form>

And that's it.

Roddy of the Frozen Peas
  • 14,380
  • 9
  • 49
  • 99
Alexander
  • 91
  • 5