From 4c8c97ffaa08fcdba5e7b99a9db0b50058f3daf9 Mon Sep 17 00:00:00 2001 From: jonniebigodes Date: Sat, 7 Dec 2019 20:04:04 +0000 Subject: [PATCH 1/9] Chore: update angular documentation --- .../angular/en/composite-component.md | 162 +++--- .../angular/en/creating-addons.md | 474 ++++++++++++++++++ content/intro-to-storybook/angular/en/data.md | 245 +++++---- .../intro-to-storybook/angular/en/deploy.md | 32 +- .../angular/en/get-started.md | 202 +++++++- .../intro-to-storybook/angular/en/screen.md | 334 ++++++++---- .../angular/en/simple-component.md | 185 ++----- content/intro-to-storybook/angular/en/test.md | 33 +- .../angular/en/using-addons.md | 222 ++++++++ .../angular/es/get-started.md | 3 + .../angular/pt/get-started.md | 2 + .../app-three-modalities-angular.png | Bin 0 -> 89857 bytes .../netlify-settings-npm.png | Bin 0 -> 59232 bytes 13 files changed, 1410 insertions(+), 484 deletions(-) create mode 100644 content/intro-to-storybook/angular/en/creating-addons.md create mode 100644 content/intro-to-storybook/angular/en/using-addons.md create mode 100644 static/intro-to-storybook/app-three-modalities-angular.png create mode 100644 static/intro-to-storybook/netlify-settings-npm.png diff --git a/content/intro-to-storybook/angular/en/composite-component.md b/content/intro-to-storybook/angular/en/composite-component.md index 423e7a8f0..53ce47df1 100644 --- a/content/intro-to-storybook/angular/en/composite-component.md +++ b/content/intro-to-storybook/angular/en/composite-component.md @@ -9,53 +9,48 @@ Last chapter we built our first component; this chapter extends what we learned ## TasklistComponent -Taskbox emphasizes pinned tasks by positioning them above default tasks. This yields two variations of `TaskListComponent` you need to create stories for: default items and default and pinned items. +Taskbox emphasizes pinned tasks by positioning them above default tasks. This yields two variations of `TaskList` you need to create stories for: default items and default and pinned items. ![default and pinned tasks](/intro-to-storybook/tasklist-states-1.png) -Since `TaskComponent` data can be sent asynchronously, we **also** need a loading state to render in the absence of a connection. In addition, an empty state is required when there are no tasks. +Since `Task` data can be sent asynchronously, we **also** need a loading state to render in the absence of a connection. In addition, an empty state is required when there are no tasks. ![empty and loading tasks](/intro-to-storybook/tasklist-states-2.png) ## Get setup -A composite component isn’t much different than the basic components it contains. Create a `TaskListComponent` component and an accompanying story file: `src/tasks/task-list.component.ts` and `src/tasks/task-list.stories.ts`. +A composite component isn’t much different than the basic components it contains. Create a `TaskList` component and an accompanying story file: `src/app/tasks/task-list.component.ts` and `src/app/tasks/task-list.stories.ts`. -Start with a rough implementation of the `TaskListComponent`. You’ll need to import the `TaskComponent` component from earlier and pass in the attributes and actions as inputs and events. +Start with a rough implementation of the `TaskList`. You’ll need to import the `Task` component from earlier and pass in the attributes and actions as inputs and events. ```typescript + import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; -import { Task } from './task.model'; +import { Task } from '../models/task.model'; @Component({ - selector: 'task-list', + selector: 'app-task-list', template: `
- loading
+
empty
+ - + - `, + ` }) -export class TaskListComponent implements OnInit { - tasks: Task[] = []; - @Input() loading: boolean = false; - @Output() onPinTask: EventEmitter = new EventEmitter(); - @Output() onArchiveTask: EventEmitter = new EventEmitter(); - constructor() {} - - ngOnInit() {} -} ``` Next create `Tasklist`’s test states in the story file. ```typescript + import { storiesOf, moduleMetadata } from '@storybook/angular'; import { CommonModule } from '@angular/common'; @@ -69,70 +64,73 @@ export const defaultTasks = [ { ...task, id: '3', title: 'Task 3' }, { ...task, id: '4', title: 'Task 4' }, { ...task, id: '5', title: 'Task 5' }, - { ...task, id: '6', title: 'Task 6' }, + { ...task, id: '6', title: 'Task 6' } ]; export const withPinnedTasks = [ ...defaultTasks.slice(0, 5), - { id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' }, + { id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' } ]; const props = { tasks: defaultTasks, onPinTask: actions.onPinTask, - onArchiveTask: actions.onArchiveTask, + onArchiveTask: actions.onArchiveTask }; storiesOf('TaskList', module) .addDecorator( moduleMetadata({ + // imports both components to allow component composition with storybook declarations: [TaskListComponent, TaskComponent], - imports: [CommonModule], + imports: [CommonModule] }) ) .add('default', () => { return { template: `
- +
`, - props, + props }; }) .add('withPinnedTasks', () => { return { template: `
- +
`, props: { ...props, - tasks: withPinnedTasks, - }, + tasks: withPinnedTasks + } }; }) .add('loading', () => { return { template: `
- +
`, - props, + props }; }) .add('empty', () => { return { template: `
- +
`, - props, + props }; }); + + ``` `addDecorator()` allows us to add some “context” to the rendering of each task. In this case we add the module metadata so we can use all the Angular components inside out stories. @@ -158,39 +156,45 @@ Our component is still rough but now we have an idea of the stories to work towa ```typescript import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; -import { Task } from './task.model'; +import { Task } from '../models/task.model'; @Component({ - selector: 'task-list', + selector: 'app-task-list', template: ` -
- - - -
- -
You have no tasks
-
Sit back and relax
-
- -
-
- - Loading cool state -
-
+
+ + + +
+ +
You have no tasks
+
Sit back and relax
+
+ +
+
+ + + Loading cool state +
- `, +
+
+ ` }) export class TaskListComponent implements OnInit { tasksInOrder: Task[] = []; - @Input() loading: boolean = false; + @Input() loading = false; + + // tslint:disable-next-line: no-output-on-prefix @Output() onPinTask: EventEmitter = new EventEmitter(); + + // tslint:disable-next-line: no-output-on-prefix @Output() onArchiveTask: EventEmitter = new EventEmitter(); @Input() @@ -241,34 +245,28 @@ So, to avoid this problem, we can use Jest to render the story to the DOM and ru Create a test file called `task-list.component.spec.ts`. Here we’ll build out our tests that make assertions about the output. ```typescript -import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { render } from '@testing-library/angular'; import { TaskListComponent } from './task-list.component'; import { TaskComponent } from './task.component'; - import { withPinnedTasks } from './task-list.stories'; -import { By } from '@angular/platform-browser'; - describe('TaskList component', () => { - let component: TaskListComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [TaskComponent, TaskListComponent], - }).compileComponents(); - })); - - it('renders pinned tasks at the start of the list', () => { - fixture = TestBed.createComponent(TaskListComponent); - component = fixture.componentInstance; - component.tasks = withPinnedTasks; - - fixture.detectChanges(); - const lastTaskInput = fixture.debugElement.query(By.css('.list-item:nth-child(1)')); - - // We expect the task titled "Task 6 (pinned)" to be rendered first, not at the end - expect(lastTaskInput.nativeElement.id).toEqual('6'); - }); + it('renders pinned tasks at the start of the list', async () => { + const mockedActions = jest.fn(); + const tree = await render(TaskListComponent, { + declarations: [TaskComponent], + componentProperties: { + tasks: withPinnedTasks, + loading: false, + onPinTask: { + emit: mockedActions, + } as any, + onArchiveTask: { + emit: mockedActions + } as any + } + }); + const component = tree.fixture.componentInstance; + expect(component.tasksInOrder[0].id).toBe('6'); }); ``` @@ -276,4 +274,4 @@ describe('TaskList component', () => { Note that we’ve been able to reuse the `withPinnedTasks` list of tasks in both story and unit test; in this way we can continue to leverage an existing resource (the examples that represent interesting configurations of a component) in more and more ways. -Notice as well that this test is quite brittle. It's possible that as the project matures, and the exact implementation of the `TaskComponent` changes --perhaps using a different classname or a `textarea` rather than an `input`--the test will fail, and need to be updated. This is not necessarily a problem, but rather an indication to be careful liberally using unit tests for UI. They're not easy to maintain. Instead rely on visual, snapshot, and visual regression (see [testing chapter](/test/)) tests where possible. +Notice as well that this test is quite brittle. It's possible that as the project matures, and the exact implementation of the `TaskComponent` changes --perhaps using a different classname or a `textarea` rather than an `input`--the test will fail, and need to be updated. This is not necessarily a problem, but rather an indication to be careful liberally using unit tests for UI. They're not easy to maintain. Instead rely on visual, snapshot, and visual regression (see [testing chapter](/test/)) tests where possible. \ No newline at end of file diff --git a/content/intro-to-storybook/angular/en/creating-addons.md b/content/intro-to-storybook/angular/en/creating-addons.md new file mode 100644 index 000000000..332220f2a --- /dev/null +++ b/content/intro-to-storybook/angular/en/creating-addons.md @@ -0,0 +1,474 @@ +--- +title: 'Creating addons' +tocTitle: 'Creating addons' +description: 'Learn how to build your own addons that will super charge your development' +--- + +In the previous chapter we were introduced to one of the key features of Storybook, its robust system of [addons](https://storybook.js.org/addons/introduction/), which can be used to enhance not only yours but also your team's developer experience and workflows. + +In this chapter we're going to take a look on how we create our own addon. You might think that writting it can be a daunting task, but actually it's not, we just need to take a couple of steps to get started and we can start writting it. + +But first thing is first, let's first scope out what our addon will do. + +## The addon we're going to write + +For this example, let's assume that our team has some design assets that are somehow related to the existing UI components. Looking at the current Storybook UI, it seems that relationship isn't really apparent. How can we fix that? + +We have our goal, now let's define what features our addon will support: + +- Display the design asset in a panel +- Support images, but also urls for embedding +- Should support multiple assets, just in case there will be multiple versions or themes + +The way we'll be attaching the list of assets to the stories is through [parameters](https://storybook.js.org/docs/configurations/options-parameter/), which is a Storybook option that allow us to inject custom parameters to our stories. The way to use it, it's quite similar on how we used a decorator in previous chapters. + + + +```javascript +storiesOf('your-component', module) + .addParameters({ + assets: ['path/to/your/asset.png'], + }) + .addDecorator(/*...*/) + .add(/*...*/); +``` + + + +## Setup + +We've outlined what our addon will do, time to setup our local development environment. We need some additional packages in our project. More specifically: + + + +- 📦 [@storybook/api](https://www.npmjs.com/package/@storybook/api) for Storybook API usage. +- 📦 [@storybook/components](https://www.npmjs.com/package/@storybook/components) to use Storybook's UI components. +- 📦 [@storybook/theming ](https://www.npmjs.com/package/@storybook/theming) for styling. +- 🛠 [@babel/preset-react](https://babeljs.io/docs/en/babel-preset-react) to transpile correctly some of React's new features. + +Open a console, navigate to your project folder and run the following command: + + + +```bash + npm install -D @storybook/api @storybook/components @storybook/theming @babel/preset-react +``` + +We'll need to make a small change to the `.babelrc` file we created earlier. We need to add a reference to the `@babel/preset-react` package. + +Your updated file should look like this: + +```json +{ + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": "current" + } + } + ], + "@babel/preset-react" + ], + "env": { + "test": { + "plugins": ["babel-plugin-require-context-hook"] + } + } +} +``` + +## Writing the addon + +We have what we need, it's time to start working on the actual addon. + +Inside the `.storybook` folder create a new folder called `addons` and inside, a file called `design-assets.js` with the following: + +```javascript +//.storybook/addons/design-assets.js +import React from 'react'; +import { AddonPanel } from '@storybook/components'; +import { addons, types } from '@storybook/addons'; + +addons.register('my/design-assets', () => { + addons.add('design-assets/panel', { + title: 'assets', + type: types.PANEL, + render: ({ active, key }) => ( + + implement + + ), + }); +}); +``` + +
We're going to use the .storybook folder as a placeholder for our addon. The reason behind this, is to maintain a straightforward approach and avoid complicating it too much. Should this addon be transformed into a actual addon it would be best to move it to a separate package with it's own file and folder structure.
+ +This is the a typical boilerplate code to get started and going over what the code is doing: + +- We're registering a new addon in our Storybook. +- Add a new UI element for our addon with some options (a title that will define our addon and the type of element used) and render it with some text. + +Starting Storybook at this point, we won't be able to see the addon just yet. Like we did earlier with the Knobs addon, we need to register our own in the `.storybook/addons.js` file. Just add the following and should be able to see it working: + +```js +import './addons/design-assets'; +``` + +![design assets addon running inside Storybook](/intro-to-storybook/create-addon-design-assets-added.png) + +Success! We have our newly created addon added to the Storybook UI. + +
Storybook allows you to add not only panels, but a whole range of different types of UI components. And most if not all of them are already created inside the @storybook/components package, so that you don't need waste too much time implementing the UI and focus on writting features.
+ +### Creating the content component + +We've completed our first objective. Time to start working on the second one. + +To complete it, we need to make some changes to our imports and introduce a new component that will display the asset information. + +Make the following changes to the addon file: + +```javascript +//.storybook/addons/design-assets.js +import React, { Fragment } from 'react'; +/* same as before */ +import { useParameter } from '@storybook/api'; + +//.storybook/addons/design-assets.js +const Content = () => { + const results = useParameter('assets', []); // story's parameter being retrieved here + + return ( + + {results.length ? ( +
    + {results.map(i => ( +
  1. {i}
  2. + ))} +
+ ) : null} +
+ ); +}; +``` + +We've created the component, modified the imports, all that's missing is to connect the component to our panel and we'll have a working addon capable of displaying information relative to our stories. + +Your code should look like the following: + +```javascript +//.storybook/addons/design-assets.js +import React, { Fragment } from 'react'; +import { AddonPanel } from '@storybook/components'; +import { useParameter } from '@storybook/api'; +import { addons, types } from '@storybook/addons'; + +const Content = () => { + const results = useParameter('assets', []); // story's parameter being retrieved here + + return ( + + {results.length ? ( +
    + {results.map(i => ( +
  1. {i}
  2. + ))} +
