mirror of
https://github.com/tenrok/vue-meta.git
synced 2026-06-14 21:12:25 +03:00
feat: refactor of object merge & make vue-router example work
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
export * from './proxy'
|
||||
export * from './remove'
|
||||
export * from './set'
|
||||
export * from './update'
|
||||
@@ -1,64 +0,0 @@
|
||||
import { markRaw } from 'vue'
|
||||
import { isObject } from '@vue/shared'
|
||||
import { MetaContext, MetainfoInput, MetainfoProxy, PathSegments } from '../types'
|
||||
import { update } from './update'
|
||||
import { remove } from './remove'
|
||||
|
||||
export function createProxy (target: MetainfoInput, handler: ProxyHandler<object>): MetainfoProxy {
|
||||
return markRaw(new Proxy(target, handler))
|
||||
}
|
||||
|
||||
export function createHandler (context: MetaContext, pathSegments: PathSegments = []): ProxyHandler<object> {
|
||||
return {
|
||||
get (target: object, key: string, receiver: object) {
|
||||
const value = Reflect.get(target, key, receiver)
|
||||
|
||||
if (!isObject(value) || key === '__vm_proxy') {
|
||||
return value
|
||||
}
|
||||
|
||||
if (!value.__vm_proxy) {
|
||||
const keyPath: PathSegments = [...pathSegments, key]
|
||||
|
||||
const handler = /* #__PURE__ */ createHandler(context, keyPath)
|
||||
Object.defineProperty(
|
||||
value,
|
||||
'__vm_proxy',
|
||||
{
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: createProxy(value, handler)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return value.__vm_proxy
|
||||
},
|
||||
set (
|
||||
target: { [key: string]: any }, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
key: string,
|
||||
value: any
|
||||
): boolean {
|
||||
const success = Reflect.set(target, key, value)
|
||||
|
||||
if (success) {
|
||||
update(context, pathSegments, key, value)
|
||||
}
|
||||
|
||||
return success
|
||||
},
|
||||
deleteProperty (
|
||||
target: { [key: string]: any },
|
||||
prop: string
|
||||
) {
|
||||
const success = Reflect.deleteProperty(target, prop)
|
||||
|
||||
if (success) {
|
||||
remove(context, pathSegments, prop)
|
||||
}
|
||||
|
||||
return success
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { MetaContext, PathSegments } from '../types'
|
||||
import { setByObject } from './set'
|
||||
import { update } from './update'
|
||||
|
||||
export function remove (context: MetaContext, pathSegments?: PathSegments, key?: string) {
|
||||
if (!key && (!pathSegments || !pathSegments.length)) {
|
||||
setByObject(context, {})
|
||||
return
|
||||
}
|
||||
|
||||
update(context, pathSegments || [], key || '', undefined)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { hasOwn, isArray } from '@vue/shared'
|
||||
import { clone } from '../utils'
|
||||
import { ActiveNode, GetActiveNode, MetaContext, PathSegments, ShadowNode, GetShadowNodes } from '../types'
|
||||
|
||||
export function resolveActive (
|
||||
context: MetaContext,
|
||||
key: string,
|
||||
pathSegments: PathSegments,
|
||||
shadowParent: ShadowNode,
|
||||
activeParent: ActiveNode
|
||||
) {
|
||||
let value
|
||||
|
||||
const isUpdatingArrayKey = isArray(activeParent)
|
||||
|
||||
let shadowLength
|
||||
if (isUpdatingArrayKey) {
|
||||
shadowLength = shadowParent ? shadowParent.length : 0
|
||||
} else {
|
||||
shadowLength = shadowParent[key] ? shadowParent[key].length : 0
|
||||
}
|
||||
|
||||
if (shadowLength > 1) {
|
||||
// Is using freeze useful? Idea is to prevent the user from messing with these options by mistake
|
||||
const getShadow: GetShadowNodes = () => Object.freeze(clone(isUpdatingArrayKey ? shadowParent : shadowParent[key]))
|
||||
const getActive: GetActiveNode = () => Object.freeze(clone(isUpdatingArrayKey ? activeParent : activeParent[key]))
|
||||
|
||||
value = context.resolve(
|
||||
key,
|
||||
pathSegments,
|
||||
getShadow,
|
||||
getActive
|
||||
)
|
||||
} else if (shadowLength) {
|
||||
value = shadowParent[key][0].value
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
delete activeParent[key]
|
||||
} else if (isUpdatingArrayKey) {
|
||||
// set new values
|
||||
for (const k in value) {
|
||||
activeParent[k] = value[k]
|
||||
}
|
||||
|
||||
// delete old values
|
||||
for (const k in activeParent) {
|
||||
if (!(k in value)) {
|
||||
delete activeParent[k]
|
||||
}
|
||||
}
|
||||
} else if (!hasOwn(activeParent, key) || activeParent[key] !== value) {
|
||||
activeParent[key] = value
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
import { isArray, isPlainObject, /**/ hasOwn } from '@vue/shared'
|
||||
import { ActiveNode, MetaContext, PathSegments, ShadowNode } from '../types'
|
||||
import { resolveActive } from './resolve'
|
||||
|
||||
export function set (
|
||||
context: MetaContext,
|
||||
key: string,
|
||||
value: any,
|
||||
shadowParent?: ShadowNode,
|
||||
activeParent?: ActiveNode,
|
||||
pathSegments: PathSegments = []
|
||||
) {
|
||||
if (!shadowParent) {
|
||||
shadowParent = context.shadow
|
||||
}
|
||||
|
||||
if (!activeParent) {
|
||||
activeParent = context.active
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
// shadow & active should always be in sync
|
||||
// if not we have bigger fish to fry
|
||||
if (!shadowParent[key]) {
|
||||
shadowParent[key] = {}
|
||||
activeParent[key] = {}
|
||||
}
|
||||
|
||||
return setByObject(
|
||||
context,
|
||||
value,
|
||||
shadowParent[key],
|
||||
activeParent[key],
|
||||
pathSegments
|
||||
)
|
||||
}
|
||||
|
||||
let idx = -1
|
||||
|
||||
// Step 1: First find the current data for this key
|
||||
|
||||
// If the activeParent is an array itself, then we are setting the key
|
||||
// for an array, so no need to use [key] on the active/shadow parents
|
||||
// ie this is to support proxy.myArrayValue[1] = 'value' (instead of proxy.myArrayValue = ['value'])
|
||||
if (isArray(activeParent)) {
|
||||
idx = shadowParent.findIndex(({ context: shadowContext }: { context: MetaContext }) => shadowContext === context)
|
||||
// Check if we already have values from other components
|
||||
} else if (!shadowParent[key]) {
|
||||
shadowParent[key] = []
|
||||
// If we already have values listed, try to find the one for the current context
|
||||
} else if (isArray(shadowParent[key])) {
|
||||
// check if we already have a value listed for this element for this context
|
||||
idx = shadowParent[key].findIndex(({ context: shadowContext }: { context: MetaContext }) => shadowContext === context)
|
||||
}
|
||||
|
||||
// Step 2: Set/update the data for this key
|
||||
|
||||
if (isArray(activeParent) && idx < 0) {
|
||||
// TODO: what now?
|
||||
console.warn('shadowParent not found')
|
||||
|
||||
// Change the array/key element in the shadowParent
|
||||
} else if (isArray(activeParent)) {
|
||||
if (value === undefined) {
|
||||
shadowParent[idx].value.splice(key, 1)
|
||||
} else {
|
||||
shadowParent[idx].value[key] = value
|
||||
}
|
||||
// if this context/key combo exists but value is undefined, remove it
|
||||
} else if (idx > -1 && value === undefined) {
|
||||
shadowParent[key].splice(idx, 1)
|
||||
// overwrite current value for context/key combo
|
||||
} else if (idx > -1) {
|
||||
shadowParent[key][idx].value = value
|
||||
|
||||
// new context/key combo so just add value
|
||||
} else if (idx === -1 && value) {
|
||||
shadowParent[key].push({ context, value })
|
||||
} else if (value === undefined) {
|
||||
delete shadowParent[key]
|
||||
}
|
||||
|
||||
// Step 3: Update the active data
|
||||
resolveActive(context, key, pathSegments, shadowParent, activeParent)
|
||||
}
|
||||
|
||||
export function setByObject (
|
||||
context: MetaContext,
|
||||
value: any,
|
||||
shadowParent? : ShadowNode,
|
||||
activeParent?: ActiveNode,
|
||||
pathSegments: PathSegments = []
|
||||
) {
|
||||
if (!shadowParent) {
|
||||
shadowParent = context.shadow
|
||||
}
|
||||
|
||||
if (!activeParent) {
|
||||
activeParent = context.active
|
||||
}
|
||||
|
||||
// cleanup properties that no longer exists
|
||||
for (const key in shadowParent) {
|
||||
if (hasOwn(value, key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (isPlainObject(shadowParent[key])) {
|
||||
setByObject(context, {}, shadowParent[key], activeParent[key], [
|
||||
...pathSegments,
|
||||
key
|
||||
])
|
||||
continue
|
||||
} /**/
|
||||
|
||||
set(context, key, undefined, shadowParent, activeParent, [
|
||||
...pathSegments,
|
||||
key
|
||||
])
|
||||
}
|
||||
|
||||
// set new values
|
||||
for (const key in value) {
|
||||
set(context, key, value[key], shadowParent, activeParent, [
|
||||
...pathSegments,
|
||||
key
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { MetaContext, PathSegments, ShadowNode, ActiveNode } from '../types'
|
||||
import { set } from './set'
|
||||
|
||||
export function update (
|
||||
context: MetaContext,
|
||||
pathSegments: PathSegments,
|
||||
key: string,
|
||||
value: any
|
||||
) {
|
||||
const { active, shadow } = context
|
||||
|
||||
let activeParent: ActiveNode = active
|
||||
let shadowParent: ShadowNode = shadow
|
||||
|
||||
for (const segment of pathSegments) {
|
||||
activeParent = activeParent[segment]
|
||||
shadowParent = shadowParent[segment]
|
||||
}
|
||||
|
||||
set(context, key, value, shadowParent, activeParent)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export { defaultConfig } from './config'
|
||||
export { createManager } from './manager'
|
||||
export { resolveOption } from './resolvers'
|
||||
export * from './useApi'
|
||||
export * from './types'
|
||||
|
||||
+19
-33
@@ -1,36 +1,31 @@
|
||||
import { App, markRaw, reactive, onUnmounted, ComponentInternalInstance } from 'vue'
|
||||
import { App, reactive, onUnmounted, ComponentInternalInstance } from 'vue'
|
||||
import { isFunction } from '@vue/shared'
|
||||
import { createProxy, createHandler, setByObject, remove } from './continuous-object-merge'
|
||||
import { PolySymbol } from './symbols'
|
||||
import { createMergedObject } from './object-merge'
|
||||
import { applyMetaPlugin } from './install'
|
||||
// import * as deepestResolver from './resolvers/deepest'
|
||||
import { Config, ActiveResolverObject, ActiveResolverMethod, MetaContext, MetainfoInput, MetaProxy } from './types'
|
||||
|
||||
let contextCounter: number = 0
|
||||
import { Config, Resolver, MetainfoInput, MetaContext, MetaProxy } from './types'
|
||||
import type { ResolveMethod } from './object-merge'
|
||||
|
||||
export type Manager = {
|
||||
readonly config: Config
|
||||
|
||||
install(app: App): void
|
||||
createMetaProxy(obj: MetainfoInput, vm?: ComponentInternalInstance): MetaProxy
|
||||
addMeta(obj: MetainfoInput, vm?: ComponentInternalInstance): MetaProxy
|
||||
}
|
||||
|
||||
export const shadow = markRaw({})
|
||||
export const active = reactive({})
|
||||
|
||||
export function createManager (config: Config, resolver: ActiveResolverObject): Manager {
|
||||
const resolve: ActiveResolverMethod = (key, pathSegments, getShadow, getActive) => {
|
||||
if (!resolver) {
|
||||
return
|
||||
}
|
||||
|
||||
export function createManager (config: Config, resolver: Resolver | ResolveMethod): Manager {
|
||||
const resolve: ResolveMethod = (options, contexts, active, key, pathSegments) => {
|
||||
if (isFunction(resolver)) {
|
||||
return resolver(key, pathSegments, getShadow, getActive)
|
||||
return resolver(options, contexts, active, key, pathSegments)
|
||||
}
|
||||
|
||||
return resolver.resolve(key, pathSegments, getShadow, getActive)
|
||||
return resolver.resolve(options, contexts, active, key, pathSegments)
|
||||
}
|
||||
|
||||
const { addSource, delSource } = createMergedObject(resolve, active)
|
||||
|
||||
// TODO: validate resolver
|
||||
const manager: Manager = {
|
||||
config,
|
||||
@@ -39,29 +34,20 @@ export function createManager (config: Config, resolver: ActiveResolverObject):
|
||||
applyMetaPlugin(app, this, active)
|
||||
},
|
||||
|
||||
createMetaProxy (obj, vm) {
|
||||
const context: MetaContext = {
|
||||
id: PolySymbol(`ctx${contextCounter++}`),
|
||||
vm: vm || undefined,
|
||||
resolve,
|
||||
shadow,
|
||||
active
|
||||
addMeta (metaObj, vm) {
|
||||
const resolveContext: MetaContext = { vm }
|
||||
if (resolver && 'setup' in resolver && isFunction(resolver.setup)) {
|
||||
resolver.setup(resolveContext)
|
||||
}
|
||||
|
||||
const unmount = <T extends Function = () => any>() => remove(context)
|
||||
// TODO: optimize initial compute
|
||||
const meta = addSource(metaObj, resolveContext, true)
|
||||
|
||||
const unmount = () => delSource(meta)
|
||||
if (vm) {
|
||||
onUnmounted(unmount)
|
||||
}
|
||||
|
||||
if (resolver && resolver.setup && isFunction(resolver)) {
|
||||
resolver.setup(context)
|
||||
}
|
||||
|
||||
setByObject(context, obj)
|
||||
|
||||
const handler = /* #__PURE__ */ createHandler(context)
|
||||
const meta = createProxy(obj, handler)
|
||||
|
||||
return {
|
||||
meta,
|
||||
unmount
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// https://github.com/microsoft/TypeScript/issues/1863
|
||||
export const IS_PROXY = Symbol('kIsProxy') as unknown as string
|
||||
export const PROXY_SOURCES = Symbol('kProxySources') as unknown as string
|
||||
export const PROXY_TARGET = Symbol('kProxyTarget') as unknown as string
|
||||
export const RESOLVE_CONTEXT = Symbol('kResolveContext') as unknown as string
|
||||
@@ -0,0 +1,83 @@
|
||||
import { PROXY_TARGET } from './constants'
|
||||
import { createProxy } from './proxy'
|
||||
import { recompute } from './recompute'
|
||||
|
||||
export type MergeSource = {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export type MergedObjectValue = boolean | number | string | MergedObject | any
|
||||
|
||||
export type MergedObject = {
|
||||
[key: string]: MergedObjectValue
|
||||
}
|
||||
|
||||
export type PathSegments = Array<string>
|
||||
|
||||
export type ResolveContext = {}
|
||||
|
||||
export type ResolveMethod = (
|
||||
options: Array<any>,
|
||||
contexts: Array<ResolveContext>,
|
||||
active: MergedObjectValue,
|
||||
key: string | number | symbol,
|
||||
pathSegments: PathSegments,
|
||||
) => MergedObjectValue
|
||||
|
||||
export type MergeContext = {
|
||||
resolve: ResolveMethod
|
||||
active: MergedObject
|
||||
sources: Array<MergeSource>
|
||||
}
|
||||
|
||||
export const createMergedObject = (resolve: ResolveMethod, active: MergedObject = {}) => {
|
||||
const sources: Array<MergeSource> = []
|
||||
|
||||
if (!active) {
|
||||
active = {}
|
||||
}
|
||||
|
||||
const context: MergeContext = {
|
||||
active,
|
||||
resolve,
|
||||
sources
|
||||
}
|
||||
|
||||
const compute = () => recompute(context)
|
||||
|
||||
const addSource = (source: MergeSource, resolveContext: ResolveContext | undefined, recompute: Boolean = false) => {
|
||||
const proxy = createProxy(context, source, resolveContext || {})
|
||||
|
||||
if (recompute) {
|
||||
compute()
|
||||
}
|
||||
|
||||
return proxy
|
||||
}
|
||||
|
||||
const delSource = (sourceOrProxy: MergeSource, recompute: boolean = true): boolean => {
|
||||
const index = sources.findIndex(src => src === sourceOrProxy || src[PROXY_TARGET] === sourceOrProxy)
|
||||
|
||||
if (index > -1) {
|
||||
sources.splice(index, 1)
|
||||
|
||||
if (recompute) {
|
||||
compute()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return {
|
||||
context,
|
||||
active,
|
||||
resolve,
|
||||
sources,
|
||||
addSource,
|
||||
delSource,
|
||||
compute
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import { markRaw } from 'vue'
|
||||
import { isArray, isObject, isPlainObject } from '@vue/shared'
|
||||
import { clone, pluck } from '../utils'
|
||||
import { IS_PROXY, PROXY_SOURCES, PROXY_TARGET, RESOLVE_CONTEXT } from './constants'
|
||||
import { recompute } from './recompute'
|
||||
import type { MergeContext, MergeSource, MergedObjectValue, PathSegments, ResolveContext } from '.'
|
||||
|
||||
export const createProxy = (context: MergeContext, target: MergeSource, resolveContext: ResolveContext, pathSegments: PathSegments = []) => {
|
||||
const handler = createHandler(context, resolveContext, pathSegments)
|
||||
const proxy = markRaw(new Proxy(target, handler))
|
||||
|
||||
if (!pathSegments.length && context.sources) {
|
||||
context.sources.push(proxy)
|
||||
}
|
||||
|
||||
return proxy
|
||||
}
|
||||
|
||||
export const createHandler: (context: MergeContext, resolveContext: ResolveContext, pathSegments: PathSegments) => ProxyHandler<any> = (context, resolveContext, pathSegments = []) => ({
|
||||
get: (target, key, receiver) => {
|
||||
if (key === IS_PROXY) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (key === PROXY_SOURCES) {
|
||||
return context.sources
|
||||
}
|
||||
|
||||
if (key === PROXY_TARGET) {
|
||||
return target
|
||||
}
|
||||
|
||||
if (key === RESOLVE_CONTEXT) {
|
||||
return resolveContext
|
||||
}
|
||||
|
||||
let value = Reflect.get(target, key, receiver)
|
||||
|
||||
if (!isObject(value)) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (!value[IS_PROXY]) {
|
||||
const keyPath: PathSegments = [...pathSegments, (key as string)]
|
||||
|
||||
value = createProxy(context, value, resolveContext, keyPath)
|
||||
target[key] = value
|
||||
}
|
||||
|
||||
return value
|
||||
},
|
||||
set: (target, key, value) => {
|
||||
const success = Reflect.set(target, key, value)
|
||||
console.warn(success, 'PROXY SET\nkey:', key, '\npath:', pathSegments, '\ntarget:', isArray(target), target, '\ncontext:\n', context)
|
||||
|
||||
if (success) {
|
||||
const isArrayItem = isArray(target)
|
||||
let hasArrayParent = false
|
||||
|
||||
let { sources: proxies, active } = context
|
||||
let activeSegmentKey
|
||||
|
||||
let index = 0
|
||||
for (const segment of pathSegments) {
|
||||
proxies = pluck(proxies, segment)
|
||||
|
||||
if (isArrayItem && index === pathSegments.length - 1) {
|
||||
activeSegmentKey = segment
|
||||
break
|
||||
}
|
||||
|
||||
if (isArray(active)) {
|
||||
hasArrayParent = true
|
||||
}
|
||||
|
||||
active = active[segment]
|
||||
index++
|
||||
}
|
||||
|
||||
if (hasArrayParent) {
|
||||
// TODO: fix that we dont have to recompute the full merged object
|
||||
// we should only have to recompute the branch that has changed
|
||||
// but there is an issue here with supporting both arrays of strings
|
||||
// as collections (parent vs parent of parent we need to trigger the
|
||||
// update from)
|
||||
recompute(context)
|
||||
return success
|
||||
}
|
||||
|
||||
let keyContexts: Array<ResolveContext> = []
|
||||
let keySources
|
||||
|
||||
if (isArrayItem) {
|
||||
keySources = proxies
|
||||
keyContexts = proxies.map(proxy => proxy[RESOLVE_CONTEXT])
|
||||
} else {
|
||||
keySources = pluck(proxies, key as string, proxy => keyContexts.push(proxy[RESOLVE_CONTEXT]))
|
||||
}
|
||||
|
||||
let resolved = context.resolve(
|
||||
keySources,
|
||||
keyContexts,
|
||||
active,
|
||||
key,
|
||||
pathSegments
|
||||
)
|
||||
|
||||
// Ensure to clone if value is an object, cause sources is an array of
|
||||
// the sourceProxies not the sources so we could trigger an endless loop when
|
||||
// updating a prop on an obj as the prop on the active object refers to
|
||||
// a prop on a proxy
|
||||
if (isPlainObject(resolved)) {
|
||||
resolved = clone(resolved)
|
||||
}
|
||||
|
||||
console.log('SET VALUE', isArrayItem, key, '\nresolved:\n', resolved, '\nsources:\n', context.sources, '\nactive:\n', active, Object.keys(active))
|
||||
|
||||
if (isArrayItem && activeSegmentKey) {
|
||||
active[activeSegmentKey] = resolved
|
||||
} else {
|
||||
active[(key as string)] = resolved
|
||||
}
|
||||
}
|
||||
|
||||
console.log('CONTEXT.ACTIVE', context.active, '\nparent:\n', target)
|
||||
return success
|
||||
},
|
||||
deleteProperty: (target, key) => {
|
||||
const success = Reflect.deleteProperty(target, key)
|
||||
console.warn('PROXY DELETE\nkey:', key, '\npath:', pathSegments, '\nparent:', isArray(target), target)
|
||||
|
||||
if (success) {
|
||||
const isArrayItem = isArray(target)
|
||||
let activeSegmentKey
|
||||
|
||||
let proxies = context.sources
|
||||
let active: MergedObjectValue = context.active as MergedObjectValue
|
||||
|
||||
let index = 0
|
||||
for (const segment of pathSegments) {
|
||||
proxies = proxies.map(proxy => proxy[segment])
|
||||
|
||||
if (isArrayItem && index === pathSegments.length - 1) {
|
||||
activeSegmentKey = segment
|
||||
break
|
||||
}
|
||||
|
||||
active = active[segment]
|
||||
index++
|
||||
}
|
||||
|
||||
// Check if the key still exists in one of the sourceProxies,
|
||||
// if so resolve the new value, if not remove the key
|
||||
if (proxies.some(proxy => (key in proxy))) {
|
||||
let keyContexts: Array<ResolveContext> = []
|
||||
let keySources
|
||||
|
||||
if (isArrayItem) {
|
||||
keySources = proxies
|
||||
keyContexts = proxies.map(proxy => proxy[RESOLVE_CONTEXT])
|
||||
} else {
|
||||
keySources = pluck(proxies, key as string, proxy => keyContexts.push(proxy[RESOLVE_CONTEXT]))
|
||||
}
|
||||
|
||||
let resolved = context.resolve(
|
||||
keySources,
|
||||
keyContexts,
|
||||
active,
|
||||
key,
|
||||
pathSegments
|
||||
)
|
||||
|
||||
if (isPlainObject(resolved)) {
|
||||
resolved = clone(resolved)
|
||||
}
|
||||
|
||||
console.log('SET VALUE', resolved)
|
||||
if (isArrayItem && activeSegmentKey) {
|
||||
active[activeSegmentKey] = resolved
|
||||
} else {
|
||||
active[(key as string)] = resolved
|
||||
}
|
||||
} else {
|
||||
delete active[key]
|
||||
}
|
||||
}
|
||||
|
||||
return success
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,94 @@
|
||||
import { isArray, isObject, isPlainObject } from '@vue/shared'
|
||||
import { clone, pluck } from '../utils'
|
||||
import { RESOLVE_CONTEXT } from './constants'
|
||||
import type { MergeContext, MergeSource, MergedObject, PathSegments, ResolveContext } from '.'
|
||||
|
||||
export const allKeys = (source?: MergeSource, ...sources: Array<MergeSource>): Array<string> => {
|
||||
const keys = source ? Object.keys(source) : []
|
||||
|
||||
if (sources) {
|
||||
for (const source of sources) {
|
||||
if (!source || !isObject(source)) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const key in source) {
|
||||
if (!keys.includes(key)) {
|
||||
keys.push(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add check for consistent types for each key (dev only)
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
export const recompute = (context: MergeContext, sources?: Array<MergeSource>, target?: MergedObject, path: PathSegments = []): void => {
|
||||
if (!path.length) {
|
||||
if (!target) {
|
||||
target = context.active
|
||||
}
|
||||
|
||||
if (!sources) {
|
||||
sources = context.sources
|
||||
}
|
||||
}
|
||||
|
||||
if (!target || !sources) {
|
||||
return
|
||||
}
|
||||
|
||||
const keys = allKeys(...sources)
|
||||
|
||||
// Clean up properties that dont exists anymore
|
||||
const targetKeys = Object.keys(target)
|
||||
for (const key of targetKeys) {
|
||||
if (!keys.includes(key)) {
|
||||
delete target[key]
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
// This assumes consistent types usages for keys across sources
|
||||
if (isPlainObject(sources[0][key])) {
|
||||
if (!target[key]) {
|
||||
target[key] = {}
|
||||
}
|
||||
|
||||
const keySources = []
|
||||
for (const source of sources) {
|
||||
if (key in source) {
|
||||
keySources.push(source[key])
|
||||
}
|
||||
}
|
||||
|
||||
recompute(context, keySources, target[key], [...path, key])
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure the target is an array if source is an array and target is empty
|
||||
if (!target[key] && isArray(sources[0][key])) {
|
||||
target[key] = []
|
||||
}
|
||||
|
||||
const keyContexts: Array<ResolveContext> = []
|
||||
const keySources = pluck(sources, key, source => keyContexts.push(source[RESOLVE_CONTEXT]))
|
||||
|
||||
let resolved = context.resolve(
|
||||
keySources,
|
||||
keyContexts,
|
||||
target[key],
|
||||
key,
|
||||
path
|
||||
)
|
||||
|
||||
if (isPlainObject(resolved)) {
|
||||
resolved = clone(resolved)
|
||||
}
|
||||
|
||||
// console.log('RESOLVED', key, resolved, 'was', target[key])
|
||||
target[key] = resolved
|
||||
}
|
||||
}
|
||||
+11
-37
@@ -1,17 +1,12 @@
|
||||
import {
|
||||
// ActiveNode,
|
||||
/* ActiveResolverSetup, ActiveResolverMethod, */ MetaContext,
|
||||
PathSegments,
|
||||
ShadowNode,
|
||||
GetActiveNode,
|
||||
GetShadowNodes
|
||||
} from '../types'
|
||||
import { ResolveContext, ResolveMethod } from '../object-merge'
|
||||
import { MetaContext } from '../types'
|
||||
import { resolveOption } from '.'
|
||||
|
||||
interface DeepestResolverMetaContext extends MetaContext {
|
||||
depth?: number
|
||||
type MergeContextDeepest = MetaContext & {
|
||||
depth: number
|
||||
}
|
||||
|
||||
export function setup (context: DeepestResolverMetaContext): void {
|
||||
export function setup (context: MergeContextDeepest): void {
|
||||
let depth: number = 0
|
||||
|
||||
if (context.vm) {
|
||||
@@ -29,30 +24,9 @@ export function setup (context: DeepestResolverMetaContext): void {
|
||||
context.depth = depth
|
||||
}
|
||||
|
||||
export function resolve (
|
||||
key: string,
|
||||
_pathSegments: PathSegments,
|
||||
getOptions: GetShadowNodes,
|
||||
getCurrentValue: GetActiveNode
|
||||
): any {
|
||||
let resolvedOption: ShadowNode | void
|
||||
|
||||
const options = getOptions()
|
||||
|
||||
for (const option of options) {
|
||||
if (!resolvedOption || resolvedOption.context.depth < option.context.depth) {
|
||||
resolvedOption = option
|
||||
}
|
||||
export const resolve: ResolveMethod = resolveOption((acc: any, context: ResolveContext) => {
|
||||
const { depth } = (context as unknown as MergeContextDeepest)
|
||||
if (!acc || depth > acc) {
|
||||
return acc
|
||||
}
|
||||
|
||||
console.log(
|
||||
'DEEPEST.RESOLVE',
|
||||
key,
|
||||
getCurrentValue(),
|
||||
options.map(({ value }) => value)
|
||||
)
|
||||
|
||||
if (resolvedOption) {
|
||||
return resolvedOption.value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
+22
-1
@@ -1 +1,22 @@
|
||||
export interface Resolver {}
|
||||
import { ResolveContext, ResolveMethod } from '../object-merge'
|
||||
|
||||
export type ResolveOptionReducer = (accumulator: any, context: ResolveContext) => ResolveMethod
|
||||
|
||||
export const resolveOption: (predicament: ResolveOptionReducer) => ResolveMethod = predicament => (options, contexts) => {
|
||||
let resolvedIndex = -1
|
||||
|
||||
contexts.reduce((acc: ResolveContext | undefined, context, index) => {
|
||||
const retval = predicament(acc, context)
|
||||
|
||||
if (retval !== acc) {
|
||||
resolvedIndex = index
|
||||
return retval
|
||||
}
|
||||
|
||||
return acc
|
||||
}, undefined)
|
||||
|
||||
if (resolvedIndex > -1) {
|
||||
return options[resolvedIndex]
|
||||
}
|
||||
}
|
||||
|
||||
+14
-46
@@ -1,11 +1,19 @@
|
||||
import { ComponentInternalInstance } from 'vue'
|
||||
import type { MergedObject, ResolveContext, ResolveMethod } from '../object-merge'
|
||||
|
||||
export type Immutable<T> = {
|
||||
readonly [P in keyof T]: Immutable<T[P]>
|
||||
}
|
||||
|
||||
export type TODO = any
|
||||
export type PathSegments = Array<string>
|
||||
|
||||
export type MetainfoInput = {
|
||||
[key: string]: TODO
|
||||
}
|
||||
|
||||
export type MetaContext = ResolveContext & {
|
||||
vm: ComponentInternalInstance | undefined
|
||||
}
|
||||
|
||||
export interface ConfigOption {
|
||||
tag?: string
|
||||
@@ -23,62 +31,22 @@ export interface Config {
|
||||
[key: string]: ConfigOption
|
||||
}
|
||||
|
||||
export interface MetainfoInput {
|
||||
[key: string]: TODO
|
||||
}
|
||||
export interface MetainfoProxy extends MergedObject {
|
||||
|
||||
export interface MetainfoProxy extends MetainfoInput {
|
||||
// Should be a symbol, but: https://github.com/microsoft/TypeScript/issues/1863
|
||||
__vm_proxy?: any // eslint-disable-line camelcase
|
||||
}
|
||||
|
||||
export interface MetainfoActive {
|
||||
[key: string]: TODO
|
||||
}
|
||||
|
||||
export type MetaContext = {
|
||||
id: string | symbol
|
||||
vm?: ComponentInternalInstance
|
||||
resolve: ActiveResolverMethod
|
||||
active: Object
|
||||
shadow: Object
|
||||
}
|
||||
|
||||
export type MetaProxy = {
|
||||
meta: MetainfoProxy
|
||||
unmount: TODO
|
||||
}
|
||||
|
||||
export type ActiveResolverSetup = (context: MetaContext) => void
|
||||
export type ActiveResolverMethod = (
|
||||
key: string,
|
||||
pathSegments: PathSegments,
|
||||
shadow: GetShadowNodes,
|
||||
active: GetActiveNode
|
||||
) => any
|
||||
export type ResolveSetup = (context: MetaContext) => void
|
||||
|
||||
export interface ActiveResolverObject {
|
||||
setup?: ActiveResolverSetup
|
||||
resolve: ActiveResolverMethod
|
||||
}
|
||||
|
||||
export interface ManagerResolverObject {
|
||||
setup: ActiveResolverSetup
|
||||
resolve: ActiveResolverMethod
|
||||
}
|
||||
|
||||
export interface ShadowNode {
|
||||
[key: string]: TODO
|
||||
}
|
||||
|
||||
export interface ActiveNode {
|
||||
[key: string]: TODO
|
||||
}
|
||||
|
||||
export interface GetShadowNodes {
|
||||
(): Array<ShadowNode>
|
||||
}
|
||||
|
||||
export interface GetActiveNode {
|
||||
(): ActiveNode
|
||||
export type Resolver = {
|
||||
setup?: ResolveSetup
|
||||
resolve: ResolveMethod
|
||||
}
|
||||
|
||||
+9
-9
@@ -3,6 +3,14 @@ import { Manager } from './manager'
|
||||
import { metaInfoKey } from './symbols'
|
||||
import { MetainfoActive, MetainfoInput, MetaProxy } from './types'
|
||||
|
||||
export function getCurrentManager (vm?: ComponentInternalInstance): Manager {
|
||||
if (!vm) {
|
||||
vm = getCurrentInstance()!
|
||||
}
|
||||
|
||||
return vm.appContext.config.globalProperties.$metaManager
|
||||
}
|
||||
|
||||
export function useMeta (obj: MetainfoInput, manager?: Manager): MetaProxy {
|
||||
const vm = getCurrentInstance()
|
||||
|
||||
@@ -15,17 +23,9 @@ export function useMeta (obj: MetainfoInput, manager?: Manager): MetaProxy {
|
||||
throw new Error('No manager or current instance')
|
||||
}
|
||||
|
||||
return manager.createMetaProxy(obj, vm || undefined)
|
||||
return manager.addMeta(obj, vm || undefined)
|
||||
}
|
||||
|
||||
export function useMetainfo (): MetainfoActive {
|
||||
return inject(metaInfoKey)!
|
||||
}
|
||||
|
||||
export function getCurrentManager (vm?: ComponentInternalInstance): Manager {
|
||||
if (!vm) {
|
||||
vm = getCurrentInstance()!
|
||||
}
|
||||
|
||||
return vm.appContext.config.globalProperties.$metaManager
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
export const pluck = (collection: Array<any>, key: string, callback?: (row: any) => void) => {
|
||||
const plucked: Array<any> = []
|
||||
|
||||
for (const row of collection) {
|
||||
if (key in row) {
|
||||
plucked.push(row[key])
|
||||
|
||||
if (callback) {
|
||||
callback(row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return plucked
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
interface DebugInterface {
|
||||
(...args: any[]): void
|
||||
warn: Function
|
||||
error: Function
|
||||
}
|
||||
|
||||
export const debugFn = (logFn: Function, setChildFns: boolean = false) => {
|
||||
const fn = (...args: any[]) => {
|
||||
try {
|
||||
throw new Error('DEBUG')
|
||||
} catch (err) {
|
||||
logFn(...args, '\n', err)
|
||||
}
|
||||
}
|
||||
|
||||
if (setChildFns) {
|
||||
fn.warn = debugFn(console.warn) // eslint-disable-line no-console
|
||||
fn.error = debugFn(console.error) // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
return fn
|
||||
}
|
||||
|
||||
export const debug: DebugInterface = debugFn(console.log, true) // eslint-disable-line no-console
|
||||
@@ -1 +1,3 @@
|
||||
export * from './clone'
|
||||
export * from './collection'
|
||||
export * from './debug'
|
||||
|
||||
Reference in New Issue
Block a user