DEV Community

Cover image for Getting Started with Microsoft's FAST Element
Burton Smith
Burton Smith

Posted on

Getting Started with Microsoft's FAST Element

If you haven’t heard already, web components have started to take off in popularity. The features they bring to the table are very compelling - framework-agnostic reusable components, strong style encapsulation, and blazingly fast performance.

A few of the more popular tools for building web component libraries include lit, StencilJS, and even the popular JavaScript frameworks can output web components now (you can play with some of them at webcomponents.dev), but for the last few weeks I have had the opportunity to work with Microsoft’s FAST Element and I am pretty impressed with it.

One thing that I struggled with as I started out, was finding a standard way to stand up dev environments so I could experiment and ideate with FAST components. The FAST team doesn’t provide a way (yet), so I went ahead and built a quick one for us to experiment with. I will be using it to generate the examples used in this article.

In your terminal or command environment run the following command and follow the instructions to set up your local environment:

npm init fast-element my-fast-components
Enter fullscreen mode Exit fullscreen mode

Creating a New Component

In the /src directory, let’s create a new folder and file called /my-search/index.ts and add our component definition to it.

import { FASTElement, customElement } from '@microsoft/fast-element';

/**
 * @tag my-search
 * 
 * @summary This is a search component
 *
 */
@customElement('my-search')
export class MySearch extends FASTElement {

}
Enter fullscreen mode Exit fullscreen mode

Here we are using FAST’s library to define and create our custom element - <my-search></my-search> - but we will also be using jsDoc to help document our component, integrate it with the Storybook, and generate our custom element manifest.

Now we can export our component from the ./src/index.ts to include it with the rest of our components in our library.

export * from './my-counter';
export * from './my-search'; // added for our new component
Enter fullscreen mode Exit fullscreen mode

Adding Stories

Storybook provides a great workspace for us to build and experiment with our components. Once we set up the initial file, our custom elements manifest will handle a lot of the heavy lifting for us.

To get started, create a file called /my-search/my-search.stories.ts and add the following contents:

import "./index";

export default {
    title: 'Components/My Search',
    component: 'my-search'
};

const Template = (args: any) => `
    <my-search></my-search>
`;

export const Default: any = Template.bind({});
Default.args = {};
Enter fullscreen mode Exit fullscreen mode

Now, we can start Storybook with the following command:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Once Storybook opens in a new browser tab, we should see a section on in the left column - Components > My Search > Default. The Default page will be blank. If you inspect the white canvas, you will see our component on the page, but since we haven’t given it anything to render the browser treats is like an empty <span> element. Let’s add some content.

Adding HTML

To add HTML to our component, let’s update out component’s class decorator in src/my-search/index.ts with the following code:

@customElement({
    name: 'my-search',
    template: html`
        <label>
            My Search
            <input type="search" />
        </label>
        <button>Search</button>
    `
})
Enter fullscreen mode Exit fullscreen mode

If your editor didn’t already, you will need to make sure you update your import statement to include the html string template decorator.

import { FASTElement, customElement, html } from '@microsoft/fast-element';
Enter fullscreen mode Exit fullscreen mode

You should now see the label, input field, and search button for our component rendered in the Default Storybook page.

Attributes or Properties

Regular HTML elements have attributes (sometimes called properties) that you can pass values to create a specific behavior. For example, the input element has attributes like type, name, value, and disabled. Those values will make the element look and behave a certain way. Since we are creating custom elements, we can define our own attributes and map them to a behavior.

Let’s start with making it possible to change the label for the input field. FAST uses the @attr decorator to identify these fields. We can add it to our component class along with the type and default value.

export class MySearch extends FASTElement {
    @attr label: string = 'My Search';
}
Enter fullscreen mode Exit fullscreen mode

Again, you will need to update import statement to include the new attr decorator.

import { FASTElement, customElement, html, attr } from '@microsoft/fast-element';
Enter fullscreen mode Exit fullscreen mode

Also, make sure to update the jsDoc comment above the class so that the values get defined in the custom element manifest and synced up with Storybook.

/**
 * @tag my-search
 * 
 * @summary This is a search component
 *
 * @attr {string} label - the label associated with the search field
 * 
 */
Enter fullscreen mode Exit fullscreen mode

