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/abas/README2.md b/abas/README2.md new file mode 100644 index 0000000..3cbd50c --- /dev/null +++ b/abas/README2.md @@ -0,0 +1,22 @@ +# 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 + 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' + + ''' + configureWebpack: { + resolve: { + alias: { + '@vue/web-component-wrapper': path.join(__dirname, '../vue-web-component-wrapper'), + }, + }, + }, + ''' +4. add dependency into package.json in block 'devDependencies' + ''' + "eslint-plugin-vue-libs": "^2.1.0" + ''' 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 + } + } +} diff --git a/dist/vue-wc-wrapper.global.js b/dist/vue-wc-wrapper.global.js index db8253a..ab07537 100644 --- a/dist/vue-wc-wrapper.global.js +++ b/dist/vue-wc-wrapper.global.js @@ -98,6 +98,54 @@ function getAttributes (node) { return res } +function spreadProps (component) { + const result = {}; + spreadNext(result, component); + return result +} + +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 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; + } + } + }); +} + function wrap (Vue, Component) { const isAsync = typeof Component === 'function' && !Component.cid; let isInitialized = false; @@ -112,13 +160,13 @@ function wrap (Vue, Component) { ? Component.options : 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 @@ -169,13 +217,13 @@ function wrap (Vue, Component) { 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: {}, @@ -195,8 +243,8 @@ function wrap (Vue, Component) { 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); + if (isInitialized && m.type === 'attributes' && m.target === self) { + syncAttribute(self, m.attributeName); } else { hasChildrenChange = true; } @@ -204,11 +252,11 @@ function wrap (Vue, Component) { 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, diff --git a/dist/vue-wc-wrapper.js b/dist/vue-wc-wrapper.js index 36b4469..ba33aef 100644 --- a/dist/vue-wc-wrapper.js +++ b/dist/vue-wc-wrapper.js @@ -95,6 +95,54 @@ function getAttributes (node) { return res } +function spreadProps (component) { + const result = {}; + spreadNext(result, component); + return result +} + +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 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; + } + } + }); +} + function wrap (Vue, Component) { const isAsync = typeof Component === 'function' && !Component.cid; let isInitialized = false; @@ -109,13 +157,13 @@ function wrap (Vue, Component) { ? Component.options : 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 @@ -166,13 +214,13 @@ function wrap (Vue, Component) { 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: {}, @@ -192,8 +240,8 @@ function wrap (Vue, Component) { 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); + if (isInitialized && m.type === 'attributes' && m.target === self) { + syncAttribute(self, m.attributeName); } else { hasChildrenChange = true; } @@ -201,11 +249,11 @@ function wrap (Vue, Component) { 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, diff --git a/package.json b/package.json index f7ec743..6abe05e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vue/web-component-wrapper", - "version": "1.2.0", + "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", diff --git a/src/index.js b/src/index.js index 4409bd7..4788a1a 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,13 +24,13 @@ export default function wrap (Vue, Component) { ? Component.options : 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 0834f03..f202c68 100644 --- a/src/utils.js +++ b/src/utils.js @@ -94,3 +94,52 @@ function getAttributes (node) { } return res } + +export function spreadProps (component) { + const result = {} + spreadNext(result, component) + return result +} + +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 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/fixtures/spreadedProperties.html b/test/fixtures/spreadedProperties.html new file mode 100644 index 0000000..e2d500f --- /dev/null +++ b/test/fixtures/spreadedProperties.html @@ -0,0 +1,48 @@ + + + + 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) => { diff --git a/test/test.js b/test/test.js index 9423d7a..665d0fd 100644 --- a/test/test.js +++ b/test/test.js @@ -16,12 +16,76 @@ 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(undefined) + + // 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') + + // 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') + 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') + 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('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') +}) + test('attributes', async () => { const { page } = await launchPage(`attributes`)