@pmwcs/base
Version:
PMWCS base module
252 lines (214 loc) • 6.47 kB
JavaScript
import { useEffect, useState, useRef, useMemo } from 'preact/hooks';
import classNames from 'classnames';
import { eventsMap } from './utils/events-map';
import { toCamel } from './utils/strings';
import { handleRef } from './component';
const reactPropFromEventName = evtName => eventsMap[evtName] || evtName;
export class FoundationElement {
constructor(onChange = null) {
this._classes = new Set();
this._events = {};
this._style = {};
this._props = {};
this._ref = null;
this._onChange = onChange;
this.onChange = this.onChange.bind(this);
this.addClass = this.addClass.bind(this);
this.removeClass = this.removeClass.bind(this);
this.hasClass = this.hasClass.bind(this);
this.setProp = this.setProp.bind(this);
this.getProp = this.getProp.bind(this);
this.removeProp = this.removeProp.bind(this);
this.setStyle = this.setStyle.bind(this);
this.addEventListener = this.addEventListener.bind(this);
this.removeEventListener = this.removeEventListener.bind(this);
this.setRef = this.setRef.bind(this);
}
onChange() {
this._onChange && this._onChange();
}
destroy() {
this._onChange = null;
this._events = {};
this._style = {};
this._props = {};
this._classes = new Set();
setTimeout(() => {
this._ref = null;
});
}
/**************************************************
* Classes
**************************************************/
addClass(className) {
if (!this._classes.has(className)) {
this._classes.add(className);
this.onChange();
}
}
removeClass(className) {
if (this._classes.has(className)) {
this._classes.delete(className);
this.onChange();
}
}
hasClass(className) {
return this._classes.has(className);
}
/**************************************************
* Props
**************************************************/
setProp(propName, value, silent = false) {
if (this._props[propName] !== value) {
this._props[propName] = value;
!silent && this.onChange();
}
}
getProp(propName) {
return this._props[propName];
}
removeProp(propName) {
if (this._props[propName] !== undefined) {
delete this._props[propName];
this.onChange();
}
}
props(propsToMerge) {
const {
className = '',
style = {}
} = propsToMerge; // handle merging events
// the foundation should be able to pass something onClick as well as a user
// This wraps them in a function that calls both
const mergedEvents = Object.entries(propsToMerge).reduce((acc, [key, possibleCallback]) => {
const existingCallback = this._events[key];
if (typeof possibleCallback === 'function' && typeof existingCallback === 'function') {
const wrappedCallback = evt => {
existingCallback(evt);
return possibleCallback(evt);
};
acc[key] = wrappedCallback;
}
return acc;
}, { ...this._events
}); // handle className
const mergedClasses = classNames(className, [...this._classes]); // handle styles
const mergedStyles = { ...this._style,
...style
};
return { ...propsToMerge,
...this._props,
...mergedEvents,
style: mergedStyles,
className: mergedClasses
};
}
/**************************************************
* Styles
**************************************************/
setStyle(propertyName, value) {
propertyName = propertyName.startsWith('--') ? propertyName : toCamel(propertyName);
if (this._style[propertyName] !== value) {
this._style[propertyName] = value;
this.onChange();
}
}
/**************************************************
* Events
**************************************************/
addEventListener(evtName, callback) {
const propName = reactPropFromEventName(evtName);
if (this._events[propName] !== callback) {
this._events[propName] = callback;
this.onChange();
}
}
removeEventListener(evtName
/*, callback */
) {
const propName = reactPropFromEventName(evtName);
if (this._events[propName]) {
delete this._events[propName];
this.onChange();
}
}
/**************************************************
* Refs
**************************************************/
setRef(el) {
if (el) {
this._ref = el;
}
}
get ref() {
return this._ref;
}
}
const emitFactory = props => (evtType, evtData, shouldBubble = false) => {
const evt = new CustomEvent(evtType, {
detail: evtData,
bubbles: shouldBubble
}); // bugfix for events coming from form elements
// and also fits with reacts form pattern better...
// This should always otherwise be null since there is no target
// for Custom Events
Object.defineProperty(evt, 'target', {
value: evtData,
writable: false
});
Object.defineProperty(evt, 'currentTarget', {
value: evtData,
writable: false
}); // Custom handling for React
const propName = evtType;
props[propName] && props[propName](evt);
return evt;
};
export const useFoundation = ({
foundation: foundationFactory,
props: inputProps,
elements: elementsInput,
api
}) => {
const [, setIteration] = useState(0);
const props = useRef(inputProps);
props.current = inputProps;
const elements = useMemo(() => Object.keys(elementsInput).reduce((acc, key) => {
acc[key] = new FoundationElement(() => {
setIteration(val => val + 1);
});
return acc;
}, {}), []);
const foundation = useMemo(() => {
// init foundation
const f = foundationFactory({ ...elements,
getProps: () => props.current,
emit: (...args) => emitFactory(props.current)(...args)
}); // handle apiRefs
api && handleRef(props.current.apiRef, api({
foundation: f,
...elements
}));
return f;
}, []);
useEffect(() => {
const f = foundation;
f.init();
api && handleRef(props.current.apiRef, api({
foundation: f,
...elements
}));
handleRef(props.current.foundationRef, f);
return () => {
f.destroy();
handleRef(props.current.apiRef, null);
handleRef(props.current.foundationRef, null);
Object.values(elements).map(element => element.destroy());
props.current = {};
};
}, [foundation, elements]);
return {
foundation: foundation,
...elements
};
};