如何在 vue.js 中定位自定义元素(原生 Web 组件)? [英] How to target custom element (native web component) in vue.js?

查看:58
本文介绍了如何在 vue.js 中定位自定义元素(原生 Web 组件)?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个自定义 Web 组件 treez-tab-folder.顾名思义,它代表一个选项卡文件夹.这是一个 jsfiddle 和一些演示其用法的图像(可能需要 Chrome 浏览器才能正常工作):

我想使用 vue.js 将 JavaScript 对象的属性绑定到选项卡文件夹的某些属性.如果我在 treez-tab 中使用 div 作为 Vue 实例的目标元素 (el: '#firstContent'),则绑定有效正如预期的那样(也见上面的 jsfiddle):

<treez-tab-folder id="tabFolder"><treez-tab title="第一个标签"><div id='firstContent'>{{message}}</div></treez-tab><treez-tab title="第二个标签"><div>第二个标签内容</div></treez-tab></treez-tab-folder><脚本>新的 Vue({el: '#firstContent',数据: {消息:'第一个标签内容'}});

但是,如果我尝试使用 自定义元素 treez-tab-folder 直接作为 Vue 实例的目标元素(el: '#tabFolder'),jsfiddle 示例停止工作:

现在标签页头构造了两次,标签页内容似乎丢失了:

<treez-tab-folder id="tabFolder"><treez-tab title="第一个标签"><div id='firstContent'>{{message}}</div></treez-tab><treez-tab title="第二个标签"><div>第二个标签内容</div></treez-tab></treez-tab-folder><脚本>新的 Vue({el: '#tabFolder',数据: {消息:'第一个标签内容'}});

=> 如何修复我的网络组件?

=> vues.js 是否禁止使用自定义元素作为目标?

我认为当 vue.js 替换/刷新 dom 时,我的组件的处理可能无法正常工作.因此我尝试实现 disconnectedCallback 来杀死剩余的元素.那没有帮助.

<小时>

这是我的自定义标签元素:

和css文件

