UNPKG

@muban/muban

Version:

Writing components for server-rendered HTML

172 lines (171 loc) 9.69 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import { watch, watchEffect } from '@vue/runtime-core'; import { ref, unref } from '@vue/reactivity'; import { extractFromHTML } from 'html-extract-data'; import typedObjectEntries from '../type-utils/typedObjectEntries'; import typedObjectKeys from '../type-utils/typedObjectKeys'; import { devtoolsComponentUpdated } from '../utils/devtools'; import { recursiveUnref } from '../utils/utils'; import { bindingsList } from './bindings'; function createBindingsHelpers(binding) { // used to trigger the "has" and "get" bindings when a new binding is added const bindingProps = ref(typedObjectKeys(binding.props)); return { hasBinding: (bindingName) => bindingProps.value.includes(bindingName), getBinding: (bindingName) => bindingProps.value && binding.props[bindingName], setBinding: (bindingName, bindingValue) => { // eslint-disable-next-line no-param-reassign binding.props[bindingName] = bindingValue; bindingProps.value = typedObjectKeys(binding.props); }, }; } export const applyBindings = (bindings, instance) => { if (bindings) { return bindings.flatMap((binding) => { // TODO: Devtools only watchEffect(() => { // "trigger" all observables for this binding by unwrapping it recursiveUnref(binding.props); // update this component instance itself devtoolsComponentUpdated(instance); }); if (binding.type === 'element') { return watch(() => unref(binding.ref), (element, oldValue, onInvalidate) => { const bindingHelpers = createBindingsHelpers(binding); // eslint-disable-next-line no-shadow const bindings = typedObjectEntries(binding.props).flatMap(([bindingName, bindingValue]) => { var _a, _b; if (!(bindingName in bindingsList)) { // eslint-disable-next-line no-console console.warn(`No binding exists for "${bindingName}", only supported bindings are [${Object.keys(bindingsList)}]`); } else if (element) { return (_b = (_a = bindingsList)[bindingName]) === null || _b === void 0 ? void 0 : _b.call(_a, element, bindingValue, bindingHelpers); } return undefined; }); onInvalidate(() => { // TODO debug // eslint-disable-next-line no-shadow bindings.forEach((binding) => binding && binding()); }); }, { immediate: true }); } if (binding.type === 'collection') { return watch( // eslint-disable-next-line no-shadow () => unref(binding.ref).map((ref) => unref(ref)), (elements, oldValue, onInvalidate) => { const bindingHelpers = createBindingsHelpers(binding); // eslint-disable-next-line no-shadow const bindings = typedObjectEntries(binding.props).flatMap(([bindingName, bindingValue]) => { if (!(bindingName in bindingsList)) { // eslint-disable-next-line no-console console.warn(`No binding exists for "${bindingName}", only supported bindings are [${Object.keys(bindingsList)}]`); } else if (elements) { return elements.flatMap((element) => { var _a, _b; return (_b = (_a = bindingsList)[bindingName]) === null || _b === void 0 ? void 0 : _b.call(_a, element, bindingValue, bindingHelpers); }); } return undefined; }); onInvalidate(() => { // eslint-disable-next-line no-shadow bindings.forEach((binding) => binding && binding()); }); }, { immediate: true }); } if (binding.type === 'component') { typedObjectEntries(binding.props).forEach(([propName, bindingValue]) => { watchEffect(() => { var _a; if (propName === '$element') { typedObjectEntries(bindingValue).forEach(([elementBindingKey, elementBindingValue]) => { var _a, _b; if (['css', 'style', 'attr', 'event'].includes(elementBindingKey)) { const element = (_a = unref(binding.ref)) === null || _a === void 0 ? void 0 : _a.element; if (element) { (_b = bindingsList[elementBindingKey]) === null || _b === void 0 ? void 0 : _b.call(bindingsList, element, elementBindingValue); } } }); } else { (_a = unref(binding.ref)) === null || _a === void 0 ? void 0 : _a.setProps({ [propName]: unref(bindingValue), }); } }); }); } else if (binding.type === 'componentCollection') { typedObjectEntries(binding.props).forEach(([propName, bindingValue]) => { watchEffect(() => { const reff = unref(binding.ref).map((r) => unref(r)); // eslint-disable-next-line no-shadow reff === null || reff === void 0 ? void 0 : reff.forEach((ref) => { watchEffect(() => { if (propName === '$element') { typedObjectEntries(bindingValue).forEach(([elementBindingKey, elementBindingValue]) => { var _a; if (['css', 'style', 'attr', 'event'].includes(elementBindingKey)) { (_a = bindingsList[elementBindingKey]) === null || _a === void 0 ? void 0 : _a.call(bindingsList, unref(ref).element, elementBindingValue); } }); } else { ref === null || ref === void 0 ? void 0 : ref.setProps({ [propName]: unref(bindingValue), }); } }); }); }); }); } else if (binding.type === 'template') { // eslint-disable-next-line no-shadow const { ref, extract, forceImmediateRender, onUpdate } = binding.props; let initialRender = true; let containerIsEmpty = false; if (ref && ref.element) { if (extract) { const extracted = extractFromHTML(ref.element, extract.config); extract.onData(extracted); } // if the container is empty, we probably want to do an initial render // otherwise, we might want to leave the container as-is initially containerIsEmpty = ref.element.innerHTML.trim() === ''; } watchEffect(() => { if (ref === null || ref === void 0 ? void 0 : ref.element) { // if neither of these are true, we should not do an initial render, // but instead only "watch" the data in the onUpdate function const shouldRenderUpdate = containerIsEmpty || forceImmediateRender || !initialRender; // TODO: attach parent component for context // pass along how the result of the update function is going to be used, // so the implementation can conditionally only invoke the watched observables, but // omit the template rendering const result = onUpdate(!shouldRenderUpdate); if (shouldRenderUpdate) { ref.element.innerHTML = Array.isArray(result) ? result.join('') : result !== null && result !== void 0 ? result : ''; } initialRender = false; // TODO: make nicer? // it takes some time for the MutationObserver to detect the newly added DOM elements // for the "ref" watcher to update and instantiate and add the new children setTimeout(() => { instance.children.forEach((component) => component.setup()); }, 1); } }); } else if (binding.type === 'bindMap') { return binding.dispose; } return undefined; }); } };