+ ) : null} +
+ ); +}; + +addons.register('my/design-assets', () => { + addons.add('design-assets/panel', { + title: 'assets', + type: types.PANEL, + render: ({ active, key }) => ( + + + + ), + }); +}); +``` + +Notice that we're using the [useParameter](https://storybook.js.org/docs/addons/api/#useparameter), this handy hook will allow us to read the information supplied by the `addParameters` option for each story, which in our case will be either a single path to a asset or a list of paths. You'll see it in effect shortly. + +### Using our addon with a story + +We've connected all the necessary pieces. But how can we see if it's actually working and showing anything? + +To do so, we're going to make a small change to the `task.stories.ts` file and add the [addParameters](https://storybook.js.org/docs/configurations/options-parameter/#per-story-options) option. + +```javascript +// src/app/tasks/task.stories.ts + +storiesOf('Task', module) + .addDecorator(withKnobs) + .addParameters({ + assets: [ + 'path/to/your/asset.png', + 'path/to/another/asset.png', + 'path/to/yet/another/asset.png', + ], + }); +/* same as before */ +``` + +Go ahead and restart your Storybook and select the Task story, you should see something like this: + +![storybook story showing contents with design assets addon](/intro-to-storybook/create-addon-design-assets-inside-story.png) + +### Showing the actual assets + +At this stage we can see that the addon is working as it should our stories, but now let's change the `Content` component to actually display the assets: + +```javascript +//.storybook/addons/design-assets.js +import React, { Fragment } from "react"; +import { AddonPanel } from "@storybook/components"; +import { useParameter, useStorybookState } from "@storybook/api"; +import { addons, types } from "@storybook/addons"; +import { styled } from "@storybook/theming"; + +const getUrl = input => { + return typeof input === "string" ? input : input.url; + + +const Iframe = styled.iframe({ + width: "100%", + height: "100%", + border: "0 none" +}); +const Img = styled.img({ + width: "100%", + height: "100%", + border: "0 none", + objectFit: "contain" +}); + +const Asset = ({ url }) => { + if (!url) { + return null; + } + if (url.match(/\.(png|gif|jpeg|tiff|svg|anpg|webp)/)) { + // do image viewer + return ; + } + + return