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