如何在vue.js中定位自定义元素(本机Web组件)?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何在vue.js中定位自定义元素(本机Web组件)?相关的知识,希望对你有一定的参考价值。

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

https://jsfiddle.net/fg4dL2rx/

enter image description here

我想使用vue.js将javascript对象的属性绑定到选项卡文件夹的某些属性。如果我在div中使用treez-tab作为Vue实例(el: '#firstContent')的目标元素,则绑定按预期工作(也见上面的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>

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

https://jsfiddle.net/etomuf8v/

enter image description here

现在选项卡标题构造了两次,标签内容似乎丢失了:

<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>

=>如何修复我的Web组件?要么

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

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


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

<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>   

和css文件

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;

}
答案

问题是Vue希望使用DOM作为模板,当它看到您的Web组件时,DOM已经被Web组件重写了。 DOM中有treez-tab-header元素不在标记中。因此,当Vue重写DOM时,它会编写这些元素,并且Web组件会执行其操作,编写更多这些元素。

解决方案是make a template具有纯粹的标记,以便Vue可以完成其工作并提出组件所需的DOM设置。

在这个片段中,我为Vue定义了一个模板,而不是从元素中读取它。我还将Vue附加到包装纸div。原则上,我可以将它附加到一个空的Web组件标记,但在这种情况下,组件的内容没有一个根节点(有两个treez-tabs),所以我不能创建它们的模板。

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组件)?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Vue.js 中保留自定义组件标签名称

如何创建从 vue.js 实例到自定义原生 Web 组件的双向绑定?

如何立即使用Vue CLI将多个Vue.js组件构建到本机Web组件?

Vue.js 组件未知的自定义元素

在 Vue.js 中嵌套组件时出现未知的自定义元素

vue.js2.0 自定义组件初体验