Vue 3 - Svgator player is always undefined

129 Views Asked by At

Currently I've made an svg animation using SVGator.

I've imported it as an asset to my application and I was trying to make it run.

I've made it work following this Docs

But, as I may need to create a bunch of animations, I've tried to make this more generic.

<script setup lang="ts">
import { ref, defineComponent, h, component, watch } from 'vue';
import exampleSvg from '@assets/example.svg?raw';
interface IComponentProperties {
  svg: string;
}
const componentProperties = withDefaults(defineProps<IComponentProperties>(), {
  svg: () => exampleSvg,
});
const element = ref<HTMLElement>();

const animatedSvg = defineComponent({
  render() {
    return h('svg', { innerHTML: componentProperties.svg, ref: element });
  },
});
function runAnimation() {
  if (!element.value) return;
  const { firstChild } = element.value;
  const svg = firstChild as any;
  svg?.svgatorPlayer?.play();
}
watch(
  () => element.value,
  () => {
    runAnimation();
  },
  {
    immediate: true,
  }
);
</script>
<template>
  <Component :is="animatedSvg" />
</template>

Here is a full vue app with the same code

Why svg?.svgatorPlayer is always null?

2

There are 2 best solutions below

0
Sayf-Eddine On

So to answer your question, it seems your firstChild is an HTMLElement with inside any svgatorPlayer provided.

You are treating the svg as a string using the ?raw.

For fixing this you must follow this answer.

Second of all after reading the documentation provided, you are not using the Javascript with the svg you are putting it as a string this is why your svgatorPlayer equals to null because it doesn't exist while the js is not executed a solution will be to put the svg in a v-html to execute it but be carefull with the XSS injections.

<template>
  <div v-html="exampleSvg"></div>
  <Component :is="animatedSvg" />
</template>
0
Vinicius Andrade On

I've made it work.

For anyone else that is struggling with it, here is my Vue3 Component that handles it.

Important: This component was designed to work with the SvgGator animated svg, using javascript and it was tested only using animations exported with Animation Start "Programmatic" option

<script setup lang="tsx">
import { Buffer } from 'buffer';
import { ref, defineComponent, h, component, watch,computed } from 'vue';
declare type ComponentSize =
    | number
    | string
    | { width: number; height: number; minHeight?: number; minWidth?: number; maxHeight?: number; maxWidth?: number };
