Cannont mount twice the same Vue 3 component - vuejs3

Im' mounting a Vue 3 component as a loading animation during a frame loading with :
createApp(App).mount(e);
When the frame is loaded, the component is erased in the html by the frame content (but not by Vue I guess). This behaviour is managed by an external system of Vue.
When I'm trying to reload the component with the same command, the component is not displayed.
If I only mount again the component, I have the following warning in the console and the component is not displayed :
"[Vue warn]: App has already been mounted.
If you want to remount the same app, move your app creation logic into a factory function and create fresh app instances for each mount - e.g. `const createMyApp = () => createApp(App)`".
I've found also a way to replicate the issue with :
const app = createApp(App)
app.mount(el);
el.innerHtml = '';
app.mount(el);
I also try to unmount the component with no more success :
const app = createApp(App);
app.mount(el);
app.unmount();
app.mount(el);
So what is the correct way to display again the same vue component when it is erased externally ?

To mount/unmount 2 instances of Vue, it is necessary to use render method.
So, for exemple below, this solution works :
var app = createApp({ render: () => h(App) })
app.mount(el);
el.innerHtml = '';
app = createApp({ render: () => h(App) })
app.mount(el);
Don't forget to import h() function with :
import { createApp, h } from 'vue';

An far more better option is to use Vue custom elements.
First, create a classical SFC with ce.vue extension :
//test.ce.vue
<template>
<div class="text-primary">Test</div>
</template>
<script>
export default {
name: 'test',
};
</script>
<style>
.text-primary {
color: red;
}
</style>
And then in the main script :
//app.js
import Test from 'test.ce.vue';
const testElement = defineCustomElement(Test);
customElements.define('test-element', testElement);
document.body.appendChild(document.createElement('test-element'));
The component will be rendered as soon as is detected in the document:
<test-component>
#shadow-root (open)
<style>
.text-primary {
color: red;
}
</style>
<div class="text-primary">Test</div>
</test-component>

Related

Lazy loading fontawesome icons in vue3 + vite not working in DEV

In my vue3+vite project I'm using the official fontawesome vue3 package (see use with vue).
In order to enable tree-shaking you need to statically load the necessary icons (or possibly all of them) in advance using library.add. See for instance the following App.vue
<script setup>
import { ref, computed } from "vue";
import { library } from "#fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "#fortawesome/vue-fontawesome";
import { definition } from "#fortawesome/free-solid-svg-icons/faTruck";
library.add(definition);
const icon = ref("");
const showIcon = () => { icon.value = `fa-solid fa-truck`; };
</script>
<template>
<button #click="showIcon">Show Truck Icon</button>
<div v-if="icon">
<font-awesome-icon :icon="icon" />
</div>
</template>
here we statically load the truck icon and when you click the button the icon shows up.
What I was trying to do is loading the icons on demand (in this case, only when the button is clicked), using the following code:
<script setup>
import { ref, computed } from "vue";
import { library } from "#fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "#fortawesome/vue-fontawesome";
const modules = import.meta.glob(
"../node_modules/#fortawesome/free-solid-svg-icons/faTruck.js",
{ eager: false, import: "definition" }
);
const icon = ref("");
const showIcon = () => {
Object.values(modules)[0]().then((elem) => {
library.add(elem);
icon.value = `fa-solid fa-truck`;
});
};
</script>
<template>
<button #click="showIcon">Show Truck Icon</button>
<div v-if="icon">
<font-awesome-icon :icon="icon" />
</div>
</template>
But this doesn't work in "develpment" (npm run dev):
it makes a call to http://localhost:5173/node_modules/#fortawesome/free-solid-svg-icons/faTruck.js
then raises an error: Uncaught (in promise) ReferenceError: exports is not defined
while it works fine when the bundle is built (npm run build then for example serve the dist folder with http-server)
I suspect the problem is related to the fact that in development mode faTruck.js module is used "as is", while it is transpiled in the build phase.
Is there a solution?
NOTE:
The example contains only the "truck" because is over-simplified, but actually any icon should be loaded; i.e. the actual path in import.meta.glob should be ../node_modules/#fortawesome/free-solid-svg-icons/fa*.js
Full steps to reproduce the issue:
npm create vue#3 # accepts all defaults
cd vue-project
npm i #fortawesome/fontawesome-svg-core #fortawesome/free-solid-svg-icons #fortawesome/vue-fontawesome
# replace src/App.vue with the one indicated above
# run in dev with
npm run dev
# or build for prod and then expose using http-server
npm run build
npx http-server dist
Explaination
According to the Vite pre-bundling docs:
Vite's dev serves all code as native ESM. Therefore, Vite must convert dependencies that are shipped as CommonJS or UMD into ESM first
But when you use glob import with dynamic variables, your modules will not be pre-bundled. Since #fortawesome/free-solid-svg-icons/faTruck.js is a CommonJS file, it can not be used directly in ESM. And you are right that Vite does transform the module on production build, so it works well on production.
You may think about the optimizeDeps.include option but unfortunately, it does not help in this situation. Even if you add your module to the include list, Vite does pre-bundle your module but it will not use that pre-bundled file for your dynamic import. It still uses the file in node_modules/#fortawesome/free-solid-svg-icons/ folder.
I'm afraid that there is no straightforward solution to your problem. See this issue
Workaround
Just make it work differently on dev and prod.
const showIcon = async () => {
let x = 'faTruck'
let definition
if (import.meta.env.PROD) {
const iconModule = await import(
`../node_modules/#fortawesome/free-solid-svg-icons/${x}.js`
)
definition = iconModule.definition
} else {
const iconModule = await import(`#fortawesome/free-solid-svg-icons`)
definition = iconModule[x]
}
library.add(definition)
icon.value = `fa-solid fa-truck`
}
With this code, you still have the benefit of lazy loading on production and a smooth dev server to work
Another approach
Hard-coding your import list like so:
const showIcon = async (iconName) => {
const listImport = {
faTruck: () => import(`#fortawesome/free-solid-svg-icons/faTruck`),
faWarning: () => import(`#fortawesome/free-solid-svg-icons/faWarning`),
}
const iconModule = await listImport[iconName]()
console.log('iconModule', iconModule)
library.add(iconModule.definition)
}
But I bet you have hundreds of icons in your list so it hardly is an option

