1
votes

Im working on responsive Vue.js app with Uikit. I must create 2 menus - one for desktop version, one for mobile. So I must define same menu items 2 times, first for desktop links slot, second time for mobile. I dont want define it 2x. How can I deal with this problem elegantly? Any ideas?

<template id="app">
  <app-layout>

    <template slot="navlinks-desktop">
      <router-link to="/link1" tag="li" exact><a>Link 1</a></router-link>
      <router-link to="/link2" tag="li" exact><a>Link 2</a></router-link>
    </template>

    <template slot="navlinks-mobile">
      <router-link to="/link1" tag="li" exact><a>Link 1</a></router-link>
      <router-link to="/link2" tag="li" exact><a>Link 2</a></router-link>
    </template>

    <transition name="fade" slot="content">
      <router-view></router-view>
    </transition>

    <p slot="footer">Footer text</p>

  </app-layout>
</template>

<template id="app-layout">
  <div class="main-container">

    <header class="uk-margin-bottom">
      <nav class="uk-navbar uk-navbar-attached">
        <div class="uk-navbar-brand uk-hidden-small">My Application</div>
        <div class="uk-navbar-flip">
          <ul class="uk-navbar-nav uk-hidden-small">
            <slot name="navlinks-desktop"></slot>
          </ul>
          <div class="uk-navbar-toggle uk-button-dropdown uk-visible-small uk-dropdown-close" data-uk-dropdown="{mode: 'click'; justify: 'nav'}">
            <div class="uk-dropdown uk-dropdown-navbar uk-dropdown-small">
              <ul class="uk-nav uk-nav-dropdown">
                <slot name="navlinks-mobile"></slot>
              </ul>
            </div>
          </div>
        </div>
        <div class="uk-navbar-brand uk-navbar-center uk-visible-small">My Application</div>
      </nav>
    </header>

    <div class="uk-container uk-container-center">
      <main>              
        <slot name="content"></slot>
      </main>
    </div>

    <footer class="uk-text-center fixed-bottom">
      <slot name="footer"></slot>
    </footer>

  </div>
</template>

EDIT: So... After some research and closer look to documentation, I must say, that this specific problem DOES NOT HAVE SOLUTION... Although it is possible to reuse slots, just render it programatically, IT IS NOT POSSIBLE TO REUSE SLOT WITH ROUTER LINK.

Example - no hacks - true "Vue way" to reuse slots:

<template id="app">
  <app-layout>

    <template slot="myslot">
      <li>THIS WORKS</li>
      <li>WILL</li>
      <li>WORK</li>
    </template>

  </app-layout>
</template>

<script>
  Vue.component('app-layout', {
    render: function (createElement) {
      var myslot = this.$slots.myslot

      return createElement('div', [
        createElement('ul', myslot),
        createElement('ul', myslot)
      ])
    }
  })
</script>

Its simple, elegant and readable. But, unfortunately, if you render component programatically, you cant use templates. So something like this...

<template id="app-layout">
  <div class="main-container">

    <header class="uk-margin-bottom">
      <nav class="uk-navbar uk-navbar-attached">
        <div class="uk-navbar-brand uk-hidden-small">My Application</div>
        <div class="uk-navbar-flip">
          <ul class="uk-navbar-nav uk-hidden-small">
            <slot name="myslot"></slot>
          </ul>
        </div>
      </nav>
    </header>

  </div>
</template>

...you can throw away and you must create whole structure, one by one programatically with createElement function, which is really painfull and error prone. Yes, you can use JSX, but then you must transpile it with Babel. And this is not what I want... Non working example:

<template id="app">
  <app-layout>

    <template slot="navlinks">
      <router-link to: "/">Home</router-link>
      <router-link to: "/products">Products</router-link>
      <router-link to: "/about">About</router-link>
    </template>

  </app-layout>
</template>

<script>
  Vue.component('app-layout', {
    render: function (createElement) {
      var navlinks = this.$slots.navlinks

      return createElement('div', [
        createElement('ul', navlinks),
        createElement('ul', navlinks)
      ])
    }
  })
</script>

In this example, second UL will be empty. And unfortunately, from the fact perspective that in Vue all vNodes must be unique, it is perfectly normal, that it will remain empty...

So the result is that I can not use structure as above. I cant have template app-layout for which I have template with wrapping elements. Finally, I rewrote it this way:

