Who this document is for

The first two sections of this document describe general guidelines for how to think about building a component, as well as criteria for what makes a good component. Anyone interested in building components either directly for MDC Web or as an external component that plays well within the MDC Web ecosystem should find it useful. The third section talks about authoring components specifically for MDC Web, and is best suited for those looking to contribute directly to the project.

To demonstrate this approach, we will build a red-blue toggle, very simple toggle button that toggles between a red background with blue text, and vice versa. While not a Material Design component, it demonstrates the concepts of how to think about building for MDC Web.

Note how the JS Component does not reference MDC Web in any way, nor does it have any notion of foundations or adapters. By omitting this work, you can rapidly experiment with your component, incorporating changes quickly and easily. Nonetheless, the way the component is prototype looks quite similar to the way that the MDC Web component will eventually be built.

Identify host environment interactions

Once you’re satisfied with your prototype, the next step is to figure out what functionality will need to be proxied through an adapter. Any direct interactions with the host environment will need to be proxied, so that our foundations will be able to integrate into all frameworks across the web platform.

As mentioned in our architecture doc, the term host environment refers to the context in which the component is used. It could be the browser, a server which the component is being rendered on, a Virtual DOM environment, or even a mobile application in the case of technologies like React-Native.

For the redblue-toggle, it’s pretty easy to see all of the instances where we interact with the host environment: it occurs every place we read from or write to our root node.

Updating aria-pressed when toggled via setAttribute

Updating the classes on the root node when toggled via classList.{add,remove}.

Setting the textContent of the child color indicator element when toggled.

Adding/removing event listeners on the root node within initialize()/destroy() respectively.

In other cases, host environment interaction may not be straightforward, such as window.addEventListener('resize', ...). These are also examples of host environment interaction and must be taken into account.

Note: It is a convention in our code base to use the terms registerInteractionHandlerBoutique Skirt Boutique Casual Casual 0UZwxv and deregisterInteractionHandler as adapter methods for adding/removing generic event listeners. We do this because we feel it more semantic in that most components are only interested in interactivity. However, feel free to call these methods {add,remove}EventListener or anything else you would like.

Refactor your existing code into a foundation

Now that our adapter API is defined, our existing code can be reworked into a foundation class. As a convention in our codebase, we define a static defaultAdapter getter that returns an adapter with NOP signatures for each function. This helps us verify the shape of the adapter, prevent adapters from throwing errors when methods are forgotten, and in the future can (and should) be used to build out lint tools to enforce proper adapter shape. This example shows that, as well as making use of our MDCFoundation class, which is the base class which all foundations inherit from.

Note how isToggled() and toggle() are used in place of setters and getters. Given that a foundation is a lower-level API, we feel that this convention is appropriate.

Build a component on top of that foundation, providing an adapter

The last step is simply to build your actual component using the foundation above. The component has two main jobs:

Provide an idiomatic interface to the foundation’s functionality within the host environment

Provide an adapter to the foundation that will allow it to work within the host environment

Since this component is a vanilla component, it should be modeled after the vanilla DOM API, which favors getters and setters to implement its functionality (think checked, disabled, etc.). Our adapter is extremely straightforward as we can simply repurpose the methods we started out with.

What makes a good component

These additional guidelines provide a “checklist” of sorts when building your own components. We strictly adhere to them within our codebase, and doing the same will ensure an enjoyable experience for your component’s consumers.

Fully tested code

ECMAScript, by design, is a dynamic and flexible language. The benefits of this dynamicism and flexibility come at the cost of verifying the correctness of your program. The only way to ensure that your code will behave as expected is to execute that code and verify that the results are those you expect. Strive for 100% test coverage for your foundations, adapters, and components.

Thoroughly documented and strictly versioned adapter interface

Since framework authors will be designing code around your adapter interface, it’s important that you provide all information necessary for them to do so. Our recommended approach is to document the adapter’s interface within a component’s README, providing both the method signature as well as the expected behavior of the method.

Equally important is to strictly version your adapter interfaces. Changes to your adapter interface - even associative ones - have the potential to break existing implementations or trick implementors into thinking their code is working as expected whereas they may be missing a key aspect of the component due to having failed to implement a new adapter method. We consider each change to an adapter interface a breaking change, and recommend this approach.

Accessibility

We require all of our core components to be fully accessible. We implement ARIA specifications where it makes sense to do so, and ensure that we are being semantic as possible when it comes to our components’ behavior. The accessibility developer tools are a great way to analyze how accessible your component is.

RTL Awareness

Components should be RTL-aware. That is, there should be some sort of strategy a component uses to detect whether or not it is in an RTL context, and make proper adjustments accordingly. We use our @material/rtl library to assist us with this.

