UNPKG

vue-ctx-injector

Version:

A tool for injecting standalone Vue.js components into HTML contexts.

200 lines (187 loc) 5.71 kB
import DOM from './helpers/DOM.js' /** * VCIComponent - Represents a VCI-managed object representation of a link * system between an HTML customized element and a Vue component. */ export default class VCIComponent { _vue = null _vCompConstructor = null _vCompInstance = null _componentPrefix = null _propPrefix = null name = null rootElement = null replaceRoot = true vComp = null propsData = {} constructor (vue, { compPrefix, propPrefix, vComp, rootElement, replaceRoot }) { this._vue = vue this._componentPrefix = compPrefix this._propPrefix = propPrefix this.vComp = vComp this.rootElement = rootElement this.replaceRoot = replaceRoot } /** * Sets the component name. * * @param {String} name - The component name to set. * @return {void} */ setName (name = null) { this.name = name } /** * Sets the current props data. * * @param {String} data - Props data. * @return {void} */ setPropsData (data) { if (this.isValidComponent()) { this.propsData = this._castProps(data) } } /** * Determines the component name validity status. * * @return {Boolean} */ isValidName () { if (!this.name) { return false } return true } /** * Determines the component definition validity status. * * @return {Boolean} */ isValidComponent () { if (!this.vComp || typeof this.vComp !== 'object') { return false } return true } /** * Starts the mounting process of the current component, by injecting to it * currently stored `propsData`. * * @return {void} */ mount () { if (this.rootElement) { this._vCompConstructor = this._vue.extend(this.vComp) this._vCompInstance = new this._vCompConstructor({ propsData: this.propsData, }) this._vCompInstance._props = this._vue.observable(this.propsData) const vm = this._vCompInstance.$mount() if (this.replaceRoot) { this._mergeComponentWithRootElement(vm.$el) } else { this.rootElement.appendChild(vm.$el) } } } /** * Starts the props' watching process on the currently linked `rootElement`. * * @return {void} */ watch () { const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type === 'attributes') { const newProps = DOM.getVCIElementProps(this._propPrefix, this.rootElement) // TODO: Look for another way to update props than re-instanciating // & mounting the whole component (needed because `propsData` is only // usable at instance creation). this._vCompInstance = new this._vCompConstructor({ propsData: this._castProps(newProps), }) const vm = this._vCompInstance.$mount() this.setPropsData(newProps) if (this.replaceRoot) { this.rootElement.innerHTML = vm.$el.innerHTML } else { this.rootElement.innerHTML = '' this.rootElement.appendChild(vm.$el) } } }) }); observer.observe(this.rootElement, { attributes: true, }) } /** * Use the props-level defined types of given internal vComp definition to * cast `initialProps` values. * * @param {Object} initialProps - Initial string-based props. * @return {Object} - The well-casted props. */ _castProps (initialProps) { let castedProps = {} for (const name in initialProps) { if (this.vComp && this.vComp.props.hasOwnProperty(name)) { const castType = this.vComp.props[name].type let castedProp = null if ([Object, Array].includes(castType)) { castedProp = JSON.parse(initialProps[name]); } else { castedProp = castType(initialProps[name]); } castedProps[name] = castedProp } } return castedProps } /** * Uses the stored `rootElement` and merge it with the given * `element`, by applying intelligent attributes merging strategy. * * @param {HTMLElement} element - The component rendered DOM element. * @return {void} */ _mergeComponentWithRootElement (element) { // Store receiving elements attrs before replacing let compElementId = element.getAttribute('id') let compElementClasses = element.classList let rootAttrCompName = this.rootElement.getAttribute(this._componentPrefix) let rootElementId = this.rootElement.getAttribute('id') let rootElementClasses = this.rootElement.classList let rootProps = [] for (const attr of this.rootElement.attributes) { if (attr.name.includes(this._propPrefix)) { rootProps[attr.name] = attr.value } } // Replace the receiving element by a new one based on injected component // -- attributes parsing & merging if (compElementId) { element.setAttribute('id', compElementId) } else if (rootElementId) { element.setAttribute('id', rootElementId) } if (compElementClasses.length) { element.classList = compElementClasses } for (const className of rootElementClasses) { if (!element.classList.contains(className)) { element.classList.add(className) } } element.setAttribute(this._componentPrefix, rootAttrCompName) for (const key in rootProps) { element.setAttribute(key, rootProps[key]) } // -- DOM injecting const idComment = document.createComment(`[vci-comp] ${this.name}`) this.rootElement.before(idComment) this.rootElement.remove() this.rootElement = element idComment.after(element) idComment.remove() } }