vue-wc-wrapper.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. const camelizeRE = /-(\w)/g;
  2. const camelize = str => {
  3. return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
  4. };
  5. const hyphenateRE = /\B([A-Z])/g;
  6. const hyphenate = str => {
  7. return str.replace(hyphenateRE, '-$1').toLowerCase()
  8. };
  9. function getInitialProps (propsList) {
  10. const res = {};
  11. propsList.forEach(key => {
  12. res[key] = undefined;
  13. });
  14. return res
  15. }
  16. function injectHook (options, key, hook) {
  17. options[key] = [].concat(options[key] || []);
  18. options[key].unshift(hook);
  19. }
  20. function callHooks (vm, hook) {
  21. if (vm) {
  22. const hooks = vm.$options[hook] || [];
  23. hooks.forEach(hook => {
  24. hook.call(vm);
  25. });
  26. }
  27. }
  28. function createCustomEvent (name, args) {
  29. return new CustomEvent(name, {
  30. bubbles: false,
  31. cancelable: false,
  32. detail: args
  33. })
  34. }
  35. const isBoolean = val => /function Boolean/.test(String(val));
  36. const isNumber = val => /function Number/.test(String(val));
  37. function convertAttributeValue (value, name, { type } = {}) {
  38. if (isBoolean(type)) {
  39. if (value === 'true' || value === 'false') {
  40. return value === 'true'
  41. }
  42. if (value === '' || value === name || value != null) {
  43. return true
  44. }
  45. return value
  46. } else if (isNumber(type)) {
  47. const parsed = parseFloat(value, 10);
  48. return isNaN(parsed) ? value : parsed
  49. } else {
  50. return value
  51. }
  52. }
  53. function toVNodes (h, children) {
  54. const res = [];
  55. for (let i = 0, l = children.length; i < l; i++) {
  56. res.push(toVNode(h, children[i]));
  57. }
  58. return res
  59. }
  60. function toVNode (h, node) {
  61. if (node.nodeType === 3) {
  62. return node.data.trim() ? node.data : null
  63. } else if (node.nodeType === 1) {
  64. const data = {
  65. attrs: getAttributes(node),
  66. domProps: {
  67. innerHTML: node.innerHTML
  68. }
  69. };
  70. if (data.attrs.slot) {
  71. data.slot = data.attrs.slot;
  72. delete data.attrs.slot;
  73. }
  74. return h(node.tagName, data)
  75. } else {
  76. return null
  77. }
  78. }
  79. function getAttributes (node) {
  80. const res = {};
  81. for (let i = 0, l = node.attributes.length; i < l; i++) {
  82. const attr = node.attributes[i];
  83. res[attr.nodeName] = attr.nodeValue;
  84. }
  85. return res
  86. }
  87. function wrap (Vue, Component) {
  88. const isAsync = typeof Component === 'function' && !Component.cid;
  89. let isInitialized = false;
  90. let hyphenatedPropsList;
  91. let camelizedPropsList;
  92. let camelizedPropsMap;
  93. function initialize (Component) {
  94. if (isInitialized) return
  95. const options = typeof Component === 'function'
  96. ? Component.options
  97. : Component;
  98. // extract props info
  99. const propsList = Array.isArray(options.props)
  100. ? options.props
  101. : Object.keys(options.props || {});
  102. hyphenatedPropsList = propsList.map(hyphenate);
  103. camelizedPropsList = propsList.map(camelize);
  104. const originalPropsAsObject = Array.isArray(options.props) ? {} : options.props || {};
  105. camelizedPropsMap = camelizedPropsList.reduce((map, key, i) => {
  106. map[key] = originalPropsAsObject[propsList[i]];
  107. return map
  108. }, {});
  109. // proxy $emit to native DOM events
  110. injectHook(options, 'beforeCreate', function () {
  111. const emit = this.$emit;
  112. this.$emit = (name, ...args) => {
  113. this.$root.$options.customElement.dispatchEvent(createCustomEvent(name, args));
  114. return emit.call(this, name, ...args)
  115. };
  116. });
  117. injectHook(options, 'created', function () {
  118. // sync default props values to wrapper on created
  119. camelizedPropsList.forEach(key => {
  120. this.$root.props[key] = this[key];
  121. });
  122. });
  123. // proxy props as Element properties
  124. camelizedPropsList.forEach(key => {
  125. Object.defineProperty(CustomElement.prototype, key, {
  126. get () {
  127. return this._wrapper.props[key]
  128. },
  129. set (newVal) {
  130. this._wrapper.props[key] = newVal;
  131. },
  132. enumerable: false,
  133. configurable: true
  134. });
  135. });
  136. isInitialized = true;
  137. }
  138. function syncAttribute (el, key) {
  139. const camelized = camelize(key);
  140. const value = el.hasAttribute(key) ? el.getAttribute(key) : undefined;
  141. el._wrapper.props[camelized] = convertAttributeValue(
  142. value,
  143. key,
  144. camelizedPropsMap[camelized]
  145. );
  146. }
  147. class CustomElement extends HTMLElement {
  148. constructor () {
  149. const self = super();
  150. self.attachShadow({ mode: 'open' });
  151. const wrapper = self._wrapper = new Vue({
  152. name: 'shadow-root',
  153. customElement: self,
  154. shadowRoot: self.shadowRoot,
  155. data () {
  156. return {
  157. props: {},
  158. slotChildren: []
  159. }
  160. },
  161. render (h) {
  162. return h(Component, {
  163. ref: 'inner',
  164. props: this.props
  165. }, this.slotChildren)
  166. }
  167. });
  168. // Use MutationObserver to react to future attribute & slot content change
  169. const observer = new MutationObserver(mutations => {
  170. let hasChildrenChange = false;
  171. for (let i = 0; i < mutations.length; i++) {
  172. const m = mutations[i];
  173. if (isInitialized && m.type === 'attributes' && m.target === self) {
  174. syncAttribute(self, m.attributeName);
  175. } else {
  176. hasChildrenChange = true;
  177. }
  178. }
  179. if (hasChildrenChange) {
  180. wrapper.slotChildren = Object.freeze(toVNodes(
  181. wrapper.$createElement,
  182. self.childNodes
  183. ));
  184. }
  185. });
  186. observer.observe(self, {
  187. childList: true,
  188. subtree: true,
  189. characterData: true,
  190. attributes: true
  191. });
  192. }
  193. get vueComponent () {
  194. return this._wrapper.$refs.inner
  195. }
  196. connectedCallback () {
  197. const wrapper = this._wrapper;
  198. if (!wrapper._isMounted) {
  199. // initialize attributes
  200. const syncInitialAttributes = () => {
  201. wrapper.props = getInitialProps(camelizedPropsList);
  202. hyphenatedPropsList.forEach(key => {
  203. syncAttribute(this, key);
  204. });
  205. };
  206. if (isInitialized) {
  207. syncInitialAttributes();
  208. } else {
  209. // async & unresolved
  210. Component().then(resolved => {
  211. if (resolved.__esModule || resolved[Symbol.toStringTag] === 'Module') {
  212. resolved = resolved.default;
  213. }
  214. initialize(resolved);
  215. syncInitialAttributes();
  216. });
  217. }
  218. // initialize children
  219. wrapper.slotChildren = Object.freeze(toVNodes(
  220. wrapper.$createElement,
  221. this.childNodes
  222. ));
  223. wrapper.$mount();
  224. this.shadowRoot.appendChild(wrapper.$el);
  225. } else {
  226. callHooks(this.vueComponent, 'activated');
  227. }
  228. }
  229. disconnectedCallback () {
  230. callHooks(this.vueComponent, 'deactivated');
  231. }
  232. }
  233. if (!isAsync) {
  234. initialize(Component);
  235. }
  236. return CustomElement
  237. }
  238. export default wrap;