Skip to content

add wrap() for Vue3 #109

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
.DS_Store
.idea
7 changes: 7 additions & 0 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type VueV2 from 'vue';
import type { Component as VueV2Component, AsyncComponent as VueV2AsyncComponent } from 'vue';
import type * as VueV3 from 'vue-v3';
import type { Component as VueV3Component } from 'vue-v3';
declare function wrap(Vue: typeof VueV2, Component: VueV2Component | VueV2AsyncComponent): CustomElementConstructor;
declare function wrap(Vue: typeof VueV3, Component: VueV3Component): CustomElementConstructor;
export default wrap;
548 changes: 317 additions & 231 deletions dist/vue-wc-wrapper.global.js

Large diffs are not rendered by default.

88 changes: 87 additions & 1 deletion dist/vue-wc-wrapper.js
Original file line number Diff line number Diff line change
@@ -95,7 +95,13 @@ function getAttributes (node) {
return res
}

function wrap (Vue, Component) {
/**
*
* @param {import("vue").Vue} Vue
* @param {import("vue").Component | import("vue").AsyncComponent} Component
* @return {CustomElementConstructor}
*/
function wrap$2 (Vue, Component) {
const isAsync = typeof Component === 'function' && !Component.cid;
let isInitialized = false;
let hyphenatedPropsList;
@@ -264,4 +270,84 @@ function wrap (Vue, Component) {
return CustomElement
}

/*! *****************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise */

var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};

function __extends(d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}

function wrap$1(Vue, Component) {
/* WIP */
var CustomElement = /** @class */ (function (_super) {
__extends(CustomElement, _super);
function CustomElement() {
var _this = _super.call(this) || this;
_this.attachShadow({ mode: 'open' });
// shadow root was attached with its mode set to open, so this shadow root is nonnull.
var shadowRoot = _this.shadowRoot;
_this._wrapper = Vue.createApp({
data: function () {
return {
props: {},
slotChildren: []
};
},
render: function () {
return Vue.h(Component, this.props);
}
}).mount(shadowRoot.host);
return _this;
}
return CustomElement;
}(HTMLElement));
return CustomElement;
}

var majorVersion = function (Vue) {
if (typeof Vue.version !== 'string') {
return null;
}
return Vue.version.split('.')[0];
};
var isV2 = function (Vue) {
return majorVersion(Vue) === '2';
};
var isV3 = function (Vue) {
return majorVersion(Vue) === '3';
};
function wrap(Vue, Component) {
if (isV2(Vue)) {
return wrap$2(Vue, Component);
}
if (isV3(Vue)) {
return wrap$1(Vue, Component);
}
throw new Error('supported vue version is v2 or v3.');
}

export default wrap;
3 changes: 3 additions & 0 deletions dist/wrapV3.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type * as _Vue from "vue-v3";
import type { Component } from "vue-v3";
export default function wrap(Vue: typeof _Vue, Component: Component): CustomElementConstructor;
20 changes: 13 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -4,13 +4,12 @@
"description": "wrap a vue component as a web component.",
"main": "dist/vue-wc-wrapper.js",
"unpkg": "dist/vue-wc-wrapper.global.js",
"types": "types/index.d.ts",
"types": "dist/index.d.ts",
"files": [
"dist",
"types/*.d.ts"
"dist"
],
"scripts": {
"test": "jest",
"test": "yarn build && jest",
"lint": "eslint src",
"build": "rollup -c",
"prepare": "rollup -c",
@@ -31,24 +30,31 @@
},
"homepage": "https://github.com/vuejs/web-component-wrapper#readme",
"devDependencies": {
"@rollup/plugin-typescript": "^8.2.1",
"eslint": "^4.16.0",
"eslint-plugin-vue-libs": "^2.1.0",
"http-server": "^0.11.1",
"jest": "^22.1.4",
"lint-staged": "^6.1.0",
"puppeteer": "^1.0.0",
"rollup": "^0.55.3",
"typescript": "^3.2.2",
"rollup": "^2.51.0",
"tslib": "^2.2.0",
"typescript": "^4.3.2",
"vue": "^2.5.13",
"vue-v3": "npm:vue@^3.0.11",
"yorkie": "^1.0.3"
},
"eslintConfig": {
"root": true,
"env": {
"browser": true
"browser": true,
"jest": true
},
"extends": "plugin:vue-libs/recommended"
},
"eslintIgnore": [
"dist/**/*"
],
"gitHooks": {
"pre-commit": "lint-staged"
},
31 changes: 22 additions & 9 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
export default {
input: 'src/index.js',
output: [
{
import typescript from '@rollup/plugin-typescript'

export default [
{
input: 'src/index.ts',
output: {
format: 'es',
file: 'dist/vue-wc-wrapper.js'
dir: './',
entryFileNames: 'dist/vue-wc-wrapper.js'
},
{
plugins: [
// https://github.com/rollup/plugins/issues/61
typescript({
declaration: true,
declarationDir: 'dist/'
})]
},
{
input: 'src/index.ts',
output: {
format: 'iife',
name: 'wrapVueWebComponent',
file: 'dist/vue-wc-wrapper.global.js'
}
]
}
},
plugins: [typescript()]
}
]
39 changes: 39 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type VueV2 from 'vue';
import type { Component as VueV2Component, AsyncComponent as VueV2AsyncComponent } from 'vue';
import type * as VueV3 from 'vue-v3';
import type { Component as VueV3Component } from 'vue-v3';

import wrapV2 from './wrapV2.js'
import wrapV3 from './wrapV3'

const majorVersion = (Vue: any): string | null => {
if (typeof Vue.version !== 'string') {
return null
}
return Vue.version.split('.')[0]
}

const isV2 = (Vue: any): Vue is typeof VueV2 => {
return majorVersion(Vue) === '2'
}

const isV3 = (Vue: any): Vue is typeof VueV3 => {
return majorVersion(Vue) === '3'
}

function wrap(Vue: typeof VueV2, Component: VueV2Component | VueV2AsyncComponent): CustomElementConstructor

function wrap(Vue: typeof VueV3, Component: VueV3Component): CustomElementConstructor

function wrap(Vue: any, Component: any): CustomElementConstructor {
if (isV2(Vue)) {
return wrapV2(Vue, Component)
}
if (isV3(Vue)) {
return wrapV3(Vue, Component)
}

throw new Error('supported vue version is v2 or v3.')
}

export default wrap
6 changes: 6 additions & 0 deletions src/index.js → src/wrapV2.js
Original file line number Diff line number Diff line change
@@ -9,6 +9,12 @@ import {
convertAttributeValue
} from './utils.js'

/**
*
* @param {import("vue").Vue} Vue
* @param {import("vue").Component | import("vue").AsyncComponent} Component
* @return {CustomElementConstructor}
*/
export default function wrap (Vue, Component) {
const isAsync = typeof Component === 'function' && !Component.cid
let isInitialized = false
31 changes: 31 additions & 0 deletions src/wrapV3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type * as _Vue from "vue-v3";
import type { Component, h } from "vue-v3";

export default function wrap(Vue: typeof _Vue, Component: Component): CustomElementConstructor {
/* WIP */

class CustomElement extends HTMLElement {
private _wrapper: _Vue.ComponentPublicInstance

constructor() {
super()
this.attachShadow({ mode: 'open' })
// shadow root was attached with its mode set to open, so this shadow root is nonnull.
const shadowRoot = this.shadowRoot as ShadowRoot;

this._wrapper = Vue.createApp({
data() {
return {
props: {},
slotChildren: []
}
},
render() {
return Vue.h(Component, this.props)
}
}).mount(shadowRoot.host)
}
}

return CustomElement
}
2 changes: 1 addition & 1 deletion test/fixtures/async.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script src="../../node_modules/vue/dist/vue.js"></script>
<script type="module">
import wrap from '../../src/index.js'
import wrap from '../../dist/vue-wc-wrapper.js'

const Component = () => new Promise(resolve => {
setTimeout(() => {
2 changes: 1 addition & 1 deletion test/fixtures/attributes.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script src="../../node_modules/vue/dist/vue.js"></script>
<script type="module">
import wrap from '../../src/index.js'
import wrap from '../../dist/vue-wc-wrapper.js'

customElements.define('my-element', wrap(Vue, {
template: `<div>{{ foo }} {{ bar }} {{ someNumber }}</div>`,
2 changes: 1 addition & 1 deletion test/fixtures/events.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script src="../../node_modules/vue/dist/vue.js"></script>
<script type="module">
import wrap from '../../src/index.js'
import wrap from '../../dist/vue-wc-wrapper.js'

customElements.define('my-element', wrap(Vue, {
template: `<div>
2 changes: 1 addition & 1 deletion test/fixtures/lifecycle.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script src="../../node_modules/vue/dist/vue.js"></script>
<script type="module">
import wrap from '../../src/index.js'
import wrap from '../../dist/vue-wc-wrapper.js'

customElements.define('my-element', wrap(Vue, {
template: `<div></div>`,
2 changes: 1 addition & 1 deletion test/fixtures/properties.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script src="../../node_modules/vue/dist/vue.js"></script>
<script type="module">
import wrap from '../../src/index.js'
import wrap from '../../dist/vue-wc-wrapper.js'

customElements.define('my-element', wrap(Vue, {
template: `<div>{{ foo }} {{ someProp }}</div>`,
2 changes: 1 addition & 1 deletion test/fixtures/slots.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script src="../../node_modules/vue/dist/vue.js"></script>
<script type="module">
import wrap from '../../src/index.js'
import wrap from '../../dist/vue-wc-wrapper.js'

customElements.define('my-element', wrap(Vue, {
template: `
228 changes: 115 additions & 113 deletions test/test.js
Original file line number Diff line number Diff line change
@@ -1,145 +1,147 @@
/* global test expect el els */
const launchPage = require('./setup')

test('properties', async () => {
const { page } = await launchPage(`properties`)

// props proxying: get
const foo = await page.evaluate(() => el.foo)
expect(foo).toBe(123)

// get camelCase
const someProp = await page.evaluate(() => el.someProp)
expect(someProp).toBe('bar')

// props proxying: set
await page.evaluate(() => {
el.foo = 234
el.someProp = 'lol'
describe('v2', () => {
test('properties', async () => {
const { page } = await launchPage(`properties`)

// props proxying: get
const foo = await page.evaluate(() => el.foo)
expect(foo).toBe(123)

// get camelCase
const someProp = await page.evaluate(() => el.someProp)
expect(someProp).toBe('bar')

// props proxying: set
await page.evaluate(() => {
el.foo = 234
el.someProp = 'lol'
})
const newFoo = await page.evaluate(() => el.vueComponent.foo)
expect(newFoo).toBe(234)
const newBar = await page.evaluate(() => el.vueComponent.someProp)
expect(newBar).toBe('lol')
})
const newFoo = await page.evaluate(() => el.vueComponent.foo)
expect(newFoo).toBe(234)
const newBar = await page.evaluate(() => el.vueComponent.someProp)
expect(newBar).toBe('lol')
})

test('attributes', async () => {
const { page } = await launchPage(`attributes`)
test('attributes', async () => {
const { page } = await launchPage(`attributes`)

// boolean
const foo = await page.evaluate(() => el.foo)
expect(foo).toBe(true)
// boolean
const foo = await page.evaluate(() => el.foo)
expect(foo).toBe(true)

// boolean="true"
const bar = await page.evaluate(() => el.bar)
expect(bar).toBe(true)
// boolean="true"
const bar = await page.evaluate(() => el.bar)
expect(bar).toBe(true)

// absence of boolean with default: true
const baz = await page.evaluate(() => el.baz)
expect(baz).toBe(true)
// absence of boolean with default: true
const baz = await page.evaluate(() => el.baz)
expect(baz).toBe(true)

// boolean="false" with default: true
const qux = await page.evaluate(() => el.qux)
expect(qux).toBe(false)
// boolean="false" with default: true
const qux = await page.evaluate(() => el.qux)
expect(qux).toBe(false)

// some-number="123"
const someNumber = await page.evaluate(() => el.someNumber)
expect(someNumber).toBe(123)
// some-number="123"
const someNumber = await page.evaluate(() => el.someNumber)
expect(someNumber).toBe(123)

// set via attribute
await page.evaluate(() => {
el.setAttribute('foo', 'foo')
el.setAttribute('bar', 'false')
el.setAttribute('baz', 'false')
el.setAttribute('qux', '')
el.setAttribute('some-number', '234')
})
// set via attribute
await page.evaluate(() => {
el.setAttribute('foo', 'foo')
el.setAttribute('bar', 'false')
el.setAttribute('baz', 'false')
el.setAttribute('qux', '')
el.setAttribute('some-number', '234')
})

// boolean="boolean"
expect(await page.evaluate(() => el.foo)).toBe(true)
expect(await page.evaluate(() => el.bar)).toBe(false)
expect(await page.evaluate(() => el.baz)).toBe(false)
expect(await page.evaluate(() => el.qux)).toBe(true)
expect(await page.evaluate(() => el.someNumber)).toBe(234)
})
// boolean="boolean"
expect(await page.evaluate(() => el.foo)).toBe(true)
expect(await page.evaluate(() => el.bar)).toBe(false)
expect(await page.evaluate(() => el.baz)).toBe(false)
expect(await page.evaluate(() => el.qux)).toBe(true)
expect(await page.evaluate(() => el.someNumber)).toBe(234)
})

test('events', async () => {
const { page } = await launchPage(`events`)
await page.evaluate(() => {
el.shadowRoot.querySelector('button').click()
test('events', async () => {
const { page } = await launchPage(`events`)
await page.evaluate(() => {
el.shadowRoot.querySelector('button').click()
})
expect(await page.evaluate(() => window.emitted)).toBe(true)
expect(await page.evaluate(() => window.emittedDetail)).toEqual([123])
})
expect(await page.evaluate(() => window.emitted)).toBe(true)
expect(await page.evaluate(() => window.emittedDetail)).toEqual([123])
})

test('slots', async () => {
const { page } = await launchPage(`slots`)
test('slots', async () => {
const { page } = await launchPage(`slots`)

const content = await page.evaluate(() => {
return el.shadowRoot.querySelector('div').innerHTML
})
expect(content).toMatch(`<div>default</div><div>foo</div>`)
const content = await page.evaluate(() => {
return el.shadowRoot.querySelector('div').innerHTML
})
expect(content).toMatch(`<div>default</div><div>foo</div>`)

// update slots
await page.evaluate(() => {
el.innerHTML = `<div>default2</div><div slot="foo">foo2</div>`
})
const newContent = await page.evaluate(() => {
return el.shadowRoot.querySelector('div').innerHTML
// update slots
await page.evaluate(() => {
el.innerHTML = `<div>default2</div><div slot="foo">foo2</div>`
})
const newContent = await page.evaluate(() => {
return el.shadowRoot.querySelector('div').innerHTML
})
expect(newContent).toMatch(`<div>default2</div><div>foo2</div>`)
})
expect(newContent).toMatch(`<div>default2</div><div>foo2</div>`)
})

test('lifecycle', async () => {
const { page, logs } = await launchPage(`lifecycle`)
test('lifecycle', async () => {
const { page, logs } = await launchPage(`lifecycle`)

expect(logs).toContain('created')
expect(logs).toContain('mounted')
expect(logs).toContain('created')
expect(logs).toContain('mounted')

await page.evaluate(() => {
el.parentNode.removeChild(el)
})
expect(logs).toContain('deactivated')
await page.evaluate(() => {
el.parentNode.removeChild(el)
})
expect(logs).toContain('deactivated')

await page.evaluate(() => {
document.body.appendChild(el)
await page.evaluate(() => {
document.body.appendChild(el)
})
expect(logs).toContain('activated')
})
expect(logs).toContain('activated')
})

test('async', async () => {
const { page } = await launchPage(`async`)
test('async', async () => {
const { page } = await launchPage(`async`)

// should not be ready yet
expect(await page.evaluate(() => els[0].shadowRoot.querySelector('div'))).toBe(null)
expect(await page.evaluate(() => els[1].shadowRoot.querySelector('div'))).toBe(null)
// should not be ready yet
expect(await page.evaluate(() => els[0].shadowRoot.querySelector('div'))).toBe(null)
expect(await page.evaluate(() => els[1].shadowRoot.querySelector('div'))).toBe(null)

// wait until component is resolved
await new Promise(resolve => {
page.on('console', msg => {
if (msg.text() === 'resolved') {
resolve()
}
// wait until component is resolved
await new Promise(resolve => {
page.on('console', msg => {
if (msg.text() === 'resolved') {
resolve()
}
})
})
})

// both instances should be initialized
expect(await page.evaluate(() => els[0].shadowRoot.textContent)).toMatch(`123 bar`)
expect(await page.evaluate(() => els[1].shadowRoot.textContent)).toMatch(`234 baz`)
// both instances should be initialized
expect(await page.evaluate(() => els[0].shadowRoot.textContent)).toMatch(`123 bar`)
expect(await page.evaluate(() => els[1].shadowRoot.textContent)).toMatch(`234 baz`)

// attribute sync should work
await page.evaluate(() => {
els[0].setAttribute('foo', '345')
})
expect(await page.evaluate(() => els[0].shadowRoot.textContent)).toMatch(`345 bar`)
// attribute sync should work
await page.evaluate(() => {
els[0].setAttribute('foo', '345')
})
expect(await page.evaluate(() => els[0].shadowRoot.textContent)).toMatch(`345 bar`)

// new instance should work
await page.evaluate(() => {
const newEl = document.createElement('my-element')
newEl.setAttribute('foo', '456')
document.body.appendChild(newEl)
// new instance should work
await page.evaluate(() => {
const newEl = document.createElement('my-element')
newEl.setAttribute('foo', '456')
document.body.appendChild(newEl)
})
expect(await page.evaluate(() => {
return document.querySelectorAll('my-element')[2].shadowRoot.textContent
})).toMatch(`456 bar`)
})
expect(await page.evaluate(() => {
return document.querySelectorAll('my-element')[2].shadowRoot.textContent
})).toMatch(`456 bar`)
})
4 changes: 3 additions & 1 deletion types/tsconfig.json → tsconfig.json
Original file line number Diff line number Diff line change
@@ -2,12 +2,14 @@
"compilerOptions": {
"strict": true,
"noEmit": true,
"allowJs": true,
"outDir": "dist",
"lib": [
"es2015",
"dom"
]
},
"include": [
"./*.ts"
"src/**/*.ts",
]
}
8 changes: 0 additions & 8 deletions types/index.d.ts

This file was deleted.

759 changes: 560 additions & 199 deletions yarn.lock

Large diffs are not rendered by default.