Przeglądaj źródła

[ui] Vue 3 - Web Component Wrapper - WIP - Loaded Vue component as web component using a Hue port of @vuejs/vue-web-component-wrapper

sreenaths 4 lat temu
rodzic
commit
9979f3842e

+ 2 - 2
desktop/core/src/desktop/js/vue/webComponentWrap.ts

@@ -1,4 +1,4 @@
-import { ComponentOptions, Component } from 'vue';
+import { ComponentOptions, Component, ComponentInternalInstance } from 'vue';
 import wrapper from './wrapper/index';
 import wrapper from './wrapper/index';
 
 
 export interface HueComponentOptions<T extends Component> extends ComponentOptions<T> {
 export interface HueComponentOptions<T extends Component> extends ComponentOptions<T> {
@@ -6,7 +6,7 @@ export interface HueComponentOptions<T extends Component> extends ComponentOptio
 }
 }
 
 
 const isRegistered = function(tag: string): boolean {
 const isRegistered = function(tag: string): boolean {
-  return document.createElement(tag).constructor !== HTMLElement;
+  return window.customElements.get(tag) !== undefined;
 }
 }
 
 
 export const wrap = <T extends Component>(
 export const wrap = <T extends Component>(

+ 197 - 0
desktop/core/src/desktop/js/vue/wrapper/index.ts

@@ -0,0 +1,197 @@
+import { Component, h, createApp, App, ComponentPublicInstance } from 'vue';
+
+import {
+  toVNodes,
+  camelize,
+  hyphenate,
+  callHooks,
+  injectHook,
+  getInitialProps,
+  createCustomEvent,
+  convertAttributeValue
+} from './utils'
+
+type ComponentInternal  = Partial<Component> &  {
+  props: { [key: string]: any }
+}
+
+type KeyHash = { [key: string]: any };
+
+export default function wrap (comp: Component) {
+
+  const component: ComponentInternal = <ComponentInternal><unknown>comp;
+
+  let isInitialized = false;
+
+  let hyphenatedPropsList: string[];
+  let camelizedPropsList: string [];
+  let camelizedPropsMap: KeyHash;
+
+  function initialize (component: ComponentInternal) {
+    if (isInitialized) return;
+
+    // extract props info
+    const propsList: string[] = Array.isArray(component.props)
+      ? component.props
+      : Object.keys(component.props || {})
+    hyphenatedPropsList = propsList.map(hyphenate)
+    camelizedPropsList = propsList.map(camelize)
+
+    const originalPropsAsObject = Array.isArray(component.props) ? {} : component.props || {}
+    camelizedPropsMap = camelizedPropsList.reduce((map: KeyHash, key, i) => {
+      map[key] = originalPropsAsObject[propsList[i]]
+      return map
+    }, {})
+
+    // In Vue3 beforeCreate and created are replaced by the setup method
+
+    // // proxy $emit to native DOM events
+    // injectHook(component, 'setup', function () {
+    //   const emit = this.$emit
+    //   this.$emit = (name, ...args) => {
+    //     this.$root.$options.customElement.dispatchEvent(createCustomEvent(name, args))
+    //     return emit.call(this, name, ...args)
+    //   }
+    // })
+
+    // injectHook(component, 'created', function () {
+    //   // sync default props values to wrapper on created
+    //   camelizedPropsList.forEach(key => {
+    //     this.$root.props[key] = this[key]
+    //   })
+    // })
+
+    // proxy props as Element properties
+    camelizedPropsList.forEach(key => {
+      Object.defineProperty(CustomElement.prototype, key, {
+        get () {
+          return this._wrapper.props[key]
+        },
+        set (newVal) {
+          this._wrapper.props[key] = newVal
+        },
+        enumerable: false,
+        configurable: true
+      })
+    })
+
+    isInitialized = true
+  }
+
+  // function syncAttribute (el: CustomElement, key: string): void {
+  //   const camelized = camelize(key)
+  //   const value = el.hasAttribute(key) ? el.getAttribute(key) : undefined
+  //   el._wrapper.props[camelized] = convertAttributeValue(
+  //     value,
+  //     key,
+  //     camelizedPropsMap[camelized]
+  //   )
+  // }
+
+  class CustomElement extends HTMLElement {
+    _wrapper: App;
+    _component?: ComponentPublicInstance;
+
+    constructor () {
+      const self = super()
+
+      //this.attachShadow({ mode: 'open' })
+
+      const wrapper: App = this._wrapper = createApp({
+        name: 'shadow-root',
+        data () {
+          return {
+            props: {},
+            slotChildren: []
+          }
+        },
+        render () {
+          return h(component, {
+            // ref: 'inner',
+            props: this.props
+          }, this.slotChildren)
+        }
+      })
+
+      // // Use MutationObserver to react to future attribute & slot content change
+      // const observer = new MutationObserver(mutations => {
+      //   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)
+      //     } else {
+      //       hasChildrenChange = true
+      //     }
+      //   }
+
+      //   if (hasChildrenChange) {
+      //     // TODO: Make this working
+      //     // wrapper.slotChildren = Object.freeze(toVNodes(
+      //     //   h,
+      //     //   this.childNodes
+      //     // ))
+      //   }
+      // })
+
+      // observer.observe(this, {
+      //   childList: true,
+      //   subtree: true,
+      //   characterData: true,
+      //   attributes: true
+      // })
+    }
+
+    get vueComponent (): ComponentPublicInstance | undefined {
+        return this._component;
+    }
+
+    connectedCallback () {
+      this._component = this._wrapper.mount(this);
+      callHooks(this._component, 'activated');
+    }
+
+    // connectedCallback () {
+    //   const wrapper = this._wrapper
+    //   if (!wrapper._isMounted) {
+    //     // initialize attributes
+    //     const syncInitialAttributes = () => {
+    //       wrapper.props = getInitialProps(camelizedPropsList)
+    //       hyphenatedPropsList.forEach(key => {
+    //         syncAttribute(this, key)
+    //       })
+    //     }
+
+    //     if (isInitialized) {
+    //       syncInitialAttributes()
+    //     } else {
+    //       // async & unresolved
+    //       component().then(resolved => {
+    //         if (resolved.__esModule || resolved[Symbol.toStringTag] === 'Module') {
+    //           resolved = resolved.default
+    //         }
+    //         initialize(resolved)
+    //         syncInitialAttributes()
+    //       })
+    //     }
+
+    //     // initialize children
+    //     wrapper.slotChildren = Object.freeze(toVNodes(
+    //       h,
+    //       this.childNodes
+    //     ))
+    //     //wrapper.mount(this.shadowRoot)
+    //   } else {
+    //     callHooks(this.vueComponent, 'activated')
+    //   }
+    // }
+
+    disconnectedCallback () {
+      callHooks(this.vueComponent, 'deactivated')
+    }
+  }
+
+  initialize(component)
+
+  return CustomElement
+}

+ 98 - 0
desktop/core/src/desktop/js/vue/wrapper/utils.ts

@@ -0,0 +1,98 @@
+import { Component, ComponentPublicInstance } from "vue"
+
+const camelizeRE = /-(\w)/g
+export const camelize = (str: string): string => {
+  return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
+}
+
+const hyphenateRE = /\B([A-Z])/g
+export const hyphenate = (str: string): string => {
+  return str.replace(hyphenateRE, '-$1').toLowerCase()
+}
+
+export function getInitialProps (propsList: string[]) {
+  const res: { [key: string]: any } = {}
+  propsList.forEach(key => {
+    res[key] = undefined
+  })
+  return res
+}
+
+export function injectHook (options: { [key: string]: any }, key: string, hook: Function) {
+  options[key] = [].concat(options[key] || [])
+  options[key].unshift(hook)
+}
+
+export function callHooks (vm: ComponentPublicInstance | undefined, hook: string) {
+  if (vm) {
+    const hooks = vm.$options[hook] || [];
+    hooks.forEach((hook: Function) => {
+      hook.call(vm)
+    });
+  }
+}
+
+export function createCustomEvent (name: string, args: any) {
+  return new CustomEvent(name, {
+    bubbles: false,
+    cancelable: false,
+    detail: args
+  })
+}
+
+const isBoolean = val => /function Boolean/.test(String(val))
+const isNumber = val => /function Number/.test(String(val))
+
+export function convertAttributeValue (value, name, { type } = {}) {
+  if (isBoolean(type)) {
+    if (value === 'true' || value === 'false') {
+      return value === 'true'
+    }
+    if (value === '' || value === name) {
+      return true
+    }
+    return value != null
+  } else if (isNumber(type)) {
+    const parsed = parseFloat(value, 10)
+    return isNaN(parsed) ? value : parsed
+  } else {
+    return value
+  }
+}
+
+export function toVNodes (h, children) {
+  const res = []
+  for (let i = 0, l = children.length; i < l; i++) {
+    res.push(toVNode(h, children[i]))
+  }
+  return res
+}
+
+function toVNode (h, node) {
+  if (node.nodeType === 3) {
+    return node.data.trim() ? node.data : null
+  } else if (node.nodeType === 1) {
+    const data = {
+      attrs: getAttributes(node),
+      domProps: {
+        innerHTML: node.innerHTML
+      }
+    }
+    if (data.attrs.slot) {
+      data.slot = data.attrs.slot
+      delete data.attrs.slot
+    }
+    return h(node.tagName, data)
+  } else {
+    return null
+  }
+}
+
+function getAttributes (node) {
+  const res = {}
+  for (let i = 0, l = node.attributes.length; i < l; i++) {
+    const attr = node.attributes[i]
+    res[attr.nodeName] = attr.nodeValue
+  }
+  return res
+}

+ 46 - 16
package-lock.json

@@ -20508,19 +20508,25 @@
       "dev": true
       "dev": true
     },
     },
     "style-loader": {
     "style-loader": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.1.3.tgz",
-      "integrity": "sha512-rlkH7X/22yuwFYK357fMN/BxYOorfnfq0eD7+vqlemSK4wEcejFF1dg4zxP0euBW8NrYx2WZzZ8PPFevr7D+Kw==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz",
+      "integrity": "sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==",
       "dev": true,
       "dev": true,
       "requires": {
       "requires": {
-        "loader-utils": "^1.2.3",
-        "schema-utils": "^2.6.4"
+        "loader-utils": "^2.0.0",
+        "schema-utils": "^3.0.0"
       },
       },
       "dependencies": {
       "dependencies": {
+        "@types/json-schema": {
+          "version": "7.0.7",
+          "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
+          "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
+          "dev": true
+        },
         "ajv": {
         "ajv": {
-          "version": "6.12.0",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
-          "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
+          "version": "6.12.6",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+          "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
           "dev": true,
           "dev": true,
           "requires": {
           "requires": {
             "fast-deep-equal": "^3.1.1",
             "fast-deep-equal": "^3.1.1",
@@ -20529,20 +20535,44 @@
             "uri-js": "^4.2.2"
             "uri-js": "^4.2.2"
           }
           }
         },
         },
+        "ajv-keywords": {
+          "version": "3.5.2",
+          "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+          "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
+          "dev": true
+        },
+        "emojis-list": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
+          "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
+          "dev": true
+        },
         "fast-deep-equal": {
         "fast-deep-equal": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
-          "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
+          "version": "3.1.3",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+          "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
           "dev": true
           "dev": true
         },
         },
+        "loader-utils": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
+          "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
+          "dev": true,
+          "requires": {
+            "big.js": "^5.2.2",
+            "emojis-list": "^3.0.0",
+            "json5": "^2.1.2"
+          }
+        },
         "schema-utils": {
         "schema-utils": {
-          "version": "2.6.5",
-          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.5.tgz",
-          "integrity": "sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ==",
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
+          "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==",
           "dev": true,
           "dev": true,
           "requires": {
           "requires": {
-            "ajv": "^6.12.0",
-            "ajv-keywords": "^3.4.1"
+            "@types/json-schema": "^7.0.6",
+            "ajv": "^6.12.5",
+            "ajv-keywords": "^3.5.2"
           }
           }
         }
         }
       }
       }

+ 1 - 1
package.json

@@ -135,7 +135,7 @@
     "prettier": "2.0.5",
     "prettier": "2.0.5",
     "sass-loader": "8.0.2",
     "sass-loader": "8.0.2",
     "source-map-loader": "1.0.0",
     "source-map-loader": "1.0.0",
-    "style-loader": "^1.1.3",
+    "style-loader": "^2.0.0",
     "stylelint": "13.6.0",
     "stylelint": "13.6.0",
     "stylelint-config-standard": "20.0.0",
     "stylelint-config-standard": "20.0.0",
     "stylelint-scss": "3.18.0",
     "stylelint-scss": "3.18.0",