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
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: