Coding Standards

The Morningstar Design System team adheres to standard coding practices and tools to build components and other features.

Document Structure

  • <!DOCTYPE html>: Use a doctype to force browsers to render in standards mode and prevent quirks-mode problems in Internet Explorer.
  • <html lang="en-us">: Set a lang attribute to assist browsers and search engines.
  • <meta charset="UTF-8">: Use UTF-8 encoding.
  • <meta http-equiv="X-UA-Compatible" content="IE=Edge">: Use the latest supported Microsoft browser mode.

Semantics

The stand-alone HTML should have value and meaning.

  • If you are wrapping paragraph text with an element, use a <p> tag.
  • If it's tabular data, use a <table> tag.
  • If you have an unordered list, use a <ul> with nested <li>s wrapping each item.
  • If we write HTML using meaningful tags, we won’t have to rewrite the markup when the UI changes.
  • Use HTML 5 tags according to the spec for semantic purposes.

Syntax Style

  • Lowercase tag names.
  • Lowercase attribute names.
  • Use four spaces (not tabs) for indentation.
  • Use double (not single) quotation marks for attribute values.
  • Don't include a trailing slash in self-closing elements – the HTML5 spec says they're optional.
  • Always include optional closing tags (i.e.,</li> or </body>).

Best Practices

  • Use as few <span>s and <div>s as possible.
  • Account for responsiveness. If a component requires responsive grid classes to render properly on smaller devices, apply those classes properly in demonstrations on a sink page and in documentation.
  • Minimize class=”” properties according to BEM methodology.
  • Use id=”” names very sparingly and only if:
    • Using a label with a for=”” attribute
    • Using a third-party library that relies on id=””s to target specific elements
  • Never use inline styles unless leveraging a third-party framework that relies on inline styling (e.g. a tooltip/popover lib that positions the element using inline top, right, bottom, or left properties).
  • Attribute order:
    • Put class attribute first (engineers read/write this most often).
    • For all other attributes, add them in the order that makes most sense to you, but keep in mind that the more consistent the attribute order, the easier the code will be to inherit.
    • Exception to class attribute first rule: When an attribute is critical to defining an element, put it first to improve readability – <input type="text" class="" id="" disabled name="" placeholder="" readonly value="">.

Reference

The MDS component library is written using BEM CSS Methodology.

Principles

  • Use longhand syntax wherever appropriate for properties. This will avoid accidental CSS resetting.
  • Use class name hyphenations and underscores according to BEM methodology.
  • Code to a standards-compliant browser first, and then fix issues in IE.
  • Keep selectors as flat as possible with regard to chaining or nesting.
  • Never style raw HTML elements. Instead, use a class.

Syntax Style

The MDS team uses sass-lint to assist in code style maintenance. For more detail you can read our full list of linting rules

  • Use spaces, not tabs, for indentation.
  • Include one space after a colon for each declaration.
  • Include one space after a selector.
  • Put each declaration on its own line for more accurate error reporting.
    • Opening curly-brace followed by new line
    • Closing curly-brace on new line
  • Use a blank line between rules.

Readability

  • Alphabetize properties.
  • End all declarations with a semicolon.
  • Comma-separated property values should include a space after each comma, i.e., box-shadow.
  • Include spaces after commas within rgb(), rgba(), hsl(), hsla(), or rect() values.
  • Don't prefix property values or color parameters with a leading zero, i.e., .5 instead of 0.5 and -.5px instead of -0.5px.
  • Lowercase all hex values, e.g., #ffffff. Lowercase letters are much easier to discern when scanning a document as they tend to have more unique shapes.
  • Don’t use shorthand hex values; instead spell out all 6 characters #ffffff.
  • Quote attribute values in selectors, i.e., input[type="text"]. They’re only optional in some cases and it’s a good practice for consistency.
  • Avoid specifying units for zero values, i.e., margin: 0; instead of margin: 0px;.

Fonts

The System does not use an icon font, but rather an SVG with an external reference technique. We reference custom fonts using @font-face and referencing a .eot and a fallback .woff.

Example

@font-face {
    font-family: "Univers";
    font-style: normal;
    font-weight: 300;
    src: url("../fonts/webfont-name.eot");
    src: url("../fonts/webfont-name.eot?#iefix") format("embedded-opentype"), url("../fonts/webfont-name.woff") format("woff");
}

Base Component

Each of the MDS web components must extend the MdsBaseComponent. This component contains common functionality for validation of props, connecting to MWC libraries, and rendering templates. The base component is implemented as an extensible ES6 class:

class MdsInput extends MdsBaseComponent {
    ...
}

Custom Element Names

