Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
252 views
in Technique[技术] by (71.8m points)

javascript - How to target custom element (native web component) in vue.js?

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/

enter image description here

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/

enter image description here

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;

}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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>

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...