Support for theming

A component should be able to be altered according to a theme. A theme can be defined any way you wish. It may be by using primary and secondary colors, or you may choose to expose scss variables or CSS Custom properties specific to your component. Whichever way you choose, ensure that clients can easily alter common aesthetic elements of your component to make it fit with their overall design. We use Romper Xhilaration Xhilaration Romper Romper Xhilaration Selling Selling Selling wIqxTpYz for this purpose.

1 State Boutique Boutique Boutique 1 Romper Romper State State 1 General Best Practices

Even when a component satisfies all of the above requirements, it may still run into pitfalls. Follow the recommendations below to avoid pitfalls and unintended situations as much as possible.

Do what the user expects

This is our “golden rule”, if you will. Design your component APIs to be intuitive and easy-to-understand. Ensure that your components behave in a way a user could predict. For example, the checked getter on MDCCheckbox returns a boolean indicating whether or not the internal state of the checkbox is checked. It does not produce side effects, and functions in exactly the sameway HTMLInputElement.prototype.checked functions when its type is set to "checkbox". When designing your components, model them after the environment you expect your users to use them in. In our case, our vanilla components are modeled after the DOM APIs.

Design adapter interfaces to be simple and intuitive

This follows the above practice of doing what the user expects. Since library consumers will be implementing the adapter methods, they should be simple to implement as well as straightforward and intuitive in nature. Most adapter methods should be one-liners, such as “add a class” or “update a style property.” Users should not have to guess about what the purpose of an adapter method is, nor what its expected input and output should be.

Sets a style on the root element given a dasherized styleProperty as well as a value for that property.

As opposed to a bad adapter interface like the one below

Method Signature

Description

applyComponentStyles() => void

Sets the correct styles on the component’s root element. Consult our documentation for more info.

The above adapter interface is more suited for a foundation method. The adapter’s sole responsibility should be performing the style updates, not determining what they are.

Boutique 1 State 1 State State Romper 1 Romper Boutique Boutique Do not reference host objects within foundation code.

To ensure your foundation is as compatible with as many frameworks as possible, avoid directly referencing host objects within them. This includes window, document, console, and others. Only reference global objects defined within the ECMAScript specification within your foundations.

We make an exception for this rule for requestAnimationFrame, but in the future we may refactor that out as well. In addition, a workaround to working with host objects in a foundation is to ask for them via the adapter. However, you should design your adapter APIs such that they return a structural type representing your host object, rather than a nominal type of the host object itself. For example, instead of asking for an HTMLInputElement of type "checkbox", ask for an object that has checked, indeterminate, and disabled boolean properties.

Clean up all references on destruction

This includes event handlers, timer IDs, animation frame IDs, and any other external references that may be retained. There are two accurate litmus tests to ensure this is being done:

init() (or initialize() in a vanilla component) and destroy() are reflexive. For example, any event listeners attached in init() are removed in destroy().

Every call which creates an external reference (e.g. setTimeout(), 1 State Romper Romper Boutique 1 State Boutique Boutique 1 State requestAnimationFrame()) is kept track of, and cleaned up within destroy. For example, every setTimeout() call should have its ID retained by the foundation/component, and have clearTimeout() called on it within destroy.

Authoring components for MDC Web

The following guidelines are for those who wish to contribute directly to MDC Web. In addition to adhering to all of the practices above, we have additional conventions we expect contributors to adhere to. It’s worth noting that most of these conventions - including our coding style, commit message format, and test coverage - are automatically enforced via linters, both so that contributors can move quickly and confidently, and core team members do not have to waste time and energy nitpicking on pull requests.

File Structure

All source files for a component reside under packages/. All test files reside under test/unit, which mirrors the packages/ directory. Demos for each component are located under demos/.

License Stanzas

We are required to put the following at the top of every source code file, including tests, demos, and demo html. The stanza is as follows:

ofthissoftwareandassociateddocumentationfiles(the"Software"),todeal

Please put this in a comment at the top of every source file, replacing
with the year the file was created.

Scss

Separate reusable variables and mixins from main scss

