diff --git a/.eslintrc.js b/.eslintrc.js index 6f13f6d..6f18eb2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -30,17 +30,18 @@ module.exports = { "comma-dangle": ["error", "never"], "no-unused-expressions": ["error", { "allowShortCircuit": true }], "no-param-reassign": ["error", { "props": false }], - 'max-len': ['error', 100, 2, { + 'max-len': ['error', 105, 2, { ignoreUrls: true, ignoreComments: true, ignoreStrings: true, ignoreTemplateLiterals: true, }], - "no-underscore-dangle": ["error", { "allow": ["__vue_custom_element__", "__detached__"] }], + "no-underscore-dangle": ["error", { "allow": ["__vue_custom_element__", "__vue_custom_element_props__", "__detached__"] }], "spaced-comment": ["error", "always", { "exceptions": ["/"] }], // allow debugger during development 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, - "class-methods-use-this": 0 + "class-methods-use-this": 0, + "import/no-extraneous-dependencies": 0 }, 'globals': { 'HTMLElement': true, diff --git a/.gitignore b/.gitignore index 97bbee4..113ab0c 100644 --- a/.gitignore +++ b/.gitignore @@ -25,8 +25,6 @@ coverage docs/esdoc/ docs/docco/ TESTS-* -.yarnrc -yarn.lock test/unit/coverage test/e2e/reports selenium-debug.log diff --git a/README.md b/README.md index 3110cfd..c174c91 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,22 @@ +## [Finally - official Web Components implementation! - check vuejs/vue-web-component-wrapper](https://github.com/vuejs/vue-web-component-wrapper) + ![Vue-custom-element](demo/assets/images/vue-custom-element-logo-text.png) -## Table of content +## Table of contents - [Demo](#demo) - [Installation](#installation) - [Description](#description) - [Example](#example) -- [Browsers support](#browsers-support) +- [ShadowDOM Example](#shadowdom-example) +- [Browsers support](#browser-support) - [Options](#options) - [How does it work?](#how-does-it-work) - [Testing](#testing) - [Caveats](#caveats) ## Demo -You can check Vue-custom-element demos at **https://karol-f.github.io/vue-custom-element/** +You can check out Vue-custom-element demos at **https://karol-f.github.io/vue-custom-element/** ## Installation @@ -28,6 +31,14 @@ import vueCustomElement from 'vue-custom-element' Vue.use(vueCustomElement); ``` +To build your web-component using vue-cli, you have to use the following command: + +```bash +vue-cli-service build --target lib --name your-component-name src/main.js +``` + +Note: the command ```vue-cli-service build --target wc``` does not work, since it using vue's own vue-web-component-wrapper. + #### Direct include If you are using Vue globally, just include `vue-custom-element.js` and it will automatically install the `Vue.customElement` method. @@ -35,7 +46,8 @@ If you are using Vue globally, just include `vue-custom-element.js` and it will ```html ``` -####Optional polyfill + +#### Optional polyfill For cross-browser compatibility (IE9+) use Custom Elements polyfill. ```html @@ -50,36 +62,36 @@ import 'document-register-element/build/document-register-element'; ## Description -`Vue-custom-element` is a tiny wrapper around Vue components. It provide seamless way to use it in HTML, plain JavaScript, Vue, React, Angular etc., using power of Custom Elements. +`Vue-custom-element` is a tiny wrapper around Vue components. It provides a seamless way to use Vue components in HTML, plain JavaScript, Vue, React, Angular etc., without manually initialising Vue. It's using power of Web Components' Custom Elements. * Works with Vue 0.12.x, 1.x and 2.x * Small - 2.7 kb min+gzip, optional polyfill - 5 kb min+gzip ### Why you might need `Vue-custom-element`? ![Vue-custom-element](demo/assets/images/vue-custom-element-why.png) -It might be confusing for users to understand difference between Vue components, Custom Elements and it's use cases. +It might be confusing for users to understand the different use cases of Vue components vs Custom Elements . -Why you might need `Vue-custom-element`? Simply, for your Vue components user's convinience. All they would need to do is include your JavaScript file and then they can: +Why might you need `Vue-custom-element`? Simple, for your Vue components user's convenience. All they will need to do is include your JavaScript file and then they can: -* include HTML tag (e.g. ``) in any time of document lifecycle. You can use your elements in e.g. SPA application just by including HTML tag - no Vue initialization or JavaScript usage is needed. Custom Elements will auto initialize when mounted into document. You can include them in e.g. Vue, Angular or React projects and browser will take care of detecting it and initialization -* use simple API that allows for interacting with underlaying Vue instance by changing attributes, props or listening to events +* include components as HTML tags (e.g. ``) at any time of the document lifecycle. You can use your elements in e.g. SPA application just by including HTML tag - no Vue initialization or JavaScript usage is needed. Custom Elements will auto initialize when mounted into document. You can include them in e.g. Vue, Angular or React projects and browser will take care of detecting it and initialization +* use a simple API that allows for interacting with underlaying Vue instance by changing attributes, props or listening to events * take advantage of features like lazy-loading, that allows for loading components on demand, only when user add them to document ### Features -* **Simplicity** - only `tag-name` and Vue component `object` is needed for `Vue.customElement()` usage -* **Compatibility** - using optional polyfill we can support wide range of browsers, including IE9+, Android and IOS +* **Simplicity** - only `tag-name` and Vue component `object` are needed for `Vue.customElement()` usage +* **Compatibility** - using the optional polyfills a wide range of browsers is supported, including IE9+, Android and IOS * **Full featured** - you can use nesting, HMR, slots, lazy-loading, native Custom Elements callbacks. * reactive props and HTML attributes * automatic props casting (numbers, booleans) so they won't be available as strings but proper data types * listening to Vue component $emit'ed events - * 'default' and 'named' slots are available, check demo for example - * Hot Module Replacement for seamless developer experience (Vue 2.x+) - * lazy-loading - you can download component after it's attached to document. Useful for e.g. UI library authors. Check demo for example - * detecting if detached callback is not invoked due to opening vue-custom-element in modal - element is then detached and attached to DOM again. It would be undesirable to destroy it immediately -* **Custom Elements v1** - compatible with latest specification. Vue-custom-element will use native implementation if supported + * 'default' and 'named' slots are available for passing static content, check out the demo for an example + * Hot Module Replacement for seamless developer experience (unminimized build, Vue 2.x+) + * lazy-loading - you can download a component after it's attached to document. Useful for e.g. UI library authors. Check out the demo for an example + * detect if detached callback is not invoked due to opening vue-custom-element in modal - element is then detached and attached to DOM again. It would be undesirable to destroy it immediately +* **Custom Elements v1** - compatible with latest specifications. Vue-custom-element will use native implementation if supported -Check demos site to see it in action. +Check out the demo site to see it in action. ## Example For additional examples and detailed description check the demos page. @@ -106,22 +118,116 @@ Vue.customElement('widget-vue', { ###### JavaScript - element API usage ``` js -document.querySelector('widget-vue')[0].prop2 // get prop value -document.querySelector('widget-vue')[0].prop2 = 'another string' // set prop value +document.querySelector('widget-vue').prop2 // get prop value +document.querySelector('widget-vue').prop2 = 'another string' // set prop value ``` You can also change `` HTML attributes and changes will be instantly reflected. -## Browsers support +## ShadowDOM Example +By default, `vue-custom-element` does not mount underneath a `ShadowDOM`. While this is easier to develop and debug, CSS stylings for the parent can contaminate the component, and stylings for the component can contaminate the parent. This is most prevalent when using prebuilt UI libraries which assume the component is the only content on the page (i.e. Vuetify). A `ShadowDOM` prevents this contamination by isolating the components and stylings within an HTML document fragment. + +In these situations, components should be mounted underneath a shadowDOM using the option +``` js +Vue.customElement('widget-vue', CustomWidget, { + shadow: true, + beforeCreateVueInstance(root) { + const rootNode = root.el.getRootNode(); + + if (rootNode instanceof ShadowRoot) { + root.shadowRoot = rootNode; + } else { + root.shadowRoot = document.head; + } + return root; + }, +}); +``` + +The additional `beforeCreateVueInstance` is only required if your Vue component has bundled stylings and you are using `css-modules` with Webpack to bundle (which is most use cases). In addition, if you are using `vue-loader` and `vue-style-loader` plugins with Webpack, you will need to pass the `shadowMode: true` option to the plugins also. This is required so the plugins know they that CSS styles should be attached under the `shadowDOM` and not in the `document.head` (which is the default behavior). + +**webpack.config.js example for scss stylings** +``` js +{ + test: /\.vue$/, + use: [ + { + loader: 'vue-loader', + options: { + shadowMode: true + } + } + ] +}, +{ + test: /\.scss$/, //as example I used scss + use: [ + { + loader: 'vue-style-loader', + options: { + shadowMode: true + } + } + ] +} +``` + +**vue.config.js for Vue CLI 3** +``` js +function enableShadowCss(config) { + const configs = [ + config.module.rule('vue').use('vue-loader'), + ]; + // based on common rules returned by `vue inspect` + const ruleSets = ['css', 'postcss', 'scss', 'sass', 'less', 'stylus']; + const ruleNames = ['vue-modules', 'vue', 'normal-modules', 'normal']; + + ruleSets.forEach((ruleSet) => { + if (config.module.rules.store.has(ruleSet)) { + ruleNames.forEach((rName) => { + if (config.module.rule(ruleSet).oneOfs.store.has(rName)) { + if (config.module.rule(ruleSet).oneOf(rName).uses.store.has('vue-style-loader')) { + configs.push(config.module.rule(ruleSet).oneOf(rName).use('vue-style-loader')); + } + } + }); + } + }); + if (!process.env.BUILD_MODE) { + process.env.BUILD_MODE = config.store.get('mode'); + } + configs.forEach((c) => c.tap((options) => { + options.shadowMode = true; + return options; + })); +} + +module.exports = { + chainWebpack: + (config) => { + enableShadowCss(config); + }, + css: { + sourceMap: true, + extract: false, + }, +}; +``` + +_Note: for stylings to work, CSS must be bundled in JS and injected at runtime. Otherwise you will need to manually link the CSS under the shadowDOM inside your component (which is obviously an anti-pattern)._ + +For additional ShadowDom examples see: https://github.com/bryanvaz/vue-custom-element-shadow-examples -| [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | [Chrome for Android](http://godban.github.io/browsers-support-badges/)
Android | -|:---------:|:---------:|:---------:|:---------:|:---------:| -| behind --flag| 54+ | 10.1+ | 42+| 55+ +## Browser support + +| [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | [iOS Safari](http://godban.github.io/browsers-support-badges/)
iOS | [Chrome for Android](http://godban.github.io/browsers-support-badges/)
Android | +|:---------:|:---------:|:---------:|:---------:|:---------:|:---------:| +| 63+ | 54+ | 10.1+ | 42+ | 10.3+ | 55+ [Custom Elements v1 support](http://caniuse.com/#feat=custom-elementsv1) -#### With optional polyfill +#### With optional polyfills | [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | [iOS Safari](http://godban.github.io/browsers-support-badges/)
iOS | [Chrome for Android](http://godban.github.io/browsers-support-badges/)
Android | |:---------:|:---------:|:---------:|:---------:|:---------:|:---------:|:---------:| @@ -154,35 +260,63 @@ Additional, optional, third parameter to `Vue.customElement()` is options object console.info('attributeChangedCallback', name, oldValue, value); }, - // in case of using vue-custom-element with modals, we destroy it after defined timeout + // Root component's definition is passed to this hook just before Vue instance creation - so you can modify it + beforeCreateVueInstance(RootComponentDefinition) { + console.info('beforeCreateVueInstance', RootComponentDefinition); + return RootComponentDefinition; + }, + + // Vue instance is created + vueInstanceCreatedCallback() { + console.info('vueInstanceCreatedCallback'); + }, + + // in case of using vue-custom-element with modals, we destroy it after defined timeout. Use "null" value if you want to manually "$destroy" it. destroyTimeout: 3000, - // only needed when using lazy-loading - when 'props' are not accessible on Custom Element registration + // only needed when using lazy-loading - 'props' are not accessible on Custom Element registration so we have to provide them props: [], // you can set shadow root for element. Only works if native implementation is available. - shadow: false + shadow: false, + + // you can set CSS that will be available in Shadow DOM. + shadowCss: '' } ``` -Callbacks are executed before lifecycle hooks from Vue component passed to Vue-custom-element. It's better idea just to use Vue component lifecycle hooks (e.g. `created`, `mounted`, `beforeDestroy`). +Example options usage: + +```javascript +import MyElement from './MyElement.vue'; + +Vue.customElement('my-element', MyElement, { + shadow: true, + shadowCss: ` + .card { + background-color: blue; + }` +}); +``` + +Callbacks are executed before the lifecycle hooks from Vue component passed to Vue-custom-element. It's a better idea to just use Vue component lifecycle hooks (e.g. `created`, `mounted`, `beforeDestroy`). ## How does it work? ![Vue-custom-element](demo/assets/images/vue-custom-element-schema.png) -Inside HTML tag of defined custom element, Vue-custom-element will create: +Inside HTML tags of the defined custom element, Vue-custom-element will create: * Proxy component for seamless Hot Module Replacement, using render function for performance (Vue 2.x+) -* Vue component passed to Vue-custom-element +* Vue component is passed to Vue-custom-element Custom Element HTML tag will expose API to interact with underlying Vue component - you can change HTML attributes or props, using JavaScript. ## Testing -For advanced access, when exposed API is not enough, defined custom element will expose Vue instance via `__vue_custom_element__` prop. +For advanced access, when exposed API is not enough, defined custom element can expose Vue instance via `getVueInstance()` method. ```javascript -console.info(document.querySelector('widget-vue').__vue_custom_element__) +console.info(document.querySelector('widget-vue').getVueInstance()) ``` ## Caveats @@ -191,7 +325,7 @@ console.info(document.querySelector('widget-vue').__vue_custom_element__) ## Contribute -#### Developement +#### Development ``` npm install npm run dev:demo diff --git a/build/build.rollup.js b/build/build.rollup.js index 8d283a0..2bd4868 100644 --- a/build/build.rollup.js +++ b/build/build.rollup.js @@ -22,10 +22,21 @@ if (!fs.existsSync('dist')) { const resolve = _path => path.resolve(__dirname, '../', _path) build([ + // browser dev { dest: resolve('dist/vue-custom-element.js'), format: 'umd', + env: 'development' + }, + { + dest: resolve('dist/vue-custom-element.min.js'), + format: 'umd', env: 'production' + }, + { + dest: resolve('dist/vue-custom-element.esm.js'), + format: 'es', + env: 'development' } ].map(genConfig)) @@ -46,11 +57,13 @@ function build (builds) { function genConfig (opts) { const config = { - entry: resolve('src/vue-custom-element.js'), - dest: opts.dest, - format: opts.format, - banner, - moduleName: 'VueCustomElement', + input: resolve('src/vue-custom-element.js'), + output: { + file: opts.dest, + name: 'VueCustomElement', + format: opts.format, + banner + }, plugins: [ node(), cjs(), @@ -71,21 +84,18 @@ function genConfig (opts) { } function buildEntry (config) { - const isProd = true; + const isProd = /min\.js$/.test(config.output.file) return rollup.rollup(config).then(bundle => { - const code = bundle.generate(config).code - if (isProd) { - var minified = (config.banner ? config.banner + '\n' : '') + uglify.minify(code, { - fromString: true, - output: { - screw_ie8: true, - ascii_only: true - } - }).code - return write(config.dest, minified, true) - } else { - return write(config.dest, code) - } + bundle.generate(config) + .then(({ code }) => { + if (isProd) { + var minified = (config.output.banner ? config.output.banner + '\n' : '') + uglify.minify(code, {}).code + return write(config.output.file, minified, true) + } else { + return write(config.output.file, code) + } + }) + .catch((e) => console.log(e)); }) } diff --git a/demo/Demos.vue b/demo/Demos.vue index 0d1bd4e..f93ceb5 100644 --- a/demo/Demos.vue +++ b/demo/Demos.vue @@ -12,12 +12,26 @@ .demos .el-collapse { margin: 2em 0; } + .demos .el-collapse-item__header, + .demos .el-collapse-item.is-active .el-collapse-item__header { + padding-left: 20px; + padding-right: 20px; + border-bottom: 1px solid #dfe6ec; + } + + .demos .el-collapse-item__wrap { + background-color: #fbfdff; + } .demos .el-collapse-item__content { font-size: 1.2em; padding: 10px 36px; } + .demos .el-button--mini, .el-button--mini.is-round { + padding: 5px 10px; + } + .demos pre[class*="language-"] { padding: 0; } @@ -40,6 +54,11 @@ width: 100%; } + .demos .el-alert--info { + background-color: #50bfff; + color: #fff; + } + .demo-card { border: 1px solid #e0e6ed; border-radius: 2px; @@ -49,6 +68,12 @@ font-size: 1.4em; } + @media screen and (max-width: 900px) { + .demo-card { + padding: 10px 10px; + } + } + .demo-card .demo-button { float: right } @@ -61,6 +86,12 @@ display: block; transition: opacity .2s; } + @media screen and (max-width: 900px) { + .card { + padding: 10px 10px; + } + } + .card h1, .card h2, .card h3, diff --git a/demo/components/DemoBasic-component.vue b/demo/components/DemoBasic-component.vue index 59fabd7..ad281be 100644 --- a/demo/components/DemoBasic-component.vue +++ b/demo/components/DemoBasic-component.vue @@ -12,7 +12,7 @@ -