diff --git a/src/index.js b/src/index.js index 4409bd7..8a1277e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,51 +1,61 @@ import { - toVNodes, - camelize, + defineComponent, + createApp, + defineAsyncComponent, + createVNode, + getCurrentInstance, +} from "vue"; +import { hyphenate, - callHooks, - injectHook, + camelize, + convertAttributeValue, + toVNodes, getInitialProps, createCustomEvent, - convertAttributeValue -} from './utils.js' + injectHook, + callHooks +} from "./utils"; -export default function wrap (Vue, Component) { - const isAsync = typeof Component === 'function' && !Component.cid - let isInitialized = false - let hyphenatedPropsList - let camelizedPropsList - let camelizedPropsMap - function initialize (Component) { - if (isInitialized) return +function wrap(Component) { + const isAsync = typeof Component === "function" && !Component.cid; + let isInitialized = false; + let hyphenatedPropsList; + let camelizedPropsList; + let camelizedPropsMap; - const options = typeof Component === 'function' - ? Component.options - : Component + function initialize(Component) { + if (isInitialized) return; + const options = + typeof Component === "function" ? Component.options : Component; // extract props info const propsList = Array.isArray(options.props) ? options.props - : Object.keys(options.props || {}) - hyphenatedPropsList = propsList.map(hyphenate) - camelizedPropsList = propsList.map(camelize) - const originalPropsAsObject = Array.isArray(options.props) ? {} : options.props || {} + : Object.keys(options.props || {}); + hyphenatedPropsList = propsList.map(hyphenate); + camelizedPropsList = propsList.map(camelize); + const originalPropsAsObject = Array.isArray(options.props) + ? {} + : options.props || {}; camelizedPropsMap = camelizedPropsList.reduce((map, key, i) => { - map[key] = originalPropsAsObject[propsList[i]] - return map - }, {}) + map[key] = originalPropsAsObject[propsList[i]]; + return map; + }, {}); // proxy $emit to native DOM events - injectHook(options, 'beforeCreate', function () { - const emit = this.$emit - this.$emit = (name, ...args) => { - this.$root.$options.customElement.dispatchEvent(createCustomEvent(name, args)) - return emit.call(this, name, ...args) - } - }) + injectHook(options, "beforeCreate", function() { + const emit = getCurrentInstance().emit; + getCurrentInstance().emit = function(name, ...args) { + this.$root.$options.customElement.dispatchEvent( + createCustomEvent(name, args) + ); + return emit.call(this, name, ...args); + }; + }); injectHook(options, 'created', function () { - // sync default props values to wrapper on created + // sync default props values to wrapper on created camelizedPropsList.forEach(key => { this.$root.props[key] = this[key] }) @@ -54,126 +64,142 @@ export default function wrap (Vue, Component) { // proxy props as Element properties camelizedPropsList.forEach(key => { Object.defineProperty(CustomElement.prototype, key, { - get () { - return this._wrapper.props[key] + get() { + return this.wrapper.props[key]; }, - set (newVal) { - this._wrapper.props[key] = newVal + set(newVal){ + Promise.resolve().then(()=>{ + this.wrapper.props[key] = newVal; + }) + + }, enumerable: false, configurable: true - }) - }) + }); + }); - isInitialized = true + isInitialized = true; } - function syncAttribute (el, key) { - const camelized = camelize(key) - const value = el.hasAttribute(key) ? el.getAttribute(key) : undefined - el._wrapper.props[camelized] = convertAttributeValue( + function syncAttribute(el, key) { + const camelized = camelize(key); + const value = el.hasAttribute(key) + ? el.getAttribute(key) + : undefined; + el.wrapper.props[camelized] = convertAttributeValue( value, key, camelizedPropsMap[camelized] - ) + ); } class CustomElement extends HTMLElement { - constructor () { - const self = super() - self.attachShadow({ mode: 'open' }) - - const wrapper = self._wrapper = new Vue({ - name: 'shadow-root', - customElement: self, - shadowRoot: self.shadowRoot, - data () { - return { - props: {}, - slotChildren: [] - } - }, - render (h) { - return h(Component, { - ref: 'inner', - props: this.props - }, this.slotChildren) - } - }) + wrapper + constructor() { + + const self = super(); + self.attachShadow({ mode: "open" }); + self._wrapper = createApp( + defineComponent({ + name: "shadow-root", + customElement: self, + shadowRoot: self.shadowRoot, + data(){ + return { + props : {}, + slotChildren : [] + } + }, + beforeCreate(){ + self.wrapper = this + }, + created(){ + const syncInitialAttributes = () => { + + this.props = getInitialProps(camelizedPropsList); + hyphenatedPropsList.forEach(key => { + syncAttribute(self, key); + }); + }; + + if (isInitialized) { + syncInitialAttributes(); + } else { + // async & unresolved + Component().then(resolved => { + if (resolved.__esModule || resolved[Symbol.toStringTag] === 'Module') { + resolved = resolved.default + } + initialize(resolved) + syncInitialAttributes() + }) + } + this.slotChildren = Object.freeze(toVNodes(self.childNodes)); + }, + render(){ + return createVNode( + isAsync ? defineAsyncComponent(Component) : Component, + { + ref: "inner", + ...this.props + }, + () => this.slotChildren + ); + }, + + }) + ); // Use MutationObserver to react to future attribute & slot content change const observer = new MutationObserver(mutations => { - let hasChildrenChange = false + console.log("ε˜εŒ–δΊ†ε—"); + let hasChildrenChange = false; for (let i = 0; i < mutations.length; i++) { - const m = mutations[i] - if (isInitialized && m.type === 'attributes' && m.target === self) { - syncAttribute(self, m.attributeName) + const m = mutations[i]; + if ( + isInitialized && + m.type === "attributes" && + m.target === self + ) { + syncAttribute(self, m.attributeName); } else { - hasChildrenChange = true + hasChildrenChange = true; } } if (hasChildrenChange) { - wrapper.slotChildren = Object.freeze(toVNodes( - wrapper.$createElement, - self.childNodes - )) + self.wrapper.slotChildren = Object.freeze(toVNodes(self.childNodes)); } - }) + }); observer.observe(self, { childList: true, subtree: true, characterData: true, attributes: true - }) + }); } - - get vueComponent () { - return this._wrapper.$refs.inner + get vueComponent(){ + return this.wrapper?.$refs?.inner; } - connectedCallback () { - const wrapper = this._wrapper - if (!wrapper._isMounted) { - // initialize attributes - const syncInitialAttributes = () => { - wrapper.props = getInitialProps(camelizedPropsList) - hyphenatedPropsList.forEach(key => { - syncAttribute(this, key) - }) - } - - if (isInitialized) { - syncInitialAttributes() - } else { - // async & unresolved - Component().then(resolved => { - if (resolved.__esModule || resolved[Symbol.toStringTag] === 'Module') { - resolved = resolved.default - } - initialize(resolved) - syncInitialAttributes() - }) - } - // initialize children - wrapper.slotChildren = Object.freeze(toVNodes( - wrapper.$createElement, - this.childNodes - )) - wrapper.$mount() - this.shadowRoot.appendChild(wrapper.$el) + connectedCallback() { + if (!this.vueComponent) { + this._wrapper.mount(this.shadowRoot); } else { - callHooks(this.vueComponent, 'activated') + callHooks(this.vueComponent, "activated"); + } } - disconnectedCallback () { - callHooks(this.vueComponent, 'deactivated') + callHooks(this.vueComponent, 'deactivated'); } } if (!isAsync) { - initialize(Component) + initialize(Component); } - return CustomElement + return CustomElement; } + +export default wrap; diff --git a/src/utils.js b/src/utils.js index c909f02..ccb59d6 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,96 +1,99 @@ -const camelizeRE = /-(\w)/g + +import {createVNode} from 'vue' +const camelizeRE = /-(\w)/g; export const camelize = str => { - return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '') -} + return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : "")); +}; -const hyphenateRE = /\B([A-Z])/g +const hyphenateRE = /\B([A-Z])/g; export const hyphenate = str => { - return str.replace(hyphenateRE, '-$1').toLowerCase() -} + return str.replace(hyphenateRE, "-$1").toLowerCase(); +}; -export function getInitialProps (propsList) { - const res = {} +export function getInitialProps(propsList) { + const res = {}; propsList.forEach(key => { - res[key] = undefined - }) - return res + res[key] = undefined; + }); + return res; } -export function injectHook (options, key, hook) { - options[key] = [].concat(options[key] || []) - options[key].unshift(hook) +export function injectHook(options, key, hook) { + const defaultHook = options[key] + if(defaultHook){ + options[key] = function(){ + hook.call(this) + defaultHook.call(this) + } + }else { + options[key] = hook; + } + } -export function callHooks (vm, hook) { - if (vm) { - const hooks = vm.$options[hook] || [] - hooks.forEach(hook => { - hook.call(vm) - }) +export function callHooks(vm, hook) { + const _hook = vm?.$options[hook] + if (_hook) { + _hook.call(vm) } } -export function createCustomEvent (name, args) { +export function createCustomEvent(name, args) { return new CustomEvent(name, { bubbles: false, cancelable: false, detail: args - }) + }); } -const isBoolean = val => /function Boolean/.test(String(val)) -const isNumber = val => /function Number/.test(String(val)) +const isBoolean = val => /function Boolean/.test(String(val)); +const isNumber = val => /function Number/.test(String(val)); -export function convertAttributeValue (value, name, { type } = {}) { +export function convertAttributeValue(value, name, { type } = {}) { if (isBoolean(type)) { - if (value === 'true' || value === 'false') { - return value === 'true' + if (value === "true" || value === "false") { + return value === "true"; } - if (value === '' || value === name || value != null) { - return true + if (value === "" || value === name || value != null) { + return true; } - return value + return value; } else if (isNumber(type)) { - const parsed = parseFloat(value, 10) - return isNaN(parsed) ? value : parsed + const parsed = parseFloat(value, 10); + return isNaN(parsed) ? value : parsed; } else { - return value + return value; } } -export function toVNodes (h, children) { - const res = [] +export function toVNodes(children) { + const res = []; for (let i = 0, l = children.length; i < l; i++) { - res.push(toVNode(h, children[i])) + res.push(toVNode(children[i])); } - return res + return res; } -function toVNode (h, node) { +export function toVNode(node) { if (node.nodeType === 3) { - return node.data.trim() ? node.data : null + return node.data.trim() ? node.data : null; } else if (node.nodeType === 1) { - const data = { - attrs: getAttributes(node), - domProps: { - innerHTML: node.innerHTML - } + let children = null + if(node.childNodes){ + children = toVNodes(node.childNodes); } - if (data.attrs.slot) { - data.slot = data.attrs.slot - delete data.attrs.slot - } - return h(node.tagName, data) + + return createVNode(node.tagName, { ...getAttributes(node) },children); } else { - return null + return null; } } -function getAttributes (node) { - const res = {} +export function getAttributes(node) { + const res = {}; for (let i = 0, l = node.attributes.length; i < l; i++) { - const attr = node.attributes[i] - res[attr.nodeName] = attr.nodeValue + const attr = node.attributes[i]; + res[attr.nodeName] = attr.nodeValue; } - return res -} + return res; +} \ No newline at end of file