Wrap third-party UI components as Vue components, using Frappe Gantt as example Want to use Gantt chart component in Vue, reference vue-echart implementation

Conversion Steps

  1. In on Mount hook, initialize, and convert callbacks to event emit
  2. Implement v-model two-way binding
    1. Initialize, then emit events in js library callbacks
    2. Pass all changes from js library to vue component (modify passed props in callback event emit function)
    3. Changes from external vue component pass to js library (use watch to implement, when vue component state changes, synchronously modify js library ui)
    4. Finally handle all changes to internal content of slots (option lists). (use updated lifecycle hook)

What are Web Components

https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements Need to wrap as a web component

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Custom Element Demo</title>
</head>

<body>
    <my-custom-element color="blue" size="20px">Click me!</my-custom-element>
    <script>
        class MyCustomElement extends HTMLElement {
            static observedAttributes = ["color", "size"];
            constructor() {
                super();
                this.attachShadow({ mode: "open" }); // Use Shadow DOM
                this.shadowRoot.innerHTML = `
                    <style>
                        :host {
                            display: inline-block;
                            padding: 10px;
                            border: 2px solid black;
                            cursor: pointer;
                        }
                    </style>
                    <slot></slot>
                `;
            }

            connectedCallback() {
                console.log("Custom element added to page.");
                this.updateStyle(); // Initialize style
                this.addEventListener("click", this.handleClick);
            }

            disconnectedCallback() {
                console.log("Custom element removed from page.");
                this.removeEventListener("click", this.handleClick);
            }

            attributeChangedCallback(name, oldValue, newValue) {
                console.log(`Attribute ${name} changed: from "${oldValue}" to "${newValue}"`);
                this.updateStyle(); // Update style when attribute changes
            }

            updateStyle() {
                const color = this.getAttribute("color") || "black";
                const size = this.getAttribute("size") || "16px";
                this.style.color = color;
                this.style.fontSize = size;
            }

            handleClick() {
                alert("Custom element clicked!");
            }
        }

        customElements.define("my-custom-element", MyCustomElement);

        // Dynamically change attributes
        setTimeout(() => {
            const customElement = document.querySelector("my-custom-element");
            customElement.setAttribute("color", "red");
            customElement.setAttribute("size", "24px");
        }, 2000);
    </script>
</body>
</html>

Problems Encountered

  1. Third-party component gantt internally updates tasks when dragging, causing watcheffect to respond in loop. Loop, causing infinite updates
1. After external tasks change, need to synchronously update js component library ui
2. And can modify tasks through js component library ui. Also need to synchronously modify to external vue tasks

Divided into two situations:

  • If js component only reads data, can use

export default defineComponent({
  props: {
    tasks: Array as PropType<Tasks>,
    options: Object,
  },
  emits: ["date-change", "update:modelValue"],
  setup(props, context) {
    const root = shallowRef<GanttElement>();
    const gantt = shallowRef<Gantt>();

    function init() {
      if (!root.value) {
        console.error("root is ", root.value);
        return;
      }

      const instance = (gantt.value = new Gantt(root.value, props.tasks, {
        ...props.options,
        on_date_change: (task) => {
          // Can get latest task data here
          context.emit("date-change", task);
          // Modify props.tasks, return through event
          // context.emit("update:modelValue",)
        },
      }));

      if (instance) {
        console.log("Gantt chart initialized successfully.");
      } else {
        console.error("Failed to initialize Gantt chart.");
      }
    }

    onMounted(() => {
      init();
    });

    watchEffect(()=> {
      if (props.tasks && gantt.value){
        console.log("tasks update")
        gantt.value.refresh(props.tasks);
      }
    })

    return {
      root,
    };
  },
  render() {
    const attrs = {} as any;
    attrs.ref = "root";
    return h(TAG_NAME, attrs);
  },
});
  • If js component modifies data, cannot use like above, because there are two modification points (vue reactivity, js internal component changing data)
    • Clarify data unidirectional flow:
      • Vue external updates tasks, pass to JS component library, trigger UI update.
      • JS component library notifies Vue through event callbacks to modify tasks, managed uniformly by Vue.
    • Avoid directly operating reactive data:
      • Use local ref copy to transfer tasks, avoid directly modifying props.tasks.

watch old and new values output same

https://stackoverflow.com/questions/62729380/vue-watch-outputs-same-oldvalue-and-newvalue

References

https://www.smashingmagazine.com/2019/02/vue-framework-third-party-javascript/#components-and-component-libraries

https://codepen.io/smashing-magazine/pen/QoLJJV