Lab 09: Input And Form Element - andreaswissel/design-systems-workshop GitHub Wiki
Hint: if you got lost, you can always check out the corresponding branch for the lab. If you want to start over, you can reset it by typing executing git reset --hard origin/lab-8
In the last lab, the first composite component has crept into our design system - without us even noticing it! The ButtonIcon variant has united the IconComponent with the ButtonComponent to form a new button spice.
In this lab, we will set us up another composite component. Thanks to the magic of standalone components, we don't even need to handle any dependencies in Storybook.
But first, let's start on the atomic level, with the InputComponent
- Generate the InputComponent
ng g c --standalone input
- Implement the input component according to the specification in the Figma reference library
Locate the Input
component in the Figma reference library and implement the InputComponent
.
Try to figure out the component API on your own!
Hint: These are the component inputs
src/app/input/input.component.ts
@Input() inputType: InputType = InputType.text; // You can find the enum definition in the model solution
@Input() placeholder: string | undefined;
@Input() value: string | undefined;
@Input() id?: string;
Once you're done, the InputComponent should be accessible through Storybooks controls and look like this:
- Generate the FormElementComponent
Next, we want to generate a component that wraps the input and adds a label to it. Since it should augment all the properties of the input, make sure to include the same component inputs.
Hint: This can be checked at compile time by implementing an interface!
ng g c --standalone form-element
- Implement hover and focus state
Much like the InputComponent, the FormElementComponent has various states. Using the :host
selector in our SCSS file, we can react to events on the host component (<app-form-element>
) and style the label accordingly:
@import "../../styles/colors";
@import "../../styles/typo";
:host {
display: flex;
flex-direction: column;
label {
@include body-typo();
cursor: pointer;
padding-bottom: 10px;
}
&:hover,
&:focus {
label {
color: $blue-400;
}
}
}
- Accessibility: Generate a unique id for each input to make labels clickable
Taking a first step towards accessible components is easy with the FormElementComponent. All we have to do is configure a unique ID that is then used for the id
attribute of the InputComponent, as well as the for
attribute of the label. The browser will then do the rest of the work for us and link these elements up, so that screenreaders and keyboard users can access this component and it's respective information more easily.
In modern browsers, we can use crypto.randomUUID()
to generate a UUID:
export class FormElementComponent implements InputApi {
+ uuid: string;
@Input() inputType: InputType = InputType.text;
@Input() placeholder?: string;
@Input() value?: string;
@Input() label = 'Input Label';
constructor() {
+ this.uuid = crypto.randomUUID();
}
}
Once everything is linked up, your :hover
and :focus
should be toggleable by the label and input alike:
- Docs: Set specific argTypes to make controls more convenient
Generally speaking, Storybook does a pretty good job of guessing the right control types from the component's inputs. In some cases, however, we need to help out a little and specify the control types manually.
This can be achieved by using the argTypes
property on the story:
export const Default: StoryObj<InputComponent> = {
args: {},
argTypes: {
placeholder: { control: 'text' },
value: { control: 'text' },
},
};
This is helpful, when input values are optional and therefore could be undefined. We can then tell Storybook what control we need most often - in this case a simple text
input.
You might have noticed now, that the text in the input labels looks not very much like the one in Figma. That's because Storybook doesn't know anything about our global styles.scss
yet. Let's change that!
- The InputComponent with it's
hover
andfocus
states is implemented in Storybook - The InputComponent's
@Input
's can be controlled via Storybook - The FormElementComponent with it's
hover
andfocus
states is implemented in Storybook - The FormElementComponent's
@Input
's can be controlled via Storybook
Input HTML file
src/app/input/input.component.html
<input
[type]="inputType"
[placeholder]="placeholder"
[value]="value"
[attr.id]="id"
/>
Input SCSS file
src/app/input/input.component.scss
@import "../../styles/colors";
@import "../../styles/effects";
:host {
display: flex;
}
input {
background: $white;
height: 40px;
border: 1px solid $gray-200;
box-sizing: border-box;
border-radius: 10px;
color: $text-default;
padding: 12px;
&:hover {
@include hover-shadow();
}
&:focus {
@include hover-shadow();
border-color: $blue-400;
outline: none;
}
}
Input component file
src/app/input/input.component.ts
import { Component, Input } from '@angular/core';
export enum InputType {
button = 'button',
checkbox = 'checkbox',
color = 'color',
date = 'date',
datetimelocal = 'datetime-local',
email = 'email',
file = 'file',
hidden = 'hidden',
image = 'image',
month = 'month',
number = 'number',
password = 'password',
radio = 'radio',
range = 'range',
reset = 'reset',
search = 'search',
submit = 'submit',
tel = 'tel',
text = 'text',
time = 'time',
url = 'url',
week = 'week',
}
export interface InputApi {
inputType: InputType;
placeholder?: string;
value?: string;
}
@Component({
selector: 'app-input',
standalone: true,
imports: [],
templateUrl: './input.component.html',
styleUrl: './input.component.scss',
})
export class InputComponent {
@Input() inputType: InputType = InputType.text;
@Input() placeholder: string | undefined;
@Input() value: string | undefined;
@Input() id?: string;
}
Input stories file
src/app/input/input.stories.ts
import { InputComponent, InputType } from './input.component';
import { Meta, StoryObj } from '@storybook/angular';
const meta: Meta<InputComponent> = {
title: 'Components/Input',
component: InputComponent,
tags: ['autodocs'],
};
export const Default: StoryObj<InputComponent> = {
args: {
inputType: InputType.text,
placeholder: 'This is a placeholder',
value: '',
id: 'uuid',
},
argTypes: {
placeholder: { control: 'text' },
value: { control: 'text' },
},
};
export default meta;
FormElement HTML file
src/app/form-element/form-element.component.html
<label [for]="uuid">{{ label }}</label>
<app-input
[placeholder]="placeholder"
[inputType]="inputType"
[value]="value"
[id]="uuid"
></app-input>
FormElement SCSS file
src/app/form-element/form-element.component.scss
@import "../../styles/colors";
@import "../../styles/typo";
:host {
display: flex;
flex-direction: column;
label {
@include body-typo();
cursor: pointer;
padding-bottom: 10px;
}
&:hover,
&:focus {
label {
color: $blue-400;
}
}
}
FormElement component file
src/app/form-element/form-element.component.ts
import { Component, Input } from '@angular/core';
import { InputApi, InputComponent, InputType } from '../input/input.component';
@Component({
selector: 'app-form-element',
standalone: true,
imports: [InputComponent],
templateUrl: './form-element.component.html',
styleUrl: './form-element.component.scss',
})
export class FormElementComponent implements InputApi {
uuid: string;
@Input() inputType: InputType = InputType.text;
@Input() placeholder: string = 'Form Element Placeholder';
@Input() value = '';
@Input() label = 'Input Label';
constructor() {
this.uuid = crypto.randomUUID();
}
}
FormElement stories file
src/app/form-element/form-element.stories.ts
import { Meta, StoryObj } from '@storybook/angular';
import { FormElementComponent } from './form-element.component';
import * as InputStories from '../input/input.stories';
const meta: Meta<FormElementComponent> = {
title: 'Components/FormElement',
component: FormElementComponent,
tags: ['autodocs'],
};
export const Default: StoryObj<FormElementComponent> = {
args: {
...InputStories.Default.args,
label: 'Form Element Label',
},
};
export default meta;
Hint: if you got lost, you can always check out the corresponding branch for the lab. If you want to start over, you can reset it by typing executing git reset --hard origin/lab-9