Vue & Nuxt Tips

Vue.js Logo

Reactive SVGs met Vue 3

Waarom Reactive SVGs gebruiken?

  • Flexibiliteit: Met Vue 3 kun je bijna elk SVG-attribuut eenvoudig binden aan de gegevens van je component.
  • Complexiteit: SVG's kunnen complexere vormen weergeven dan eenvoudige CSS en Vue 3's reactiviteit maakt het eenvoudig om ze te animeren.
  • LOC's: Deze methode vereist minder code dan je zou verwachten, waardoor je SFC's lichter worden.

Voorbeeld uit de echte wereld: Een halfronde gestippelde statusbalk

Deze statusbalk bestaat uit twee SVG cirkels: één voor de achtergrond om het totaal aantal stappen weer te geven, en een andere om de huidige voortgang weer te geven. Vue 3 werkt de lijnarray van de voortgangscirkel dynamisch bij om de huidige voortgang of status weer te geven. De SVG zorgt voor een duidelijke en interactieve visualisatie van de voortgang met heel weinig code en logica. De SVG's zijn inline en Vue 3 controleert alle relevante gegevens. Bekijk de broncode om te begrijpen hoe het werkt.

2 / 10
<template>
    <div class="progress">
        <div class="progress__circle">
            <svg :width="widthPixels" :height="heightPixels">
                <circle :r="radius" :cy="centerPixel" :cx="centerPixel" :stroke-width="strokeWidth"
                    class="progress__circle__dashes progress__circle__dashes--background"> </circle>
                <circle :r="radius" :cy="centerPixel" :cx="centerPixel" :stroke-width="strokeWidth"
                    class="progress__circle__dashes progress__circle__dashes--progress"> </circle>
            </svg>
        </div>
        <div class="progress__labelslot">
            <slot></slot>
        </div>
    </div>
</template>
  
<style>
.progress {
    display: flex;
    position: relative;
    justify-content: center;
    align-items: center;
}

.progress__labelslot {
    position: absolute;
    font-size: 2rem;
    font-weight: bold;
    user-select: none;
}

.progress circle {
    fill: transparent;
    user-select: none;
    pointer-events: none;
}

/* NOTE v-bind in CSS - new in Vue 3 */
/* https://vuejs.org/api/sfc-css-features.html#v-bind-in-css */
.progress__circle {
    transform: rotate(v-bind(startAngleString));
}

.progress__circle__dashes {
    stroke-linecap: butt;
}

/* https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray */

.progress__circle__dashes--background {
    stroke-dasharray: v-bind(baseDashes);
    stroke: #ecf0f444;
}

.progress__circle__dashes--progress {
    stroke-dasharray: v-bind(progressDashes);
    stroke: #42b883;
    filter: drop-shadow(0px 0px 10px #42b883);
}
</style>
  
<script lang="ts" setup>
const props = defineProps({
    numberOfStepsTotal: {
        type: Number,
        default: 5
    },
    currentProgress: {
        type: Number,
        default: 0
    },
});

const widthPixels = 300;
const heightPixels = widthPixels;
const strokeWidth = 45;
const gapSize = 15;
const dashStoppingDistance = 1_000;
const startAngle = 140;
const startAngleString = `${startAngle}deg`;
const centerPixel = Math.round(widthPixels / 2);
const radius = Math.floor(widthPixels / 2 - strokeWidth);
// * 0.725 we got a semi circle
const circumference = 2 * Math.PI * radius * 0.725;

/**
 * returns an array which contains
 * pixel length for dashes and gaps and a long gap at the end
 * @param {number} numberOfSteps - number of dashes
 * @returns {number[]} [25,15,25,15,25,1000], [gap, pause, gap, pause, gap, long pause]
 */
const getBaseDashArray = (numberOfSteps: number): number[] => {
    numberOfSteps = Math.max(numberOfSteps, 1);
    let arr: number[] = [];
    const pixelsForGaps = (numberOfSteps - 1) * gapSize;
    const pixelPerDash = (circumference - pixelsForGaps) / numberOfSteps;
    // -1 to push the last step after the for loop with a long dash pause
    for (let i = 0; i < numberOfSteps - 1; i++) {
        arr.push(pixelPerDash, gapSize);
    }
    arr.push(pixelPerDash, dashStoppingDistance);
    return arr;
};

const baseDashes = computed<number[]>(() => {
    return getBaseDashArray(props.numberOfStepsTotal);
});

// returns a slice of baseDashes
const progressDashes = computed<number[]>(() => {
    let dashes: number[];
    if (props.currentProgress <= 0) {
        dashes = [0];
    } else {
        // *2 to capture every dash with it's following dash pause
        // -1 to ignore the last pause - which gets replaced by the long pause
        const stepsToSlice = Math.min(
            props.currentProgress * 2 - 1,
            props.numberOfStepsTotal * 2 - 1
        );
        dashes = baseDashes.value.slice(0, stepsToSlice);
    }
    // add the dash stopping distance like we did in getBaseDashArray
    dashes.push(dashStoppingDistance);
    return dashes;
});
</script>

Andere toepassingen

Deze techniek is niet beperkt tot statusbalken, maar kan worden uitgebreid naar verschillende soorten visualisaties die profiteren van reactiviteit, zoals mascotte-animaties, laadindicatoren, tutorial-pijlen/aanwijzers, educatieve animaties, gamification-assets, enz. De combinatie van Vue 3 en SVG's biedt een krachtige maar ondergewaardeerde toolset voor het maken van interactieve en visueel aantrekkelijke webcomponenten.

Er zijn bepaalde pakketten die ik in bijna elk project gebruik. Hier zijn er een paar van:Pakketten die ook erg nuttig zijn:
  • Nuxt i18n - Internationalisering voor Nuxt-projecten.
  • Vite PWA - Zero-config PWA Plugin voor Nuxt 3.
  • Vuefire - Vue plugin waarmee je Firebase-gegevens aan je Vue-componenten kunt binden.
  • Nuxt gtag - Google Tag integratie met ondersteuning voor Google Analytics 4, Google Ads en meer.
  • Ionic - Ionic is een complete open-source SDK voor de ontwikkeling van hybride mobiele apps.

Hoe kan ik uw project ondersteunen?