├── .editorconfig ├── .gitattributes ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── babel.config.js ├── circle.yml ├── example ├── index.js └── poi.config.js ├── package.json ├── src ├── index.js └── mustUseDomProp.js ├── test └── index.test.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | /dist 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) EGOIST <0x142857@gmail.com> (github.com/egoist) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-html 2 | 3 | [![NPM version](https://img.shields.io/npm/v/vue-html.svg?style=flat-square)](https://npmjs.com/package/vue-html) [![NPM downloads](https://img.shields.io/npm/dm/vue-html.svg?style=flat-square)](https://npmjs.com/package/vue-html) [![Build Status](https://img.shields.io/circleci/project/egoist/vue-html/master.svg?style=flat-square)](https://circleci.com/gh/egoist/vue-html) 4 | 5 | > Use tagged template string in Vue.js render function 6 | 7 | ## Why is this useful? 8 | 9 | If you want to use Vue without a bundler / transpiler, this library will (reasonably) make your app smaller: 10 | 11 | - Vue (runtime + template compiler): 32kB gzipped 12 | - Vue (runtime + vue-html): 23kB gzipped 13 | 14 | **What's the downside?** No handy sugars like `v-model` support. 15 | 16 | ## Install 17 | 18 | ```bash 19 | $ npm install --save vue-html 20 | ``` 21 | 22 | CDN versions: 23 | 24 | - `UMD`: https://unpkg.com/vue-html/dist/html.js (exposed as `window.HTML`) 25 | - `ESM`: https://unpkg.com/vue-html/dist/html.es.js 26 | 27 | ## Usage 28 | 29 | [![Edit vue-html-example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/50qxwm44mx) 30 | 31 | ```js 32 | import Vue from 'vue' 33 | import HTML from 'vue-html' 34 | 35 | Vue.use(HTML) 36 | 37 | const Todos = { 38 | props: ['todos'], 39 | render(html) { 40 | return html` 41 | 50 | ` 51 | } 52 | } 53 | 54 | new Vue({ 55 | el: '#app', 56 | data: { 57 | todos: ['Conquer the world', 'Rewrite Peco'], 58 | todo: '' 59 | }, 60 | methods: { 61 | add() { 62 | this.todos.push(this.todo) 63 | this.todo = '' 64 | } 65 | }, 66 | render(html) { 67 | return html` 68 |
69 | (this.todo = e.target.value)} 72 | /> 73 | 74 |
75 | <${Todos} todos=${this.todos} /> 76 |
77 | ` 78 | } 79 | }) 80 | ``` 81 | 82 | The usage is very similar to Vue JSX except that the `html` function is powered by [HTM (Hyperscript Tagged Markup)](https://github.com/developit/htm). 83 | 84 | ### Using Components 85 | 86 | ```js 87 | const App = { 88 | render(html) { 89 | return html` 90 |
91 | <${Todos} /> 92 | <${Todos}> or with children 93 |
94 | ` 95 | } 96 | } 97 | ``` 98 | 99 | You can also use the traditional way of using local / global components: 100 | 101 | ```js 102 | const App = { 103 | render(html) { 104 | return html` 105 |
106 | ` 107 | }, 108 | components: { 109 | Todos 110 | } 111 | } 112 | ``` 113 | 114 | ## Contributing 115 | 116 | 1. Fork it! 117 | 2. Create your feature branch: `git checkout -b my-new-feature` 118 | 3. Commit your changes: `git commit -am 'Add some feature'` 119 | 4. Push to the branch: `git push origin my-new-feature` 120 | 5. Submit a pull request :D 121 | 122 | ## License 123 | 124 | [MIT](https://egoist.mit-license.org/) © [EGOIST](https://github.com/egoist) 125 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['poi/babel', 'power-assert'] 3 | } 4 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/repo 5 | docker: 6 | - image: circleci/node:latest-browsers 7 | branches: 8 | ignore: 9 | - gh-pages # list of branches to ignore 10 | - /release\/.*/ # or ignore regexes 11 | steps: 12 | - checkout 13 | - restore_cache: 14 | key: dependency-cache-{{ checksum "yarn.lock" }} 15 | - run: 16 | name: install dependences 17 | command: yarn 18 | - save_cache: 19 | key: dependency-cache-{{ checksum "yarn.lock" }} 20 | paths: 21 | - ./node_modules 22 | - run: 23 | name: test 24 | command: yarn test 25 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import HTML from 'vue-html' // eslint-disable-line import/no-unresolved 3 | 4 | Vue.use(HTML) 5 | 6 | const Todos = { 7 | props: ['todos'], 8 | render(html) { 9 | return html` 10 | 19 | ` 20 | } 21 | } 22 | 23 | new Vue({ 24 | el: '#app', 25 | data: { 26 | todos: ['Conquer the world', 'Rewrite Peco'], 27 | todo: '' 28 | }, 29 | methods: { 30 | add() { 31 | this.todos.push(this.todo) 32 | this.todo = '' 33 | } 34 | }, 35 | render(html) { 36 | return html` 37 |
38 | { 42 | this.todo = e.target.value 43 | } 44 | } 45 | /> 46 | 47 |
48 | <${Todos} todos=${this.todos} /> 49 |
50 | ` 51 | } 52 | }) 53 | -------------------------------------------------------------------------------- /example/poi.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | exports.entry = 'example/index' 4 | 5 | exports.configureWebpack = { 6 | resolve: { 7 | alias: { 8 | 'vue-html$': path.join(__dirname, '../src') 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-html", 3 | "version": "1.0.0", 4 | "description": "Use tagged template string in Vue.js render function", 5 | "license": "MIT", 6 | "repository": "egoist/vue-html", 7 | "author": { 8 | "name": "EGOIST", 9 | "email": "0x142857@gmail.com", 10 | "url": "http://github.com/egoist" 11 | }, 12 | "scripts": { 13 | "test": "npm run lint && npm run test:unit", 14 | "test:unit": "poi puppet --test --plugin @poi/puppet --framework mocha", 15 | "lint": "xo", 16 | "build": "bili --format umd,cjs,es,es-min,umd-min --js buble --module-name HTML --inline --name html", 17 | "example": "poi -so --config example/poi.config.js", 18 | "prepublishOnly": "npm run build" 19 | }, 20 | "files": [ 21 | "dist" 22 | ], 23 | "xo": { 24 | "extends": [ 25 | "rem", 26 | "plugin:prettier/recommended" 27 | ], 28 | "envs": [ 29 | "browser", 30 | "mocha" 31 | ], 32 | "rules": { 33 | "unicorn/filename-case": "off", 34 | "no-new": "off" 35 | } 36 | }, 37 | "main": "dist/html.js", 38 | "module": "dist/html.es.js", 39 | "keywords": [ 40 | "vue", 41 | "htm", 42 | "html" 43 | ], 44 | "devDependencies": { 45 | "@poi/plugin-puppet": "^0.1.3", 46 | "babel-preset-power-assert": "^3.0.0", 47 | "bili": "^3.4.2", 48 | "eslint-config-prettier": "^3.3.0", 49 | "eslint-config-rem": "^4.0.0", 50 | "eslint-plugin-prettier": "^3.0.0", 51 | "htm": "^2.0.0", 52 | "poi": "^12.2.4", 53 | "power-assert": "^1.6.1", 54 | "prettier": "^1.15.3", 55 | "vue": "^2.5.21", 56 | "xo": "^0.23.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import htm from 'htm' 2 | import mustUseDomProp from './mustUseDomProp' 3 | 4 | export default Vue => { 5 | Vue.mixin({ 6 | beforeCreate() { 7 | const createElement = this.$createElement.bind(this) 8 | const h = (tag, attrs, ...children) => { 9 | return createElement(tag, attrs && getVNodeData(tag, attrs), children) 10 | } 11 | this.$$createElement = createElement 12 | this.$createElement = htm.bind(h) 13 | } 14 | }) 15 | } 16 | 17 | function getVNodeData(tag, attrs) { 18 | const data = {} 19 | 20 | const basics = ['slot', 'key', 'ref', 'refInFor', 'class', 'style'] 21 | 22 | for (const key of Object.keys(attrs)) { 23 | if (key.substring(0, 2) === 'on') { 24 | // OnClick => on: {click} 25 | data.on = data.on || {} 26 | const newKey = lowerCaseFirstLetter(key.substring(2)) 27 | data.on[newKey] = attrs[key] 28 | } else if (key.substring(0, 8) === 'nativeOn') { 29 | // NativeOnClick => nativeOn: {click} 30 | data.nativeOn = data.nativeOn || {} 31 | const newKey = lowerCaseFirstLetter(key.substring(8)) 32 | data.nativeOn[newKey] = attrs[key] 33 | } else if (key.substring(0, 8) === 'domProps') { 34 | // DomPropsInnerHTML => domProps: {innerHTML} 35 | data.domProps = data.domProps || {} 36 | const newKey = lowerCaseFirstLetter(key.substring(8)) 37 | data.domProps[newKey] = attrs[key] 38 | } else if (key.substring(0, 2) === 'v-') { 39 | data.directives = data.directives || [] 40 | const name = key.substring(2) 41 | data.directives.push({ 42 | name, 43 | value: attrs[key] 44 | }) 45 | } else if (mustUseDomProp(tag, attrs.type, key)) { 46 | data.domProps = data.domProps || {} 47 | data.domProps[key] = attrs[key] 48 | } else if (basics.indexOf(key) > -1) { 49 | data[key] = attrs[key] 50 | } else { 51 | // All others props => {attrs: props} 52 | data.attrs = data.attrs || {} 53 | data.attrs[key] = attrs[key] 54 | } 55 | } 56 | 57 | return data 58 | } 59 | 60 | function lowerCaseFirstLetter(string) { 61 | return string.charAt(0).toLowerCase() + string.substring(1) 62 | } 63 | -------------------------------------------------------------------------------- /src/mustUseDomProp.js: -------------------------------------------------------------------------------- 1 | const acceptValue = ['input', 'textarea', 'option', 'select'] 2 | 3 | export default (tag, type, attr) => { 4 | return ( 5 | (attr === 'value' && acceptValue.includes(tag) && type !== 'button') || 6 | (attr === 'selected' && tag === 'option') || 7 | (attr === 'checked' && tag === 'input') || 8 | (attr === 'muted' && tag === 'video') 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import Vue from 'vue' 3 | import HTML from '../src' 4 | 5 | Vue.use(HTML) 6 | 7 | describe('main', () => { 8 | it('works', () => { 9 | const vm = new Vue({ 10 | render(html) { 11 | return html` 12 |
hello
13 | ` 14 | } 15 | }).$mount() 16 | assert(vm.$el.textContent === 'hello') 17 | }) 18 | 19 | it('transform vue-specific attributes', () => { 20 | const vm = new Vue({ 21 | data: { count: 0 }, 22 | methods: { 23 | handleClick() { 24 | this.count++ 25 | } 26 | }, 27 | render(html) { 28 | return html` 29 |
30 | 31 |
32 | ` 33 | } 34 | }).$mount() 35 | vm.$el.dispatchEvent(new Event('click')) 36 | vm._watcher.run() 37 | assert(vm.count === 1) 38 | 39 | const hi = vm.$el.querySelector('.hi') 40 | assert(hi.textContent === 'hi') 41 | 42 | const foo = vm.$el.querySelector('#foo') 43 | assert(foo.textContent === 'hi') 44 | }) 45 | }) 46 | --------------------------------------------------------------------------------