<template id="app">
  <div class="main-container">

    <header class="uk-margin-bottom">
      <nav class="uk-navbar uk-navbar-attached">
        <div class="uk-navbar-brand uk-hidden-small">My Application</div>
        <div class="uk-navbar-flip">
          <ul class="uk-navbar-nav uk-hidden-small">
            <router-link to="/link1" tag="li" exact><a>Link 1</a></router-link>
            <router-link to="/link2" tag="li" exact><a>Link 2</a></router-link>
          </ul>
          <div class="uk-navbar-toggle uk-button-dropdown uk-visible-small uk-dropdown-close" data-uk-dropdown="{mode: 'click'; justify: 'nav'}">
            <div class="uk-dropdown uk-dropdown-navbar uk-dropdown-small">
              <ul class="uk-nav uk-nav-dropdown">
                <router-link to="/link1" tag="li" exact><a>Link 1</a></router-link>
                <router-link to="/link2" tag="li" exact><a>Link 2</a></router-link>
              </ul>
            </div>
          </div>
        </div>
        <div class="uk-navbar-brand uk-navbar-center uk-visible-small">My Application</div>
      </nav>
    </header>

    <div class="uk-container uk-container-center">
      <main>              
        <transition name="fade" slot="content">
          <router-view></router-view>
        </transition>
      </main>
    </div>

    <footer class="uk-text-center fixed-bottom">
      <p slot="footer">Footer text</p>
    </footer>

  </div>
</template>

No two separate templates, no slots. But no programmatical template rendering also. So I kept my example application clean and easy to understand. And that was the point...

PS: STILL, I WILL BE VERY HAPPY, IF I AM WRONG AND SOMEONE SHOW ME HOW TO REUSE SLOT WITH ROUTER LINK.

EDIT2: I kicked Uikit and used Bulma instead. This allows me to create better, more readable structure.

<!-- APPLICATION VIEW -->
  <template id="app">
    <app-layout>

      <template slot="navbar-items">
        <router-link to="/" class="navbar-item" exact>Home</router-link>
        <router-link to="/about" class="navbar-item" exact>About</router-link>
      </template>

      <transition name="fade" mode="out-in" slot="content">
        <keep-alive>
          <router-view></router-view>
        </keep-alive>
      </transition>

      <p slot="footer">&copy;2017 Wal De Mar</p>

    </app-layout>
  </template>
<!-- APPLICATION VIEW -->

<!-- TEMPLATE FOR APP VIEW -->
  <template id="app-layout">
    <div class="container">

      <header>
        <nav class="navbar">
          <div class="navbar-brand">
            <a href="/" class="navbar-item">BRAND</a>
            <div class="navbar-burger" data-target="main-menu">
              <span></span>
              <span></span>
              <span></span>
            </div>
          </div>
          <div id="main-menu" class="navbar-menu">
            <div class="navbar-end">
              <slot name="navbar-items"></slot>
            </div>
          </div>
        </nav>
      </header>

      <main>
        <slot name="content"></slot>
      </main>

      <footer>
        <slot name="footer"></slot>
      </footer>

    </div>
  </template>
<!-- TEMPLATE FOR APP VIEW -->

<script>
  Vue.component('app-layout', {
    template: '#app-layout'
  })

  new Vue({
    template: '#app',
    router,
  }).$mount('#app')
</script>
4
Build the menu items as own component and just add the component into the slot?Frnak
Use JavaScript, v-for.Tatsuyuki Ishi
V-for? Hmmm ... sounds promising... But how? Any example? Or link to example?user6748331
Hi, navlinks-desktop and navlinks-mobile look exactly the same. Why not use just one slot named navlinks for example and use it in the different menu wrappers?Nora
Yes, this is what I want. But how? Can you rewrite my templates?user6748331

4 Answers

2
votes

You can use a named slot more than once. In the snippet below, I have a component that takes two slots and another component that feeds it the same slot twice.

Vue.component('twoMenus', {
  template: '#two-menu-template'
});

