Auf der Arbeit wurde ich mit dem Problem konfrontiert, dynamische Inhalte in einem OffCanvas darzustellen. Beispielsweise ein Inhaltsverzeichnis oder die mobile Hauptnavigation. Insgesamt gab es über fünf verschiedene Fälle, die abgedeckt werden sollten. Normalerweise würde man vermutlich eine theme
Property an die entsprechende Komponente übergeben. Damit wären die Inhalte aber nicht mehr im Kontext ihrer Komponenten und die OffCanvas Komponente würde immer größer werden. Eine Alternative bietet portal-vue.
Mit portal-vue kann man Elemente an beliebiger Stelle im DOM rendern. Als Beispiel nutzen wir die beiden oben genannten Fälle: Inhaltsverzeichnis und Hauptnavigation. Das Ziel ist es die Inhalte dieser beiden Komponenten an einer zentralen Stelle in der App.vue
auszuspielen.
Disclaimer: Bei den Beispielen in diesem Beitrag handelt es sich um eine stark vereinfachte Version des Problems. Außerdem gehe ich davon aus, dass du entweder schon ein Vue Projekt aufgesetzt hast oder weißt wie das geht.
portal-vue installieren
Installier portal-vue mit einem der folgenden Befehle.
yarn add portal-vue npm install --save portal-vue
Anschließend fügst du die folgenden Zeilen in deine main.js
ein.
import PortalVue from 'portal-vue' Vue.use(PortalVue)
Konfiguration
Anschließend erstellen wir insgesamt drei Komponenten über das Terminal:
touch src/components/OffCanvas.vue touch src/components/MainNavigation.vue touch src/components/TableOfContents.vue
Nun haben wir alles, was wir brauchen und können uns um den Code kümmern. Als erstes konfigurieren wir den zentralen Ort, wo die Inhalte – also der OffCanvas – angezeigt werden sollen. Dazu binden wir unsere neuen Komponenten ein.
// App.vue <template> <div id="app"> <portal-target name="offCanvas" /> <main-navigation /> <table-of-contents /> <div id="content"> <h2 id="headline1">Headline 1</h2> <p>Lorem ipsum dolor sit amet.</p> <h2 id="headline2">Headline 2</h2> <p>Lorem ipsum dolor sit amet.</p> <h2 id="headline3">Headline 3</h2> <p>Lorem ipsum dolor sit amet.</p> </div> </div> </template> <script> import MainNavigation from '@/components/MainNavigation' import TableOfContents from '@/components/TableOfContents' export default { components: { MainNavigation, TableOfContents } } </script>
Dem Portal haben wir den Namen „offCanvas“ genannt. Diesen müssen wir uns für unser Inhaltsverzeichnis und unsere Hauptnavigation merken. Aber kümmern wir uns erst einmal um den OffCanvas.
// OffCanvas.vue <template> <div> <div v-if="show" class="backdrop" @click="close" /> <div :style="show ? 'visibility: visible;' : 'visibility: hidden;'" :class="`offcanvas${show ? ' show' : ''}`" > <div class="offcanvas__header"> <h2>{{ headline }}</h2> <button title="Close" @click="close">X</button> </div> <div class="offcanvas__body"> <slot /> </div> </div> </div> </template> <script> export default { name: 'OffCanvas', props: { headline: { type: String, required: true }, }, computed: { show () { return this.$store.getters.showOffCanvas }, }, methods: { close () { this.$store.commit('toggleOffCanvas', false) this.$emit('close') }, }, } </script> <style lang="scss" scoped> .backdrop { position: fixed; top: 0; left: 0; height: 100vh; width: 100vw; background-color: rgba(0, 0, 0, 0.8); z-index: 100; } .offcanvas { position: fixed; top: 0; left: 0; height: 100vh; width: 20%; background-color: #fff; z-index: 101; border-right: 1px solid rgba(0, 0, 0, 0.2); transition: transform 0.3s ease-in-out; transform: translateX(-100%); &.show { transform: none; } &__header { display: flex; justify-content: space-between; } } </style>
Die OffCanvas
Komponente bekommt eine headline
mit. Ob der OffCanvas angezeigt wird oder nicht, wird über den Vuex Store entschieden, aber dazu später mehr. Zunächst binden wir diesen Code in unsere Hauptnavigation und unser Inhaltsverzeichnis ein.
OffCanvas einbinden
// MainNavigation.vue <template> <div> <button title="open menu" @click="openMenu">Open menu</button> <portal v-if="usePortal" to="offCanvas"> <off-canvas headline="Main navigation" @close="usePortal = false"> MAIN NAVIGATION CONTENT </off-canvas> </portal> </div> </template> <script> import OffCanvas from '@/components/OffCanvas' export default { name: 'MainNavigation', components: { OffCanvas }, data: () => ({ usePortal: false }), methods: { openMenu () { this.usePortal = true this.$store.commit('toggleOffCanvas', true) }, }, } </script>
// TableOfContents.vue <template> <div> <button @click="openOffCanvas">Table of contents</button> <portal v-if="usePortal" to="offCanvas"> <off-canvas headline="Table Of Contents" @close="usePortal = false"> <ul @click="$store.commit('toggleOffCanvas', false)"> <li><a href="#headline1">Headline 1</a></li> <li><a href="#headline2">Headline 2</a></li> <li><a href="#headline3">Headline 3</a></li> </ul> </off-canvas> </portal> </div> </template> <script> import OffCanvas from '@/components/OffCanvas' export default { name: 'TableOfContents', components: { OffCanvas }, data: () => ({ usePortal: true }), methods: { openOffCanvas () { this.usePortal = true this.$store.commit('toggleOffCanvas', true) }, }, } </script>
Zu guter Letzt zeige ich euch noch die Vuex Store Konfiguration.
// src/store/index.js export const state = () => ({ showOffCanvas: false, }) export const mutations = { toggleOffCanvas (state, show) { state.showOffCanvas = show }, } export const getters = { showOffCanvas: state => state.showOffCanvas, }
Fertig! Beim Klick auf einen der beiden Buttons öffnet sich der OffCanvas inklusive Backdrop und dem dazugehörigen Inhalt.
Hat euch der Beitrag geholfen? Schreibt es gerne in die Kommentare. :)