Binding Attributes to Templates

To help provide some autocomplete functionality, we can add our component’s class as a type on our template string.

template: html<MySearch>`
Enter fullscreen mode Exit fullscreen mode

Now, let’s replace the “My Search” text with the value provided in the attribute field. We can do this with some template string interpolation and an arrow function that returns our attribute (calling attributes and methods using this arrow function syntax is important for efficient template updates).

template: html<MySearch>`
    <label>
        ${x => x.label}
        <input type="search" />
    </label>
    <button>Search</button>
`
Enter fullscreen mode Exit fullscreen mode

Adding Attributes in Storybook

If we update the template in our my-search.stories.ts with a label attribute and value, we should see it reflected in our Storybook page.

const Template = (args: any) => `
    <my-search label="Site Search"></my-search>
`;
Enter fullscreen mode Exit fullscreen mode

Rather than making a code change every time we want to see different values passed to our component, we can leverage Storybook controls and interact with our components though the UI.

const Template = (args: any) => `
    <my-search label="${args.label}"></my-search>
`;
Enter fullscreen mode Exit fullscreen mode

For some reason the default value is not automatically defined, so we will need to add it to the args section of our default export.

export default {
    title: 'Components/My Search',
    component: 'my-search',
    args: {
       label: 'My Search' 
    }
};
Enter fullscreen mode Exit fullscreen mode

In the “Addons” panel under the “Controls” tab you should see a section called “Properties” with an input for the label attribute.

If you don’t see the “Addons” panel to the right or bottom of your component, click the menu button in the upper-left side of the page and select “Show addons”. We will be using that a lot.

Slots

Attributes are a great way to pass data like strings, numbers, objects, and arrays into components, but sometimes you need to be able to pass markup or HTML into a component. That’s exactly what slots are for. It’s basically a placeholder for your HTML to go.

In our component, we will use a slot so we can pass content into our search button. Let’s start by updating our template with a slot inside of the button. We can also define default content by adding it between the opening and closing <slot> tags. If slotted content exists, it will override our default slot content.

template: html<MySearch>`
    <label>
        ${x => x.label}
        <input type="search" />
    </label>
    <button>
        <slot>Search</slot>
    </button>
`
Enter fullscreen mode Exit fullscreen mode

Let’s also update our jsDoc comments above our component to include it in our custom elements manifest.

/**
 * @tag my-search
 * 
 * @summary This is a search component
 *
 * @attr {string} label - the label associated with the search field
 * 
 * @slot default - content displayed in the search button
 * 
 */
Enter fullscreen mode Exit fullscreen mode

You should now see a new section in your Storybook controls called “Slots”. Now we can wire that attribute up in our template in the my-search.stories.ts file as well as a default value for the argument.

export default {
    title: 'Components/My Search',
    component: 'my-search',
    args: {
       label: 'My Search',
       default: 'Search'
    }
};

const Template = (args: any) => `
    <my-search label="${args.label}">${args.default}</my-search>
`;
Enter fullscreen mode Exit fullscreen mode

New we can pass in any value we want like “Submit” or an emoji (“😉”). We can even create a new template in Storybook and pull in an icon library.

// using Bootstrap icons - https://icons.getbootstrap.com/
const IconTemplate = (args: any) => `
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.0/font/bootstrap-icons.css" />
    <my-search label="${args.label}">${args.default}</my-search>
`;

export const Icon: any = IconTemplate.bind({});
Icon.args = {
    default: "<i class=\"bi bi-search\"></i>"
};
Enter fullscreen mode Exit fullscreen mode

Named Slots

If you would like to have multiple areas where you can add markup to your components, you can use named slots.

Events

We use attributes and slots to pass data into our components, but sometimes we need to get data out of our components. We can do this through emitting events.

We interact with native HTMl element events all of the time - onClick, onInput, onBlur, etc. FAST makes this pretty easy for us using the $emit() method provided in the FASTElement class our component inherits from.

Listening for Events

In our component, we want to emit an event any time a user triggers the search event on our input or clicks on our search button. To do this, let’s add two event handler methods to our component class that will emit our own “find” event.

export class MySearch extends FASTElement {
    @attr label: string = 'My Search';

    private searchHandler() {
        this.$emit('find');
    }

    private buttonClickHandler() {
        this.$emit('find');
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we can wire the methods up to our component’s template.

template: html<MySearch>`
    <label>
        ${x => x.label}
        <input type="search" @search="${x => x.searchHandler()}" />
    </label>
    <button @click="${x => x.buttonClickHandler()}">
        <slot>Search</slot>
    </button>