Per the W3C specification for custom elements, all custom elements must include a hyphen "-" in the name. This is how browsers distinguish native HTML elements from custom elements when parsing.

  • Prefix all custom element names with mds-.
  • Any component names that contain two or more words should use a hyphen between each word, for example "Data Table" would become <mds-data-table>.
  • Custom element names must be unique.

Props

Guidelines

  • Props are always kebab-case when written as custom element attributes in HTML.
  • Props are always camelCase when referenced in custom element Javascript.
  • All components must render something to the page when called with no props. This allows for rapid prototyping and reduces debugging time for implementers.
  • Boolean props should not use is in the name.
    • For example, use visible instead of isVisible.
  • Don't use multiple props when a single prop will suffice.
    • For example, don't have both an iconVisible prop and an iconName prop, simply allow the presence of iconName to dictate whether or not an icon is visible in the component.
  • When a component contains a both a slotted area and a prop that control the same part of the component's template, the prop's content will always take precedence.
    • <mds-button text="This text will win">This text will lose</mds-button>
  • When a prop and a slot control the same part of a component's template the prop and the slot should use the same name.
  • If the component has a single, unnamed, default slot, the component must include a slotPropOverrideMapping that determines which of the component’s props can be used to override content passed via the default slot.

Required Props

All components must support the following props:

  • class - receives a space-separated list of CSS classes to be added to the class attribute of the custom element
  • id - sets the id attribute of the custom element

Naming Conventions

  • When a prop maps directly to an element's HTML attribute, use the same name as the HTML attribute.
    • For example, <mds-input>'s value attribute maps directly to the <input/> element's value attribute in the component’s template.
  • When choosing prop names, assess existing MDS web component API's to find similar terms and follow those same conventions to foster alignment across the library.
    • For example, if <mds-checkbox> has a labelHidden boolean property, <mds-switch> should use that same prop name and not something similar like labelVisible or textHidden.
  • Use nouns for props that signify a feature or attribute of a component. Use past tense verbs for props that signify the state a component can exist in. Test that your propNames comply with this rule by fitting them in these sentences:
    • Features or attributes: [Component] has [propName].
      • Checkbox has a hiddenLabel.
      • Link has an underline.
      • Card has supplementalContent.
      • Button has text.
      • Button has a leftIcon.
    • State: [Component] is [propName].
      • Checkbox is checked.
      • Link is disabled.
      • Modal is hidden.
  • Do not use verbs within propNames
    • Instead of showImage use visibleImage - fulfills the pattern: "[Component] has a visible image."
    • Instead of hideLabel use hiddenLabel - fulfills the pattern: "[Component] has a hidden label."
  • Do not use negatives within propNames
    • Instead of noUnderline with a default of true, use underline with a default of false - fulfills the pattern: "[Component] has an underline."
    • Instead of preventVisitedStyling with a default of true, use visitedStyling with a default of false.

Types

Every prop must specify a type.

  • The following prop types will be validated by the MdsBaseComponent:
    • String
    • Number
    • Array
    • Object

