preact-material-components
Version:
preact wrapper for "Material Components for the web"
209 lines (182 loc) • 5.85 kB
text/typescript
import MDCComponent from '@material/base/component';
import {MDCRipple} from '@material/ripple';
import {bind} from 'bind-decorator';
import {Component, VNode} from 'preact';
import {SoftMerge} from './types';
export interface IMaterialComponentOwnProps {
ripple?: boolean;
}
export interface IMaterialComponentOwnState {}
export type MaterialComponentProps<PropType> = SoftMerge<
PropType & IMaterialComponentOwnProps,
JSX.HTMLAttributes
>;
export type MaterialComponentState<StateType> = StateType &
IMaterialComponentOwnState;
const doNotRemoveProps = ['disabled'];
/**
* Base class for every Material component in this package
* NOTE: every component should add a ref by the name of `control` to its root dom for autoInit Properties
*
* @export
* @class MaterialComponent
* @extends {Component}
*/
export abstract class MaterialComponent<
PropType extends {[prop: string]: any},
StateType extends {[prop: string]: any}
> extends Component<
MaterialComponentProps<PropType>,
MaterialComponentState<StateType>
> {
public MDComponent?: MDCComponent<any, any>;
/**
* Attributes inside this array will be check for boolean value true
* and will be converted to mdc classes
*/
protected abstract mdcProps: string[];
/** This will again be used to add apt classname to the component */
protected abstract componentName: string;
/**
* Props of which change the MDComponent will be informed.
* Override to use.
* When used do not forget to include this.afterComponentDidMount() at the end of your componentDidMount function.
* Requires this.MDComponent to be defined.
*/
protected mdcNotifyProps?: string[];
/** The final class name given to the dom */
protected classText?: string | null;
protected ripple?: MDCRipple | null;
protected control?: Element;
public render(props): VNode {
if (!this.classText) {
this.classText = this.buildClassName(props);
}
// Fetch a VNode
const componentProps = props;
const userDefinedClasses =
componentProps.className || componentProps.class || '';
// We delete class props and add them later in the final
// step so every component does not need to handle user specified classes.
if (componentProps.class) {
delete componentProps.class;
}
if (componentProps.className) {
delete componentProps.className;
}
const element = this.materialDom(componentProps);
let propName = 'attributes';
if ('props' in element) {
propName = 'props';
// @ts-ignore
element.props = element.props || {};
} else {
element.attributes = element.attributes || {};
}
// @ts-ignore
element[propName].className = `${userDefinedClasses} ${this.getClassName(
element
)}`
.split(' ')
.filter(
(value, index, self) => self.indexOf(value) === index && value !== ''
) // Unique + exclude empty class names
.join(' ');
// Clean this shit of proxy attributes
this.mdcProps.forEach(prop => {
// TODO: Fix this better
if (prop in doNotRemoveProps) {
return;
}
// @ts-ignore
delete element[propName][prop];
});
return element;
}
/** Attach the ripple effect */
public componentDidMount() {
if (this.props.ripple && this.control) {
this.ripple = new MDCRipple(this.control);
}
}
public componentWillReceiveProps(
nextProps: MaterialComponentProps<PropType>
) {
if (this.MDComponent && this.mdcNotifyProps) {
for (const prop of this.mdcNotifyProps) {
if (this.props[prop] !== nextProps[prop]) {
this.MDComponent[prop] = nextProps[prop];
}
}
}
for (const prop of this.mdcProps) {
if (this.props[prop] !== nextProps[prop]) {
this.classText = this.buildClassName(nextProps);
break;
}
}
}
public componentWillUnmount() {
if (this.ripple) {
this.ripple.destroy();
}
}
protected afterComponentDidMount() {
if (this.MDComponent && this.mdcNotifyProps) {
for (const prop of this.mdcNotifyProps) {
this.MDComponent[prop] = this.props[prop];
}
}
}
// Shared setter for the root element ref
protected setControlRef(control?: Element) {
this.control = control;
}
/** Build the className based on component names and mdc props */
protected buildClassName(props: MaterialComponentProps<PropType>) {
// Class name based on component name
let classText = 'mdc-' + this.componentName;
// Loop over mdcProps to turn them into classNames
for (const propKey in props) {
if (props.hasOwnProperty(propKey)) {
const prop = props[propKey];
if (typeof prop === 'boolean' && prop) {
if (this.mdcProps.indexOf(propKey) !== -1) {
classText += ` mdc-${this.componentName}--${propKey}`;
}
}
}
}
return classText;
}
/** Returns the class name for element */
protected getClassName(element: VNode) {
if (!element) {
return '';
}
let propName = 'attributes';
if ('props' in element) {
propName = 'props';
// @ts-ignore
element.props = element.props || {};
} else {
element.attributes = element.attributes || {};
}
// @ts-ignore
const attrs = (element[propName] = element[propName] || {});
let classText = this.classText;
if (attrs.class) {
classText += ' ' + attrs.class;
}
if (attrs.className && attrs.className !== attrs.class) {
classText += ' ' + attrs.className;
}
return classText;
}
/** Components must implement this method for their specific DOM structure */
protected abstract materialDom(
props: MaterialComponentProps<PropType>
): VNode;
}
export default MaterialComponent;