The Morningstar Design System team adheres to standard coding practices and tools to build components and other features.
<!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.The stand-alone HTML should have value and meaning.
<p>
tag.<table>
tag.<ul>
with nested <li>
s wrapping each item.</li>
or </body>
).<span>
s and <div>
s as possible.class=””
properties according to BEM methodology.id=””
names very sparingly and only if:for=””
attributeid=””
s to target specific elementsclass
attribute first (engineers read/write this most often).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="">
.The MDS component library is written using BEM CSS Methodology.
The MDS team uses sass-lint to assist in code style maintenance. For more detail you can read our full list of linting rules
box-shadow
.rgb()
, rgba()
, hsl()
, hsla()
, or rect()
values..5
instead of 0.5
and -.5px
instead of -0.5px
.#ffffff
. Lowercase letters are much easier to discern when scanning a document as they tend to have more unique shapes.#ffffff
.input[type="text"]
. They’re only optional in some cases and it’s a good practice for consistency.margin: 0;
instead of margin: 0px;
.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
.
@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");
}
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 {
...
}
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.
mds-
.<mds-data-table>
.is
in the name.visible
instead of isVisible
.iconVisible
prop and an iconName
prop, simply allow the presence of iconName
to dictate whether or not an icon is visible in the component.<mds-button text="This text will win">This text will lose</mds-button>
slotPropOverrideMapping
that determines which of the component’s props can be used to override content passed via the default slot.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 elementid
- sets the id
attribute of the custom element<mds-input>
's value
attribute maps directly to the <input/>
element's value
attribute in the component’s template.<mds-checkbox>
has a labelHidden
boolean property, <mds-switch>
should use that same prop name and not something similar like labelVisible
or textHidden
.Checkbox
has a hiddenLabel
.Link
has an underline
.Card
has supplementalContent
.Button
has text
.Button
has a leftIcon
.Checkbox
is checked
.Link
is disabled
.Modal
is hidden
.showImage
use visibleImage
- fulfills the pattern: "[Component] has a visible image."hideLabel
use hiddenLabel
- fulfills the pattern: "[Component] has a hidden label."noUnderline
with a default of true
, use underline
with a default of false
- fulfills the pattern: "[Component] has an underline."preventVisitedStyling
with a default of true
, use visitedStyling
with a default of false
.Every prop must specify a type.
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.
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.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.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
}
}
...
}
}
...
}
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 are sections within a component’s template that can be injected with content contained between the custom element tags.
The text and/or icons for a button can be passed to the component within the custom element tags like this:
<mds-button>
<mds-icon name="email"></mds-icon> 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();
}
The button with icon component contains two types of content: the icon and the button text. Rather than defining every possible type of content within the button'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 card.
MdsButton template pseudocode:
<label class="mds-button__input-outer-wrapper" for="${this.id}">
<input id="${this.id}" class="mds-button__input" />
<slot name="icon">${iconLeft}</slot>
${text or default slot}
<slot name="icon-right">${iconRight}</slot>
</label>
Usage - your-product.html:
<mds-button>
<mds-icon slot="icon" name="gear--s"></mds-icon> Settings <mds-icon slot="icon-right" name="caret-down--s"></mds-icon>
</mds-button>
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.
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.
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 allow implementers to access component state and trigger component functionality.
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.
open()
to toggleVisibility()
advanceStep()
to goToNextStep()
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.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.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.
Web components will trigger custom events when a meaningful state change has occurred within the component, examples include but are not limited to:
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.
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 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>
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.