Skip to main content
LocalTapiola Turva

Dev Standards #

This is a component style guide created and enforced internally by the core team of Duet Design System, for the purpose of standardizing Duet components.

This component style guide is heavily based Stencil.js’s style guide, but has been modified to suit our own needs.

File structure #

Example from @duetds/components:

├── duet-button
│   ├── duet-button.scss
│   ├── duet-button.tsx
│   ├── duet-button.spec.ts
│   ├── duet-button.e2e.ts
│   └── readme.md
├── card-card
│   ├── duet-card.scss
│   ├── duet-card.tsx
│   ├── duet-card.spec.ts
│   ├── duet-card.e2e.ts
│   └── readme.md

Naming #

HTML tag #

Prefix #

The prefix has a major role when you are creating a collection of components intended to be used across different projects, like @duetds/components. Web Components are not scoped because they are globally declared within the webpage, which means an “unique” prefix is needed to prevent collisions. The prefix is also able help to quickly identify the collection of an component. Additionally, web components are required to contain a “-” dash within the tag name, so using the first section to namespace our components is a natural fit.

DO NOT do this:

<component>
<custom-component>
<stnl-component>

Instead, use “duet” prefix for all components:

<duet-button>
<duet-nav>

Name #

Different disciplines use component names to communicate about them. Hence, they must be short, meaningful and pronounceable.

Modifiers #

When several components are related and/or coupled, it is a good idea to share the name, and then add different modifiers, for example:

<duet-card>
<duet-card-header>
<duet-card-content>

Component (TS class) #

The name of the ES6 class of the component SHOULD HAVE a prefix as well even though there is no risk of collision. This way naming stays consistent everywhere.

@Component({
  tag: 'duet-button'
})
export class DuetButton { ... }

@Component({
  tag: 'duet-menu'
})
export class DuetMenu { ... }

TypeScript #

  1. Follow @typescript-eslint/recommended

  2. Variable decorators should be inlined.

@Prop() variation: string = "default"
@Element() element: HTMLElement
  1. Event decorator should be multi-line
@Event() clicked: EventEmitter
handleClick(e: Event) {
  this.clicked.emit(e)
}
  1. Use private variables and methods as much possible: They are useful to detect deadcode and enforce encapsulation. Note that this is a feature which TypeScript provides to help harden your code, but using private, public or protected does not make a difference in the actual JavaScript output.

  2. Code with Method/Prop/Event/Component decorators should have jsdocs: This allows for documentation generation and for better user experience in an editor that has TypeScript intellisense.

Managing state #

Components in Duet Design System are “presentational” and their only responsibility is to present something to the DOM or render it into the Sketch app that the designers use. Components SHOULD NOT manage state unless it’s absolutely essential to their function (e.g. an “open” boolean for a Menu).

It’s up to the consumers of the design system (the development teams) to choose and build their applications using technology and structure(s) that best works for them.

Code organization #

Newspaper Metaphor from The Robert C. Martin's Clean Code

The source file should be organized like a newspaper article, with the highest level summary at the top, and more and more details further down. Functions called from the top function come directly below it, and so on down to the lowest level and most detailed functions at the bottom. This is a good way to organize the source code, even though IDEs make the location of functions less important, since it is so easy to navigate in and out of them.

High level example (commented) #

  @Component({
    tag: "duet-something",
    styleUrl: "duet-button.scss",
    shadow: true, //or "scoped: true" depending on the use case
  })
  export class DuetSomething {

    /**
     * 1. Own Properties
     * Always set the type if a default value has not
     * been set. If a default value is being set, then type
     * is already inferred. List the own properties in
     * alphabetical order. Note that because these properties
     * do not have the @Prop() decorator, they will not be exposed
     * publicly on the host element, but only used internally.
     */
    num: number;
    someText = "default";

    /**
     * 2. Reference to host HTML element.
     * Inlined decorator
     */
    @Element() element: HTMLElement;

    /**
     * 3. State() variables
     * Inlined decorator, alphabetical order.
     */
    @State() isValidated: boolean;
    @State() status = 0;

    /**
     * 4. Public Property API
     * Inlined decorator, alphabetical order. These are
     * different than "own properties" in that public props
     * are exposed as properties and attributes on the host element.
     * Requires JSDocs for public API documentation.
     */
    @Prop() content: string;
    @Prop() enabled: boolean;
    @Prop() menuId: string;
    @Prop() type = "overlay";

    /**
     * Prop lifecycle events go below the Prop they listen to.
     * This makes sense since both statements are strongly connected.
     * - If renaming variable you must also update name in @Watch()
     * - Code is easier to follow and maintain.
     */
    @Prop() swipeEnabled = true;
    @Watch("swipeEnabled")
    swipeEnabledChanged(newSwipe: boolean, oldSwipe: boolean) {
      this.updateState();
    }

    /**
     * 5. Events section
     * Inlined decorator, alphabetical order.
     * Requires JSDocs for public API documentation.
     */
    @Event() duetClose: EventEmitter;
    @Event() duetDrag: EventEmitter;
    @Event() duetOpen: EventEmitter;

    /**
     * 6. Component lifecycle events
     * Ordered by their natural call order, for example
     * WillLoad should go before DidLoad.
     */
    componentWillLoad() {}
    componentDidLoad() {}
    componentWillUpdate() {}
    componentDidUpdate() {}
    componentDidUnload() {}

    /**
     * 7. Listeners
     * It is ok to place them in a different location
     * if makes more sense in the context. Recommend
     * starting a listener method with "on".
     * Always use two lines.
     */
    @Listen("click", { enabled: false })
    onClick(ev: UIEvent) {
      console.log("hi!")
    }

    /**
     * 8. Public methods API
     * These methods are exposed on the host element.
     * Always use two lines.
     * Requires JSDocs for public API documentation.
     */
    @Method()
    async open(): Promise<boolean> {
      return true;
    }

    /**
     * 9. Local methods
     * Internal component logic. These methods cannot be
     * called from the host element.
     */
    prepareAnimation(): Promise<void> {
      ...
    }

    /**
     * 10. render() function
     * Always the last one in the class.
     */
    render() {
      return (
        <div class="duet-component">
          <slot />
        </div>
      );
    }
  }

Troubleshooting #

If you experience any issues while getting set up with Duet Components, please head over to the Support page for more guidelines and help.