`,
Enter fullscreen mode Exit fullscreen mode

As you can see, FAST uses a different syntax for listening for events on elements. Rather than the normal onSearch or onClick, they use @search and @click. If you have ever used Vue.js, then FAST’s attribute and event binding syntax should look familiar.

Now we should be able to listen for an onFind event on our <my-search> element. You can do this by adding an onFind attribute to your element or using JavaScript to select our element and adding an event listener.

// select the element from the DOM
const mySearch = document.querySelector('my-search');

// add an event listener for our custom event and log it to the console
mySearch.addEventListener('find', e => console.log(e));
Enter fullscreen mode Exit fullscreen mode

Capturing Events in Storybook

Rather than having to constantly add event listeners to our code or to the console any time we want to test our events, we can wire them up in Storybook and it will capture it for us. In our story’s default export, we add a new parameters property to the object where we can define our custom events.

export default {
    title: 'Components/My Search',
    component: 'my-search',
    args: {
       label: 'My Search',
       default: 'Search',
       ['--font-size']: '1rem',
       ['--padding']: '0.25rem'
    },
    parameters: {
        actions: {
            handles: ['find'],
        },
    },
};
Enter fullscreen mode Exit fullscreen mode

Now can we can see the event logged in the “Actions” tab along with the event information whenever our filter event is emitted.

Using Directives

The last thing we need to do is to add our input value to the emitted event so we can use it. We can select an element within our custom element using querySelector.

const input = this.shadowRoot.querySelector('input');
Enter fullscreen mode Exit fullscreen mode

There’s nothing wrong with this approach, but FAST provides us with a number of directives that make common tasks simpler. In this case we can use the [ref() directive](https://www.fast.design/docs/fast-element/using-directives#the-ref-directive) to reference the element in the component’s context (this).

First, let’s add ref('searchInput') to our input element (make sure you import ref from @microsoft/fast-element).

template: html<MySearch>`
    <label>
        ${x => x.label}
        <input type="search" ${ref('searchInput')} @search="${x => x.searchHandler()}" />
    </label>
    <button @click="${x => x.buttonClickHandler()}">
        <slot>Search</slot>
    </button>
`,
Enter fullscreen mode Exit fullscreen mode

Next, we can add a property to our class that matches the string in our ref and assign it the type HTMLInputElement.

export class MySearch extends FASTElement {
    @attr label: string = 'My Search';
    searchInput: HTMLInputElement;
Enter fullscreen mode Exit fullscreen mode

Finally, let’s emit the input value of the input box with the search event.

private searchHandler() {
    this.$emit('find', this.searchInput.value);
}

private buttonClickHandler() {
    this.$emit('find', this.searchInput.value);
}
Enter fullscreen mode Exit fullscreen mode

When we go back to Storybook, input a value, and press the “Enter” key or click the search button, we should now see the input’s value under the detail property of the event data.

{
    bubbles: true,
    cancelBubble: false,
    cancelable: true,
    composed: true,
    currentTarget: HTMLDivElement,
    defaultPrevented: false,
    detail: "ergferf",      // <--
    eventPhase: 3,
    isTrusted: false,
    returnValue: true,
    srcElement: MySearch,
    target: undefined,
    timeStamp: 22556.699999928474,
    type: "find"
}
Enter fullscreen mode Exit fullscreen mode

Styling

I am planning on creating a separate post dedicated to styling web components, so this will be a basic overview of how to add styling to FAST elements.

We add styles by adding a styles property to our component definition class decorator and prefixing our template string with css.

@customElement({
    name: 'my-search',
    template: html<MySearch>`
        <label>
            ${x => x.label}
            <input type="search" />
        </label>
        <button>
            <slot>Search</slot>
        </button>
    `,
    styles: css``
})
Enter fullscreen mode Exit fullscreen mode

We will also need to make sure we import css from FAST.

import { FASTElement, customElement, html, attr, css } from '@microsoft/fast-element';
Enter fullscreen mode Exit fullscreen mode

Let’s add some basic styling and then we can break it down.

styles: css<MySearch>`
    :host {
        --font-size: 1rem;
        --padding: 0.25rem;

        font-size: var(--font-size);
        display: block;
    }

    input {
        font-size: var(--font-size);
        padding: var(--padding);
    }

    button {
        cursor: pointer;
        font-size: var(--font-size);
        padding: var(--padding);
    }