vue & vitest, data-v-[random-number] attribute being added to html

Introduction: I am working with vite and vitest, I am doing snapshot tests for components that are 100% template and do not contain any logic
Problem: data-v-[random-number] It is being added to the root element of each component and snapshots are always different
What i want: Understand why im getting this data-v-[random-number] and if possible, a way to avoid this problem
Short example code:
BaseText.vue:
<script setup lang="ts"></script>
<template>
<span><slot></slot></span>
</template>
BaseText.spec.ts:
import { describe, it, expect } from "vitest";
import { shallowMount } from "#vue/test-utils";
import BaseText from "./BaseText.vue";
describe(name, () => {
const wrapper = shallowMount(BaseText);
it("MatchSnapshot", () => {
expect(wrapper.html()).toMatchSnapshot();
});
});
Error when runing tests:
- Expected ""<span data-v-25e5131c=""></span>""
+ Received ""<span data-v-b3462088=""></span>""

Vue - using props on custom elements fails using createApp and mount?

We would like to pass props to custom elements that uses createApp
// index.html
<div id="my-root">
<my-element prop1="abc"></my-element>
</div>
// my-element.vue
<script lang="ts" setup>
const props = defineProps<{ prop1: number }>();
</script>
<template>
{{props.prop1}}
</template>
This works fine, but as our custome element get bigger we would like to register components and use e.g pinia and other tools. Do use those we need to add createApp and mount it. But then prop1 is always undefined
// main.ts
import ...<lots of imports>
import AppCe from "./AppWebComponent.ce.vue";
import { createPinia } from "pinia";
// Adding code below is causing prop1 to be undefined - if we skip this part, prop1 works fine
const pinia = createPinia();
const app = createApp(App);
app.use(pinia).use(ConfirmDialog);
app.component(...<lots of components>);
app.mount("#my-root");
const ceApp = defineCustomElement(AppCe);
customElements.define("my-element", ceApp);
update:
Here's a sample without: https://stackblitz.com/edit/vue3-script-setup-with-vite-56rizn?file=src/my-element/my-element-main.js
And here's a sample with the createApp: https://stackblitz.com/edit/vue3-script-setup-with-vite-gtkbaq?file=index.html
Any idea on how we could solve this?
We have a fallback, that is to do a getElementById and read the attribute value in the mounted callback - but that is not an optimal solution.
Thanks for any ideas!
update2:
Here's an attempt using #duannex suggestion. We're getting closer, the app is availible, components registered, but still no sigar. : https://stackblitz.com/edit/vue3-script-setup-with-vite-ofwcjt?file=src/my-element/defineCustomElementWrapped.js
Based on update2 with the wrapped defineCustomElement; Just pass the props to the render function:
render() {
return h(component, this.$props)
},
https://stackblitz.com/edit/vue3-script-setup-with-vite-vfdnvg?file=src/my-element/defineCustomElementWrapped.js

