-
-
Save doeixd/bd70a6dac0b981d505e7f360532b0bdb to your computer and use it in GitHub Desktop.
import { createContext as unCreateContext } from 'unctx' | |
import { createFallback, isString } from './utilities.mjs' | |
// Setup | |
export function init() { | |
window.__registry = { | |
initial_component_contexts: {}, | |
component_instances: {} | |
} | |
ensureRunAfterDOM(_ => { | |
const everyElement = [...document.querySelectorAll('*')] | |
for (let element of everyElement) { | |
addFrameworkListenersToElement(element) | |
} | |
createObserver((mutations) => { | |
const addedElements = getAddedElements(mutations) | |
for (let addedElement of addedElements) { | |
addFrameworkListenersToElement(addedElement) | |
} | |
}, {childList: true, subtree: true})(document) | |
}) | |
} | |
export function getContext () { | |
return { | |
context: {}, | |
args: {} | |
} | |
} | |
export function initialComponentContexts() { | |
return window?.__registry?.context?.initial_component_contexts | |
} | |
export function getInstance(name, key) { | |
let isNameAndKey = isString(name) && isString(key) | |
if (isNameAndKey) return window?.__registry?.component_instances?.[name]?.[key] | |
if (name) return window?.__registry?.component_instances?.[name] | |
return window?.__registry?.component_instances | |
} | |
export function createInitialContextObject({name, type = 'single', data}) { | |
return { | |
...data, | |
state: {}, | |
props: {}, | |
events: {}, | |
roles: {}, | |
mount: [], | |
name, | |
parent: undefined, | |
components: [], | |
type, | |
condition: (element) => { | |
return element.tagName.toLowerCase() == name.toLowerCase() | |
}, | |
} | |
} | |
export function createComponent(name, constructor, data) { | |
const ctx = unCreateContext(name) | |
getContext = ctx.use | |
let componentContext = createInitialContextObject({name, data}) | |
function call(args = {}, fn) { | |
let context = { | |
context: componentContext, | |
args, | |
ctx, | |
call | |
} | |
// ctx.set(context, true) | |
return ctx.call(context, fn) | |
} | |
call({}, constructor) | |
getContext().__registry | |
} | |
export function component(keyNameOrFn, key) { | |
let component_context = getContext()?.context | |
let createReturnValue = (_instance = instances) => createFallback(_instance, _instance?.element || {}) | |
const isNameAndKey = isString(keyNameOrFn) && isString(key) | |
const isFn = keyNameOrFn instanceof Function | |
// const isKey = isString(keyNameOrFn) && !key | |
if (isNameAndKey) { | |
const instance = getInstance(keyNameOrFn, key) | |
return createReturnValue(instance) | |
} | |
if (isFn) { | |
let instances = Object.entries(window?.__registry?.component_instances) | |
let toReturn = [] | |
for (let instance of instances) { | |
if(keyNameOrFn?.(instance)) { | |
toReturn.push(createReturnValue(instance)) | |
} | |
} | |
} | |
let name = component_context?.name || (isString(keyNameOrFn) ? keyNameOrFn : undefined) | |
let type = component_context?.type || 'single' | |
let instances = getInstance(name) | |
let initial_component_contexts = initialComponentContexts() | |
let is_single = type.toLowerCase().includes('single') | |
if (is_single) return createReturnValue() | |
return instances.map(createReturnValue) | |
} | |
export function find() { | |
} | |
export function condition (cb) { | |
let context = getContext()?.context | |
context['condition'] = cb | |
} | |
export function key (cb) { | |
let context = getContext()?.context | |
context['key'] = cb | |
} | |
export function mount (cb) { | |
let context = getContext()?.context | |
context['mount'] ||= [] | |
context['mount'].push(cb) | |
} | |
export function prop(key, value) { | |
let context = getContext()?.context | |
context[key] = value | |
} | |
export function on (condition, cb) { | |
let context = getContext()?.context | |
context['events'] ||= [] | |
context['events'].push({condition, cb}) | |
} | |
export function attr (name, cb, initial) { | |
let context = getContext()?.context | |
context['attributes'] ||= [] | |
context['attributes'].push({name, cb, initial}) | |
} | |
export function dispatch(name, cb) { | |
let context = getContext()?.context | |
context['dispatch'] ||= [] | |
context['dispatch'].push({name, cb}) | |
} | |
export function unsubscribe(condition, cb) { | |
let context = getContext()?.context | |
context['unsubscribe'] ||= [] | |
context['unsubscribe'].push({condition, cb}) | |
} | |
export function state () { | |
let context = getContext()?.context | |
let _state = {} | |
context['state'] ||= _state | |
return _state | |
} | |
export const role_symbol = Symbol('role') | |
export const role_name_symbol = Symbol('role_name') | |
export function role(name, cb) { | |
let context = getContext()?.context | |
let full_context = getContext() | |
let isRole = Object.hasOwn(role_symbol, full_context) | |
let isCondition = name && !(cb instanceof Function) | |
if (isCondition) { | |
} | |
if (isRole) { | |
let role_context = getContext() | |
return role_context | |
} | |
if (cb instanceof Function) { | |
let _role = {} | |
context['role'] ||= _role | |
context['role'][name] = { | |
[role_symbol]: true, | |
[role_name_symbol]: name, | |
} | |
let ctx = unCreateContext() | |
let og_getContext = getContext | |
getContext = ctx.use | |
ctx.call(createFallback(context['role'][name], context), cb) | |
getContext = og_getContext | |
} | |
} | |
export function el (condition) { | |
const context = getContext() | |
let element = context?.element || context?.context?.element | |
if (!condition) { | |
return context?.element || context?.context?.element | |
} | |
if (isString(condition) && element) { | |
return element.querySelector(string) | |
} | |
if (condition instanceof Function && element) { | |
return [...element.querySelectorAll('*')].find(condition) | |
} | |
} |
roles, need default properties
roles need default condition
every instance of a component needs its own version of state and roles and such
you can just store this on the element object itself?
netsted roles
access all components of type from the be
object and do stuff to them
when you call be.componentName(#id)
you can get a specific component?
add context to event listener
add proxys to element objects
values api.
component.values.count = {
initial: 0,
get() {},
set() {},
}
some how scope this to the element
I am writing a front end javascript framework, and am debating between the following two apis, help me decide which one to go with.
Api 1:
const counter = be.counter
counter.values.count = instance => ({
get(oldValue) {
return oldValue
},
set(newValue) {
instance.values.count = newValue
instance.roles.count.innerText = newValue
}
})
counter.roles.inc.onClick = (e, instance) => {
instance.values.count += 1
}
Api 2:
const counter = createComponent('counter', () => {
value('count', {
get(oldValue) {
return oldValue
},
set(newValue) {
role('count').innerText = newValue
return newValue
}
})
role('inc', () => {
on('click', () => {
value('count') += 1
value('count').set(
value('count') + 1
)
set_value('count', 0 + 1)
})
})
})
Make it so custom events can return a value, and can have a callback.
add .find to component instance, and roles.
add .roles to component instance, and find.
when a new item is added to the dom, and it is of a component type / or a role type, generate a new key for it, and check it against the list of already hydrated elements
import { createContext as unCreateContext } from 'unctx'
const isObj = o => (typeof o === 'object' && o !== null)
// Faux Class Proxy Composable Hooks
// FC Proxy Context
let getContext = () => {return {
context: {},
args: {}
}}
export function createContext(contextName, initialObject ={}) {
const ctx = unCreateContext(contextName)
getContext = ctx.use
const context = {
context: initialObject,
args: {}
}
function call(args = {}, fn) {
context.args = args
ctx.set(context, true)
return ctx.call(context, fn)
}
const initialProxy = new Proxy(initialObject, {
get(target, propName) {
const catchAllGetter = target?.__values?.[propName]?.__get
const oldValue = target?.__values?.[propName]?.__value
if (catchAllGetter) return call({propName, isInitial:true}, _ => catchAllGetter?.({target, propName, oldValue}))
// if (!target?.__values?.[keyName])
console.log({target, propName})
return target.__values[propName].__value
},
set(target, propName, value) {
const catchAllSetter = target?.__values?.[propName]?.__set
const oldValue = target?.__values?.[propName]?.__value
let newValue = value
if (catchAllSetter) {
const result = call({ propName, value, isInitial: true, oldValue, newValue: value }, _ => catchAllSetter?.({ target, propName, value, newValue:value, oldValue }))
if (result) newValue = result
}
target.__values[propName].__value = newValue
return true
}
})
return function getKeyProxyFromContext (keyName, fauxProxyFn, initialValue) {
// getContext = ctx.use
initialValue = {
__value: initialValue
}
if(!initialObject?.__values) initialObject.__values = {}
initialObject.__values[keyName] = initialValue;
//const thisValue = initialObject?.__values[keyName]
const thisKeyOfContext = initialProxy?.[keyName]
if(!fauxProxyFn && keyName) return thisKeyOfContext
if(!fauxProxyFn && !keyName) return initialProxy
//Else, there is a faux proxy fn, and a key name
const proxyHandler = {
get(target, propName) {
const getPropHandler = thisKeyOfContext?.__getProp?.[propName]
const args = { keyName, getContext, target, propName, value: thisKeyOfContext?.[propName] }
if (getPropHandler) return call(args, _ => getPropHandler(args))
const catchAllGetter = thisKeyOfContext?.[keyName]?.__get
if (catchAllGetter) return call(args, _ => catchAllGetter?.(args))
return Reflect.get(target?.__value, prop)
},
defineProperty(target, propName, descriptor) {
const definePropHandler = thisKeyOfContext?.__defineProp?.[propName]
if (definePropHandler) return call({propName}, _ => definePropHandler({ target, propName, descriptor }))
return Reflect.defineProperty(target?.__value, propName, descriptor)
},
has(target, propName) {
const hasPropHandler = thisKeyOfContext?.__has?.[propName]
if (hasPropHandler) return call({ propName}, _ => hasPropHandler({ target, propName }))
return Reflect.has(target?.__value, propName)
},
set(target, propName, value) {
const setPropHandler = thisKeyOfContext?.__setProp?.[propName]
const args = { keyName, getContext, target, propName, value, oldValue: thisKeyOfContext[propName], newValue: value }
if (setPropHandler) {
let newValue = call({args}, _ => setPropHandler(args))
if(newValue === undefined) newValue = value
thisKeyOfContext[newValue] = newValue
return true
}
const catchAllSetter = thisKeyOfContext?.[keyName]?.__set
debugger;
if (catchAllSetter) {
let newValue = call({propName}, _ => catchAllSetter?.(args))
if(newValue === undefined) newValue = value
thisKeyOfContext[newValue] = newValue
return true
}
return Reflect.set(target?.__value, prop, value)
},
apply(target, thisArg, argumentsList) {
const applyPropHandler = thisKeyOfContext?.__apply?.[propName]
if (applyPropHandler) return call({propName}, _ => applyPropHandler?.())
return Reflect.apply(target?.__value, thisArg, argumentsList)
}
}
const proxy = new Proxy(initialValue, proxyHandler)
if(!initialObject?.__proxies) initialObject.__proxies = {}
if(!initialObject?.__proxies?.[keyName]) initialObject.__proxies[keyName] = {}
initialObject.__proxies[keyName] = proxy
context.args ||={}
context.args.propName = keyName
ctx.call(context, fauxProxyFn)
getContext = () => {return {
context: {},
args: {}
}}
return (args) => {
if (args !== undefined) {
initialProxy[keyName] = args
return initialProxy[keyName]
}
return initialProxy[keyName]
}
}
}
export function get(fn) {
const ctx = getContext()
// if (ctx.propName) {
ctx.context.__values[propName] ??= {}
ctx.context.__values[propName].__get = fn
// }
}
export function set(fn) {
const ctx = getContext()
console.log({ctx})
if (ctx.args.propName) {
ctx.context.__values[ctx.args.propName] ??= {}
ctx.context.__values[ctx.args.propName].__set = fn
}
}
// const CounterContext = createContext('countercontext')
// const Counter = CounterContext('counter', () => {
// set(args => {
// console.log(`old: ${args.oldValue}, new: ${args.newValue} `)
// })
// }, 0)
// Counter() = 5
// function createComponent (name, constructor, initialData = {}) {
// const ComponentContext = createContext(name, {
// constructor,
// example: constructor(initialData),
// initialData,
// instances: {}
// })
// component.forEach(component => {
// const key = determineKey(component)
// ComponentContext().instances[key] = component
// constructor(component)
// })
// }
Here are some suggestions for how this faux class context system could handle arrays and deep nested objects:
Arrays:
Wrap array mutations like push/pop/splice in proxy set handlers to trigger reactivity.
Use something like immer to produce a new copy of the array when it changes to avoid proxy observeability issues.
Allow registering custom handlers for array functions like map/filter/reduce that can trigger updates.
Support observable arrays out of the box with built-in handling for common mutations.
Deep objects:
Recursively proxy nested objects to maintain reactivity all the way down.
Use a "reactive" function that ensures object is proxied before adding to state.
Track proxies by target object reference to avoid duplicated nesting.
Proxy objects lazily on first get to avoid overhead until needed.
Provide utility functions like setDeep and getDeep that work for proxies.
Allow customizing depth of reactive nesting.
Batch nested updates to avoid over-rendering.
Explicitly proxy sub-objects when needed instead of always recursing.
In general:
Minimize performance impact by keeping proxies lean.
Educate on best practices like avoiding loops over proxies.
Provide escape hatches to drop down to raw values when needed.
Focus the reactivity on app state rather than all data.
The goal would be keeping full reactivity while optimizing performance. Some clever caching, batching and avoiding unnecessary proxies could help significantly.
export function createObserver(fn, options) {
const observer = new MutationObserver(fn)
return function (element) {
if (isString(element)) element = document.querySelector(element)
observer.observe(element, options)
}
}
function getAddedElements (mutations) {
let addedElements = []
for (let mutation of mutations) {
for (let element of mutation.addedNodes) {
addedElements.push(element)
const descendants = element?.querySelectorAll?.('*') || []
addedElements.push(...descendants)
}
}
return addedElements
}
export function ensureRunAfterDOM (fn) {
let handleDOMLoaded = Fn(fn)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', handleDOMLoaded);
} else {
handleDOMLoaded();
}
}
export function Fn (fn) {
return function (...args) {
return fn(...args)
}
}
export function isString (value) {
return typeof value === 'string' || value instanceof String
}
export function isObject (o) {
return (typeof o === 'object' && o !== null)
}
export function makeEventName (name) {
name = name.replace(/^on/, '')
// name = name[0].toLowerCase() + name.substring(1)
return name.toLowerCase()
}
export function createEvent (eventName, data) {
return new CustomEvent(eventName, data)
}
export function el(e) {
return e.currentTarget
}
export function createFallback (...objs) {
const select = (field, ...moreObjs) => {
for (let obj of [...objs, ...moreObjs]) {
if (obj?.[field] || Object.hasOwn(obj, field)) return {
obj,
value: obj[field]
}
}
}
return new Proxy ({}, {
get(target, prop, receiver) {
return select(prop, target)?.value
},
set(target, prop, value) {
let obj = select(prop, target)?.obj
if (obj) {
obj[prop] = value
} else {
target[prop] = value
}
}
})
}
On removed from dom needs to be implimeted