new Vue({
  el: '#app',
  components: {
    slotDoubler: {
      template: '#slot-doubler-template'
    }
  }
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="app">
  <two-menus>
    <div slot="menu1">I am a menu</div>
    <div slot="menu2">I am a different menu</div>
  </two-menus>
  <hr>
  <slot-doubler>
    <div slot="reusable">This menu appears twice!</div>
  </slot-doubler>
</div>

<template id="two-menu-template">
  <div>
    <div>The first menu is here:
      <slot name="menu1"></slot>
    </div>
    <div>And the second is here:
      <slot name="menu2"></slot>
    </div>
  </div>
</template>

<template id="slot-doubler-template">
  <div>
    <two-menus>
      <div slot="menu1">
        <slot name="reusable"></slot>
      </div>
      <div slot="menu2">
        <slot name="reusable"></slot>
      </div>
    </two-menus>
  </div>
</template>
0
votes

You could create a separate component that contains the navigation links and use it instead of the slots.

<!-- NavLinks.vue -->
<template>
  <router-link to="/link1" tag="li" exact><a>Link 1</a></router-link>
  <router-link to="/link2" tag="li" exact><a>Link 2</a></router-link>
</template>

<template id="app">
  <app-layout>
    <transition name="fade" slot="content">
      <router-view></router-view>
    </transition>

    <p slot="footer">Footer text</p>

  </app-layout>
</template>

<template id="app-layout">
  <div class="main-container">

    <header class="uk-margin-bottom">
      <nav class="uk-navbar uk-navbar-attached">
        <div class="uk-navbar-brand uk-hidden-small">My Application</div>
        <div class="uk-navbar-flip">
          <ul class="uk-navbar-nav uk-hidden-small">
            <nav-links></nav-links>
          </ul>
          <div class="uk-navbar-toggle uk-button-dropdown uk-visible-small uk-dropdown-close" data-uk-dropdown="{mode: 'click'; justify: 'nav'}">
            <div class="uk-dropdown uk-dropdown-navbar uk-dropdown-small">
              <ul class="uk-nav uk-nav-dropdown">
                <nav-links></nav-links>
              </ul>
            </div>
          </div>
        </div>
        <div class="uk-navbar-brand uk-navbar-center uk-visible-small">My Application</div>
      </nav>
    </header>

    <div class="uk-container uk-container-center">
      <main>              
        <slot name="content"></slot>
      </main>
    </div>

    <footer class="uk-text-center fixed-bottom">
      <slot name="footer"></slot>
    </footer>

  </div>
</template>
0
votes

if i need to populate two menus with one data i would go with another much easier and good approach.

you can have a javascript menu object and via v-for populate the menu items. you can have full access to your dom and manipulate it as you wish. i have written a little demo for you below

if you need to duplicate you can move the v-for template to a component and pass the menu object to it

new Vue({
  el:"#app",
  data : {
    menu : [
      {
        // have each menu item as js object
        text : 'Home',
        to : { name : 'somewhere'},
        href : '/somewhere'
      },
      {
        // you can even have a divider
        divider : true
      },
      {
        text : 'About',
        to : { name : 'somewhere'},
        href : '/somewhere'
      },
      {
        // have each menu item as js object
        text : 'Services',
        to : { name : 'somewhere'},
        href : '/somewhere',
        children : [
          // you can even have child menu
        ]
      },
    ]
  }
})
.mobile-menu {
    background : grey;
    border : black solid 3px;
}
<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
<div id="app">

  
  <ul class="desktop-menu"> 
    <template v-for="item in menu">
      <li v-if="item.divider">
        <span class="divider" >--------</span>
      </li>
      <li v-else>
        <router-link :to="item.to" v-text="item.text"></router-link>
      </li>
    </template>
  </ul>
  
  <ul class="mobile-menu"> 
    <template v-for="item in menu">
      <li v-if="item.divider">
        <span class="divider" >--------</span>
      </li>
      <li v-else>
        <router-link :to="item.to" v-text="item.text"></router-link>
      </li>
    </template>
  </ul>
</div>
0
votes

If i understand your requirement you can use the same slot any number of time.

just copy and paste the same slot in two places

```html

<header class="uk-margin-bottom">
  <nav class="uk-navbar uk-navbar-attached">
    <div class="uk-navbar-brand uk-hidden-small">My Application</div>
    <div class="uk-navbar-flip">
      <ul class="uk-navbar-nav uk-hidden-small">
        <slot name="navlinks-desktop"></slot>
      </ul>
      <div class="uk-navbar-toggle uk-button-dropdown uk-visible-small uk-dropdown-close" data-uk-dropdown="{mode: 'click'; justify: 'nav'}">
        <div class="uk-dropdown uk-dropdown-navbar uk-dropdown-small">
          <ul class="uk-nav uk-nav-dropdown">
        <slot name="navlinks-desktop"></slot>
          </ul>
        </div>
      </div>
    </div>
    <div class="uk-navbar-brand uk-navbar-center uk-visible-small">My Application</div>
  </nav>
</header>

<div class="uk-container uk-container-center">
  <main>              
    <slot name="content"></slot>
  </main>
</div>

<footer class="uk-text-center fixed-bottom">
  <slot name="footer"></slot>
</footer>

```