This repo contains a POC implementation of Button
, Icon
, FloatingLabel
and TextField
Material Components written in dojo using tsx syntax.
- To implement Dojo widgets adhering to the Material design spec
- To utilise the material design components (MDC) librarty provided by google.
- To use the MDC foundations and adapters to apply appropiate classes and responses to user input and interaction.
- To deliver easy to use, a11y compliant material widgets written in typescript.
This POC uses the foundation / adapter approach similar to the official material-components-web-react implementation.
Widgets utilising this approach must create the required html structure using the documented class names. Each complex component (such as TextField
or FloatingLabel
) requires a specific Foundation
class to be instantiated and passed an adapter
. The adapter
provides the Foundation
class with funcions such as addClass
, removeClass
etc and accessors such as isFocused
, value
etc... These provide a means for the foundation to respond to inputs and enact change onto the domnodes.
Adapters must be written for each component and provided to their Foundation
on creation. The Adapter is an object used to provide functions and properties to the comonent foundation such that it can access and manipulate the domnodes you create. For example it may have an addClass
function that when called adds a class to your widget.
Details of the required adapters for each component are available in each component's documenttation.
Without an adapter, a Foundation class would not be able to interact with your widget.
Mdc Foundation classes exist for each of the more complex components which require changes / interactions to maniupulate the appearance / state of the widget. They essentially encapsulate the business / display logic of each component such that they can consistently be created in multiple languages.
In some cases, such as TextField
, the Foundation
also requires access to the input domNode
, this is achieved using a simple meta
.
Simple components such as Button
do not require a Foundation.
To create @dojo/material
we should utilise the foundations
and adapters
as used within this POC repo.
Material components appear to fall into two categories; simple widgets which require only correct dom elements and class names, and complex input widgets which require the use of foundations
and adapters
. These allow the underlying material interaction logic to add / remove classes from your components and show / hide labels / animations etc.
The foundations files do not appear to have typings available so your dojo build may complain when you are using them. We may need to either write dummy / full typigns for these or set our tsconfig to allow js imports for this project.
Simple (non input) widgets such as Button
and Card
require only appropriate dom elements to be created with material classes applied. A complete description of the required dom and css classes can be found in the documentation for each component, button docs can be found here.
We can import the apropriate material
css by creating an index.css
file to sit alongside each of our components that imports the appropriate css from node_modules
.
/* button/index.css */
@import '~@material/button/dist/mdc.button.css';
We then import this css file into our component implementation
/* button/index.tsx */
import './index.css';
In the case of Button
, the base class is mdc-button
with longer modifier classes such as mdc-button--outlined
. As these have hypens etc in them and are outside of our source control we will be unable to use them with css-modules
and thus the index.css
file for each component will likely remain empty apart from the material import.
Some more complex widgets contain a constants
object that could be used to import the css class names, but I have found these class names to be incomplete for some components and did not like the idea of hardcoding some classnames whilst importing others, so I simply hardcoded them all.
A complex widget is one that provides a Foundation
class that contains the components interaction / class application logic. These need to be imported (no typings) and newed up with an Adapter
. The Adapter
is an object containing functions that allow the Foundation
to manipulate and inspect your component.
The most basic Adapter
will have addClass
/ removeClass
/ hasClass
functions. I've found these best implemented using a ClassList
Set
which is used to populate the root
classes
object when rendering the component.
/* text-field/index.tsx */
private classList: new Set<string>();
private _adapter = {
addClass: (className: string) => {
this.classList.add(className);
this.invalidate();
},
removeClass: (className: string) => {
this.classList.delete(className);
this.invalidate();
},
hasClass: (className: string) => {
return this.classList.has(className);
}
};
private _foundation = new MDCTextFieldFoundation(this._adapter, {});
The Foundation
must be initialised and destroyed when the component is created and destroyed, this can be done using onAttach
and onDetach
.
protected onAttach() {
this._foundation.init();
}
protected onDetach() {
this._foundation.destroy();
}
Following this approach, all you need do in the render
function is ...this.classList
to add the foundation classes to your widget. Please see text-field/index.tsx
for the full example.
We could create a BaseWidget
that contains a simple adapter
as above which would reduce the boiler plate for each component and give us consistency across the library.
Some adapters, such as the text-field
adapter require access to the input
dom node, we can achieve this using a simple Meta
. I have implemented this as Node.ts
within this example project as is used as such:
private _adapter {
// ...
getNativeInput: () => {
return this.meta(Node).get('input');
},
// ...
}
When nesting components, ie. an Icon
trailing inside a Text-Field
, material expects you to pass extra classes to the child widget. To implement this quickly I have allowed such widgets to accept a classes
property that is mixed into their root
classes at render time. If we were writing full-blown dojo widgets, we would use ThemedMixin
and extraClasses
but I thought this to be overkill at this point.
/* text-field/index.tsx */
render() {
// ...
{trailingIcon ? <Icon classes={['mdc-text-field__icon']} icon={trailingIcon} /> : null}
// ...
}
/* icon/index.tsx */
render() {
const {
icon,
classes = []
} = this.properties;
return (
<i classes={[ 'material-icons', ...classes ]}>
{icon}
</i>
);
}
Ripple is a visual effect used throughout the material component library to animate ui elements in response to user interaction. The mdc react implementation utilises a HOC to achieve this by wrapping each component that requires a ripple effect before rendering it.
I have started to investigate how we should do this in our Dojo material library and believe it could be done using an outer widget that decorate
s it's children. This should be done within the widget so the user does not have to manually create a Ripple
widget.
render() {
// ...
if (this.properties.ripple) {
return (
<Ripple target="targetKey">
{ myWidgetRoot }
</Ripple>
);
} else {
return myWidgetRoot;
}
}
The @material/mdc
project uses SCSS
extensively and is compiled to css
with bullet-proofed css-variables
. This means that the SCSS-variables
used within the styles are baked into the generated css
with a css-variable
directly afterwards as a fall back.
Due to this, we can override the compiled colours etc with our own css-variables
. For example:
/* button.css */
@import '@material/button/dist/mdc.button.css';
/* after importing the css, override the variables */
:root {
--mdc-theme-primary: red;
}
/* the button will now render red */
Currently this POC is built as an app using the dojo build app
cli command. For this to be a published package that developers can install and use it will need to have a build pipeline created similar to that of @dojo/widgets
.
- Bring the components in line with
a11y
etc offerings in@dojo/widgets
- Abstract out common parts such as base adapters
- Create further components to complete the library:
Card
/Select
etc... - Investigate feasibility of
ripple
implementation