Problem creating a WebComponent from a SFC with Vue 3.2.9

I am trying to create a web component in Vue3. For this I use the Vue cli with the target library. Everything works as expected.
The problem is that I can't get any values from the props parameter of the setup function.
If I use the component as a Vue component the props work. As a web component i cant get any value from the props parameter
Its seams that the property wc-Test is not forwarded properly.
Does anyone have any ideas?
The Component Code
<template>
<a class="btn">
<slot></slot>
</a>
</template>
<script>
import {version } from "vue";
export default {
name: 'xn-button',
props: {
variant: {
default: 'normal',
type: String
}
},
setup(props, context) {
console.log(`Vue: ${version}`)
console.log(props)
console.log(props.variant)
},
}
</script>
<style></style>
Usage as Part of Vue Library:
<xn-button variant="Vue-Test">Test1</xn-button>
Usage as WebComponent:
<xn-button variant="wc-Test">Test2</xn-button>
Console output:
image

React-tooltip and Next.js SSR issue

I use the react-tooltip library in my Next.js app.
I noticed that every time I refresh a website while visiting a page that uses the tooltip I get an error:
react-dom.development.js:88 Warning: Prop `dangerouslySetInnerHTML` did not match.
CSS classes are different on the client and on the server
The weird part is I do not get that error while navigating from a random page to a page that uses the react-tooltip.
The tooltip related code:
<StyledPopularityTooltipIcon src="/icons/tooltip.svg" alt="question mark" data-tip="hello world" />
<ReactTooltip
effect="solid"
className="tooltip"
backgroundColor="#F0F0F0"
arrowColor="#F0F0F0"
clickable={true}
/>
I had the same issue, I had to use state to detect when component has been mounted, and show the tooltip only after that.
P.S. You don't see the error when navigating, because the page is not rendered on server when you navigate, it's all front-end :)
In case you are using any server-side rendering (like Next.js) - you will need to make sure your component is mounted first before showing the react-tooltip.
I fixed this by using the following:
import React, { useEffect, useState } from 'react';
const [isMounted,setIsMounted] = useState(false); // Need this for the react-tooltip
useEffect(() => {
setIsMounted(true);
},[]);
return (<div>
{isMounted && <ReactTooltip id={"mytip"} effect={"solid"} />}
<span data-tip={"Tip Here"} data-for={"mytip"}>Hover me</span>
</div>)
You should wrap your JSX in the following component:
import React, { useEffect, useState } from 'react';
const NoSsr = ({ children }): JSX.Element => {
const [isMounted, setMount] = useState(false);
useEffect(() => {
setMount(true);
}, []);
return <>{isMounted ? children : null}</>;
};
export default NoSsr;
Like this:
<NoSsr>
<YourJSX />
</NoSsr>
If you are working with NEXTJS this might be a good approach, you can check the documentation here as well, also if you are working with data-event, globalEventOff or any other prop and is not hiding or not working in your localhost, this only occurs in Development Strict Mode. ReactTooltip works fine in Production code with React 18. So you can set reactStrictMode : false, in your next.config.js to test it locally and then set it back to true, hope this helps :) info reference here
import dynamic from 'next/dynamic'
const ReactTooltip = dynamic(() => import('react-tooltip'), { ssr : false });
function Home() {
return (
<div>
<Button
data-tip
data-event="click focus"
data-for="toolTip"
onClick={():void => ()}
/>
<ReactTooltip id="toolTip" globalEventOff="click"/>
</div>
)
}
export default Home

Resources