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 #
- One component per file.
- One component per directory. Though it may make sense to group similar components into the same directory, we’ve found it’s easier to document components when each one has its own directory.
- Implementation (.tsx) and styles of a component should live in the same directory.
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-header>
Name #
Different disciplines use component names to communicate about them. Hence, they must be short, meaningful and pronounceable.
- Noun rather than verb: Components are not actions, they are conceptually “things.” It is better to use nouns instead of verbs, such as “animation” instead of “animating.” “input,” “tab,” “navigation,” and “menu” are some examples.
- Meaningful: Not over specific, not overly abstract.
- Short: Maximum of 2 or 3 words.
- Pronounceable: We want to be able talk about them.
- Custom element spec compliant: Don’t use reserved names. Reserved names include: annotation-xml, font-face, font-face-src, font-face-uri, font-face-format, font-face-name, missing-glyph, color-profile.
- Examples:
<duet-button>, <duet-date-picker>, <duet-header>
.
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 #
-
Variable decorators should be inlined.
@Prop() variation: string = "default"
@Element() element: HTMLElement
- Event decorator should be multi-line
@Event() clicked: EventEmitter
handleClick(e: Event) {
this.clicked.emit(e)
}
-
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
orprotected
does not make a difference in the actual JavaScript output. -
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";
/**
* If a watcher function is strongly connected to a single property,
* then it should be placed directly below the Prop it listens to.
*/
@Prop() swipeEnabled = true;
@Watch("swipeEnabled")
swipeEnabledChanged(newSwipe: boolean, oldSwipe: boolean) {
// do something when swipeEnabled changes
}
/**
* If a single function needs to be called when any one of
* multiple properties change, then the Watch decorators
* can be stacked as below.
*/
@Watch("content")
@Watch("menuId")
@Watch("type")
multiplePropertiesChanged() {
// do something when any of the above properties change
}
/**
* 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.
*/
connectedCallback() {}
componentWillLoad() {}
componentDidLoad() {}
componentWillUpdate() {}
componentDidUpdate() {}
disconnectedCallback() {}
/**
* 7. Component event handling
* 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></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.