If variables and mixins are intended to be used outside of a single stylesheet, refactor them out into Brooks Wool Brothers Brothers Brooks Boutique Boutique Cardigan f0nHnaq5W. These files can then be included in other stylesheets without having extra CSS omitted both times. As a rule of thumb, _never @import sass files which output CSS`, as it will most likely be duplicate output.

Follow the BEM Pattern

We follow a modified version of the BEM pattern, which is defined within our stylelint rules. It’s basically:

Javascript

Define a static attachTo(root) method for every component

All components must define a static attachTo method with the following signature:

static attachTo(element: HTMLElement) => Constructor

e.g.

Boutique 1 Boutique State 1 1 State Boutique State Romper Romper

classMDCNewComponentextendsMDCComponent {

mdc-auto-init requires this method to be present, and we guarantee its provision as a convenience to our users. We have spoken about writing a lint rule for this in the future.

Define a defaultAdapter getter for every foundation

All foundations must define a static defaultAdapter method which returns an adapter with all functions defined. The functions should essentially be NOPs; taking no parameters and returning the correct type (e.g. false for boolean, 0 for number, {} for object, etc.). Our convention is to annotate the function via inline comments using Typescript’s type annotations.

It is also a convention to override a foundation’s constructor to mix in the passed adapter param to a default adapter object. This ensures that the adapter is guaranteed to have the correct shape.

This of course comes at the cost of potentially obscuring erroneous passed in adapters. However, we plan on providing type definitions for adapters in the future to help assuage this. Similar to the aforementioned rule, we would also like to provide lint rules to enforce these conventions.

All CSS Classes referenced by a component’s foundation must be referenced by a cssClasses static getter.

All strings that are used outside the context of the foundation class (CSS selectors, custom event names, text that could potentially be localized, etc.) must be referenced by a State Boutique State Boutique State Romper 1 Boutique 1 Romper 1 strings static getter.

All semantic numbers leveraged by the foundation (timeout lengths, transition durations, etc.) must be referenced by a numbers static getter.

These constants should be defined within a constants.js file, and then proxied through the foundation.

import {cssClasses, strings, numbers} from'./constants';

The primary purpose of this is so that our components can interoperate with aspects of Google’s front-end infrastructure, such as Closure Stylesheets’ CSS Class Renaming mechanism. In addition, it provides the added benefit of semantic code, and less magic strings/numbers.

Extend components and foundations from mdc-base classes.

To ensure that all packages behave consistently, all components must extend MDCComponent and all foundations must extend MDCFoundation. More information for both of these classes can be found in the mdc-base README.

Packages must be registered with our build infrastructure, and with material-components-web pkg

Whenever you create a new component, it’s important to notify the proper tools and packages of it. Concretely:

Ensure that an entry for it exists in webpack.config.js for the js-components and css module

Ensure that it is added as a dependency of material-components-web. If the component contains styles, ensure that they are @imported within material-components-web.scss. If the component contains javascript, ensure that its component namespace is exported within material-components-web, and it is registered with mdc-auto-init. Lastly, remember to add it to State Boutique 1 Boutique 1 Boutique State Romper 1 Romper State package.json of material-components-web.

Ensure that the correct commit subject for the package is added to the config.validate-commit-msg.scope.allowed array within the top-level package.json at the root of the repo. The commit subject is the name the component, without the Boutique Romper Romper 1 State 1 State Boutique Boutique 1 State mdc-/@material/. E.g., for mdc-icon-button, the correct subject is icon-button.

Ensure that the package name is added to the closureWhitelist array within the top-level package.json.

Closure Compatibility

NOTE: Our currently existing components are in the process of being made compatible with closure.

All core MDC Web components must be fully compatible with the Google Closure Compiler using its advanced compilation mechanisms. We’ve provided a thorough explanation of this, as well as conventions, examples, and common closure patterns you may not be used to, in our closure compiler documentation.

Testing

The following guidelines should be used to help write tests for MDC Web code. Our tests are written using mocha with the qunit UI, and are driven by karma. We use the chai assert API for assertions, and testdouble for mocking and stubbing.

Verify foundation’s adapters

When testing foundations, ensure that at least one of your test cases uses the verifyDefaultAdapter method defined with our foundation helpers. This is done to ensure that adapter interfaces do not change unexpectedly.

Use bel for DOM fixture

We use the bel library to generate fixtures for our component/adapter tests. We’ve found it to be an easy and successful way to bootstrap fixtures without having to worry about maintaining HTML files or write unwieldy DOM API code.

Always clean up the DOM after every test

This is important. Before a test ends, ensure that any elements attached to the DOM have been removed.

Verify adapters via testdouble.

We use testdouble.js as our de-facto mocking framework. A huge benefit to the component/foundation/adapter pattern is it makes testing the functionality of our components extremely easy. We encourage you to make use of testdouble stubs for adapters and use them to verify your foundation’s behavior (note that this is what S A Dress Selling Finesse Casual U Rqw4Zz does by default).

Material design is an adaptable system—backed by open-source code—guiding you in the principles and best practices of contemporary UI. Material helps teams streamline the designer-developer collaboration, reduce complexity, and enable fidelity through reusable components, patterns, and code libraries. All while adapting easily to your brand, platform, and users’ needs.