export interface ILoadingLogoComponentProperties {
    base64RawSvg?: string;
    speed?: number;
    indeterminate?: boolean;
    iterations?: number;
    fps?: number;
    direction?: number;
    size?: ComponentSize;
}
export interface IAnimatedPlayer {
    play: () => void;
    pause: () => void;
    stop: () => void;
    toggle: () => void;
    restart: () => void;
    setSpeed: (speed: number) => void;
    indeterminate: () => void;
    setIterations: (iterations: number) => void;
    setFps: (fps: number) => void;
    setDirection: (direction: number) => void;
    isPlaying: () => boolean;
    setOptions: (options?: IAnimatedPlayerOptions) => void;
    state: () => string;
}
export interface IAnimatedPlayerOptions {
    speed?: number;
    iterations?: number;
    fps?: number;
    direction?: number;
    indeterminate: boolean;
}
export interface ISvgElementAndScript {
    svgElement: any;
    script: string;
}
const componentProperties = withDefaults(defineProps<ILoadingLogoComponentProperties>(), {
    direction: 1,
    fps: 60,
    indeterminate: false,
    iterations: 1,
    speed: 1,
});
const animatedSvgElement = ref<HTMLElement>();
const animatedSvg = ref<any>(createComponent());
const componentKey = ref<number>(0);
const animatedPlayer = ref<IAnimatedPlayer>();
function createComponent(): any {
    return defineComponent({
        render() {
            if (isBase64(componentProperties.base64RawSvg)) {
                return h('svg', {
                    ref: animatedSvgElement,
                    innerHTML: decodeBase64(componentProperties.base64RawSvg),
                    style: {
                        ...size.value,
                    },
                });
            }
            return returnInvalidSvg();
        },
    });
}
const size = computed(() => {
    if (typeof componentProperties.size === 'number')
        return { width: componentProperties.size, height: componentProperties.size };
    if (typeof componentProperties.size === 'string')
        return { width: componentProperties.size, height: componentProperties.size };
    return componentProperties.size;
});
function isBase64(text?: string) {
    if (!text) return false;
    const base64Regex = /^[A-Za-z0-9+/=]+$/;
    return base64Regex.test(text);
}
function decodeBase64(text?: string) {
    if (!text) return '';
    return Buffer.from(text, 'base64');
}
function returnInvalidSvg() {
    return (
        <div>
            Invalid SVG. <b>base64RawSvg</b> Must be a valid base64 svg{' '}
        </div>
    );
}
function getSvgElementAndScript(): ISvgElementAndScript | undefined | null {
    if (!animatedSvgElement.value) return null;
    const { firstChild } = animatedSvgElement.value;
    if (!firstChild) return null;
    for (const child of firstChild?.childNodes ?? []) {
        if (child.nodeName !== 'script') continue;
        if (!child.textContent) continue;
        return { svgElement: firstChild, script: child.textContent };
    }
    return null;
}
function canRunAnimation(svgElementAndScript?: ISvgElementAndScript | null) {
    if (!svgElementAndScript) return false;
    if (!svgElementAndScript.svgElement) return false;
    return !!svgElementAndScript.script;
}
function createOptionsBasedOnComponentProperties(): IAnimatedPlayerOptions {
    return {
        speed: componentProperties.speed,
        direction: componentProperties.direction,
        fps: componentProperties.fps,
        indeterminate: componentProperties.indeterminate,
        iterations: componentProperties.iterations,
    };
}
function startEmbededScript(script: string): void {
    try {
        eval(script);
    } catch (e) {
        throw new Error('Failed to run embeded script');
    }
}
function runAnimation() {
    const svgElementAndScript = getSvgElementAndScript();
    if (!canRunAnimation(svgElementAndScript)) return;
    const { svgElement, script } = svgElementAndScript!;
    try {
        startEmbededScript(script);
        if (!svgElement.svgatorPlayer) return;
        const svgatorPlayer = svgElement.svgatorPlayer;
        const options = createOptionsBasedOnComponentProperties();
        animatedPlayer.value = createAnimatedPlayer(svgatorPlayer, options);
        animatedPlayer.value.play();
    } catch (e) {
        console.log(e);
    }
}
function incrementComponentKey() {
    componentKey.value += 1;
}
function createAnimatedPlayer(player?: any, options?: IAnimatedPlayerOptions): IAnimatedPlayer {
    let _isPlaying = true;
    function play() {
        if (!player) return;
        player.play();
        _isPlaying = true;
    }
    function pause() {
        if (!player) return;
        player.pause();
        _isPlaying = false;
    }
    function stop() {
        if (!player) return;
        player.stop();
        _isPlaying = false;
    }
    function toggle() {
        if (!player) return;
        _isPlaying ? pause() : play();
    }
    function isPlaying(): boolean {
        return _isPlaying;
    }
    function restart() {
        if (!player) return;
        _isPlaying = false;
        player.restart();
        _isPlaying = true;
        console.log('restarted');
    }
    function state() {
        if (!player) return 'not_inialized';
        return player.state;
    }
    function setSpeed(speed: number) {
        if (!player) return;
        if (speed >= 100) {
            player.set('speed', 100);
            return;
        }
        if (speed <= 0) {
            player.set('speed', 0);
            return;
        }
        player.set('speed', speed);
    }
    function setDirection(direction: number) {
        if (!player) return;
        player.set('direction', direction);
    }
    function setFps(fps: number) {
        if (!player) return;
        if (fps >= 100) {
            player.set('fps', 100);
            return;
        }
        if (fps <= 0) {
            player.set('fps', 0);
            return;
        }
        player.set('fps', fps);
    }

    function indeterminate() {
        setIterations(0);
    }
    function setIterations(interactions: number) {
        if (!player) return;
        player.set('iterations', interactions);
    }
    function setOptions(options?: IAnimatedPlayerOptions) {
        const persistedOptions: IAnimatedPlayerOptions = {
            speed: 1,
            direction: 1,
            fps: 60,
            iterations: 0,
            indeterminate: true,
            ...options,
        };
        setSpeed(persistedOptions.speed!);
        setDirection(persistedOptions.direction!);
        setFps(persistedOptions.fps!);
        if (persistedOptions.indeterminate) indeterminate();
        else setIterations(persistedOptions.iterations ?? 0);
    }
    setOptions(options);
    return {
        play,
        pause,
        toggle,
        stop,
        isPlaying,
        restart,
        setSpeed,
        setDirection,
        setFps,
        setOptions,
        indeterminate,
        setIterations,
        state,
    };
}

watch(
    () => componentProperties.size,
    () => {
        animatedSvg.value = createComponent();
        incrementComponentKey();
    },
    { immediate: true },
);
watch(
    () => componentProperties.base64RawSvg,
    () => {
        animatedSvg.value = createComponent();
        incrementComponentKey();
    },
    {
        immediate: true,
    },
);

watch(
    () => animatedSvgElement.value,
    () => {
        runAnimation();
        console.log(animatedPlayer.value?.state());
    },
    {
        immediate: true,
    },
);
watch(
    () => componentProperties.speed,
    () => {
        animatedPlayer.value?.setSpeed(componentProperties.speed);
    },
    {
        immediate: true,
    },
);
watch(
    () => componentProperties.direction,
    () => {
        animatedPlayer.value?.setDirection(componentProperties.direction);
    },
    {
        immediate: true,
    },
);
watch(
    () => componentProperties.fps,
    () => {
        animatedPlayer.value?.setFps(componentProperties.fps);
    },
    {
        immediate: true,
    },
);
watch(
    () => componentProperties.indeterminate,
    () => {
        animatedPlayer.value?.indeterminate();
    },
    {
        immediate: true,
    },
);
watch(
    () => componentProperties.iterations,
    () => {
        animatedPlayer.value?.setIterations(componentProperties.iterations ?? 0);
    },
    {
        immediate: true,
    },
);
</script>
<template>
  <Component :is="animatedSvg" :key="componentKey" />
</template>

Pre requisites

You may need to install the following packages in order to work:

DEPENDENCIES

  • "buffer": "^6.0.3"

DEV_DEPENDENCIES

  • "@vitejs/plugin-vue-jsx": "^3.0.1"
  • "vite-svg-loader": "^4.0.0"

And modify your vite.config.ts to implemente those plugins

Imports

import vueJsx from '@vitejs/plugin-vue-jsx';
import svgLoader from 'vite-svg-loader';

Plugins

 plugins: [
    svgLoader(),
    vueJsx(),
   //...rest of plugins
]

You can checkout a fully working example here