@vue/web-component-wrapper
Advanced tools
Comparing version 1.1.4 to 1.2.0
@@ -22,2 +22,7 @@ var wrapVueWebComponent = (function () { | ||
function injectHook (options, key, hook) { | ||
options[key] = [].concat(options[key] || []); | ||
options[key].unshift(hook); | ||
} | ||
function callHooks (vm, hook) { | ||
@@ -98,36 +103,75 @@ if (vm) { | ||
function wrap (Vue, Component) { | ||
const options = typeof Component === 'function' | ||
? Component.options | ||
: Component; | ||
const isAsync = typeof Component === 'function' && !Component.cid; | ||
let isInitialized = false; | ||
let hyphenatedPropsList; | ||
let camelizedPropsList; | ||
let camelizedPropsMap; | ||
// inject hook to proxy $emit to native DOM events | ||
options.beforeCreate = [].concat(options.beforeCreate || []); | ||
options.beforeCreate.unshift(function () { | ||
const emit = this.$emit; | ||
this.$emit = (name, ...args) => { | ||
this.$root.$options.customElement.dispatchEvent(createCustomEvent(name, args)); | ||
return emit.call(this, name, ...args) | ||
}; | ||
}); | ||
function initialize (Component) { | ||
if (isInitialized) return | ||
// extract props info | ||
const propsList = Array.isArray(options.props) | ||
? options.props | ||
: Object.keys(options.props || {}); | ||
const hyphenatedPropsList = propsList.map(hyphenate); | ||
const camelizedPropsList = propsList.map(camelize); | ||
const originalPropsAsObject = Array.isArray(options.props) ? {} : options.props || {}; | ||
const camelizedPropsMap = camelizedPropsList.reduce((map, key, i) => { | ||
map[key] = originalPropsAsObject[propsList[i]]; | ||
return map | ||
}, {}); | ||
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 || {}; | ||
camelizedPropsMap = camelizedPropsList.reduce((map, key, i) => { | ||
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, 'created', function () { | ||
// sync default props values to wrapper on created | ||
camelizedPropsList.forEach(key => { | ||
this.$root.props[key] = this[key]; | ||
}); | ||
}); | ||
// proxy props as Element properties | ||
camelizedPropsList.forEach(key => { | ||
Object.defineProperty(CustomElement.prototype, key, { | ||
get () { | ||
return this._wrapper.props[key] | ||
}, | ||
set (newVal) { | ||
this._wrapper.props[key] = newVal; | ||
}, | ||
enumerable: false, | ||
configurable: 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( | ||
value, | ||
key, | ||
camelizedPropsMap[camelized] | ||
); | ||
} | ||
class CustomElement extends HTMLElement { | ||
static get observedAttributes () { | ||
return hyphenatedPropsList | ||
} | ||
constructor () { | ||
super(); | ||
this.attachShadow({ mode: 'open' }); | ||
const wrapper = this._wrapper = new Vue({ | ||
@@ -139,3 +183,3 @@ name: 'shadow-root', | ||
return { | ||
props: getInitialProps(camelizedPropsList), | ||
props: {}, | ||
slotChildren: [] | ||
@@ -152,8 +196,19 @@ } | ||
// Use MutationObserver to react to slot content change | ||
const observer = new MutationObserver(() => { | ||
wrapper.slotChildren = Object.freeze(toVNodes( | ||
wrapper.$createElement, | ||
this.childNodes | ||
)); | ||
// Use MutationObserver to react to future attribute & slot content change | ||
const observer = new MutationObserver(mutations => { | ||
let hasChildrenChange = false; | ||
for (let i = 0; i < mutations.length; i++) { | ||
const m = mutations[i]; | ||
if (isInitialized && m.type === 'attributes' && m.target === this) { | ||
syncAttribute(this, m.attributeName); | ||
} else { | ||
hasChildrenChange = true; | ||
} | ||
} | ||
if (hasChildrenChange) { | ||
wrapper.slotChildren = Object.freeze(toVNodes( | ||
wrapper.$createElement, | ||
this.childNodes | ||
)); | ||
} | ||
}); | ||
@@ -175,2 +230,22 @@ observer.observe(this, { | ||
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 | ||
@@ -182,6 +257,2 @@ wrapper.slotChildren = Object.freeze(toVNodes( | ||
wrapper.$mount(); | ||
// sync default props values to wrapper | ||
camelizedPropsList.forEach(key => { | ||
wrapper.props[key] = this.vueComponent[key]; | ||
}); | ||
this.shadowRoot.appendChild(wrapper.$el); | ||
@@ -196,28 +267,8 @@ } else { | ||
} | ||
} | ||
// watch attribute change and sync | ||
attributeChangedCallback (attrName, oldVal, newVal) { | ||
const camelized = camelize(attrName); | ||
this._wrapper.props[camelized] = convertAttributeValue( | ||
newVal, | ||
attrName, | ||
camelizedPropsMap[camelized] | ||
); | ||
} | ||
if (!isAsync) { | ||
initialize(Component); | ||
} | ||
// proxy props as Element properties | ||
camelizedPropsList.forEach(key => { | ||
Object.defineProperty(CustomElement.prototype, key, { | ||
get () { | ||
return this._wrapper.props[key] | ||
}, | ||
set (newVal) { | ||
this._wrapper.props[key] = newVal; | ||
}, | ||
enumerable: false, | ||
configurable: true | ||
}); | ||
}); | ||
return CustomElement | ||
@@ -224,0 +275,0 @@ } |
@@ -19,2 +19,7 @@ const camelizeRE = /-(\w)/g; | ||
function injectHook (options, key, hook) { | ||
options[key] = [].concat(options[key] || []); | ||
options[key].unshift(hook); | ||
} | ||
function callHooks (vm, hook) { | ||
@@ -95,36 +100,75 @@ if (vm) { | ||
function wrap (Vue, Component) { | ||
const options = typeof Component === 'function' | ||
? Component.options | ||
: Component; | ||
const isAsync = typeof Component === 'function' && !Component.cid; | ||
let isInitialized = false; | ||
let hyphenatedPropsList; | ||
let camelizedPropsList; | ||
let camelizedPropsMap; | ||
// inject hook to proxy $emit to native DOM events | ||
options.beforeCreate = [].concat(options.beforeCreate || []); | ||
options.beforeCreate.unshift(function () { | ||
const emit = this.$emit; | ||
this.$emit = (name, ...args) => { | ||
this.$root.$options.customElement.dispatchEvent(createCustomEvent(name, args)); | ||
return emit.call(this, name, ...args) | ||
}; | ||
}); | ||
function initialize (Component) { | ||
if (isInitialized) return | ||
// extract props info | ||
const propsList = Array.isArray(options.props) | ||
? options.props | ||
: Object.keys(options.props || {}); | ||
const hyphenatedPropsList = propsList.map(hyphenate); | ||
const camelizedPropsList = propsList.map(camelize); | ||
const originalPropsAsObject = Array.isArray(options.props) ? {} : options.props || {}; | ||
const camelizedPropsMap = camelizedPropsList.reduce((map, key, i) => { | ||
map[key] = originalPropsAsObject[propsList[i]]; | ||
return map | ||
}, {}); | ||
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 || {}; | ||
camelizedPropsMap = camelizedPropsList.reduce((map, key, i) => { | ||
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, 'created', function () { | ||
// sync default props values to wrapper on created | ||
camelizedPropsList.forEach(key => { | ||
this.$root.props[key] = this[key]; | ||
}); | ||
}); | ||
// proxy props as Element properties | ||
camelizedPropsList.forEach(key => { | ||
Object.defineProperty(CustomElement.prototype, key, { | ||
get () { | ||
return this._wrapper.props[key] | ||
}, | ||
set (newVal) { | ||
this._wrapper.props[key] = newVal; | ||
}, | ||
enumerable: false, | ||
configurable: 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( | ||
value, | ||
key, | ||
camelizedPropsMap[camelized] | ||
); | ||
} | ||
class CustomElement extends HTMLElement { | ||
static get observedAttributes () { | ||
return hyphenatedPropsList | ||
} | ||
constructor () { | ||
super(); | ||
this.attachShadow({ mode: 'open' }); | ||
const wrapper = this._wrapper = new Vue({ | ||
@@ -136,3 +180,3 @@ name: 'shadow-root', | ||
return { | ||
props: getInitialProps(camelizedPropsList), | ||
props: {}, | ||
slotChildren: [] | ||
@@ -149,8 +193,19 @@ } | ||
// Use MutationObserver to react to slot content change | ||
const observer = new MutationObserver(() => { | ||
wrapper.slotChildren = Object.freeze(toVNodes( | ||
wrapper.$createElement, | ||
this.childNodes | ||
)); | ||
// Use MutationObserver to react to future attribute & slot content change | ||
const observer = new MutationObserver(mutations => { | ||
let hasChildrenChange = false; | ||
for (let i = 0; i < mutations.length; i++) { | ||
const m = mutations[i]; | ||
if (isInitialized && m.type === 'attributes' && m.target === this) { | ||
syncAttribute(this, m.attributeName); | ||
} else { | ||
hasChildrenChange = true; | ||
} | ||
} | ||
if (hasChildrenChange) { | ||
wrapper.slotChildren = Object.freeze(toVNodes( | ||
wrapper.$createElement, | ||
this.childNodes | ||
)); | ||
} | ||
}); | ||
@@ -172,2 +227,22 @@ observer.observe(this, { | ||
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 | ||
@@ -179,6 +254,2 @@ wrapper.slotChildren = Object.freeze(toVNodes( | ||
wrapper.$mount(); | ||
// sync default props values to wrapper | ||
camelizedPropsList.forEach(key => { | ||
wrapper.props[key] = this.vueComponent[key]; | ||
}); | ||
this.shadowRoot.appendChild(wrapper.$el); | ||
@@ -193,28 +264,8 @@ } else { | ||
} | ||
} | ||
// watch attribute change and sync | ||
attributeChangedCallback (attrName, oldVal, newVal) { | ||
const camelized = camelize(attrName); | ||
this._wrapper.props[camelized] = convertAttributeValue( | ||
newVal, | ||
attrName, | ||
camelizedPropsMap[camelized] | ||
); | ||
} | ||
if (!isAsync) { | ||
initialize(Component); | ||
} | ||
// proxy props as Element properties | ||
camelizedPropsList.forEach(key => { | ||
Object.defineProperty(CustomElement.prototype, key, { | ||
get () { | ||
return this._wrapper.props[key] | ||
}, | ||
set (newVal) { | ||
this._wrapper.props[key] = newVal; | ||
}, | ||
enumerable: false, | ||
configurable: true | ||
}); | ||
}); | ||
return CustomElement | ||
@@ -221,0 +272,0 @@ } |
{ | ||
"name": "@vue/web-component-wrapper", | ||
"version": "1.1.4", | ||
"version": "1.2.0", | ||
"description": "wrap a vue component as a web component.", | ||
@@ -5,0 +5,0 @@ "main": "dist/vue-wc-wrapper.js", |
@@ -42,2 +42,10 @@ # @vue/web-component-wrapper [![CircleCI](https://circleci.com/gh/vuejs/vue-web-component-wrapper.svg?style=shield)](https://circleci.com/gh/vuejs/vue-web-component-wrapper) | ||
Note it works with async components as well - the async component factory will only be called when an instance of the custom element is created on the page: | ||
``` js | ||
const CustomElement = wrap(Vue, () => import(`MyComponent.vue`)) | ||
window.customElements.define('my-element', CustomElement) | ||
``` | ||
## Interface Proxying Details | ||
@@ -44,0 +52,0 @@ |
19054
479
97