@pmwcs/base
Version:
PMWCS base module
266 lines (226 loc) • 6.51 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 }
}