@v4fire/client
Version:
V4Fire client core library
290 lines (232 loc) • 6.37 kB
text/typescript
/*!
* V4Fire Client Core
* https://github.com/V4Fire/Client
*
* Released under the MIT license
* https://github.com/V4Fire/Client/blob/master/LICENSE
*/
/**
* [[include:core/component/vnode/README.md]]
* @packageDocumentation
*/
import { components } from 'core/component/const';
import type { RenderContext, VNode, VNodeData, NormalizedScopedSlot } from 'core/component/engines';
import type { ComponentInterface, ComponentMeta } from 'core/component/interface';
import type {
ComponentVNodeData,
ComponentModelVNodeData,
PatchComponentVDataOptions
} from 'core/component/vnode/interface';
export * from 'core/component/vnode/interface';
/**
* Returns a component render context object from the specified vnode
*
* @param component - component name or meta object
* @param vnode
* @param [parent] - parent component instance
*/
export function getComponentRenderCtxFromVNode(
component: string | ComponentMeta,
vnode: VNode,
parent?: ComponentInterface
): RenderContext {
const
data = getComponentDataFromVNode(component, vnode);
return {
parent: Object.cast(parent),
children: vnode.children ?? [],
props: data.props,
listeners: <Record<string, CanArray<Function>>>data.on,
slots: () => data.slots,
scopedSlots: <Record<string, NormalizedScopedSlot>>data.scopedSlots,
injections: undefined,
data: {
ref: data.ref,
refInFor: data.refInFor,
on: <Record<string, CanArray<Function>>>data.on,
nativeOn: <Record<string, CanArray<Function>>>data.nativeOn,
attrs: data.attrs,
class: data.class,
staticClass: data.staticClass,
style: data.style,
directives: data.directives
}
};
}
/**
* Returns a component data object from the specified vnode
*
* @param component - component name or a meta object
* @param vnode
*/
export function getComponentDataFromVNode(component: string | ComponentMeta, vnode: VNode): ComponentVNodeData {
const
vData = vnode.data ?? {},
{slots, model} = (<Dictionary>vData);
const res = <ComponentVNodeData>{
ref: vData.ref,
refInFor: vData.refInFor,
attrs: {},
props: {},
model: (<Dictionary>vData).model,
directives: Array.concat([], vData.directives),
slots: {...<Dictionary>slots},
scopedSlots: {...vData.scopedSlots},
on: {...vData.on},
nativeOn: {...vData.nativeOn},
class: [].concat(vData.class ?? []),
staticClass: vData.staticClass,
style: vData.style
};
const
meta = Object.isString(component) ? components.get(component) : component;
if (!meta) {
res.attrs = vData.attrs ?? res.attrs;
return res;
}
const
componentModel = meta.params.model;
if (model != null && componentModel) {
const
// eslint-disable-next-line @typescript-eslint/unbound-method
{value, callback} = <ComponentModelVNodeData>model,
{prop, event} = componentModel;
if (prop != null && event != null) {
res.props[prop] = value;
res.on[event] = callback;
}
}
const
vAttrs = vData.attrs,
propsObj = meta.component.props;
for (let keys = Object.keys(propsObj), i = 0; i < keys.length; i++) {
res.props[keys[i]] = undefined;
}
if (vAttrs) {
for (let keys = Object.keys(vAttrs), i = 0; i < keys.length; i++) {
const
key = keys[i],
prop = key.camelize(false),
val = vAttrs[key];
if (propsObj[prop]) {
res.props[prop] = val;
} else {
res.attrs[key] = val;
}
}
}
if (slots == null && vnode.children) {
const
{children} = vnode;
let
hasSlots = false;
for (let i = 0; i < children.length; i++) {
const
node = children[i],
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
data = node?.data ?? {};
const
{attrs} = data;
if (attrs?.slot != null) {
hasSlots = true;
res.slots[attrs.slot] = node;
}
}
if (!hasSlots) {
res.slots.default = children;
}
}
return res;
}
/**
* Patches the specified component VNode data object by using another VNode data object
*
* @param data - VNode data object
* @param anotherData - another VNode data object
* @param [opts] - additional options
*/
export function patchComponentVData(
data: CanUndef<VNodeData | ComponentVNodeData>,
anotherData: CanUndef<VNodeData | ComponentVNodeData>,
opts?: PatchComponentVDataOptions
): CanUndef<VNodeData | ComponentVNodeData> {
if (anotherData == null || data == null) {
return data;
}
data.staticClass = data.staticClass ?? '';
if (Object.isTruly(anotherData.staticClass)) {
data.staticClass += ` ${anotherData.staticClass}`;
}
if (Object.isTruly(anotherData.class)) {
data.class = Array.concat([], data.class, anotherData.class);
}
if (Object.isTruly(anotherData.style)) {
data.style = parseStyle(data.style, parseStyle(anotherData.style));
}
if (Object.isTruly(anotherData.attrs) && opts?.patchAttrs) {
data.attrs = Object.assign(data.attrs ?? {}, anotherData.attrs);
}
data.ref = anotherData.ref;
data.refInFor = anotherData.refInFor;
data.directives = Array.concat([], data.directives, anotherData.directives);
data.on = data.on ?? {};
if (anotherData.nativeOn) {
const
{on} = data;
for (let o = anotherData.nativeOn, keys = Object.keys(o), i = 0; i < keys.length; i++) {
const key = keys[i];
on[key] = Array.concat([], on[key], o[key]);
}
}
return data;
}
/**
* Parses the specified style value and returns a dictionary with styles
*
* @param style - original style
* @param [acc] - accumulator
*
* @example
* ```js
* // {color: 'red', background: 'blue'}
* parseStyle(['color: red', {background: 'blue'}])
* ```
*/
export function parseStyle(
style: CanUndef<CanArray<string | object>>,
acc: Dictionary<string> = {}
): Dictionary<string> {
if (!Object.isTruly(style)) {
return acc;
}
if (Object.isDictionary(style)) {
Object.assign(acc, style);
return acc;
}
if (Object.isString(style)) {
const
styles = style.split(';');
for (let i = 0; i < styles.length; i++) {
const
rule = styles[i];
if (rule.trim().length === 0) {
continue;
}
const chunks = rule.split(':');
acc[chunks[0].trim()] = chunks[1].trim();
}
return acc;
}
if (Object.isArray(style)) {
for (let i = 0; i < style.length; i++) {
const
el = style[i];
if (Object.isDictionary(el)) {
Object.assign(acc, el);
} else {
parseStyle(<CanArray<string>>el, acc);
}
}
}
return acc;
}