treez-tab-folder {背景色:#f2f2f2;宽度:100%;高度:100%;填充顶部:2px;字体系列:Arial,sans-serif;字体大小:12px;}treez-tab-folder-header {左边距:-3px;颜色:#777777;}treez-tab-header {背景色:#f2f2f2;显示:内联块;左边距:2px;填充:8px;填充顶部:1px;填充底部:3px;边框:1px 实心;边框颜色:#cccccc;box-shadow: 2px -2px 2px -2px rgba(0,0,0,0.2);变换:翻译(0px,1px);}treez-tab-header:悬停{背景色:#e1e1e1;}treez-tab-header.selected {边框底部:无;背景色:#e1e1e1;变换:翻译(0px,2px);填充顶部:2px;}树形标签{背景色:#e1e1e1;边框顶部:1px 实心;边框颜色:#cccccc;边框底部:无;box-shadow: 2px -2px 2px -2px rgba(0,0,0,0.2);高度:100%;垂直对齐:底部;}

解决方案

问题是 Vue 期望使用 DOM 作为模板,而当它看到你的 web 组件时,DOM 已经被 web 重写了零件.DOM 中有 treez-tab-header 元素不在标记中.因此,当 Vue 重写 DOM 时,它会编写这些元素,而 Web 组件会做它自己的事情,编写更多这些元素.

解决方案是制作一个模板,该模板具有纯正的标记,以便 Vue 可以做它的事情并提出您的组件期望的 DOM 设置.

在这个片段中,我为 Vue 定义了一个模板,而不是从元素中读取它.我还将 Vue 附加到包装器 div.原则上,我可以将它附加到一个空的 web 组件标签,但在这种情况下,组件的内容没有单个根节点(有两个 treez-tabs)所以我无法为它们制作模板.

class TreezTabFolderHeader extends HTMLElement {}window.customElements.define('treez-tab-folder-header', TreezTabFolderHeader);class TreezTabHeader 扩展 HTMLElement {}window.customElements.define('treez-tab-header', TreezTabHeader);类 TreezTabFolder 扩展了 HTMLElement {构造函数(){极好的();this.tabFolderHeader = 未定义;}连接回调(){如果 (!this.tabFolderHeader) {this.style.display = 'block';this.tabFolderHeader = document.createElement('treez-tab-folder-header');this.insertBefore(this.tabFolderHeader, this.firstChild);}}断开连接回调(){而(this.firstChild){this.removeChild(this.firstChild);}}}window.customElements.define('treez-tab-folder', TreezTabFolder);类 TreezTab 扩展 HTMLElement {构造函数(){极好的();console.log('tab 构造函数');this.tabHeader = 未定义;}静态获取观察属性(){返回 ['标题'];}连接回调(){console.log('连接回调');如果 (!this.tabHeader) {var headers = this.parentElement.children[0];this.tabHeader = this.createTabHeader(this.parentElement);this.tabHeader.innerText = this.title;headers.appendChild(this.tabHeader);this.showFirstTab(this.parentElement);}}断开连接回调(){console.log('断开的回调');而(this.firstChild){this.removeChild(this.firstChild);}}采用回调(){console.log('采用回调');}attributeChangedCallback(attr, oldValue, newValue) {if (attr === 'title' && this.tabHeader) {this.tabHeader.innerText = newValue;}}createTabHeader(tabs) {var tabHeader = document.createElement('treez-tab-header');tabHeader.onclick = () =>{var tabHeaders = tabs.children[0].children;for (var index = 1; index < tabs.children.length; index++) {var tab = tabs.children[index];tab.style.display = '无';var tabHeader = tabHeaders[index - 1];tabHeader.classList.remove('selected')}this.style.display = 'block';this.tabHeader.classList.add('selected')};返回tabHeader;}showFirstTab(标签){var firstHeader = tabs.children[0].children[0];firstHeader.classList.add('selected')tabs.children[1].style.display = "block"for (var index = 2; index < tabs.children.length; index++) {tabs.children[index].style.display = "none";}}}window.customElements.define('treez-tab', TreezTab);新的 Vue({el: '#app',模板: '#app-template',数据: {消息:'第一个标签内容'}});

treez-tab-folder {背景颜色:#f2f2f2;宽度:100%;高度:100%;填充顶部:2px;字体系列:Arial、sans-serif;字体大小:12px;}treez-tab-folder-header {左边距:-3px;颜色:#777777;}treez-tab-header {背景颜色:#f2f2f2;显示:内联块;左边距:2px;填充:8px;填充顶部:1px;填充底部:3px;边框:1px 实心;边框颜色:#cccccc;box-shadow: 2px -2px 2px -2px rgba(0, 0, 0, 0.2);变换:翻译(0px,1px);}treez-tab-header:悬停{背景颜色:#e1e1e1;}treez-tab-header.selected {边框底部:无;背景颜色:#e1e1e1;变换:翻译(0px,2px);填充顶部:2px;}树形标签{背景颜色:#e1e1e1;边框顶部:1px 实心;边框颜色:#cccccc;边框底部:无;box-shadow: 2px -2px 2px -2px rgba(0, 0, 0, 0.2);高度:100%;垂直对齐:底部;}

<script src="//unpkg.com/vue@latest/dist/vue.js"></script><div id='app'></div><模板 id="应用模板"><treez-tab-folder id="tabFolder"><treez-tab title="第一个标签"><div id='firstContent'>{{message}}</div></treez-tab><treez-tab title="第二个标签"><div>第二个标签内容</div></treez-tab></treez-tab-folder></template>

I have a custom web component treez-tab-folder. As the name suggests it represents a tab folder. Here is a jsfiddle and some image to demonstrate its usage (might require Chrome browser to work correctly):

https://jsfiddle.net/fg4dL2rx/

I would like to use vue.js to bind the properties of a JavaScript object to some attributes of the tab folder. If I use a div inside a treez-tab as target element for the Vue instance (el: '#firstContent'), the binding works as expected (also see above jsfiddle):

<body>
    <treez-tab-folder id="tabFolder">   
        <treez-tab title="First tab">
            <div id='firstContent'>{{message}}</div>
        </treez-tab>

        <treez-tab title="Second tab">
            <div>Second tab content</div>
        </treez-tab>
    </treez-tab-folder>

    <script>
           new Vue({
                el: '#firstContent',
                data: {
                    message: 'First tab content'
                }
            });  
    </script>
</body>

However, if I try to use the custom element treez-tab-folder directly as target element for the Vue instance (el: '#tabFolder'), the jsfiddle example stops working:

https://jsfiddle.net/etomuf8v/

Now the tab headers are constructed twice and the tab content seems to be missing:

<body>
    <treez-tab-folder id="tabFolder">   
        <treez-tab title="First tab">
            <div id='firstContent'>{{message}}</div>
        </treez-tab>

        <treez-tab title="Second tab">
            <div>Second tab content</div>
        </treez-tab>
    </treez-tab-folder>

    <script>
           new Vue({
                el: '#tabFolder',
                data: {
                    message: 'First tab content'
                }
            });  
    </script>
</body>

=> How can I fix my web component? or

=> Does vues.js forbit to use custom elements as target?

I thought that the disposal of my components might not work correctly when vue.js is replacing/refreshing the dom. Therefore I tried to implement disconnectedCallback to kill remaining elements. That did not help.


Here are my custom tab elements:

<script>

            class TreezTabFolderHeader extends HTMLElement {}
            window.customElements.define('treez-tab-folder-header', TreezTabFolderHeader);

            class TreezTabHeader extends HTMLElement {}
            window.customElements.define('treez-tab-header', TreezTabHeader);

            class TreezTabFolder extends HTMLElement {

                constructor(){
                    super();
                    this.tabFolderHeader=undefined;
                }

                connectedCallback() {
                    if(!this.tabFolderHeader){
                        this.style.display='block';
                        this.tabFolderHeader = document.createElement('treez-tab-folder-header');
                        this.insertBefore(this.tabFolderHeader, this.firstChild);
                    }
                }

                disconnectedCallback(){
                    while (this.firstChild) {
                        this.removeChild(this.firstChild);
                    }
                }
            }
            window.customElements.define('treez-tab-folder', TreezTabFolder);


            class TreezTab extends HTMLElement {

                constructor(){
                    super();
                    console.log('tab constructor');
                    this.tabHeader=undefined;
                }

                static get observedAttributes() {
                    return ['title']; 
                }               

                connectedCallback() {
                    console.log('connected callback');
                    if(!this.tabHeader){
                        var headers = this.parentElement.children[0];
                        this.tabHeader = this.createTabHeader(this.parentElement);
                        this.tabHeader.innerText = this.title;
                        headers.appendChild(this.tabHeader);
                        this.showFirstTab(this.parentElement);
                    }
                }

                disconnectedCallback(){
                    console.log('disconnected callback');
                    while (this.firstChild) {
                        this.removeChild(this.firstChild);
                    }
                }

                adoptedCallback(){
                    console.log('adopted callback');
                }

                attributeChangedCallback(attr, oldValue, newValue) {
                    if(attr==='title' && this.tabHeader){
                        this.tabHeader.innerText= newValue;                       
                    }
                }

                createTabHeader(tabs){
                    var tabHeader = document.createElement('treez-tab-header'); 
                    tabHeader.onclick=()=>{
                        var tabHeaders = tabs.children[0].children;
                        for(var index=1;index<tabs.children.length;index++){ 
                            var tab = tabs.children[index];                        
                            tab.style.display='none';

                            var tabHeader = tabHeaders[index-1];
                            tabHeader.classList.remove('selected')
                        }                       

                        this.style.display='block';
                        this.tabHeader.classList.add('selected')                        
                    };
                    return tabHeader;
                }

                showFirstTab(tabs){
                    var firstHeader = tabs.children[0].children[0];
                    firstHeader.classList.add('selected')
                    tabs.children[1].style.display="block"                    
                    for(var index=2;index<tabs.children.length;index++){                            
                            tabs.children[index].style.display="none";
                    }                         
                }
            }
            window.customElements.define('treez-tab', TreezTab);

        </script>   

and the css file

treez-tab-folder {
    background-color:#f2f2f2;
    width:100%;
    height:100%;
    padding-top:2px;
     font-family: Arial,sans-serif;
    font-size: 12px;
}

treez-tab-folder-header {
    margin-left:-3px;   
    color: #777777;  
}

treez-tab-header {   
    background-color:#f2f2f2;
    display:inline-block;
    margin-left: 2px;

    padding:8px;
    padding-top:1px;
    padding-bottom:3px;
    border: 1px solid;
    border-color:#cccccc;

    box-shadow: 2px -2px 2px -2px rgba(0,0,0,0.2); 
    transform: translate(0px, 1px);        
}

treez-tab-header:hover {
    background-color:#e1e1e1;
}

treez-tab-header.selected {
    border-bottom: none;
    background-color:#e1e1e1;
    transform: translate(0px, 2px); 
    padding-top:2px; 
}

treez-tab {   
    background-color:#e1e1e1;
    border-top: 1px solid;
    border-color:#cccccc;
    border-bottom: none;
    box-shadow: 2px -2px 2px -2px rgba(0,0,0,0.2); 
    height:100%;
    vertical-alignment:bottom;

}

解决方案

The problem is that Vue expects to use the DOM as a template, and by the time it sees your web component, the DOM has already been rewritten by the web component. There are treez-tab-header elements in the DOM that are not in the markup. So when Vue rewrites the DOM, it writes those elements, and the web component does its thing, writing more of those elements.

The solution is to make a template that has the unadulterated markup so that Vue can do its thing and come up with the DOM setup that your component expects.

In this snippet, I define a template for Vue rather than reading it from the element. I also attach the Vue to a wrapper div. In principle, I could attach it to an empty web component tag, but in this case, the contents of the component don't have a single root node (there are two treez-tabs) so I can't make a template of them.

class TreezTabFolderHeader extends HTMLElement {}
window.customElements.define('treez-tab-folder-header', TreezTabFolderHeader);

class TreezTabHeader extends HTMLElement {}
window.customElements.define('treez-tab-header', TreezTabHeader);

class TreezTabFolder extends HTMLElement {

  constructor() {
    super();
    this.tabFolderHeader = undefined;
  }

  connectedCallback() {
    if (!this.tabFolderHeader) {
      this.style.display = 'block';
      this.tabFolderHeader = document.createElement('treez-tab-folder-header');
      this.insertBefore(this.tabFolderHeader, this.firstChild);
    }
  }

  disconnectedCallback() {
    while (this.firstChild) {
      this.removeChild(this.firstChild);
    }
  }
}
window.customElements.define('treez-tab-folder', TreezTabFolder);


class TreezTab extends HTMLElement {

  constructor() {
    super();
    console.log('tab constructor');
    this.tabHeader = undefined;
  }

  static get observedAttributes() {
    return ['title'];
  }

  connectedCallback() {
    console.log('connected callback');
    if (!this.tabHeader) {
      var headers = this.parentElement.children[0];
      this.tabHeader = this.createTabHeader(this.parentElement);
      this.tabHeader.innerText = this.title;
      headers.appendChild(this.tabHeader);
      this.showFirstTab(this.parentElement);
    }
  }

  disconnectedCallback() {
    console.log('disconnected callback');
    while (this.firstChild) {
      this.removeChild(this.firstChild);
    }
  }

  adoptedCallback() {
    console.log('adopted callback');
  }

  attributeChangedCallback(attr, oldValue, newValue) {
    if (attr === 'title' && this.tabHeader) {
      this.tabHeader.innerText = newValue;
    }
  }

  createTabHeader(tabs) {
    var tabHeader = document.createElement('treez-tab-header');
    tabHeader.onclick = () => {
      var tabHeaders = tabs.children[0].children;
      for (var index = 1; index < tabs.children.length; index++) {
        var tab = tabs.children[index];
        tab.style.display = 'none';

        var tabHeader = tabHeaders[index - 1];
        tabHeader.classList.remove('selected')
      }

      this.style.display = 'block';
      this.tabHeader.classList.add('selected')
    };
    return tabHeader;
  }

  showFirstTab(tabs) {
    var firstHeader = tabs.children[0].children[0];
    firstHeader.classList.add('selected')
    tabs.children[1].style.display = "block"
    for (var index = 2; index < tabs.children.length; index++) {
      tabs.children[index].style.display = "none";
    }
  }
}
window.customElements.define('treez-tab', TreezTab);

new Vue({
  el: '#app',
  template: '#app-template',
  data: {
    message: 'First tab content'
  }
});

treez-tab-folder {
  background-color: #f2f2f2;
  width: 100%;
  height: 100%;
  padding-top: 2px;
  font-family: Arial, sans-serif;
  font-size: 12px;
}

treez-tab-folder-header {
  margin-left: -3px;
  color: #777777;
}

treez-tab-header {
  background-color: #f2f2f2;
  display: inline-block;
  margin-left: 2px;
  padding: 8px;
  padding-top: 1px;
  padding-bottom: 3px;
  border: 1px solid;
  border-color: #cccccc;
  box-shadow: 2px -2px 2px -2px rgba(0, 0, 0, 0.2);
  transform: translate(0px, 1px);
}

treez-tab-header:hover {
  background-color: #e1e1e1;
}

treez-tab-header.selected {
  border-bottom: none;
  background-color: #e1e1e1;
  transform: translate(0px, 2px);
  padding-top: 2px;
}

treez-tab {
  background-color: #e1e1e1;
  border-top: 1px solid;
  border-color: #cccccc;
  border-bottom: none;
  box-shadow: 2px -2px 2px -2px rgba(0, 0, 0, 0.2);
  height: 100%;
  vertical-alignment: bottom;
}

<script src="//unpkg.com/vue@latest/dist/vue.js"></script>

<div id='app'></div>

<template id="app-template">
	<treez-tab-folder id="tabFolder">   
		<treez-tab title="First tab">
        <div id='firstContent'>{{message}}</div>
		</treez-tab>

		<treez-tab title="Second tab">
        <div>Second tab content</div>
		</treez-tab>
	</treez-tab-folder>
</template>

这篇关于如何在 vue.js 中定位自定义元素(原生 Web 组件)?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