'How to use control-value-accessor in storyBook?

I have a custom input component and want to create story (using storybook)

input.component.ts

@Component({
  selector: 'app-input',
  templateUrl: './input.component.html',
  styleUrls: ['./input.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => InputComponent),
      multi: true,
    },
  ],
})
export class InputComponent implements ControlValueAccessor
{
    @Input() config: any;
    .... 
}

Input.story.ts

import { CommonModule } from '@angular/common';
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Story, Meta } from '@storybook/angular/types-6-0';
import { InputComponent } from 'projects/sharemac-library/input/src/components/input/input.component';
import { moduleMetadata } from '@storybook/angular';
import { InputModule } from 'projects/sharemac-library/input/src/input.module';

export default {
  title: 'Example/Input',
  component: InputComponent,
  decorators: [
    moduleMetadata({
      imports: [FormsModule, CommonModule, ReactiveFormsModule],
      declarations: [InputComponent]
    })
  ],
} as Meta;

const Template: Story<InputComponent> = (args: InputComponent) => ({
  props: args
});

export const Primary = Template.bind({});
Primary.args = {
  config: {
    type: 'text',
    styleType: 'bordered',
    size: 'md',
    clearable: true,
    label: 'First name',
    placeHolder: 'E.G. Jon Doe',
    suffix: '%',
    prefix: '%',
    errorMessages: {
      required: 'Field is required',
      minlength: 'Min length error',
      maxlength: 'Max length error'
    }
  }
};

When I am trying to run storybook, it compiles successfully but in the console, I have the following error: NullInjectorError: No provider for NgControl!

I have imported FormsModule and ReactiveFormsModule in Input.story.ts but still have the same error, what am I doing wrong? or missing?



Solution 1:[1]

try below code.

Input.component.html


<div class="input-wrapper" [class]="{ 'light-theme': !isDark }">
  <ng-container *ngIf="label">
    <label [for]="fieldName" class="label">
      {{ label }}
      <span [class.is-required]="isRequired" *ngIf="isRequired">*</span>
    </label>
  </ng-container>
  <div class="input-inner">
    <div [class]="classes" [class.right-side-icon]="svgObject?.isRightSide">
      <input
        [type]="inputType"
        [id]="fieldName"
        [name]="fieldName"
        [placeholder]="placeHolder"
        [(ngModel)]="value"
        (change)="pushChanges($event.target.value)"
        (blur)="onTouched()"
        [class.invalid]="
          formField?.invalid && (formField?.dirty || formField?.touched)
        "
        [readonly]="isReadonly"
      />
      <ng-container *ngIf="svgObject?.hasIcon">
        <div class="icon">
          <core-icons
            [name]="svgObject?.name"
            [height]="svgObject?.height"
            [width]="svgObject?.width"
            [fillColorPrimary]="svgObject?.fillColor"
          ></core-icons>
        </div>
      </ng-container>
      <ng-container *ngIf="innerLabelObject?.hasLabel">
        <label class="inner-label" (click)="onLabelClick.emit($event)">{{
          innerLabelObject?.label
        }}</label>
      </ng-container>
    </div>
  </div>
</div>

Input.component.ts

import {
  Component,
  EventEmitter,
  Input,
  Output,
  forwardRef,
} from '@angular/core';
import {
  ControlValueAccessor,
  FormControl,
  FormGroup,
  NG_VALUE_ACCESSOR,
} from '@angular/forms';

@Component({
  selector: 'core-input',
  templateUrl: './input.component.html',
  styleUrls: ['./input.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => InputComponent),
      multi: true,
    },
  ],
})
export class InputComponent implements ControlValueAccessor {
  @Input() parentForm!: FormGroup;
  @Input() isDark: boolean = true;
  @Input() isPrimary: boolean = true;
  @Input() label?: string;
  @Input() isRequired?: boolean = true;
  @Input() fieldName!: string;
  @Input() placeHolder?: string;
  @Input() isReadonly?: boolean;
  /**
   * What background color to use
   */
  @Input()
  backgroundColor?: string;

  @Input()
  color?: string;

  /**
   * How large should the button be?
   */
  @Input()
  inputType: string = 'text';

  @Input()
  size: string = 'large';

  @Input()
  svgObject?: any;

  @Input()
  innerLabelObject?: any;

  /**
   * Optional label click handler
   */
  @Output()
  onLabelClick = new EventEmitter<Event>();

  _value: string = '';

  get formField(): FormControl {
    return this.parentForm?.get(this.fieldName) as FormControl;
  }

  onChanged: Function = () => {};
  onTouched: Function = () => {};

  get value(): any {
    return this._value;
  }

  set value(v: any) {
    if (v !== this._value) {
      this._value = v;
      this.onChanged(v);
    }
  }

  writeValue(value: any): void {
    this._value = value || '';
  }

  pushChanges(value: any) {
    this.onChanged = value;
  }

  registerOnChange(fn: any): void {
    this.onChanged = fn; // <-- save the function
    this.onTouched();
  }
  registerOnTouched(fn: any): void {
    this.onTouched = fn; // <-- save the function
  }

  setDisabledState(isDisabled: boolean): void {}

  public get classes(): string[] {
    const mode = this.isPrimary ? 'input-box--primary' : 'input-box--secondary';
    return ['input-box', mode];
  }
}

Input.stories.ts


// also exported from '@storybook/angular' if you can deal with breaking changes in 6.1
import { Story, Meta } from '@storybook/angular/types-6-0';
import { InputComponent, CoreComponentsLibModule } from 'core-components';
import { moduleMetadata } from '@storybook/angular';
import { NzInputModule } from 'ng-zorro-antd/input';
import { CUSTOM_ELEMENTS_SCHEMA, forwardRef } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';

// More on default export: https://storybook.js.org/docs/angular/writing-stories/introduction#default-export
export default {
  title: 'Example/Input',
  component: InputComponent,
  // More on argTypes: https://storybook.js.org/docs/angular/api/
  decorators: [
    moduleMetadata({
      //? Imports both components to allow component composition with Storybook
      declarations: [],
      imports: [NzInputModule, CoreComponentsLibModule, FormsModule],
      schemas: [CUSTOM_ELEMENTS_SCHEMA],
      providers: [
        {
          provide: NG_VALUE_ACCESSOR,
          useExisting: forwardRef(() => InputComponent),
          multi: true,
        },
      ],
    }),
  ],
  argTypes: {
    isDark: { control: 'boolean' },
    isRequired: { control: 'boolean' },
    isPrimary: { control: 'boolean' },
    backgroundColor: { control: 'color' },
    inputType: {
      options: ['text', 'password', 'number', 'date'],
      control: { type: 'select' },
    },
    size: { control: 'radio', options: ['small', 'medium', 'large'] },
    color: { control: 'color' },
    lable: { control: 'text' },
  },
  parameters: {
    docs: {
      page: null,
    },
  },
} as Meta;

// More on component templates: https://storybook.js.org/docs/angular/writing-stories/introduction#using-args
const Template: Story<InputComponent> = (args: InputComponent) => ({
  props: args,
});

// More on args: https://storybook.js.org/docs/angular/writing-stories/args
export const Default = Template.bind({});
Default.args = {
  placeHolder: 'Enter email',
  inputType: 'text',
  label: 'Email',
  size: 'large',
  isRequired: true,
  isPrimary: true,
  svgObject: {
    hasIcon: false,
    isRightSide: true,
    name: 'accepted',
    width: '14px',
    height: '14px',
    fillColor: '#fff',
  },
  // innerLabelObject: { hasLabel: 'true', label: 'Max' },
  fieldName: 'email',
};


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Manoj Gohel