Skip to content

Commit

Permalink
Merge pull request #26413 from storybookjs/valentin/add-input-support…
Browse files Browse the repository at this point in the history
…-for-angular

Angular: Add support for Angular's input signals
  • Loading branch information
valentinpalkovic committed Mar 20, 2024
2 parents 45e7d3f + 607e2bf commit c2fd3a4
Show file tree
Hide file tree
Showing 12 changed files with 1,341 additions and 1,202 deletions.
27 changes: 14 additions & 13 deletions code/frameworks/angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
],
"scripts": {
"check": "node ../../../scripts/node_modules/.bin/tsc",
"prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/tsc.ts"
"prep": "rimraf dist && node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/tsc.ts"
},
"dependencies": {
"@storybook/builder-webpack5": "workspace:*",
Expand Down Expand Up @@ -65,18 +65,18 @@
},
"devDependencies": {
"@analogjs/vite-plugin-angular": "^0.2.24",
"@angular-devkit/architect": "^0.1700.5",
"@angular-devkit/build-angular": "^17.0.5",
"@angular-devkit/core": "^17.0.5",
"@angular/animations": "^17.0.5",
"@angular/cli": "^17.0.5",
"@angular/common": "^17.0.5",
"@angular/compiler": "^17.0.5",
"@angular/compiler-cli": "^17.0.5",
"@angular/core": "^17.0.5",
"@angular/forms": "^17.0.5",
"@angular/platform-browser": "^17.0.5",
"@angular/platform-browser-dynamic": "^17.0.5",
"@angular-devkit/architect": "^0.1703.0",
"@angular-devkit/build-angular": "^17.3.0",
"@angular-devkit/core": "^17.3.0",
"@angular/animations": "^17.3.0",
"@angular/cli": "^17.3.0",
"@angular/common": "^17.3.0",
"@angular/compiler": "^17.3.0",
"@angular/compiler-cli": "^17.3.0",
"@angular/core": "^17.3.0",
"@angular/forms": "^17.3.0",
"@angular/platform-browser": "^17.3.0",
"@angular/platform-browser-dynamic": "^17.3.0",
"@types/cross-spawn": "^6.0.2",
"@types/tmp": "^0.2.3",
"cross-spawn": "^7.0.3",
Expand Down Expand Up @@ -115,6 +115,7 @@
},
"builders": "dist/builders/builders.json",
"bundler": {
"post": "./scripts/postbuild.js",
"tsConfig": "tsconfig.build.json"
},
"gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae17"
Expand Down
14 changes: 14 additions & 0 deletions code/frameworks/angular/scripts/postbuild.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* This postbuild fix is needed to add a ts-ignore to the generated public-types.d.ts file.
* The AngularCore.InputSignal and AngularCore.InputSignalWithTransform types do not exist in Angular
* versions < 17.2. In these versions, the unresolved types will error and prevent Storybook from starting/building.
* This postbuild script adds a ts-ignore statement above the unresolved types to prevent the errors.
*/

const fs = require('fs');
const path = require('path');