`
Enter fullscreen mode Exit fullscreen mode

:host

The first thing you may have noticed is the strange :host selector. This targets our custom element’s tag - <my-search>. This allows us to apply styles to the tag as well as define global styles for the element.

Custom elements apply the display: inline; style by default, so in our case we added the display: block; to ensure this would render the full width of the element’s parent.

Generic Selectors

You may have also noticed that we have HTML elements input and button as selectors. Don’t freak out, this was intentional. One of the nice things about the Shadow DOM is that it provides a layer of encapsulation. This means the component isn’t affected by the styles outside of the component and the component’s styles don’t bleed out to the rest of the application. We don’t have to worry about these styles affecting any of the other inputs or buttons in our application.

CSS Custom Properties or CSS Variables

Since we cannot affect the styles of our components externally, we can add CSS Custom Properties (aka - CSS Variables) to our component to provide some customizations. With these defined, we can apply targeted custom styles to our components.

my-search {
    --font-size: 1.5rem;
    --padding: 1rem;
}
Enter fullscreen mode Exit fullscreen mode

Or even with inline styles in our markup:

<my-search style="--font-size: 1.5rem;--padding: 1rem;"></my-search>
Enter fullscreen mode Exit fullscreen mode

Adding Custom Properties to Storybook

First, we will need to update our jsDoc with our new custom properties to include them in our custom elements manifest.

/**
 * @tag my-search
 * 
 * @summary This is a search component
 *
 * @attr {string} label - the label associated with the search field
 * 
 * @slot default - content displayed in the search button
 * 
 * @cssprop [--font-size=1rem] - Controls the font size for all elements in the component
 * @cssprop [--padding=0.25rem] - Controls the padding for the `input` and `button` elements
 * 
 */
Enter fullscreen mode Exit fullscreen mode

We should now see a new section in controls tab called “CSS Custom Properties” with our properties listed. Let’s add some default values to our story’s default export.

export default {
    title: 'Components/My Search',
    component: 'my-search',
    args: {
       label: 'My Search',
       default: 'Search',
       ['--font-size']: '1rem',
       ['--padding']: '0.25rem'
    }
};
Enter fullscreen mode Exit fullscreen mode

Now, let’s wire them up to our Storybook template (feel free to do the same with our Icon template).

const Template = (args: any) => `
    <style>
        my-search {
            --font-size: ${args['--font-size']};
            --padding: ${args['--padding']};
        }
    </style>
    <my-search label="${args.label}">${args.default}</my-search>
`;
Enter fullscreen mode Exit fullscreen mode

Next Steps

Having your markup, styles, and logic all in the same file may not be desirable, especially for complex components. You can split these out into separate files. Check out the my-counter component included in the project as an example.

You can view the completed code here.

Conclusion

You now have everything we need to get started with building components, but this is just scratching the surface of what web components and FAST Element can do. If you are interested in learning more, check out their documentation and join their discord.

Oldest comments (2)

Collapse
 
maxart2501 profile image
Massimo Artizzu

FAST is usually quite nice, but there are some issues that make the experience suboptimal:

  • the official documentation is lackluster and unrefined;
  • you have to return true on every event listener, or the event is cancelled;
  • the documentation needs to be improved;
  • if you want to bind a listener or an attribute to the host element, you can wrap your template in a <template> tag, which is kinda deceptive as it isn't an actual <template> element;
  • the documentation... is really poor;
  • because of how FAST handles things, you can't use a repeat directive directly inside a when, you have to wrap it in an element;
  • geez, the documentation needs some loving ASAP!
  • you can't make a bundle of your components, as FAST is designed to work with ES modules only;
  • ... have I mentioned the documentation?
Collapse
 
stuffbreaker profile image
Burton Smith

I hear you on the the documentation (hence this little write-up 😉 ).