In addition, an "Enum" prop type (like React's) can be constructed by using the String or Number prop type and providing an array of acceptable values. See the prop definition syntax examples below.

Default Values

  • Whenever possible specify a default value for a prop
  • Use default values to construct a "no prop" example. This is what renders on the page when no prop values are passed in by the implementer.

Validation

  • All props must specify a type. The base component will throw a console error if an incorrectly typed value is passed to a component.
  • In addition to type validation, a values key containing an array of valid values can be added to the prop declaration. The base component will throw a console error if a value is provided that does not exist in the values array.
  • Some props are only valid when another prop is set to a specific value. For example, the checked prop on <mds-button> is invalid unless the el prop is set to radio or checkbox. If checked is passed to <mds-button> without changing el the base component will throw a console error.
  • Additional prop validators will be added to the base component as new validation requirements emerge.
  • Custom validators may be needed for some props. The base component allows for a custom validator to be passed in following the Vuejs custom validator syntax.

Prop Definition

class MdsInput extends MdsBaseComponent {
  static get defaultProps() {
    return {
      // Simple String type validation
      ariaDescribedby: {
        type: String
      },
      // Simulated "Enum" prop type, String plus an array of valid values.
      autocapitalize: {
        type: String,
        values: ['off', 'none', 'on', 'sentences', 'words', 'characters'],
        default: 'off'
      },
      autocorrect: {
        type: String,
        values: ['on', 'off'],
        default: 'off'
      },
      // Simple Boolean type validation
      autofocus: {
        type: Boolean
      },
      class: {
        type: String
      },
      // Boolean that defaults to false
      disabled: {
        default: false,
        type: Boolean
      },
      // Custom validation
      minValue: {
        default: false,
        type: String,
        validator: function(value) {
            return value > 1
        }
      }
      ...
    }
  }
...
}

Default Props

MDS Web Components render something when invoked without any props. This default rendering is defined by configuring default values for some of the component’s props. For example:

<mds-button></mds-button>

Should render a primary, medium-sized button with no icons and text that reads "Button Text." To create the default rendering for a component, try to meet a consumer’s expectation for what that component should look like when rendered on the page. For example:

<mds-button></mds-button>

Should not render a large, icon-only, flat button. Often consumers will invoke a component without any props to test the component’s default behavior within their application.

Slots

Slots are sections within a component’s template that can be injected with content contained between the custom element tags.

Examples

Default Slot

The text for a button can be passed to the component within the custom element tags like this:

<mds-button> Send an email </mds-button>

In the MdsButton class we define the template like so:

static get template() {
  return `
    <button class="mds-button">
      <slot>Default button text</slot>
    </button>
  `.trim();
}

Named Slots

The optional text variation of the label component contains two types of content: the label text and the optional text. Rather than defining every possible type of content within the label's template and controlling that content via props, a more flexible solution is to use <slot>s and allow implementers to insert their own content as needed into the label.

MdsLabel template pseudocode:

<label class="mds-form__label">
    <slot>${this.text}</slot>
    <span class="mds-form__label-optional"><slot name="optional-text">${optionalText}</slot></span>
</label>

Usage - your-product.html:

<mds-label for="profession-select" optional="true"> Profession <slot name="optional-text">(Optional)</slot>
</mds-label>

Slots & Props

Every slottable portions of a component's template must also be controllable via props. This allows MDS web components to fully comply with MWC's "app config always wins" principle. To adhere to this principle a prop's value will always override anything passed in via a slot. For example, an <mds-button> can receive its text via the default slot or via a text prop. If both are provided, the text prop will win.

Naming Conventions

In the case of a default or "unnamed" slot, the component must be configured with a slotPropOverride that tells the template which prop to check before rendering slot content.

class MdsButton extends MdsBaseComponent {
  static get defaultProps() {
    ...
  }

  static get slotPropOverrideMappings() {
    return {
      default: ['text'] // prop(s) that will override the default slot
    }
  }
}

For named slots, the name attribute on the <slot> element in the template must match the prop name that can override the slot.

Prop Override Mappings

In addition to the default prop override behavior that’s required for any slot, there may be additional props whose presence should prevent slot content from rendering. These conditions can be configured via the slotPropOverrideMappings getter in the component definition. This getter should return an object where the keys are the names of slots in the template and the values are an array containing one or more props. If the props specified in the array are set to anything except their default values, then the slot specified by the key will not be rendered.

In the following example, a slot named "supplementalContent" will not be rendered if the prop "imageSrc" or the prop "graph" is set to anything other than their default values.

static get slotPropOverrideMappings() {
    return {
        supplementalContent: ['imageSrc', 'graph'] // will override named slot
    }
}

The "supplementalContent" slot can also be overridden via a prop called supplementalContent. Any prop matching the name of a named slot is automatically added to the slotPropOverrideMappings. So even though the array specified is this:

[‘imageSrc’, ‘graph’]

The props checked in the rendering process are actually:

[‘imageSrc’, ‘graph’, ‘supplementalContent’]

Methods

Methods allow implementers to access component state and trigger component functionality.

Instance Methods

MDSWC components should define all functionality via instance methods. For example, given the following custom element markup:

<mds-modal id="pirate-ninja-modal">
    <h1>Ninjas vs. Pirates</h1>
</mds-modal>

The component must use an instance method to open the modal:

const pirateNinjaModal = document.getElementById('pirate-ninja-modal');
pirateNinjaModal.open();

Not a static class method:

MdsModal.open('pirate-ninja-modal');

Instance methods make it easier to reason about the objects you're dealing with in script and allows components to be "responsible" for their own actions and state.

Naming Conventions

  • Methods should use succinct, present tense verbs to describe the action being performed and the desired result.
    • For example, prefer open() to toggleVisibility()
    • Prefer advanceStep() to goToNextStep()
  • Avoid using the words get and set in method names. All component props have getters and setters defined by default in the base component. If you need to trigger specific functionality based on a prop's value changing, use the custom element spec attributeChangedCallback method to monitor that specific prop and perform the functionality needed.

Static Methods

  • Avoid using static methods.
  • A static getter method called defaultProps() is required to be defined. This is where all the default prop values and validators are configured and this method is referenced by the base component to do initial component construction and prop validation.

Events

Native DOM Events

Custom elements, by definition, create an additional DOM element around their inner HTML contents. For this reason, a simple custom element, like an input, will have a DOM structure that looks like this:

<mds-input>
    <input class="mds-form__input" />
</mds-input>

Most native DOM events triggered on the <input/> will bubble up to the parent <mds-input> element so that event listeners can be bound to the parent element without issue:

const input = document.querySelector('mds-input');
input.addEventListener('keyup', (e) => { console.log(`The input's value is ${e.target.value}`) })

There are a small number of events that do not bubble. In those cases, the MDS standard is to listen for those events within the custom element’s class and re-trigger them at the level of the custom element’s outer wrapper. Two events that do not bubble are the focus and blur events for input elements. To allow end users to bind event listeners to the mds-input component consistently, the blur and focus events for the underlying <input/> are listened for and re-triggered at the <mds-input> level.

mds_input.js

class MdsInput extends MdsBaseComponent {
  ...
  bindEventHandlers() {
    const input = this.querySelector('input');
    input.addEventListener('blur', (e) => {
      // the 'blur' event does not bubble,
      // trigger a blur event on the parent element
      var event = document.createEvent('HTMLEvents');
      event.initEvent('blur', false, false);
      this.dispatchEvent(event);
    });
  }

  render() {
    this.innerHTML = this.template;
    this.bindEventHandlers();
  }
}

customElements.define('mds-input', MdsInput);

This "forced bubbling" pattern will be implemented on a case-by-case basis for components that are expected to trigger native DOM events. When building a component refer to the MDN Web Events documentation to research which events your component should trigger and whether or not you need to add any "forced bubbling" code to your component’s class.

Custom Events

Web components will trigger custom events when a meaningful state change has occurred within the component, examples include but are not limited to:

  • A modal has opened or closed
  • A data table’s data has been refreshed/reloaded
  • A stepper’s active step has been changed

Web components will not trigger custom events with every prop or attribute value change, nor when a component is re-rendered. If monitoring for change at that level is required a MutationObserver can be used by the consuming product team.

Custom events will be triggers on the parent custom element wrapper. This means a "modal opened" event will be triggered on the <mds-modal/> element. Components will not "broadcast" events by triggering them at the document or window level.

Naming Conventions

Custom event names will follow the pattern of mds-[component-name]-[event-name], where [component-name] is the name of the MDS component triggering the event and [event-name] is a past tense verb describing what happened.

When a modal is opened a mds-modal-opened event should be triggered. When a stepper step has changed a mds-stepper-step-activated event should be triggered and likely an accompanying mds-stepper-step-deactivated event.

Data for Custom Events

Data should rarely be provided with custom events. In many cases the occurrence of the event is all that's needed for an implementer to connect their product with the component. When opening or closing a modal, additional data is not needed, since open vs. closed are Boolean attributes.

<mds-modal id="my-cool-modal>
  <h1>Hello World!</h1>
</mds-modal>

<script>
  const modal= document.getElementById('my-cool-modal');
  modal.addEventListener('mds-modal-opened', () => { console.log(`Modal opened!`) } )
</script>

Similarly, a custom stepper event, mds-stepper-step-activated, would not need to include which step was activated since that data could easily be retrieved from the component after the event has fired:

<script>
    const stepper = document.getElementById('my-cool-stepper');
    stepper.addEventListener('mds-stepper-step-activated', () => {
        console.log(`The current step is ${stepper.activeStepNumber}`)
    })
</script>

The only case that warrants passing additional data along with a custom event is when a component leaves a state that cannot be retrieved from the component after the event has fired. For example, when the mds-stepper-step-deactivated event is triggered the component keeps no record of the previous step number. In this case the previous step number should be sent along with the custom event.

<script>
    const stepper = document.getElementById('my-cool-stepper');
    stepper.addEventListener('mds-stepper-step-deactivated', (previousStep) => {
        console.log(`The previous step was ${previousStep}`)
    })
</script>

Extending Components

Because MDS web components are vanilla JS classes, there is nothing that prevents a product team from extending one of those classes to create their own functionality. For example, a product team may want to extend the MdsInput component to add custom validation for credit cards. This can be achieved following the ES6 extends pattern.

class CreditCardInput extends MdsInput {
  render() {
    super();
    bindValidationMethods();
  }

  bindValidationMethods() {
    this.addEventListener('blur', validateCreditCardNumber);
  }
}

The MDS base component documentation should be consulted for further documentation on all the available methods and guidance on which methods should be extended and which should not.

The MDS team leverages extensive tooling to support adherence to our coding standards and automate code preprocessing and packaging.

HTML

CSS

Web Components

Build Tools