Vue.js: Including same instance of component multiple times in page

11,445

There's a number of different ways you can handle this. It looks like you've started down the event bus path. Another option could be to use shared app state (see Vuex).

What I've done is similar to shared state, but just using app (same would apply to a common parent component) data. The shared object is passed to both instances of the component. If an item is selected, the appropriate entry is toggled. Since the object is shared, both components stay in sync.

If there was no common parent component, you'd have to look at events or state.

Take a look and see if that helps.

Vue.component('guitar-filters', {

    props: [ 'data' ],

    data: function() {
        return {
            isMobile: false
        }
    },

    mounted: function() {
        var comp = this;
        this.setIsMobile();
        window.addEventListener('resize', function() {
            comp.setIsMobile();
        });
    },

    methods: {
        setIsMobile: function() {
            this.isMobile = (window.innerWidth <= 900) ? true : false;
        }
    },

    template: `
        <ul class="filters" :class="{mobile: isMobile}">

        <li>
            All
        </il>

        <li>
            Series
            <instrument-filters :list="data.seriesFilters"/>
        </li>

        <li>
            Body Shape
            <instrument-filters :list="data.bodyFilters"/>
        </li>

    </ul>
    `

});

Vue.component('instrument-filters', {

    props : [ 'list', ],

    methods: {
        toggle(toggleItem) {
          let itemInList = this.list.find((item) => item.value === toggleItem.value);        
        	itemInList.selected = !itemInList.selected;       
        },
    },

    template: `
    		<ul>
          <li v-for="item in list" :class="{ 'selected' : item.selected }" @click="toggle(item)">{{ item.label }}</li>
        </ul>
    `

});

new Vue({
  el: "#app",
  data: {
    filterData: {
      seriesFilters: [
        { label: 'All', value: 'All', selected: false },
        { label: 'Frontier', value: 'Frontier', selected: false },
        { label: 'Legacy', value: 'Legacy', selected: false },
        { label: 'USA', value: 'USA', selected: false },
      ],
      bodyFilters: [
        { label: 'All', value: 'All', selected: false },
        { label: 'Concert', value: 'Concert', selected: false },
        { label: 'Concertina', value: 'Concertina', selected: false },
        { label: 'Concerto', value: 'Concerto', selected: false },
        { label: 'Orchestra', value: 'Orchestra', selected: false },
      ],
    }
  },
});
ul {
 margin-left:20px; 
}
ul > li {
  cursor: pointer;
}
ul.filters > li > ul > li.selected::before {
   content: "✔️";
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
   <header>
      <!-- desktop -->
      <guitar-filters :data="filterData" />
   </header>

   <!-- mobile -->
   <guitar-filters :data="filterData" />
</div>

Fiddle: https://jsfiddle.net/nigelw/jpasfkxb

Share:
11,445

Related videos on Youtube

Justin D
Author by

Justin D

Updated on July 03, 2022

Comments

  • Justin D
    Justin D over 1 year

    What I am trying to accomplish: I have some filters that display on a page to filter the products that display on the page. In mobile, I want to hide these filters behind a button that, once pressed, will show the filters in a slide out menu from the side.

    While I can duplicate the same components on the page twice, the components are not the exact same instance, that is, clicking on a filter will trigger that function to filter the products on the page, but it sets its own data attributes, which I am using to say "if data attribute 'selected' is true, add a 'selected' class to the component. When I resize the window, the other instance of the component does not have the 'selected' data attribute marked as 'true'.

    I expect this, because, from the docs:

    Notice that when clicking on the buttons, each one maintains its own, separate count. That’s because each time you use a component, a new instance of it is created.

    ...but what would be the best way to do this?

    I played around with the idea of just setting a class 'mobile' on the component, and the .mobile css would style the components differently, but I need for it to break out where it is nested.

    e.g.

    <body>
       <header>
          <!-- desktop -->
          <guitar-filters>
       <header>
    
       <!-- mobile -->
       <guitar-filters>
    </body
    

    Here is my Vue component 'guitar-filters' that displays several components called 'instrument-filter':

    Vue.component('guitar-filters', {
    
        data: function() {
    
            return {
                isMobile: false
            }
    
        },
    
        mounted: function() {
    
            var comp = this;
            this.setIsMobile();
    
            window.addEventListener('resize', function() {
                comp.setIsMobile();
            });
    
        },
    
        methods: {
    
            setIsMobile: function() {
    
                this.isMobile = (window.innerWidth <= 900) ? true : false;
    
            }
    
        },
    
        template: `
            <ul class="filters" :class="{mobile: isMobile}">
    
            <li>
                All
            </il>
    
            <li>
                Series
                <ul>
                    <instrument-filter filter-by="series" filter="All">All</instrument-filter>
                    <instrument-filter filter-by="series" filter="Frontier">Frontier</instrument-filter>
                    <instrument-filter filter-by="series" filter="Legacy">Legacy</instrument-filter>
                    <instrument-filter filter-by="series" filter="USA">USA</instrument-filter>
                </ul>
            </li>
    
            <li>
                Body Shape
                <ul>
                    <instrument-filter filter-by="bodyType" filter="All">All</instrument-filter>
                    <instrument-filter filter-by="bodyType" filter="Concert">Concert</instrument-filter>
                    <instrument-filter filter-by="bodyType" filter="Concertina">Concertina</instrument-filter>
                    <instrument-filter filter-by="bodyType" filter="Concerto">Concerto</instrument-filter>
                    <instrument-filter filter-by="bodyType" filter="Orchestra">Orchestra</instrument-filter>
                </ul>
            </li>
    
        </ul>
        `
    
    });
    

    The instrument-filter component:

    Vue.component('instrument-filter', {
    
        data: function() {
    
            return {
                selected: false
            }
    
        },
    
        props : [
            'filterBy',
            'filter'
    
        ],
    
        methods: {
    
            addFilter: function() {
                this.$root.$emit('addFilter',{filterBy: this.filterBy,filter: this.filter});
            },
    
            clearFilter: function() {
                this.$root.$emit('clearFilter',{filterBy: this.filterBy,filter: this.filter});
            }
    
        },
    
        template: `
            <li :class="{ 'selected' : selected }" @click="selected = !selected; selected ? addFilter() : clearFilter()"><slot></slot></li>
        `
    
    });
    

    .css:

    ul.filters > li > ul > li.selected::before {
       content: "✔️";
       ...
    }
    

    The goal is to have a filter have the 'selected' class in both instances. If I click on 'concert' body shape, and then resize the window to mobile breakpoint, the other instance of that filter component will be selected also.

    EDIT: I could hack this. I could move one instance of the component with javascript, but I'm learning Vue, and want to do this the Vue way and best practices.

  • Justin D
    Justin D over 4 years
    Thank you! This does exactly what I need to do, and it helps me to learn more about Vue! Props!