Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: Angular function Input in component is not set from story args #20309

Open
loicpetit opened this issue Dec 16, 2022 · 5 comments
Open

Comments

@loicpetit
Copy link

Describe the bug

Example of component

Component

import { Component, EventEmitter, HostBinding, Input, Output } from '@angular/core';

export interface MillSelectorMill {
	code: string;
	description: string;
	selected: boolean;
}
@Component({
	selector: 'copo-mill-selector',
	templateUrl: './mill-selector.component.html',
	styleUrls: ['./mill-selector.component.css']
})
export class MillSelectorComponent {
	@HostBinding('class.passthrough') readonly passthrough = true;
	@Input() mills?: MillSelectorMill[] = [];
	@Input() validateChange?: (mill: MillSelectorMill) => Promise<boolean>;
	@Output() readonly millsChange = new EventEmitter<MillSelectorMill[]>(true);
	private isValidating = false;

	async onToggle($event: MouseEvent, millToToggle: MillSelectorMill) {
		$event.preventDefault();
		$event.stopPropagation();
		if (!this.mills || this.isValidating) {
			return false;
		}
		const isValidated = await this.validate(millToToggle);
		if (isValidated) {
			this.mills = this.mills.map(mill => {
				if (mill.code === millToToggle.code) {
					return {
						...mill,
						selected: !millToToggle.selected
					};
				}
				return mill;
			});
			this.millsChange.emit(this.mills);
		}
		return false;
	}

	private async validate(millToToggle: MillSelectorMill) {
		if (!this.validateChange) {
			return true;
		}
		this.isValidating = true;
		const isValidated = await this.validateChange(millToToggle);
		this.isValidating = false;
		return isValidated;
	}
}

HTML

<div class="grid-6 copo-container">
	<div class="grid-column-one-full card-info-block">
		<div class="card-content-title" translate>ecall-off.mill-section</div>
		
		<div *ngIf="mills && mills.length > 0" class="card-content-label required" translate>ecall-off.select-a-mill</div>
		<div *ngIf="!mills || mills.length === 0" class="card-content-label" translate>ecall-off.no-mills</div>

		<div *ngIf="mills && mills.length > 0">
			<mat-checkbox *ngFor="let mill of mills; let i = index;" [checked]="mill.selected" (click)="onToggle($event, mill)"
				class="mills-spacing">
				{{ mill.description }}
				</mat-checkbox>
		</div>
	</div>
</div>

Stories

import { action } from '@storybook/addon-actions';
import { Meta, moduleMetadata, Story } from '@storybook/angular';

import { DEFAULT_MODULE_IMPORTS } from '../../../stories/stories.helper';

import { MillSelectorComponent, MillSelectorMill } from './mill-selector.component';

export default <Meta>{
	title: 'Order/ECallOff/MillSelector',
	component: MillSelectorComponent,
	argTypes: {
		millsChange: {
			action: 'mills change'
		},
		validateChange: {
			type: 'function',
			control: 'function' // not deduced automatically, required for functions to be pass as input
		}
	},
	decorators: [
		moduleMetadata({
			declarations: [ MillSelectorComponent ],
			imports: DEFAULT_MODULE_IMPORTS,
			providers: []
		})
	]
};
const TEMPLATE: Story<MillSelectorComponent> = args => ({
	props: { ...args }
});

export const Default = TEMPLATE.bind({});
Default.args = {
	mills: <MillSelectorMill[]>[
		{
			code: 'millA',
			description: 'Liege',
			selected: false
		}, {
			code: 'millB',
			description: 'Florange',
			selected: false
		}, {
			code: 'millC',
			description: 'Desvres',
			selected: false
		}
	]
};

export const Empty = TEMPLATE.bind({});

export const WithLotsOfMills = TEMPLATE.bind({});
WithLotsOfMills.args = {
	mills: <MillSelectorMill[]>[
		{
			code: 'millA',
			description: 'mill A',
			selected: true
		}, {
			code: 'millB',
			description: 'mill A',
			selected: false
		}, {
			code: 'millC',
			description: 'mill A',
			selected: true
		}, {
			code: 'millD',
			description: 'mill D',
			selected: false
		}, {
			code: 'millE',
			description: 'mill E',
			selected: true
		}, {
			code: 'millF',
			description: 'mill F',
			selected: false
		}, {
			code: 'millG',
			description: 'mill G',
			selected: true
		}, {
			code: 'millH',
			description: 'mill H',
			selected: false
		}, {
			code: 'millI',
			description: 'mill I',
			selected: true
		}, {
			code: 'millJ',
			description: 'mill J',
			selected: false
		}, {
			code: 'millK',
			description: 'mill K',
			selected: true
		}
	]
};

async function validateMillChange(mill: MillSelectorMill): Promise<boolean> {
	return new Promise<boolean>((resolve, _reject) => {
		setTimeout(() => {
			const isValidated = mill.code !== 'millB';
			action('validate mill change')({ mill, isValidated });
			resolve(isValidated);
		}, 1000);
	});
}

export const WithValidator = TEMPLATE.bind({});
WithValidator.args = {
	validateChange: validateMillChange,
	mills: <MillSelectorMill[]>[
		{
			code: 'millA',
			description: 'Liege',
			selected: false
		}, {
			code: 'millB',
			description: 'Florange',
			selected: false
		}, {
			code: 'millC',
			description: 'Desvres',
			selected: false
		}
	]
};

In Meta argsType property, if I dont force the control property of validateChange to something (function in my case) the function is not set in the WithValidator story and so the value stay undefined.

That issue is only for Input which are functions, so I set the control value to 'function' but in fact just the control property is needed.

I found the resolution by debugging the storybook sources in the browser. I find a function cleanArgsDecorator which check the arguments has either an action or a control property.


const cleanArgsDecorator = (storyFn, context) => {
    if (!context.argTypes || !context.args) {
        return storyFn();
    }
    const argsToClean = context.args;
    context.args = Object.entries(argsToClean).reduce((obj, [key, arg]) => {
        const argType = context.argTypes[key];
        // Only keeps args with a control or an action in argTypes
        if (argType.action || argType.control) {
            return Object.assign(Object.assign({}, obj), { [key]: arg });
        }
        return obj;
    }, {});
    return storyFn();
};

If it is wanted to be like this for function please update the documentation to describe that case, else something is missing for Angular Inputs which are of type function.

To Reproduce

No response

System

No response

Additional context

No response

@nilsel
Copy link

nilsel commented Mar 31, 2023

Got the same issue with functions in stories being undefined, after a big haul upgrade from Angular 9 to 15 and latest Storybook (6.5.16). Stories that worked before started showing these errors:
ctx.onClickHandler is not a function

Adding the function to argTypes fixed it:

argTypes: {
  onClickHandler: { type: 'function', control: 'function'},

Just found this issue & workaround after a few days of searching, thanks!

@richard-ncs
Copy link

I spent many hours trying to figure out how to pass in arguments as functions and this issue just saved me.

Can I suggest that it be added to the documentation in the meantime until it can be fixed?

@paddotk
Copy link

paddotk commented Dec 11, 2023

This fix doesn't work for me. I added

myFunc: {
    type: 'function',
    control: 'function'
  }

to argTypes but I still get ctx.myFunc is not a function
I'm on Storybook 7.5.2.

@valentinpalkovic
Copy link
Contributor

Can you try to upgrade to Storybook 7 and figure out whether the issue still exists?

@paddotk
Copy link

paddotk commented Dec 13, 2023

I'm on Storybook 7.5.2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants