From cb14f1eb9c4dc663011cb91ed6d447f8acbdfc4e Mon Sep 17 00:00:00 2001 From: jili Date: Tue, 8 Jan 2019 11:19:55 +0100 Subject: [PATCH 01/17] spread props utils --- .gitignore | 2 + dist/vue-wc-wrapper.global.js | 502 ++++++++++++++++++---------------- dist/vue-wc-wrapper.js | 185 +++++++------ src/index.js | 4 +- src/utils.js | 20 ++ 5 files changed, 389 insertions(+), 324 deletions(-) diff --git a/.gitignore b/.gitignore index fd4f2b0..a45a18c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules .DS_Store +.idea/ +package-lock.json \ No newline at end of file diff --git a/dist/vue-wc-wrapper.global.js b/dist/vue-wc-wrapper.global.js index db8253a..0050cc4 100644 --- a/dist/vue-wc-wrapper.global.js +++ b/dist/vue-wc-wrapper.global.js @@ -1,272 +1,292 @@ var wrapVueWebComponent = (function () { -'use strict'; - -const camelizeRE = /-(\w)/g; -const camelize = str => { - return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '') -}; - -const hyphenateRE = /\B([A-Z])/g; -const hyphenate = str => { - return str.replace(hyphenateRE, '-$1').toLowerCase() -}; - -function getInitialProps (propsList) { - const res = {}; - propsList.forEach(key => { - res[key] = undefined; - }); - return res -} - -function injectHook (options, key, hook) { - options[key] = [].concat(options[key] || []); - options[key].unshift(hook); -} - -function callHooks (vm, hook) { - if (vm) { - const hooks = vm.$options[hook] || []; - hooks.forEach(hook => { - hook.call(vm); - }); + 'use strict' + + const camelizeRE = /-(\w)/g + const camelize = str => { + return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '') + } + + const hyphenateRE = /\B([A-Z])/g + const hyphenate = str => { + return str.replace(hyphenateRE, '-$1').toLowerCase() + } + + function getInitialProps (propsList) { + const res = {} + propsList.forEach(key => { + res[key] = undefined + }) + return res } -} - -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)); - -function convertAttributeValue (value, name, { type } = {}) { - if (isBoolean(type)) { - if (value === 'true' || value === 'false') { - return value === 'true' + + function injectHook (options, key, hook) { + options[key] = [].concat(options[key] || []) + options[key].unshift(hook) + } + + function callHooks (vm, hook) { + if (vm) { + const hooks = vm.$options[hook] || [] + hooks.forEach(hook => { + hook.call(vm) + }) } - if (value === '' || value === name) { - return true + } + + 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)) + + function convertAttributeValue (value, name, { type } = {}) { + if (isBoolean(type)) { + if (value === 'true' || value === 'false') { + return value === 'true' + } + if (value === '' || value === name) { + return true + } + return value != null + } else if (isNumber(type)) { + const parsed = parseFloat(value, 10) + return isNaN(parsed) ? value : parsed + } else { + return value } - return value != null - } else if (isNumber(type)) { - const parsed = parseFloat(value, 10); - return isNaN(parsed) ? value : parsed - } else { - return value } -} -function toVNodes (h, children) { - const res = []; - for (let i = 0, l = children.length; i < l; i++) { - res.push(toVNode(h, children[i])); + function toVNodes (h, children) { + const res = [] + for (let i = 0, l = children.length; i < l; i++) { + res.push(toVNode(h, children[i])) + } + return res } - return res -} - -function toVNode (h, node) { - if (node.nodeType === 3) { - return node.data.trim() ? node.data : null - } else if (node.nodeType === 1) { - const data = { - attrs: getAttributes(node), - domProps: { - innerHTML: node.innerHTML + + function toVNode (h, node) { + if (node.nodeType === 3) { + return node.data.trim() ? node.data : null + } else if (node.nodeType === 1) { + const data = { + attrs: getAttributes(node), + domProps: { + innerHTML: node.innerHTML + } } - }; - if (data.attrs.slot) { - data.slot = data.attrs.slot; - delete data.attrs.slot; + if (data.attrs.slot) { + data.slot = data.attrs.slot + delete data.attrs.slot + } + return h(node.tagName, data) + } else { + return null } - return h(node.tagName, data) - } else { - return null } -} -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; + 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 + } + return res } - return res -} - -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 - - 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 spreadProps (component) { + const result = {} + appendProps(result, component) + console.log(result) + return result } - 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] - ); + function appendProps (result, component) { + for (const key in component.props) { + const camelKey = camelize(key) + if (!(camelKey in result)) { + result[camelKey] = component.props[key] + } + } + + if (component.extends) { + appendProps(result, component.extends) + } } - class CustomElement extends HTMLElement { - constructor () { - super(); - this.attachShadow({ mode: 'open' }); - - const wrapper = this._wrapper = new Vue({ - name: 'shadow-root', - customElement: this, - shadowRoot: this.shadowRoot, - data () { - return { - props: {}, - slotChildren: [] - } - }, - render (h) { - return h(Component, { - ref: 'inner', - props: this.props - }, this.slotChildren) - } - }); - - // 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 - )); + 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 + + const options = typeof Component === 'function' + ? Component.options + : Component + + options.props = spreadProps(options) + // 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) } - }); - observer.observe(this, { - childList: true, - subtree: true, - characterData: true, - attributes: true - }); + }) + + 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 } - get vueComponent () { - return this._wrapper.$refs.inner + 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] + ) } - connectedCallback () { - const wrapper = this._wrapper; - if (!wrapper._isMounted) { + 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) + } + }) + + // 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 === self) { + syncAttribute(self, m.attributeName) + } else { + hasChildrenChange = true + } + } + if (hasChildrenChange) { + wrapper.slotChildren = Object.freeze(toVNodes( + wrapper.$createElement, + self.childNodes + )) + } + }) + observer.observe(self, { + childList: true, + subtree: true, + characterData: true, + attributes: true + }) + } + + 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 { + 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(); - }); + 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) + } else { + callHooks(this.vueComponent, 'activated') } - // initialize children - wrapper.slotChildren = Object.freeze(toVNodes( - wrapper.$createElement, - this.childNodes - )); - wrapper.$mount(); - this.shadowRoot.appendChild(wrapper.$el); - } else { - callHooks(this.vueComponent, 'activated'); + } + + disconnectedCallback () { + callHooks(this.vueComponent, 'deactivated') } } - disconnectedCallback () { - callHooks(this.vueComponent, 'deactivated'); + if (!isAsync) { + initialize(Component) } - } - if (!isAsync) { - initialize(Component); + return CustomElement } - return CustomElement -} - -return wrap; - -}()); + return wrap +}()) diff --git a/dist/vue-wc-wrapper.js b/dist/vue-wc-wrapper.js index 36b4469..cf488eb 100644 --- a/dist/vue-wc-wrapper.js +++ b/dist/vue-wc-wrapper.js @@ -1,32 +1,32 @@ -const camelizeRE = /-(\w)/g; +const camelizeRE = /-(\w)/g const camelize = str => { return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '') -}; +} -const hyphenateRE = /\B([A-Z])/g; +const hyphenateRE = /\B([A-Z])/g const hyphenate = str => { return str.replace(hyphenateRE, '-$1').toLowerCase() -}; +} function getInitialProps (propsList) { - const res = {}; + const res = {} propsList.forEach(key => { - res[key] = undefined; - }); + res[key] = undefined + }) return res } function injectHook (options, key, hook) { - options[key] = [].concat(options[key] || []); - options[key].unshift(hook); + options[key] = [].concat(options[key] || []) + options[key].unshift(hook) } function callHooks (vm, hook) { if (vm) { - const hooks = vm.$options[hook] || []; + const hooks = vm.$options[hook] || [] hooks.forEach(hook => { - hook.call(vm); - }); + hook.call(vm) + }) } } @@ -38,8 +38,8 @@ function createCustomEvent (name, 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)) function convertAttributeValue (value, name, { type } = {}) { if (isBoolean(type)) { @@ -51,7 +51,7 @@ function convertAttributeValue (value, name, { type } = {}) { } return value != null } else if (isNumber(type)) { - const parsed = parseFloat(value, 10); + const parsed = parseFloat(value, 10) return isNaN(parsed) ? value : parsed } else { return value @@ -59,9 +59,9 @@ function convertAttributeValue (value, name, { type } = {}) { } function toVNodes (h, children) { - const res = []; + const res = [] for (let i = 0, l = children.length; i < l; i++) { - res.push(toVNode(h, children[i])); + res.push(toVNode(h, children[i])) } return res } @@ -75,10 +75,10 @@ function toVNode (h, node) { domProps: { innerHTML: node.innerHTML } - }; + } if (data.attrs.slot) { - data.slot = data.attrs.slot; - delete data.attrs.slot; + data.slot = data.attrs.slot + delete data.attrs.slot } return h(node.tagName, data) } else { @@ -87,55 +87,76 @@ function toVNode (h, node) { } function getAttributes (node) { - const res = {}; + 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 } +function spreadProps (component) { + const result = {} + appendProps(result, component) + console.log(result) + return result +} + +function appendProps (result, component) { + for (const key in component.props) { + const camelKey = camelize(key) + if (!(camelKey in result)) { + result[camelKey] = component.props[key] + } + } + + if (component.extends) { + appendProps(result, component.extends) + } +} + function wrap (Vue, Component) { - const isAsync = typeof Component === 'function' && !Component.cid; - let isInitialized = false; - let hyphenatedPropsList; - let camelizedPropsList; - let camelizedPropsMap; + const isAsync = typeof Component === 'function' && !Component.cid + let isInitialized = false + let hyphenatedPropsList + let camelizedPropsList + let camelizedPropsMap function initialize (Component) { if (isInitialized) return const options = typeof Component === 'function' ? Component.options - : Component; + : Component + options.props = spreadProps(options) // 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]]; + map[key] = originalPropsAsObject[propsList[i]] return map - }, {}); + }, {}) // proxy $emit to native DOM events injectHook(options, 'beforeCreate', function () { - const emit = this.$emit; + const emit = this.$emit this.$emit = (name, ...args) => { - this.$root.$options.customElement.dispatchEvent(createCustomEvent(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]; - }); - }); + this.$root.props[key] = this[key] + }) + }) // proxy props as Element properties camelizedPropsList.forEach(key => { @@ -144,35 +165,35 @@ function wrap (Vue, Component) { return this._wrapper.props[key] }, set (newVal) { - this._wrapper.props[key] = newVal; + 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; + 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 () { - super(); - this.attachShadow({ mode: 'open' }); + const self = super() + self.attachShadow({ mode: 'open' }) - const wrapper = this._wrapper = new Vue({ + const wrapper = self._wrapper = new Vue({ name: 'shadow-root', - customElement: this, - shadowRoot: this.shadowRoot, + customElement: self, + shadowRoot: self.shadowRoot, data () { return { props: {}, @@ -185,32 +206,32 @@ function wrap (Vue, Component) { props: this.props }, this.slotChildren) } - }); + }) // Use MutationObserver to react to future attribute & slot content change const observer = new MutationObserver(mutations => { - let hasChildrenChange = false; + 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); + 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, - this.childNodes - )); + self.childNodes + )) } - }); - observer.observe(this, { + }) + observer.observe(self, { childList: true, subtree: true, characterData: true, attributes: true - }); + }) } get vueComponent () { @@ -218,50 +239,50 @@ function wrap (Vue, Component) { } connectedCallback () { - const wrapper = this._wrapper; + const wrapper = this._wrapper if (!wrapper._isMounted) { // initialize attributes const syncInitialAttributes = () => { - wrapper.props = getInitialProps(camelizedPropsList); + wrapper.props = getInitialProps(camelizedPropsList) hyphenatedPropsList.forEach(key => { - syncAttribute(this, key); - }); - }; + syncAttribute(this, key) + }) + } if (isInitialized) { - syncInitialAttributes(); + syncInitialAttributes() } else { // async & unresolved Component().then(resolved => { if (resolved.__esModule || resolved[Symbol.toStringTag] === 'Module') { - resolved = resolved.default; + resolved = resolved.default } - initialize(resolved); - syncInitialAttributes(); - }); + initialize(resolved) + syncInitialAttributes() + }) } // initialize children wrapper.slotChildren = Object.freeze(toVNodes( wrapper.$createElement, this.childNodes - )); - wrapper.$mount(); - this.shadowRoot.appendChild(wrapper.$el); + )) + wrapper.$mount() + this.shadowRoot.appendChild(wrapper.$el) } 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 } -export default wrap; +export default wrap diff --git a/src/index.js b/src/index.js index 4409bd7..e273ea2 100644 --- a/src/index.js +++ b/src/index.js @@ -6,7 +6,8 @@ import { injectHook, getInitialProps, createCustomEvent, - convertAttributeValue + convertAttributeValue, + spreadProps } from './utils.js' export default function wrap (Vue, Component) { @@ -23,6 +24,7 @@ export default function wrap (Vue, Component) { ? Component.options : Component + options.props = spreadProps(options) // extract props info const propsList = Array.isArray(options.props) ? options.props diff --git a/src/utils.js b/src/utils.js index 0834f03..d06640c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -94,3 +94,23 @@ function getAttributes (node) { } return res } + +export function spreadProps (component) { + const result = {} + appendProps(result, component) + console.log(result) + return result +} + +function appendProps (result, component) { + for (const key in component.props) { + const camelKey = camelize(key) + if (!(camelKey in result)) { + result[camelKey] = component.props[key] + } + } + + if (component.extends) { + appendProps(result, component.extends) + } +} From 3f0a5b52d516ba8641b75d0080c849414f308f06 Mon Sep 17 00:00:00 2001 From: jili Date: Tue, 8 Jan 2019 11:40:27 +0100 Subject: [PATCH 02/17] new test for spread props --- dist/vue-wc-wrapper.global.js | 518 +++++++++++++------------- dist/vue-wc-wrapper.js | 167 ++++----- src/index.js | 3 +- test/fixtures/spreadedProperties.html | 41 ++ 4 files changed, 387 insertions(+), 342 deletions(-) create mode 100644 test/fixtures/spreadedProperties.html diff --git a/dist/vue-wc-wrapper.global.js b/dist/vue-wc-wrapper.global.js index 0050cc4..6fcaa7f 100644 --- a/dist/vue-wc-wrapper.global.js +++ b/dist/vue-wc-wrapper.global.js @@ -1,292 +1,294 @@ var wrapVueWebComponent = (function () { - 'use strict' - - const camelizeRE = /-(\w)/g - const camelize = str => { - return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '') - } - - const hyphenateRE = /\B([A-Z])/g - const hyphenate = str => { - return str.replace(hyphenateRE, '-$1').toLowerCase() - } - - function getInitialProps (propsList) { - const res = {} - propsList.forEach(key => { - res[key] = undefined - }) - return res - } - - function injectHook (options, key, hook) { - options[key] = [].concat(options[key] || []) - options[key].unshift(hook) +'use strict'; + +const camelizeRE = /-(\w)/g; +const camelize = str => { + return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '') +}; + +const hyphenateRE = /\B([A-Z])/g; +const hyphenate = str => { + return str.replace(hyphenateRE, '-$1').toLowerCase() +}; + +function getInitialProps (propsList) { + const res = {}; + propsList.forEach(key => { + res[key] = undefined; + }); + return res +} + +function injectHook (options, key, hook) { + options[key] = [].concat(options[key] || []); + options[key].unshift(hook); +} + +function callHooks (vm, hook) { + if (vm) { + const hooks = vm.$options[hook] || []; + hooks.forEach(hook => { + hook.call(vm); + }); } - - function callHooks (vm, hook) { - if (vm) { - const hooks = vm.$options[hook] || [] - hooks.forEach(hook => { - hook.call(vm) - }) +} + +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)); + +function convertAttributeValue (value, name, { type } = {}) { + if (isBoolean(type)) { + if (value === 'true' || value === 'false') { + return value === 'true' } + if (value === '' || value === name) { + return true + } + return value != null + } else if (isNumber(type)) { + const parsed = parseFloat(value, 10); + return isNaN(parsed) ? value : parsed + } else { + return value } +} - function createCustomEvent (name, args) { - return new CustomEvent(name, { - bubbles: false, - cancelable: false, - detail: args - }) +function toVNodes (h, children) { + const res = []; + for (let i = 0, l = children.length; i < l; i++) { + res.push(toVNode(h, children[i])); } - - const isBoolean = val => /function Boolean/.test(String(val)) - const isNumber = val => /function Number/.test(String(val)) - - function convertAttributeValue (value, name, { type } = {}) { - if (isBoolean(type)) { - if (value === 'true' || value === 'false') { - return value === 'true' - } - if (value === '' || value === name) { - return true + return res +} + +function toVNode (h, node) { + if (node.nodeType === 3) { + return node.data.trim() ? node.data : null + } else if (node.nodeType === 1) { + const data = { + attrs: getAttributes(node), + domProps: { + innerHTML: node.innerHTML } - return value != null - } else if (isNumber(type)) { - const parsed = parseFloat(value, 10) - return isNaN(parsed) ? value : parsed - } else { - return value + }; + if (data.attrs.slot) { + data.slot = data.attrs.slot; + delete data.attrs.slot; } + return h(node.tagName, data) + } else { + return null } +} - function toVNodes (h, children) { - const res = [] - for (let i = 0, l = children.length; i < l; i++) { - res.push(toVNode(h, children[i])) - } - return res +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; } - - function toVNode (h, node) { - if (node.nodeType === 3) { - return node.data.trim() ? node.data : null - } else if (node.nodeType === 1) { - const data = { - attrs: getAttributes(node), - domProps: { - innerHTML: node.innerHTML - } - } - if (data.attrs.slot) { - data.slot = data.attrs.slot - delete data.attrs.slot - } - return h(node.tagName, data) - } else { - return null + return res +} + +function spreadProps (component) { + const result = {}; + appendProps(result, component); + console.log(result); + return result +} + +function appendProps (result, component) { + for (const key in component.props) { + const camelKey = camelize(key); + if (!(camelKey in result)) { + result[camelKey] = component.props[key]; } } - 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 - } - return res + if (component.extends) { + appendProps(result, component.extends); } - - function spreadProps (component) { - const result = {} - appendProps(result, component) - console.log(result) - return result +} + +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 + + const options = typeof Component === 'function' + ? Component.options + : Component; + + //spread props + options.props = spreadProps(options); + // 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 appendProps (result, component) { - for (const key in component.props) { - const camelKey = camelize(key) - if (!(camelKey in result)) { - result[camelKey] = component.props[key] - } - } - - if (component.extends) { - appendProps(result, component.extends) - } + 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] + ); } - 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 - - const options = typeof Component === 'function' - ? Component.options - : Component - - options.props = spreadProps(options) - // 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) + 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) } - }) - - 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 + }); + + // 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 === self) { + syncAttribute(self, m.attributeName); + } else { + hasChildrenChange = true; + } + } + if (hasChildrenChange) { + wrapper.slotChildren = Object.freeze(toVNodes( + wrapper.$createElement, + self.childNodes + )); + } + }); + observer.observe(self, { + childList: true, + subtree: true, + characterData: true, + attributes: 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] - ) + get vueComponent () { + return this._wrapper.$refs.inner } - 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) - } - }) - - // 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 === self) { - syncAttribute(self, m.attributeName) - } else { - hasChildrenChange = true - } - } - if (hasChildrenChange) { - wrapper.slotChildren = Object.freeze(toVNodes( - wrapper.$createElement, - self.childNodes - )) - } - }) - observer.observe(self, { - childList: true, - subtree: true, - characterData: true, - attributes: true - }) - } - - get vueComponent () { - return this._wrapper.$refs.inner - } - - connectedCallback () { - const wrapper = this._wrapper - if (!wrapper._isMounted) { + 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) + const syncInitialAttributes = () => { + wrapper.props = getInitialProps(camelizedPropsList); + hyphenatedPropsList.forEach(key => { + syncAttribute(this, key); + }); + }; + + if (isInitialized) { + syncInitialAttributes(); } else { - callHooks(this.vueComponent, 'activated') + // async & unresolved + Component().then(resolved => { + if (resolved.__esModule || resolved[Symbol.toStringTag] === 'Module') { + resolved = resolved.default; + } + initialize(resolved); + syncInitialAttributes(); + }); } - } - - disconnectedCallback () { - callHooks(this.vueComponent, 'deactivated') + // initialize children + wrapper.slotChildren = Object.freeze(toVNodes( + wrapper.$createElement, + this.childNodes + )); + wrapper.$mount(); + this.shadowRoot.appendChild(wrapper.$el); + } else { + callHooks(this.vueComponent, 'activated'); } } - if (!isAsync) { - initialize(Component) + disconnectedCallback () { + callHooks(this.vueComponent, 'deactivated'); } + } - return CustomElement + if (!isAsync) { + initialize(Component); } - return wrap -}()) + return CustomElement +} + +return wrap; + +}()); diff --git a/dist/vue-wc-wrapper.js b/dist/vue-wc-wrapper.js index cf488eb..36ce7f9 100644 --- a/dist/vue-wc-wrapper.js +++ b/dist/vue-wc-wrapper.js @@ -1,32 +1,32 @@ -const camelizeRE = /-(\w)/g +const camelizeRE = /-(\w)/g; const camelize = str => { return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '') -} +}; -const hyphenateRE = /\B([A-Z])/g +const hyphenateRE = /\B([A-Z])/g; const hyphenate = str => { return str.replace(hyphenateRE, '-$1').toLowerCase() -} +}; function getInitialProps (propsList) { - const res = {} + const res = {}; propsList.forEach(key => { - res[key] = undefined - }) + res[key] = undefined; + }); return res } function injectHook (options, key, hook) { - options[key] = [].concat(options[key] || []) - options[key].unshift(hook) + options[key] = [].concat(options[key] || []); + options[key].unshift(hook); } function callHooks (vm, hook) { if (vm) { - const hooks = vm.$options[hook] || [] + const hooks = vm.$options[hook] || []; hooks.forEach(hook => { - hook.call(vm) - }) + hook.call(vm); + }); } } @@ -38,8 +38,8 @@ function createCustomEvent (name, 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)); function convertAttributeValue (value, name, { type } = {}) { if (isBoolean(type)) { @@ -51,7 +51,7 @@ function convertAttributeValue (value, name, { type } = {}) { } return value != null } else if (isNumber(type)) { - const parsed = parseFloat(value, 10) + const parsed = parseFloat(value, 10); return isNaN(parsed) ? value : parsed } else { return value @@ -59,9 +59,9 @@ function convertAttributeValue (value, name, { type } = {}) { } function toVNodes (h, children) { - const res = [] + const res = []; for (let i = 0, l = children.length; i < l; i++) { - res.push(toVNode(h, children[i])) + res.push(toVNode(h, children[i])); } return res } @@ -75,10 +75,10 @@ function toVNode (h, node) { domProps: { innerHTML: node.innerHTML } - } + }; if (data.attrs.slot) { - data.slot = data.attrs.slot - delete data.attrs.slot + data.slot = data.attrs.slot; + delete data.attrs.slot; } return h(node.tagName, data) } else { @@ -87,76 +87,77 @@ function toVNode (h, node) { } function getAttributes (node) { - const res = {} + 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 } function spreadProps (component) { - const result = {} - appendProps(result, component) - console.log(result) + const result = {}; + appendProps(result, component); + console.log(result); return result } function appendProps (result, component) { for (const key in component.props) { - const camelKey = camelize(key) + const camelKey = camelize(key); if (!(camelKey in result)) { - result[camelKey] = component.props[key] + result[camelKey] = component.props[key]; } } if (component.extends) { - appendProps(result, component.extends) + appendProps(result, component.extends); } } function wrap (Vue, Component) { - const isAsync = typeof Component === 'function' && !Component.cid - let isInitialized = false - let hyphenatedPropsList - let camelizedPropsList - let camelizedPropsMap + const isAsync = typeof Component === 'function' && !Component.cid; + let isInitialized = false; + let hyphenatedPropsList; + let camelizedPropsList; + let camelizedPropsMap; function initialize (Component) { if (isInitialized) return const options = typeof Component === 'function' ? Component.options - : Component + : Component; - options.props = spreadProps(options) + //spread props + options.props = spreadProps(options); // 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]] + map[key] = originalPropsAsObject[propsList[i]]; return map - }, {}) + }, {}); // proxy $emit to native DOM events injectHook(options, 'beforeCreate', function () { - const emit = this.$emit + const emit = this.$emit; this.$emit = (name, ...args) => { - this.$root.$options.customElement.dispatchEvent(createCustomEvent(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] - }) - }) + this.$root.props[key] = this[key]; + }); + }); // proxy props as Element properties camelizedPropsList.forEach(key => { @@ -165,30 +166,30 @@ function wrap (Vue, Component) { return this._wrapper.props[key] }, set (newVal) { - this._wrapper.props[key] = newVal + 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 + 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 self = super(); + self.attachShadow({ mode: 'open' }); const wrapper = self._wrapper = new Vue({ name: 'shadow-root', @@ -206,32 +207,32 @@ function wrap (Vue, Component) { props: this.props }, this.slotChildren) } - }) + }); // Use MutationObserver to react to future attribute & slot content change const observer = new MutationObserver(mutations => { - let hasChildrenChange = false + let hasChildrenChange = false; for (let i = 0; i < mutations.length; i++) { - const m = mutations[i] + const m = mutations[i]; if (isInitialized && m.type === 'attributes' && m.target === self) { - syncAttribute(self, m.attributeName) + syncAttribute(self, m.attributeName); } else { - hasChildrenChange = true + hasChildrenChange = true; } } if (hasChildrenChange) { wrapper.slotChildren = Object.freeze(toVNodes( wrapper.$createElement, self.childNodes - )) + )); } - }) + }); observer.observe(self, { childList: true, subtree: true, characterData: true, attributes: true - }) + }); } get vueComponent () { @@ -239,50 +240,50 @@ function wrap (Vue, Component) { } connectedCallback () { - const wrapper = this._wrapper + const wrapper = this._wrapper; if (!wrapper._isMounted) { // initialize attributes const syncInitialAttributes = () => { - wrapper.props = getInitialProps(camelizedPropsList) + wrapper.props = getInitialProps(camelizedPropsList); hyphenatedPropsList.forEach(key => { - syncAttribute(this, key) - }) - } + syncAttribute(this, key); + }); + }; if (isInitialized) { - syncInitialAttributes() + syncInitialAttributes(); } else { // async & unresolved Component().then(resolved => { if (resolved.__esModule || resolved[Symbol.toStringTag] === 'Module') { - resolved = resolved.default + resolved = resolved.default; } - initialize(resolved) - syncInitialAttributes() - }) + initialize(resolved); + syncInitialAttributes(); + }); } // initialize children wrapper.slotChildren = Object.freeze(toVNodes( wrapper.$createElement, this.childNodes - )) - wrapper.$mount() - this.shadowRoot.appendChild(wrapper.$el) + )); + wrapper.$mount(); + this.shadowRoot.appendChild(wrapper.$el); } 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 } -export default wrap +export default wrap; diff --git a/src/index.js b/src/index.js index e273ea2..88e5a07 100644 --- a/src/index.js +++ b/src/index.js @@ -24,7 +24,8 @@ export default function wrap (Vue, Component) { ? Component.options : Component - options.props = spreadProps(options) + //spread props + options.props = spreadProps(options) // extract props info const propsList = Array.isArray(options.props) ? options.props diff --git a/test/fixtures/spreadedProperties.html b/test/fixtures/spreadedProperties.html new file mode 100644 index 0000000..87a640d --- /dev/null +++ b/test/fixtures/spreadedProperties.html @@ -0,0 +1,41 @@ + + + + From 94fd1b693b4189944fe42ff1970b42dc984f1bdd Mon Sep 17 00:00:00 2001 From: jili Date: Tue, 8 Jan 2019 11:43:51 +0100 Subject: [PATCH 03/17] new test for spread props --- test/fixtures/spreadedProperties.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fixtures/spreadedProperties.html b/test/fixtures/spreadedProperties.html index 87a640d..acd3a3b 100644 --- a/test/fixtures/spreadedProperties.html +++ b/test/fixtures/spreadedProperties.html @@ -10,7 +10,7 @@ }, } } -var Parent1 = { +var Parent1 = { extends: Parent0, props: { p1: { From 4c1088917efb47bc7c1978ef2b5e141ce2437a32 Mon Sep 17 00:00:00 2001 From: jili Date: Tue, 8 Jan 2019 11:49:31 +0100 Subject: [PATCH 04/17] new test --- test/fixtures/spreadedProperties.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fixtures/spreadedProperties.html b/test/fixtures/spreadedProperties.html index acd3a3b..87a640d 100644 --- a/test/fixtures/spreadedProperties.html +++ b/test/fixtures/spreadedProperties.html @@ -10,7 +10,7 @@ }, } } -var Parent1 = { +var Parent1 = { extends: Parent0, props: { p1: { From 3a899b8d827dbd2f41848b8ce85ea0051fa5de2e Mon Sep 17 00:00:00 2001 From: jili Date: Tue, 8 Jan 2019 11:51:05 +0100 Subject: [PATCH 05/17] try lint-staged --- src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 88e5a07..2dde0eb 100644 --- a/src/index.js +++ b/src/index.js @@ -24,8 +24,8 @@ export default function wrap (Vue, Component) { ? Component.options : Component - //spread props - options.props = spreadProps(options) + // spread props + options.props = spreadProps(options) // extract props info const propsList = Array.isArray(options.props) ? options.props From 91fd583b65bb0b0c4d35944643282f44b34f9a56 Mon Sep 17 00:00:00 2001 From: jili Date: Tue, 8 Jan 2019 12:34:18 +0100 Subject: [PATCH 06/17] fix minxins props --- src/utils.js | 18 ++++++- test/fixtures/spreadedProperties.html | 78 +++++++++++++++------------ 2 files changed, 61 insertions(+), 35 deletions(-) diff --git a/src/utils.js b/src/utils.js index d06640c..2d8d856 100644 --- a/src/utils.js +++ b/src/utils.js @@ -109,8 +109,24 @@ function appendProps (result, component) { result[camelKey] = component.props[key] } } - + if (component.mixins) { + appendMixinsProps(result, component.mixins) + } if (component.extends) { appendProps(result, component.extends) } } + +function appendMixinsProps (result, mixins) { + mixins.forEach(function (mixin) { + if (mixin.props) { + for (const key in mixin.props) { + const camelKey = camelize(key) + if (!(camelKey in result)) { + result[camelKey] = mixin.props[key] + } + } + } + }) +} + diff --git a/test/fixtures/spreadedProperties.html b/test/fixtures/spreadedProperties.html index 87a640d..0c810c9 100644 --- a/test/fixtures/spreadedProperties.html +++ b/test/fixtures/spreadedProperties.html @@ -1,41 +1,51 @@ - + From e3426d92d3ce3256fd1947960c31fc9b87cbb0de Mon Sep 17 00:00:00 2001 From: jili Date: Tue, 8 Jan 2019 12:48:45 +0100 Subject: [PATCH 07/17] fix spread mixin props --- dist/vue-wc-wrapper.global.js | 530 +++++++++++++++++----------------- dist/vue-wc-wrapper.js | 185 ++++++------ 2 files changed, 372 insertions(+), 343 deletions(-) diff --git a/dist/vue-wc-wrapper.global.js b/dist/vue-wc-wrapper.global.js index 6fcaa7f..c1f740b 100644 --- a/dist/vue-wc-wrapper.global.js +++ b/dist/vue-wc-wrapper.global.js @@ -1,294 +1,308 @@ var wrapVueWebComponent = (function () { -'use strict'; - -const camelizeRE = /-(\w)/g; -const camelize = str => { - return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '') -}; - -const hyphenateRE = /\B([A-Z])/g; -const hyphenate = str => { - return str.replace(hyphenateRE, '-$1').toLowerCase() -}; - -function getInitialProps (propsList) { - const res = {}; - propsList.forEach(key => { - res[key] = undefined; - }); - return res -} - -function injectHook (options, key, hook) { - options[key] = [].concat(options[key] || []); - options[key].unshift(hook); -} - -function callHooks (vm, hook) { - if (vm) { - const hooks = vm.$options[hook] || []; - hooks.forEach(hook => { - hook.call(vm); - }); + 'use strict' + + const camelizeRE = /-(\w)/g + const camelize = str => { + return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '') } -} - -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)); - -function convertAttributeValue (value, name, { type } = {}) { - if (isBoolean(type)) { - if (value === 'true' || value === 'false') { - return value === 'true' - } - if (value === '' || value === name) { - return true + + const hyphenateRE = /\B([A-Z])/g + const hyphenate = str => { + return str.replace(hyphenateRE, '-$1').toLowerCase() + } + + function getInitialProps (propsList) { + const res = {} + propsList.forEach(key => { + res[key] = undefined + }) + return res + } + + function injectHook (options, key, hook) { + options[key] = [].concat(options[key] || []) + options[key].unshift(hook) + } + + function callHooks (vm, hook) { + if (vm) { + const hooks = vm.$options[hook] || [] + hooks.forEach(hook => { + hook.call(vm) + }) } - return value != null - } else if (isNumber(type)) { - const parsed = parseFloat(value, 10); - return isNaN(parsed) ? value : parsed - } else { - return value } -} -function toVNodes (h, children) { - const res = []; - for (let i = 0, l = children.length; i < l; i++) { - res.push(toVNode(h, children[i])); + function createCustomEvent (name, args) { + return new CustomEvent(name, { + bubbles: false, + cancelable: false, + detail: args + }) } - return res -} - -function toVNode (h, node) { - if (node.nodeType === 3) { - return node.data.trim() ? node.data : null - } else if (node.nodeType === 1) { - const data = { - attrs: getAttributes(node), - domProps: { - innerHTML: node.innerHTML + + const isBoolean = val => /function Boolean/.test(String(val)) + const isNumber = val => /function Number/.test(String(val)) + + function convertAttributeValue (value, name, { type } = {}) { + if (isBoolean(type)) { + if (value === 'true' || value === 'false') { + return value === 'true' + } + if (value === '' || value === name) { + return true } - }; - if (data.attrs.slot) { - data.slot = data.attrs.slot; - delete data.attrs.slot; + return value != null + } else if (isNumber(type)) { + const parsed = parseFloat(value, 10) + return isNaN(parsed) ? value : parsed + } else { + return value } - return h(node.tagName, data) - } else { - return null } -} -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; + function toVNodes (h, children) { + const res = [] + for (let i = 0, l = children.length; i < l; i++) { + res.push(toVNode(h, children[i])) + } + return res } - return res -} - -function spreadProps (component) { - const result = {}; - appendProps(result, component); - console.log(result); - return result -} - -function appendProps (result, component) { - for (const key in component.props) { - const camelKey = camelize(key); - if (!(camelKey in result)) { - result[camelKey] = component.props[key]; + + function toVNode (h, node) { + if (node.nodeType === 3) { + return node.data.trim() ? node.data : null + } else if (node.nodeType === 1) { + const data = { + attrs: getAttributes(node), + domProps: { + innerHTML: node.innerHTML + } + } + if (data.attrs.slot) { + data.slot = data.attrs.slot + delete data.attrs.slot + } + return h(node.tagName, data) + } else { + return null } } - if (component.extends) { - appendProps(result, component.extends); + 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 + } + return res } -} - -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 - - const options = typeof Component === 'function' - ? Component.options - : Component; - - //spread props - options.props = spreadProps(options); - // 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 spreadProps (component) { + const result = {} + appendProps(result, component) + console.log(result) + return result } - 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] - ); + function appendProps (result, component) { + for (const key in component.props) { + const camelKey = camelize(key) + if (!(camelKey in result)) { + result[camelKey] = component.props[key] + } + } + if (component.mixins) { + appendMixinsProps(result, component.mixins) + } + if (component.extends) { + appendProps(result, component.extends) + } } - 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: [] + function appendMixinsProps (result, mixins) { + mixins.forEach(function (mixin) { + if (mixin.props) { + for (const key in mixin.props) { + const camelKey = camelize(key) + if (!(camelKey in result)) { + result[camelKey] = mixin.props[key] } - }, - render (h) { - return h(Component, { - ref: 'inner', - props: this.props - }, this.slotChildren) } - }); - - // 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 === self) { - syncAttribute(self, m.attributeName); - } else { - hasChildrenChange = true; - } - } - if (hasChildrenChange) { - wrapper.slotChildren = Object.freeze(toVNodes( - wrapper.$createElement, - self.childNodes - )); + } + }) + } + + 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 + + const options = typeof Component === 'function' + ? Component.options + : Component + + // spread props + options.props = spreadProps(options) + // 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) } - }); - observer.observe(self, { - childList: true, - subtree: true, - characterData: true, - attributes: true - }); + }) + + 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 } - get vueComponent () { - return this._wrapper.$refs.inner + 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] + ) } - connectedCallback () { - const wrapper = this._wrapper; - if (!wrapper._isMounted) { + 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) + } + }) + + // 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 === self) { + syncAttribute(self, m.attributeName) + } else { + hasChildrenChange = true + } + } + if (hasChildrenChange) { + wrapper.slotChildren = Object.freeze(toVNodes( + wrapper.$createElement, + self.childNodes + )) + } + }) + observer.observe(self, { + childList: true, + subtree: true, + characterData: true, + attributes: true + }) + } + + 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 { + 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(); - }); + 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) + } else { + callHooks(this.vueComponent, 'activated') } - // initialize children - wrapper.slotChildren = Object.freeze(toVNodes( - wrapper.$createElement, - this.childNodes - )); - wrapper.$mount(); - this.shadowRoot.appendChild(wrapper.$el); - } else { - callHooks(this.vueComponent, 'activated'); + } + + disconnectedCallback () { + callHooks(this.vueComponent, 'deactivated') } } - disconnectedCallback () { - callHooks(this.vueComponent, 'deactivated'); + if (!isAsync) { + initialize(Component) } - } - if (!isAsync) { - initialize(Component); + return CustomElement } - return CustomElement -} - -return wrap; - -}()); + return wrap +}()) diff --git a/dist/vue-wc-wrapper.js b/dist/vue-wc-wrapper.js index 36ce7f9..180d831 100644 --- a/dist/vue-wc-wrapper.js +++ b/dist/vue-wc-wrapper.js @@ -1,32 +1,32 @@ -const camelizeRE = /-(\w)/g; +const camelizeRE = /-(\w)/g const camelize = str => { return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '') -}; +} -const hyphenateRE = /\B([A-Z])/g; +const hyphenateRE = /\B([A-Z])/g const hyphenate = str => { return str.replace(hyphenateRE, '-$1').toLowerCase() -}; +} function getInitialProps (propsList) { - const res = {}; + const res = {} propsList.forEach(key => { - res[key] = undefined; - }); + res[key] = undefined + }) return res } function injectHook (options, key, hook) { - options[key] = [].concat(options[key] || []); - options[key].unshift(hook); + options[key] = [].concat(options[key] || []) + options[key].unshift(hook) } function callHooks (vm, hook) { if (vm) { - const hooks = vm.$options[hook] || []; + const hooks = vm.$options[hook] || [] hooks.forEach(hook => { - hook.call(vm); - }); + hook.call(vm) + }) } } @@ -38,8 +38,8 @@ function createCustomEvent (name, 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)) function convertAttributeValue (value, name, { type } = {}) { if (isBoolean(type)) { @@ -51,7 +51,7 @@ function convertAttributeValue (value, name, { type } = {}) { } return value != null } else if (isNumber(type)) { - const parsed = parseFloat(value, 10); + const parsed = parseFloat(value, 10) return isNaN(parsed) ? value : parsed } else { return value @@ -59,9 +59,9 @@ function convertAttributeValue (value, name, { type } = {}) { } function toVNodes (h, children) { - const res = []; + const res = [] for (let i = 0, l = children.length; i < l; i++) { - res.push(toVNode(h, children[i])); + res.push(toVNode(h, children[i])) } return res } @@ -75,10 +75,10 @@ function toVNode (h, node) { domProps: { innerHTML: node.innerHTML } - }; + } if (data.attrs.slot) { - data.slot = data.attrs.slot; - delete data.attrs.slot; + data.slot = data.attrs.slot + delete data.attrs.slot } return h(node.tagName, data) } else { @@ -87,77 +87,92 @@ function toVNode (h, node) { } function getAttributes (node) { - const res = {}; + 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 } function spreadProps (component) { - const result = {}; - appendProps(result, component); - console.log(result); + const result = {} + appendProps(result, component) + console.log(result) return result } function appendProps (result, component) { for (const key in component.props) { - const camelKey = camelize(key); + const camelKey = camelize(key) if (!(camelKey in result)) { - result[camelKey] = component.props[key]; + result[camelKey] = component.props[key] } } - + if (component.mixins) { + appendMixinsProps(result, component.mixins) + } if (component.extends) { - appendProps(result, component.extends); + appendProps(result, component.extends) } } +function appendMixinsProps (result, mixins) { + mixins.forEach(function (mixin) { + if (mixin.props) { + for (const key in mixin.props) { + const camelKey = camelize(key) + if (!(camelKey in result)) { + result[camelKey] = mixin.props[key] + } + } + } + }) +} + function wrap (Vue, Component) { - const isAsync = typeof Component === 'function' && !Component.cid; - let isInitialized = false; - let hyphenatedPropsList; - let camelizedPropsList; - let camelizedPropsMap; + const isAsync = typeof Component === 'function' && !Component.cid + let isInitialized = false + let hyphenatedPropsList + let camelizedPropsList + let camelizedPropsMap function initialize (Component) { if (isInitialized) return const options = typeof Component === 'function' ? Component.options - : Component; + : Component - //spread props - options.props = spreadProps(options); + // spread props + options.props = spreadProps(options) // 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]]; + map[key] = originalPropsAsObject[propsList[i]] return map - }, {}); + }, {}) // proxy $emit to native DOM events injectHook(options, 'beforeCreate', function () { - const emit = this.$emit; + const emit = this.$emit this.$emit = (name, ...args) => { - this.$root.$options.customElement.dispatchEvent(createCustomEvent(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]; - }); - }); + this.$root.props[key] = this[key] + }) + }) // proxy props as Element properties camelizedPropsList.forEach(key => { @@ -166,30 +181,30 @@ function wrap (Vue, Component) { return this._wrapper.props[key] }, set (newVal) { - this._wrapper.props[key] = newVal; + 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; + 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 self = super() + self.attachShadow({ mode: 'open' }) const wrapper = self._wrapper = new Vue({ name: 'shadow-root', @@ -207,32 +222,32 @@ function wrap (Vue, Component) { props: this.props }, this.slotChildren) } - }); + }) // Use MutationObserver to react to future attribute & slot content change const observer = new MutationObserver(mutations => { - let hasChildrenChange = false; + let hasChildrenChange = false for (let i = 0; i < mutations.length; i++) { - const m = mutations[i]; + const m = mutations[i] if (isInitialized && m.type === 'attributes' && m.target === self) { - syncAttribute(self, m.attributeName); + syncAttribute(self, m.attributeName) } else { - hasChildrenChange = true; + hasChildrenChange = true } } if (hasChildrenChange) { wrapper.slotChildren = Object.freeze(toVNodes( wrapper.$createElement, self.childNodes - )); + )) } - }); + }) observer.observe(self, { childList: true, subtree: true, characterData: true, attributes: true - }); + }) } get vueComponent () { @@ -240,50 +255,50 @@ function wrap (Vue, Component) { } connectedCallback () { - const wrapper = this._wrapper; + const wrapper = this._wrapper if (!wrapper._isMounted) { // initialize attributes const syncInitialAttributes = () => { - wrapper.props = getInitialProps(camelizedPropsList); + wrapper.props = getInitialProps(camelizedPropsList) hyphenatedPropsList.forEach(key => { - syncAttribute(this, key); - }); - }; + syncAttribute(this, key) + }) + } if (isInitialized) { - syncInitialAttributes(); + syncInitialAttributes() } else { // async & unresolved Component().then(resolved => { if (resolved.__esModule || resolved[Symbol.toStringTag] === 'Module') { - resolved = resolved.default; + resolved = resolved.default } - initialize(resolved); - syncInitialAttributes(); - }); + initialize(resolved) + syncInitialAttributes() + }) } // initialize children wrapper.slotChildren = Object.freeze(toVNodes( wrapper.$createElement, this.childNodes - )); - wrapper.$mount(); - this.shadowRoot.appendChild(wrapper.$el); + )) + wrapper.$mount() + this.shadowRoot.appendChild(wrapper.$el) } 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 } -export default wrap; +export default wrap From 82669efc1d9a0689d42b114e12bd399e693eac88 Mon Sep 17 00:00:00 2001 From: jili Date: Tue, 8 Jan 2019 14:27:22 +0100 Subject: [PATCH 08/17] test for spreaded props --- test/fixtures/spreadedProperties.html | 38 ++++++++++++------------- test/test.js | 40 +++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 22 deletions(-) diff --git a/test/fixtures/spreadedProperties.html b/test/fixtures/spreadedProperties.html index 0c810c9..5f62b7b 100644 --- a/test/fixtures/spreadedProperties.html +++ b/test/fixtures/spreadedProperties.html @@ -2,50 +2,48 @@ - + diff --git a/test/test.js b/test/test.js index 9423d7a..97ed47e 100644 --- a/test/test.js +++ b/test/test.js @@ -16,12 +16,48 @@ test('properties', async () => { el.foo = 234 el.someProp = 'lol' }) - const newFoo = await page.evaluate(() => el.vueComponent.foo) + const newFoo = await page.evaluate(() => el.vueComponent.foo) expect(newFoo).toBe(234) - const newBar = await page.evaluate(() => el.vueComponent.someProp) + const newBar = await page.evaluate(() => el.vueComponent.someProp) expect(newBar).toBe('lol') }) +test('spreadedProperties', async () => { + const { page } = await launchPage(`spreadedProperties`) + + // props from 'extends' + const p0 = await page.evaluate(() => el.p0) + expect(p0).toBe('p0') + + // props from 'extends' + const p1 = await page.evaluate(() => el.p1) + expect(p1).toBe('p1') + + // props from 'mixins' + const m0 = await page.evaluate(() => el.m0) + expect(m0).toBe('m0') + + // props from 'mixins' + const m1 = await page.evaluate(() => el.m1) + expect(m1).toBe('m1') + + // props proxying: set + await page.evaluate(() => { + el.p0 = 'new-p0' + el.p1 = 'new-p1' + el.m0 = 'new-m0' + el.m1 = 'new-m1' + }) + const newP0 = await page.evaluate(() => el.vueComponent.p0) + expect(newP0).toBe('new-p0') + const newP1 = await page.evaluate(() => el.vueComponent.p1) + expect(newP1).toBe('new-p1') + const newM0 = await page.evaluate(() => el.vueComponent.m0) + expect(newM0).toBe('new-m0') + const newM1 = await page.evaluate(() => el.vueComponent.m1) + expect(newM1).toBe('new-m1') +}) + test('attributes', async () => { const { page } = await launchPage(`attributes`) From 8d75625e715e79d99eb0a9ab287a91bb1fd2db08 Mon Sep 17 00:00:00 2001 From: Jiang Li Date: Wed, 9 Jan 2019 14:54:36 +0100 Subject: [PATCH 09/17] check array props --- src/index.js | 6 ++---- src/utils.js | 49 +++++++++++++++++++++++++++++++------------------ test/setup.js | 1 + 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/index.js b/src/index.js index 2dde0eb..4788a1a 100644 --- a/src/index.js +++ b/src/index.js @@ -27,12 +27,10 @@ export default function wrap (Vue, Component) { // spread props options.props = spreadProps(options) // extract props info - const propsList = Array.isArray(options.props) - ? options.props - : Object.keys(options.props || {}) + const propsList = Object.keys(options.props || {}) hyphenatedPropsList = propsList.map(hyphenate) camelizedPropsList = propsList.map(camelize) - const originalPropsAsObject = Array.isArray(options.props) ? {} : options.props || {} + const originalPropsAsObject = options.props || {} camelizedPropsMap = camelizedPropsList.reduce((map, key, i) => { map[key] = originalPropsAsObject[propsList[i]] return map diff --git a/src/utils.js b/src/utils.js index 2d8d856..f202c68 100644 --- a/src/utils.js +++ b/src/utils.js @@ -97,34 +97,47 @@ function getAttributes (node) { export function spreadProps (component) { const result = {} - appendProps(result, component) - console.log(result) + spreadNext(result, component) return result } -function appendProps (result, component) { - for (const key in component.props) { - const camelKey = camelize(key) - if (!(camelKey in result)) { - result[camelKey] = component.props[key] - } +function spreadNext (result, component) { + if (component.props) { + appendProps(result, component.props) } + if (component.mixins) { - appendMixinsProps(result, component.mixins) + component.mixins.forEach(function (mixin) { + spreadNext(result, mixin) + }) } if (component.extends) { - appendProps(result, component.extends) + spreadNext(result, component.extends) + } +} + +function appendProps (result, props) { + if (Array.isArray(props)) { + processArrayProps(result, props) + } else { + processObjectProps(result, props) } } -function appendMixinsProps (result, mixins) { - mixins.forEach(function (mixin) { - if (mixin.props) { - for (const key in mixin.props) { - const camelKey = camelize(key) - if (!(camelKey in result)) { - result[camelKey] = mixin.props[key] - } +function processObjectProps (result, props) { + for (const key in props) { + const camelKey = camelize(key) + if (!(camelKey in result)) { + result[camelKey] = props[key] + } + } +} +function processArrayProps (result, props) { + props.forEach(function (prop) { + if (typeof prop === 'string') { + const camelKey = camelize(prop) + if (!(camelKey in result)) { + result[camelKey] = undefined } } }) diff --git a/test/setup.js b/test/setup.js index 24e59e1..a69ac1b 100644 --- a/test/setup.js +++ b/test/setup.js @@ -20,6 +20,7 @@ module.exports = async function launchPage (name) { } beforeAll(async () => { + jest.setTimeout(10000) browser = await puppeteer.launch(puppeteerOptions) server = createServer({ root: process.cwd() }) await new Promise((resolve, reject) => { From 2b68079a04b6af5ba549cfc0e4c680b5906b0ccd Mon Sep 17 00:00:00 2001 From: Jiang Li Date: Wed, 9 Jan 2019 15:38:41 +0100 Subject: [PATCH 10/17] more tests for spread props --- dist/vue-wc-wrapper.global.js | 55 ++++++++++++++++----------- dist/vue-wc-wrapper.js | 55 ++++++++++++++++----------- test/fixtures/spreadedProperties.html | 10 +++-- test/test.js | 26 +++++++++++++ 4 files changed, 99 insertions(+), 47 deletions(-) diff --git a/dist/vue-wc-wrapper.global.js b/dist/vue-wc-wrapper.global.js index c1f740b..a22b451 100644 --- a/dist/vue-wc-wrapper.global.js +++ b/dist/vue-wc-wrapper.global.js @@ -100,34 +100,47 @@ var wrapVueWebComponent = (function () { function spreadProps (component) { const result = {} - appendProps(result, component) - console.log(result) + spreadNext(result, component) return result } - function appendProps (result, component) { - for (const key in component.props) { - const camelKey = camelize(key) - if (!(camelKey in result)) { - result[camelKey] = component.props[key] - } + function spreadNext (result, component) { + if (component.props) { + appendProps(result, component.props) } + if (component.mixins) { - appendMixinsProps(result, component.mixins) + component.mixins.forEach(function (mixin) { + spreadNext(result, mixin) + }) } if (component.extends) { - appendProps(result, component.extends) + spreadNext(result, component.extends) } } - function appendMixinsProps (result, mixins) { - mixins.forEach(function (mixin) { - if (mixin.props) { - for (const key in mixin.props) { - const camelKey = camelize(key) - if (!(camelKey in result)) { - result[camelKey] = mixin.props[key] - } + function appendProps (result, props) { + if (Array.isArray(props)) { + processArrayProps(result, props) + } else { + processObjectProps(result, props) + } + } + + function processObjectProps (result, props) { + for (const key in props) { + const camelKey = camelize(key) + if (!(camelKey in result)) { + result[camelKey] = props[key] + } + } + } + function processArrayProps (result, props) { + props.forEach(function (prop) { + if (typeof prop === 'string') { + const camelKey = camelize(prop) + if (!(camelKey in result)) { + result[camelKey] = undefined } } }) @@ -150,12 +163,10 @@ var wrapVueWebComponent = (function () { // spread props options.props = spreadProps(options) // extract props info - const propsList = Array.isArray(options.props) - ? options.props - : Object.keys(options.props || {}) + const propsList = Object.keys(options.props || {}) hyphenatedPropsList = propsList.map(hyphenate) camelizedPropsList = propsList.map(camelize) - const originalPropsAsObject = Array.isArray(options.props) ? {} : options.props || {} + const originalPropsAsObject = options.props || {} camelizedPropsMap = camelizedPropsList.reduce((map, key, i) => { map[key] = originalPropsAsObject[propsList[i]] return map diff --git a/dist/vue-wc-wrapper.js b/dist/vue-wc-wrapper.js index 180d831..d062ab8 100644 --- a/dist/vue-wc-wrapper.js +++ b/dist/vue-wc-wrapper.js @@ -97,34 +97,47 @@ function getAttributes (node) { function spreadProps (component) { const result = {} - appendProps(result, component) - console.log(result) + spreadNext(result, component) return result } -function appendProps (result, component) { - for (const key in component.props) { - const camelKey = camelize(key) - if (!(camelKey in result)) { - result[camelKey] = component.props[key] - } +function spreadNext (result, component) { + if (component.props) { + appendProps(result, component.props) } + if (component.mixins) { - appendMixinsProps(result, component.mixins) + component.mixins.forEach(function (mixin) { + spreadNext(result, mixin) + }) } if (component.extends) { - appendProps(result, component.extends) + spreadNext(result, component.extends) } } -function appendMixinsProps (result, mixins) { - mixins.forEach(function (mixin) { - if (mixin.props) { - for (const key in mixin.props) { - const camelKey = camelize(key) - if (!(camelKey in result)) { - result[camelKey] = mixin.props[key] - } +function appendProps (result, props) { + if (Array.isArray(props)) { + processArrayProps(result, props) + } else { + processObjectProps(result, props) + } +} + +function processObjectProps (result, props) { + for (const key in props) { + const camelKey = camelize(key) + if (!(camelKey in result)) { + result[camelKey] = props[key] + } + } +} +function processArrayProps (result, props) { + props.forEach(function (prop) { + if (typeof prop === 'string') { + const camelKey = camelize(prop) + if (!(camelKey in result)) { + result[camelKey] = undefined } } }) @@ -147,12 +160,10 @@ function wrap (Vue, Component) { // spread props options.props = spreadProps(options) // extract props info - const propsList = Array.isArray(options.props) - ? options.props - : Object.keys(options.props || {}) + const propsList = Object.keys(options.props || {}) hyphenatedPropsList = propsList.map(hyphenate) camelizedPropsList = propsList.map(camelize) - const originalPropsAsObject = Array.isArray(options.props) ? {} : options.props || {} + const originalPropsAsObject = options.props || {} camelizedPropsMap = camelizedPropsList.reduce((map, key, i) => { map[key] = originalPropsAsObject[propsList[i]] return map diff --git a/test/fixtures/spreadedProperties.html b/test/fixtures/spreadedProperties.html index 5f62b7b..d920cc3 100644 --- a/test/fixtures/spreadedProperties.html +++ b/test/fixtures/spreadedProperties.html @@ -18,9 +18,12 @@ } } }; + var mixin2 = { + props: ['m2a', 'm2b'] + }; var Parent0 = { - mixins:[mixin0, mixin1], + mixins:[mixin0, mixin1, mixin2], props: { p0: { type: String, @@ -40,10 +43,11 @@ customElements.define('my-element', wrap(Vue, { extends: Parent1, - template: `
{{p0}} {{p1}} {{m0}} {{m1}}
`, + props:['c1', 'c2'], + template: `
{{p0}} {{p1}} {{m0}} {{m1}} {{c1}}{{c2}}{{c3}}{{m2a}} {{m2b}} {{m2c}}
`, })); window.el = document.querySelector('my-element'); - + diff --git a/test/test.js b/test/test.js index 97ed47e..688b406 100644 --- a/test/test.js +++ b/test/test.js @@ -41,12 +41,24 @@ test('spreadedProperties', async () => { const m1 = await page.evaluate(() => el.m1) expect(m1).toBe('m1') + // array props + const c1 = await page.evaluate(() => el.c1) + const c2 = await page.evaluate(() => el.c2) + const m2a = await page.evaluate(() => el.m2a) + const m2b = await page.evaluate(() => el.m2b) + expect(c1).toBe(undefined) + expect(c2).toBe(undefined) + expect(m2a).toBe(undefined) + expect(m2b).toBe(undefined) + // props proxying: set await page.evaluate(() => { el.p0 = 'new-p0' el.p1 = 'new-p1' el.m0 = 'new-m0' el.m1 = 'new-m1' + el.c1 = 'new-c1' + el.m2a = 'new-m2a' }) const newP0 = await page.evaluate(() => el.vueComponent.p0) expect(newP0).toBe('new-p0') @@ -56,6 +68,20 @@ test('spreadedProperties', async () => { expect(newM0).toBe('new-m0') const newM1 = await page.evaluate(() => el.vueComponent.m1) expect(newM1).toBe('new-m1') + const newC1 = await page.evaluate(() => el.vueComponent.c1) + expect(newC1).toBe('new-c1') + const newM2a = await page.evaluate(() => el.vueComponent.m2a) + expect(newM2a).toBe('new-m2a') + + // set via attribute + await page.evaluate(() => { + el.setAttribute('c1', 'foo') + el.setAttribute('m1', 'bar') + el.setAttribute('m2a', 'bla') + }) + expect(await page.evaluate(() => el.c1)).toBe('foo') + expect(await page.evaluate(() => el.m1)).toBe('bar') + expect(await page.evaluate(() => el.m2a)).toBe('bla') }) test('attributes', async () => { From 9c9468b5d21747487b77cd96e5c109911abff960 Mon Sep 17 00:00:00 2001 From: Jiang Li Date: Wed, 9 Jan 2019 15:41:48 +0100 Subject: [PATCH 11/17] new build --- dist/vue-wc-wrapper.global.js | 557 +++++++++++++++++----------------- dist/vue-wc-wrapper.js | 180 +++++------ 2 files changed, 369 insertions(+), 368 deletions(-) diff --git a/dist/vue-wc-wrapper.global.js b/dist/vue-wc-wrapper.global.js index a22b451..ab07537 100644 --- a/dist/vue-wc-wrapper.global.js +++ b/dist/vue-wc-wrapper.global.js @@ -1,319 +1,320 @@ var wrapVueWebComponent = (function () { - 'use strict' - - const camelizeRE = /-(\w)/g - const camelize = str => { - return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '') - } - - const hyphenateRE = /\B([A-Z])/g - const hyphenate = str => { - return str.replace(hyphenateRE, '-$1').toLowerCase() - } - - function getInitialProps (propsList) { - const res = {} - propsList.forEach(key => { - res[key] = undefined - }) - return res - } - - function injectHook (options, key, hook) { - options[key] = [].concat(options[key] || []) - options[key].unshift(hook) +'use strict'; + +const camelizeRE = /-(\w)/g; +const camelize = str => { + return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '') +}; + +const hyphenateRE = /\B([A-Z])/g; +const hyphenate = str => { + return str.replace(hyphenateRE, '-$1').toLowerCase() +}; + +function getInitialProps (propsList) { + const res = {}; + propsList.forEach(key => { + res[key] = undefined; + }); + return res +} + +function injectHook (options, key, hook) { + options[key] = [].concat(options[key] || []); + options[key].unshift(hook); +} + +function callHooks (vm, hook) { + if (vm) { + const hooks = vm.$options[hook] || []; + hooks.forEach(hook => { + hook.call(vm); + }); } - - function callHooks (vm, hook) { - if (vm) { - const hooks = vm.$options[hook] || [] - hooks.forEach(hook => { - hook.call(vm) - }) +} + +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)); + +function convertAttributeValue (value, name, { type } = {}) { + if (isBoolean(type)) { + if (value === 'true' || value === 'false') { + return value === 'true' } + if (value === '' || value === name) { + return true + } + return value != null + } else if (isNumber(type)) { + const parsed = parseFloat(value, 10); + return isNaN(parsed) ? value : parsed + } else { + return value } +} - function createCustomEvent (name, args) { - return new CustomEvent(name, { - bubbles: false, - cancelable: false, - detail: args - }) +function toVNodes (h, children) { + const res = []; + for (let i = 0, l = children.length; i < l; i++) { + res.push(toVNode(h, children[i])); } - - const isBoolean = val => /function Boolean/.test(String(val)) - const isNumber = val => /function Number/.test(String(val)) - - function convertAttributeValue (value, name, { type } = {}) { - if (isBoolean(type)) { - if (value === 'true' || value === 'false') { - return value === 'true' - } - if (value === '' || value === name) { - return true + return res +} + +function toVNode (h, node) { + if (node.nodeType === 3) { + return node.data.trim() ? node.data : null + } else if (node.nodeType === 1) { + const data = { + attrs: getAttributes(node), + domProps: { + innerHTML: node.innerHTML } - return value != null - } else if (isNumber(type)) { - const parsed = parseFloat(value, 10) - return isNaN(parsed) ? value : parsed - } else { - return value + }; + if (data.attrs.slot) { + data.slot = data.attrs.slot; + delete data.attrs.slot; } + return h(node.tagName, data) + } else { + return null } +} - function toVNodes (h, children) { - const res = [] - for (let i = 0, l = children.length; i < l; i++) { - res.push(toVNode(h, children[i])) - } - return res +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; } - - function toVNode (h, node) { - if (node.nodeType === 3) { - return node.data.trim() ? node.data : null - } else if (node.nodeType === 1) { - const data = { - attrs: getAttributes(node), - domProps: { - innerHTML: node.innerHTML - } - } - if (data.attrs.slot) { - data.slot = data.attrs.slot - delete data.attrs.slot - } - return h(node.tagName, data) - } else { - return null - } + return res +} + +function spreadProps (component) { + const result = {}; + spreadNext(result, component); + return result +} + +function spreadNext (result, component) { + if (component.props) { + appendProps(result, component.props); } - 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 - } - return res + if (component.mixins) { + component.mixins.forEach(function (mixin) { + spreadNext(result, mixin); + }); } - - function spreadProps (component) { - const result = {} - spreadNext(result, component) - return result + if (component.extends) { + spreadNext(result, component.extends); } +} - function spreadNext (result, component) { - if (component.props) { - appendProps(result, component.props) - } - - if (component.mixins) { - component.mixins.forEach(function (mixin) { - spreadNext(result, mixin) - }) - } - if (component.extends) { - spreadNext(result, component.extends) - } +function appendProps (result, props) { + if (Array.isArray(props)) { + processArrayProps(result, props); + } else { + processObjectProps(result, props); } +} - function appendProps (result, props) { - if (Array.isArray(props)) { - processArrayProps(result, props) - } else { - processObjectProps(result, props) +function processObjectProps (result, props) { + for (const key in props) { + const camelKey = camelize(key); + if (!(camelKey in result)) { + result[camelKey] = props[key]; } } - - function processObjectProps (result, props) { - for (const key in props) { - const camelKey = camelize(key) +} +function processArrayProps (result, props) { + props.forEach(function (prop) { + if (typeof prop === 'string') { + const camelKey = camelize(prop); if (!(camelKey in result)) { - result[camelKey] = props[key] + result[camelKey] = undefined; } } - } - function processArrayProps (result, props) { - props.forEach(function (prop) { - if (typeof prop === 'string') { - const camelKey = camelize(prop) - if (!(camelKey in result)) { - result[camelKey] = undefined - } - } - }) - } - - 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 - - const options = typeof Component === 'function' - ? Component.options - : Component - - // spread props - options.props = spreadProps(options) - // extract props info - const propsList = Object.keys(options.props || {}) - hyphenatedPropsList = propsList.map(hyphenate) - camelizedPropsList = propsList.map(camelize) - const originalPropsAsObject = 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 () { + }); +} + +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 + + const options = typeof Component === 'function' + ? Component.options + : Component; + + // spread props + options.props = spreadProps(options); + // extract props info + const propsList = Object.keys(options.props || {}); + hyphenatedPropsList = propsList.map(hyphenate); + camelizedPropsList = propsList.map(camelize); + const originalPropsAsObject = 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 { - 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) - } - }) - - // 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 === self) { - syncAttribute(self, m.attributeName) - } else { - hasChildrenChange = true - } - } - if (hasChildrenChange) { - wrapper.slotChildren = Object.freeze(toVNodes( - wrapper.$createElement, - self.childNodes - )) - } - }) - observer.observe(self, { - childList: true, - subtree: true, - characterData: true, - attributes: true - }) - } + 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; + } - get vueComponent () { - return this._wrapper.$refs.inner - } + 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] + ); + } - connectedCallback () { - const wrapper = this._wrapper - if (!wrapper._isMounted) { - // initialize attributes - const syncInitialAttributes = () => { - wrapper.props = getInitialProps(camelizedPropsList) - hyphenatedPropsList.forEach(key => { - syncAttribute(this, key) - }) + 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: [] } - - if (isInitialized) { - syncInitialAttributes() + }, + render (h) { + return h(Component, { + ref: 'inner', + props: this.props + }, this.slotChildren) + } + }); + + // 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 === self) { + syncAttribute(self, m.attributeName); } else { - // async & unresolved - Component().then(resolved => { - if (resolved.__esModule || resolved[Symbol.toStringTag] === 'Module') { - resolved = resolved.default - } - initialize(resolved) - syncInitialAttributes() - }) + hasChildrenChange = true; } - // initialize children + } + if (hasChildrenChange) { wrapper.slotChildren = Object.freeze(toVNodes( wrapper.$createElement, - this.childNodes - )) - wrapper.$mount() - this.shadowRoot.appendChild(wrapper.$el) - } else { - callHooks(this.vueComponent, 'activated') + self.childNodes + )); } - } + }); + observer.observe(self, { + childList: true, + subtree: true, + characterData: true, + attributes: true + }); + } + + get vueComponent () { + return this._wrapper.$refs.inner + } - disconnectedCallback () { - callHooks(this.vueComponent, 'deactivated') + 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); + } else { + callHooks(this.vueComponent, 'activated'); } } - if (!isAsync) { - initialize(Component) + disconnectedCallback () { + callHooks(this.vueComponent, 'deactivated'); } + } - return CustomElement + if (!isAsync) { + initialize(Component); } - return wrap -}()) + return CustomElement +} + +return wrap; + +}()); diff --git a/dist/vue-wc-wrapper.js b/dist/vue-wc-wrapper.js index d062ab8..ba33aef 100644 --- a/dist/vue-wc-wrapper.js +++ b/dist/vue-wc-wrapper.js @@ -1,32 +1,32 @@ -const camelizeRE = /-(\w)/g +const camelizeRE = /-(\w)/g; const camelize = str => { return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '') -} +}; -const hyphenateRE = /\B([A-Z])/g +const hyphenateRE = /\B([A-Z])/g; const hyphenate = str => { return str.replace(hyphenateRE, '-$1').toLowerCase() -} +}; function getInitialProps (propsList) { - const res = {} + const res = {}; propsList.forEach(key => { - res[key] = undefined - }) + res[key] = undefined; + }); return res } function injectHook (options, key, hook) { - options[key] = [].concat(options[key] || []) - options[key].unshift(hook) + options[key] = [].concat(options[key] || []); + options[key].unshift(hook); } function callHooks (vm, hook) { if (vm) { - const hooks = vm.$options[hook] || [] + const hooks = vm.$options[hook] || []; hooks.forEach(hook => { - hook.call(vm) - }) + hook.call(vm); + }); } } @@ -38,8 +38,8 @@ function createCustomEvent (name, 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)); function convertAttributeValue (value, name, { type } = {}) { if (isBoolean(type)) { @@ -51,7 +51,7 @@ function convertAttributeValue (value, name, { type } = {}) { } return value != null } else if (isNumber(type)) { - const parsed = parseFloat(value, 10) + const parsed = parseFloat(value, 10); return isNaN(parsed) ? value : parsed } else { return value @@ -59,9 +59,9 @@ function convertAttributeValue (value, name, { type } = {}) { } function toVNodes (h, children) { - const res = [] + const res = []; for (let i = 0, l = children.length; i < l; i++) { - res.push(toVNode(h, children[i])) + res.push(toVNode(h, children[i])); } return res } @@ -75,10 +75,10 @@ function toVNode (h, node) { domProps: { innerHTML: node.innerHTML } - } + }; if (data.attrs.slot) { - data.slot = data.attrs.slot - delete data.attrs.slot + data.slot = data.attrs.slot; + delete data.attrs.slot; } return h(node.tagName, data) } else { @@ -87,103 +87,103 @@ function toVNode (h, node) { } function getAttributes (node) { - const res = {} + 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 } function spreadProps (component) { - const result = {} - spreadNext(result, component) + const result = {}; + spreadNext(result, component); return result } function spreadNext (result, component) { if (component.props) { - appendProps(result, component.props) + appendProps(result, component.props); } if (component.mixins) { component.mixins.forEach(function (mixin) { - spreadNext(result, mixin) - }) + spreadNext(result, mixin); + }); } if (component.extends) { - spreadNext(result, component.extends) + spreadNext(result, component.extends); } } function appendProps (result, props) { if (Array.isArray(props)) { - processArrayProps(result, props) + processArrayProps(result, props); } else { - processObjectProps(result, props) + processObjectProps(result, props); } } function processObjectProps (result, props) { for (const key in props) { - const camelKey = camelize(key) + const camelKey = camelize(key); if (!(camelKey in result)) { - result[camelKey] = props[key] + result[camelKey] = props[key]; } } } function processArrayProps (result, props) { props.forEach(function (prop) { if (typeof prop === 'string') { - const camelKey = camelize(prop) + const camelKey = camelize(prop); if (!(camelKey in result)) { - result[camelKey] = undefined + result[camelKey] = undefined; } } - }) + }); } function wrap (Vue, Component) { - const isAsync = typeof Component === 'function' && !Component.cid - let isInitialized = false - let hyphenatedPropsList - let camelizedPropsList - let camelizedPropsMap + const isAsync = typeof Component === 'function' && !Component.cid; + let isInitialized = false; + let hyphenatedPropsList; + let camelizedPropsList; + let camelizedPropsMap; function initialize (Component) { if (isInitialized) return const options = typeof Component === 'function' ? Component.options - : Component + : Component; // spread props - options.props = spreadProps(options) + options.props = spreadProps(options); // extract props info - const propsList = Object.keys(options.props || {}) - hyphenatedPropsList = propsList.map(hyphenate) - camelizedPropsList = propsList.map(camelize) - const originalPropsAsObject = options.props || {} + const propsList = Object.keys(options.props || {}); + hyphenatedPropsList = propsList.map(hyphenate); + camelizedPropsList = propsList.map(camelize); + const originalPropsAsObject = options.props || {}; camelizedPropsMap = camelizedPropsList.reduce((map, key, i) => { - map[key] = originalPropsAsObject[propsList[i]] + map[key] = originalPropsAsObject[propsList[i]]; return map - }, {}) + }, {}); // proxy $emit to native DOM events injectHook(options, 'beforeCreate', function () { - const emit = this.$emit + const emit = this.$emit; this.$emit = (name, ...args) => { - this.$root.$options.customElement.dispatchEvent(createCustomEvent(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] - }) - }) + this.$root.props[key] = this[key]; + }); + }); // proxy props as Element properties camelizedPropsList.forEach(key => { @@ -192,30 +192,30 @@ function wrap (Vue, Component) { return this._wrapper.props[key] }, set (newVal) { - this._wrapper.props[key] = newVal + 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 + 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 self = super(); + self.attachShadow({ mode: 'open' }); const wrapper = self._wrapper = new Vue({ name: 'shadow-root', @@ -233,32 +233,32 @@ function wrap (Vue, Component) { props: this.props }, this.slotChildren) } - }) + }); // Use MutationObserver to react to future attribute & slot content change const observer = new MutationObserver(mutations => { - let hasChildrenChange = false + let hasChildrenChange = false; for (let i = 0; i < mutations.length; i++) { - const m = mutations[i] + const m = mutations[i]; if (isInitialized && m.type === 'attributes' && m.target === self) { - syncAttribute(self, m.attributeName) + syncAttribute(self, m.attributeName); } else { - hasChildrenChange = true + hasChildrenChange = true; } } if (hasChildrenChange) { wrapper.slotChildren = Object.freeze(toVNodes( wrapper.$createElement, self.childNodes - )) + )); } - }) + }); observer.observe(self, { childList: true, subtree: true, characterData: true, attributes: true - }) + }); } get vueComponent () { @@ -266,50 +266,50 @@ function wrap (Vue, Component) { } connectedCallback () { - const wrapper = this._wrapper + const wrapper = this._wrapper; if (!wrapper._isMounted) { // initialize attributes const syncInitialAttributes = () => { - wrapper.props = getInitialProps(camelizedPropsList) + wrapper.props = getInitialProps(camelizedPropsList); hyphenatedPropsList.forEach(key => { - syncAttribute(this, key) - }) - } + syncAttribute(this, key); + }); + }; if (isInitialized) { - syncInitialAttributes() + syncInitialAttributes(); } else { // async & unresolved Component().then(resolved => { if (resolved.__esModule || resolved[Symbol.toStringTag] === 'Module') { - resolved = resolved.default + resolved = resolved.default; } - initialize(resolved) - syncInitialAttributes() - }) + initialize(resolved); + syncInitialAttributes(); + }); } // initialize children wrapper.slotChildren = Object.freeze(toVNodes( wrapper.$createElement, this.childNodes - )) - wrapper.$mount() - this.shadowRoot.appendChild(wrapper.$el) + )); + wrapper.$mount(); + this.shadowRoot.appendChild(wrapper.$el); } 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 } -export default wrap +export default wrap; From 1d4f88467468c15ac4892748af0b36430eb0b61b Mon Sep 17 00:00:00 2001 From: Jiang Li Date: Wed, 9 Jan 2019 15:46:50 +0100 Subject: [PATCH 12/17] set version 1.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f7ec743..831c8ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vue/web-component-wrapper", - "version": "1.2.0", + "version": "1.2.1", "description": "wrap a vue component as a web component.", "main": "dist/vue-wc-wrapper.js", "unpkg": "dist/vue-wc-wrapper.global.js", From 0c1687607008926b343d36501cf663549fcfeec8 Mon Sep 17 00:00:00 2001 From: Jiang Li Date: Wed, 9 Jan 2019 15:54:17 +0100 Subject: [PATCH 13/17] set version 1.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 831c8ed..6abe05e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vue/web-component-wrapper", - "version": "1.2.1", + "version": "1.3.0", "description": "wrap a vue component as a web component.", "main": "dist/vue-wc-wrapper.js", "unpkg": "dist/vue-wc-wrapper.global.js", From 2c03372b1f719c88ef37b11d560ca4ba34ecce63 Mon Sep 17 00:00:00 2001 From: Jiang Li Date: Wed, 9 Jan 2019 16:02:11 +0100 Subject: [PATCH 14/17] clean test --- test/fixtures/spreadedProperties.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fixtures/spreadedProperties.html b/test/fixtures/spreadedProperties.html index d920cc3..8b60b0c 100644 --- a/test/fixtures/spreadedProperties.html +++ b/test/fixtures/spreadedProperties.html @@ -44,7 +44,7 @@ customElements.define('my-element', wrap(Vue, { extends: Parent1, props:['c1', 'c2'], - template: `
{{p0}} {{p1}} {{m0}} {{m1}} {{c1}}{{c2}}{{c3}}{{m2a}} {{m2b}} {{m2c}}
`, + template: `
{{p0}} {{p1}} {{m0}} {{m1}} {{c1}}{{c2}}{{m2a}} {{m2b}}
`, })); window.el = document.querySelector('my-element'); From 87fa7d98c6851245980682188b8a783064c71be3 Mon Sep 17 00:00:00 2001 From: Jiang Li Date: Wed, 9 Jan 2019 16:11:28 +0100 Subject: [PATCH 15/17] test array props in inheritance --- test/fixtures/spreadedProperties.html | 7 +------ test/test.js | 4 +++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/test/fixtures/spreadedProperties.html b/test/fixtures/spreadedProperties.html index 8b60b0c..e2d500f 100644 --- a/test/fixtures/spreadedProperties.html +++ b/test/fixtures/spreadedProperties.html @@ -24,12 +24,7 @@ var Parent0 = { mixins:[mixin0, mixin1, mixin2], - props: { - p0: { - type: String, - default: 'p0' - } - } + props: ['p0'] }; var Parent1 = { extends: Parent0, diff --git a/test/test.js b/test/test.js index 688b406..665d0fd 100644 --- a/test/test.js +++ b/test/test.js @@ -27,7 +27,7 @@ test('spreadedProperties', async () => { // props from 'extends' const p0 = await page.evaluate(() => el.p0) - expect(p0).toBe('p0') + expect(p0).toBe(undefined) // props from 'extends' const p1 = await page.evaluate(() => el.p1) @@ -76,10 +76,12 @@ test('spreadedProperties', async () => { // set via attribute await page.evaluate(() => { el.setAttribute('c1', 'foo') + el.setAttribute('p1', 'foo2') el.setAttribute('m1', 'bar') el.setAttribute('m2a', 'bla') }) expect(await page.evaluate(() => el.c1)).toBe('foo') + expect(await page.evaluate(() => el.p1)).toBe('foo2') expect(await page.evaluate(() => el.m1)).toBe('bar') expect(await page.evaluate(() => el.m2a)).toBe('bla') }) From 2c6a4f1efa1a6d6ac5f0dbc17268e80b1f59a9ea Mon Sep 17 00:00:00 2001 From: jili Date: Thu, 28 Feb 2019 11:42:19 +0100 Subject: [PATCH 16/17] add readme for abas --- abas/README2.md | 16 ++++++++++++++++ abas/WcIconMixin.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 abas/README2.md create mode 100644 abas/WcIconMixin.js diff --git a/abas/README2.md b/abas/README2.md new file mode 100644 index 0000000..37079e2 --- /dev/null +++ b/abas/README2.md @@ -0,0 +1,16 @@ +# This readme is only for abas + +1. Checkout this wrapper repo into the same level of the project what you want to convert. e.g. abas-elements +2. To fix the icons styles + Put 'WcIconMixin.js' file into abas-elements/src/mixins +3. Adjust 'vue.config.js' in abas-elements project by adding alias for the '@vue/web-component-wrapper' + + ''' + configureWebpack: { + resolve: { + alias: { + '@vue/web-component-wrapper': path.join(__dirname, '../vue-web-component-wrapper'), + }, + }, + }, + ''' \ No newline at end of file diff --git a/abas/WcIconMixin.js b/abas/WcIconMixin.js new file mode 100644 index 0000000..fc720dc --- /dev/null +++ b/abas/WcIconMixin.js @@ -0,0 +1,28 @@ +import { dom } from '@fortawesome/fontawesome-svg-core' + +/** + * @mixin + */ +export default { + mounted () { + const id = 'fa-styles' + const shadowRoot = this.getShadowRoot(this) + if (shadowRoot && !shadowRoot.getElementById(id)) { + const faStyles = document.createElement('style') + faStyles.setAttribute('id', id) + faStyles.textContent = dom.css() + shadowRoot.appendChild(faStyles) + } + }, + methods: { + getShadowRoot (obj) { + if (obj.$parent) { + if (obj.$parent.$options && obj.$parent.$options.shadowRoot) { + return obj.$parent.$options.shadowRoot + } + return this.getShadowRoot(obj.$parent) + } + return null + } + } +} From e5fa6abd38b4a05be9b0e952f1e3337fefc4b170 Mon Sep 17 00:00:00 2001 From: jili Date: Thu, 28 Feb 2019 12:23:39 +0100 Subject: [PATCH 17/17] update readme --- abas/README2.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/abas/README2.md b/abas/README2.md index 37079e2..3cbd50c 100644 --- a/abas/README2.md +++ b/abas/README2.md @@ -3,6 +3,8 @@ 1. Checkout this wrapper repo into the same level of the project what you want to convert. e.g. abas-elements 2. To fix the icons styles Put 'WcIconMixin.js' file into abas-elements/src/mixins + Add 'import WcIconMixin from '../mixins/WcIconMixin';' in Icon.vue + Add mixin 'mixins: [WcIconMixin],' in Icon.vue 3. Adjust 'vue.config.js' in abas-elements project by adding alias for the '@vue/web-component-wrapper' ''' @@ -13,4 +15,8 @@ }, }, }, - ''' \ No newline at end of file + ''' +4. add dependency into package.json in block 'devDependencies' + ''' + "eslint-plugin-vue-libs": "^2.1.0" + '''