|
import React, { Component } from "react"; |
|
import { effect, reactive } from "@vue/reactivity"; |
|
import isEqual from "lodash.isequal"; |
|
|
|
type Props = { |
|
props: any; |
|
}; |
|
|
|
type MethodThis<D> = { |
|
$data?: D; |
|
}; |
|
|
|
type RenderThis<M, D> = { |
|
$data?: D; |
|
$methods?: M; |
|
}; |
|
|
|
type Config<M, D, P> = { |
|
globalMethods?: () => any; |
|
globalData?: () => any; |
|
methods?: (this: MethodThis<D>) => M; |
|
data?: () => D; |
|
onMount?: (o: Props) => void; |
|
onDestroy?: () => void; |
|
$data?: D; |
|
$methods?: M; |
|
$globalMethods?: any; |
|
$globalData?: any; |
|
render: ( |
|
this: RenderThis<M, D>, |
|
opt: RenderOptions<M, D, P> |
|
) => React.ReactNode; |
|
}; |
|
|
|
type RenderOptions<M, D, P> = { |
|
config: Config<M, D, P>; |
|
props: P; |
|
}; |
|
|
|
let globalMethods = {}; |
|
let globalData = reactive({}); |
|
|
|
export function Apex<M, D, P>(config: Config<M, D, P>) { |
|
return class EffectRenderer extends Component<P> { |
|
tree: null | React.ReactNode; |
|
$data: D; |
|
$methods: M; |
|
|
|
constructor(props) { |
|
super(props); |
|
this.tree = null; |
|
this.makeDataReactive(); |
|
} |
|
|
|
componentDidMount() { |
|
this.run(); |
|
} |
|
|
|
componentDidUpdate(prevProps: Readonly<{}>): void { |
|
if (isEqual(prevProps, this.props)) { |
|
return; |
|
} |
|
// re-create tree if props have changed |
|
this._createTree(); |
|
} |
|
|
|
componentWillUnmount() { |
|
config.onDestroy && config.onDestroy(); |
|
this.tree = null; |
|
} |
|
|
|
makeDataReactive() { |
|
const _data = config.data ? config.data() : {}; |
|
if (_data instanceof Promise) { |
|
throw new Error( |
|
"`data` needs to be a sync method, if you have data that might come from network, please inject it `onMount` or by triggering a possible `$method` " |
|
); |
|
} |
|
this.$data = <D>reactive(_data); |
|
} |
|
|
|
async run() { |
|
var self = this; |
|
this.$methods = <M>(config.methods ? config.methods() : {}); |
|
Object.keys(this.$methods).forEach((key) => { |
|
self.$methods[key] = self.$methods[key].bind(config); |
|
}); |
|
|
|
const newGlobalMethods = |
|
(config.globalMethods && config.globalMethods()) || {}; |
|
Object.keys(newGlobalMethods).forEach((key) => { |
|
if (globalMethods[key]) { |
|
console.warn(`Overlapping key:${key} on global methods`); |
|
} |
|
globalMethods[key] = newGlobalMethods[key].bind({ |
|
$globalData: globalData, |
|
}); |
|
}); |
|
|
|
const newGlobalData = (config.globalData && config.globalData()) || {}; |
|
Object.keys(newGlobalData).forEach((key) => { |
|
if (globalData[key]) { |
|
console.warn(`Overlapping key:${key} on global methods`); |
|
} |
|
globalData[key] = newGlobalData[key]; |
|
}); |
|
|
|
Object.assign(config, { |
|
$methods: self.$methods, |
|
$data: self.$data, |
|
$globalData: globalData, |
|
$globalMethods: globalMethods, |
|
}); |
|
|
|
if (config.onMount) { |
|
Promise.resolve( |
|
config.onMount({ |
|
props: self.props, |
|
}) |
|
).then(() => { |
|
this._createTree(); |
|
}); |
|
} |
|
|
|
effect(() => { |
|
console.log({ effectProps: self.props }); |
|
this._createTree(); |
|
}); |
|
} |
|
|
|
_createTree() { |
|
const render = config.render.bind(config); |
|
const self = this; |
|
this.tree = render({ |
|
config, |
|
props: self.props, |
|
}); |
|
this.forceUpdate(); |
|
} |
|
|
|
render() { |
|
return this.tree; |
|
} |
|
}; |
|
} |