const filePath = path.join(__dirname, '../dist/client/public-types.d.ts');
const fileContent = fs.readFileSync(filePath, 'utf8');
const newContent = fileContent.replaceAll(/(type AngularInputSignal)/g, '// @ts-ignore\n$1');
fs.writeFileSync(filePath, newContent, 'utf8');
39 changes: 34 additions & 5 deletions code/frameworks/angular/src/client/public-types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/ban-ts-comment */
import {
AnnotatedStoryFn,
Args,
Expand All @@ -9,7 +11,7 @@ import {
StrictArgs,
ProjectAnnotations,
} from '@storybook/types';
import { EventEmitter } from '@angular/core';
import * as AngularCore from '@angular/core';
import { AngularRenderer } from './types';

export type { Args, ArgTypes, Parameters, StrictArgs } from '@storybook/types';
Expand All @@ -21,27 +23,54 @@ export type { AngularRenderer };
*
* @see [Default export](https://storybook.js.org/docs/formats/component-story-format/#default-export)
*/
export type Meta<TArgs = Args> = ComponentAnnotations<AngularRenderer, TransformEventType<TArgs>>;
export type Meta<TArgs = Args> = ComponentAnnotations<
AngularRenderer,
TransformComponentType<TArgs>
>;

/**
* Story function that represents a CSFv2 component example.
*
* @see [Named Story exports](https://storybook.js.org/docs/formats/component-story-format/#named-story-exports)
*/
export type StoryFn<TArgs = Args> = AnnotatedStoryFn<AngularRenderer, TransformEventType<TArgs>>;
export type StoryFn<TArgs = Args> = AnnotatedStoryFn<
AngularRenderer,
TransformComponentType<TArgs>
>;

/**
* Story object that represents a CSFv3 component example.
*
* @see [Named Story exports](https://storybook.js.org/docs/formats/component-story-format/#named-story-exports)
*/
export type StoryObj<TArgs = Args> = StoryAnnotations<AngularRenderer, TransformEventType<TArgs>>;
export type StoryObj<TArgs = Args> = StoryAnnotations<
AngularRenderer,
TransformComponentType<TArgs>
>;

export type Decorator<TArgs = StrictArgs> = DecoratorFunction<AngularRenderer, TArgs>;
export type Loader<TArgs = StrictArgs> = LoaderFunction<AngularRenderer, TArgs>;
export type StoryContext<TArgs = StrictArgs> = GenericStoryContext<AngularRenderer, TArgs>;
export type Preview = ProjectAnnotations<AngularRenderer>;

/**
* Utility type that transforms InputSignal and EventEmitter types
*/
type TransformComponentType<T> = TransformInputSignalType<TransformEventType<T>>

// @ts-ignore Angular < 17.2 doesn't export InputSignal
type AngularInputSignal<T> = AngularCore.InputSignal<T>
// @ts-ignore Angular < 17.2 doesn't export InputSignalWithTransform
type AngularInputSignalWithTransform<T, U> = AngularCore.InputSignalWithTransform<T, U>

type AngularHasSignal = typeof AngularCore extends { input: infer U } ? true : false;
type InputSignal<T> = AngularHasSignal extends true ? AngularInputSignal<T> : never;
type InputSignalWithTransform<T, U> = AngularHasSignal extends true ? AngularInputSignalWithTransform<T, U> : never;

type TransformInputSignalType<T> = {
[K in keyof T]: T[K] extends InputSignal<infer E> ? E : T[K] extends InputSignalWithTransform<any, infer U> ? U : T[K];
};

type TransformEventType<T> = {
[K in keyof T]: T[K] extends EventEmitter<infer E> ? (e: E) => void : T[K];
[K in keyof T]: T[K] extends AngularCore.EventEmitter<infer E> ? (e: E) => void : T[K];
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Component, Input, Output, EventEmitter, input } from '@angular/core';

@Component({
// Needs to be a different name to the CLI template button
selector: 'storybook-signal-button',
template: ` <button
type="button"
(click)="onClick.emit($event)"
[ngClass]="classes"
[ngStyle]="{ 'background-color': backgroundColor }"
>
{{ label() }}
</button>`,
styleUrls: ['./button.css'],
})
export default class SignalButtonComponent {
/**
* Is this the principal call to action on the page?
*/
primary = input(false);

/**
* What background color to use
*/
@Input()
backgroundColor?: string;

/**
* How large should the button be?
*/
size = input('medium', {
transform: (val: 'small' | 'medium') => val,
});

/**
* Button contents
*/
label = input.required({ transform: (val: string) => val.trim() });

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

public get classes(): string[] {
const mode = this.primary() ? 'storybook-button--primary' : 'storybook-button--secondary';

return ['storybook-button', `storybook-button--${this.size()}`, mode];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.storybook-button {
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-weight: 700;
border: 0;
border-radius: 3em;
cursor: pointer;
display: inline-block;
line-height: 1;
}
.storybook-button--primary {
color: white;
background-color: #1ea7fd;
}
.storybook-button--secondary {
color: #333;
background-color: transparent;
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
}
.storybook-button--small {
font-size: 12px;
padding: 10px 16px;
}
.storybook-button--medium {
font-size: 14px;
padding: 11px 20px;
}
.storybook-button--large {
font-size: 16px;
padding: 12px 24px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Meta, StoryObj } from '@storybook/angular';
import { fn } from '@storybook/test';
import SignalButtonComponent from './button.component';

// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
const meta: Meta<SignalButtonComponent> = {
component: SignalButtonComponent,
tags: ['autodocs'],
argTypes: {
backgroundColor: {
control: 'color',
},
// The following argTypes are necessary,
// because Compodoc does not support Angular's new input and output signals yet
primary: {
type: 'boolean',
},
size: {
control: {
type: 'radio',
},
options: ['small', 'medium'],
},
label: {
type: 'string',
},
},
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
args: {
onClick: fn(),
primary: false,
size: 'medium',
},
};

export default meta;
type Story = StoryObj<SignalButtonComponent>;

// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Primary: Story = {
args: {
primary: true,
label: 'Button',
},
};

export const Secondary: Story = {
args: {
label: 'Button',
},
};

export const Medium: Story = {
args: {
size: 'medium',
label: 'Button',
},
};

export const Small: Story = {
args: {
size: 'small',
label: 'Button',
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Component, Input, Output, EventEmitter, input } from '@angular/core';

@Component({
// Needs to be a different name to the CLI template button
selector: 'storybook-signal-button',
template: ` <button
type="button"
(click)="onClick.emit($event)"
[ngClass]="classes"
[ngStyle]="{ 'background-color': backgroundColor }"
>
{{ label() }}
</button>`,
styleUrls: ['./button.css'],
})
export default class SignalButtonComponent {
/**
* Is this the principal call to action on the page?
*/
primary = input(false);

/**
* What background color to use
*/
@Input()
backgroundColor?: string;

/**
* How large should the button be?
*/
size = input('medium', {
transform: (val: 'small' | 'medium') => val,
});

/**
* Button contents
*/
label = input.required({ transform: (val: string) => val.trim() });

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

public get classes(): string[] {
const mode = this.primary() ? 'storybook-button--primary' : 'storybook-button--secondary';

return ['storybook-button', `storybook-button--${this.size()}`, mode];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.storybook-button {
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-weight: 700;
border: 0;
border-radius: 3em;
cursor: pointer;
display: inline-block;
line-height: 1;
}
.storybook-button--primary {
color: white;
background-color: #1ea7fd;
}
.storybook-button--secondary {
color: #333;
background-color: transparent;
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
}
.storybook-button--small {
font-size: 12px;
padding: 10px 16px;
}
.storybook-button--medium {
font-size: 14px;
padding: 11px 20px;
}
.storybook-button--large {
font-size: 16px;
padding: 12px 24px;
}
Loading

0 comments on commit c2fd3a4

Please sign in to comment.