Problem creating a WebComponent from a SFC with Vue 3.2.9 - web-component

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

Related

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

How to get current route via useRoute outside of router-view component

How can I get the current route using useRoute for a component that's outside of the <router-view />? Is this possible?
Breadcrumbs.vue
<script setup>
import {useRoute} from 'vue-router'
const route = useRoute()
console.log(route.name) // undefined
</script>
App.vue
<template>
<Breadcrumbs />
<router-view />
</template>
The alternative is that I have to put <Breadcrumbs /> at the top of every single view component, and I was hoping to avoid that and instead just include it once in my App.vue
route.name is undefined in your example because that's the initial value when the component is rendered before the route has resolved.
For the component to reactively update based on the current route, use a watcher (e.g., watch or watchEffect):
import { watchEffect } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
watchEffect(() => {
console.log(route.name)
})
demo
In your main file, try to mount app like this
router.isReady().then(() => {
app.mount('#app');
});
then useRoute() should be ready in your component

Error when using v-bind "is" in Dynamic Vue 3 Componet in Nuxt 3 - Failed to resolve component

I am getting an error (below) when trying to provide the "is" attribute inside an object in v-bind on a dynamic component. I am using Nuxt3.
Error: [Vue warn]: Failed to resolve component: component
If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.
{ is: 'AppLink', to: 'myRoute' }
Invalid value used as weak map key
Example when used in a component:
<component v-bind="{ is: 'AppLink', to: 'myRoute' }">
<slot></slot>
</component>
This is what AppLink component looks like:
<template>
<nuxt-link>
<slot></slot>
</nuxt-link>
</template>
<script>
export default {
name: 'AppLink'
};
If I do it like this, it all works as expected wtih no errors:
<component is="AppLink" to="myRoute" }">
<slot></slot>
</component>
Any thoughts interpreting the error would be great. Thanks.
This can happen if you don't set your components to global (not recommended).
import { defineNuxtConfig } from 'nuxt'
export default defineNuxtConfig({
components: {
global: true,
dirs: ['~/components']
},
})
See the docs on dynamic components about this.

Cannont mount twice the same Vue 3 component

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>

Moment.js with Vuejs

I try to print out date time using like the following in vue-for
{{ moment().format('MMMM Do YYYY, h:mm:ss a') }}
but, it does not appear. It's just a blank. How I can try to use moment in vue?
With your code, the vue.js is trying to access the moment() method from its scope.
Hence you should use a method like this:
methods: {
moment: function () {
return moment();
}
},
If you want to pass a date to the moment.js, I suggest to use filters:
filters: {
moment: function (date) {
return moment(date).format('MMMM Do YYYY, h:mm:ss a');
}
}
<span>{{ date | moment }}</span>
[demo]
If your project is a single page application, (eg project created by vue init webpack myproject),
I found this way is most intuitive and simple:
In main.js
import moment from 'moment'
Vue.prototype.moment = moment
Then in your template, simply use
<span>{{moment(date).format('YYYY-MM-DD')}}</span>
In your package.json in the "dependencies" section add moment:
"dependencies": {
"moment": "^2.15.2",
...
}
In the component where you would like to use moment, import it:
<script>
import moment from 'moment'
...
And in the same component add a computed property:
computed: {
timestamp: function () {
return moment(this.<model>.attributes['created-at']).format('YYYY-MM-DD [at] hh:mm')
}
}
And then in the template of this component:
<p>{{ timestamp }}</p>
I made it work with Vue 2.0 in single file component.
npm install moment in folder where you have vue installed
<template>
<div v-for="meta in order.meta">
{{ getHumanDate(meta.value.date) }}
</div>
</template>
<script>
import moment from 'moment';
export default {
methods: {
getHumanDate : function (date) {
return moment(date, 'YYYY-MM-DD').format('DD/MM/YYYY');
}
}
}
</script>
Here is an example using a 3rd party wrapper library for Vue called vue-moment.
In addition to binding Moment instance into Vue's root scope, this library includes moment and duration filters.
This example includes localization and is using ES6 module imports, an official standard, instead of NodeJS's CommonJS module system requires.
import Vue from 'vue';
import moment from 'moment';
import VueMoment from 'vue-moment';
// Load Locales ('en' comes loaded by default)
require('moment/locale/es');
// Choose Locale
moment.locale('es');
Vue.use(VueMoment, { moment });
Now you can use the Moment instance directly in your Vue templates without any additional markup:
<small>Copyright {{ $moment().year() }}</small>
Or the filters:
<span>{{ 3600000 | duration('humanize') }}</span>
<!-- "an hour" -->
<span>{{ [2, 'years'] | duration('add', 1, 'year') | duration('humanize') }}</span>
<!-- "3 years" -->
// plugins/moment.js
import moment from 'moment';
moment.locale('ru');
export default function install (Vue) {
Object.defineProperties(Vue.prototype, {
$moment: {
get () {
return moment;
}
}
})
}
// main.js
import moment from './plugins/moment.js';
Vue.use(moment);
// use this.$moment in your components
For moment.js at Vue 3
npm install moment --save
Then in any component
import moment from 'moment'
...
export default {
created: function () {
this.moment = moment;
},
...
<div class="comment-line">
{{moment(new Date()).format('DD.MM.YYYY [ ] HH:mm')}}
</div>
Moment.js with Vue3 js
npm install moment --save # npm
yarn add moment # yarn
Main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import moment from 'moment'
const app = createApp(App)
app.config.globalProperties.$moment = moment
app.use(router).mount('#app')
Used moments in vue3 js component
{{ $moment(item.created_at).format("YYYY-MM-DD") }} // 2021-07-03
global members are not available by default in your <template>'s scope. But you can easily pass them on using computed properties.
computed: {
moment: () => moment,
console: () => console,
window: () => window
}
Now you can use any of them in your template. i.e: console.log(moment(), window).
Note this doesn't add any overhead.
vue-moment
very nice plugin for vue project and works very smoothly with the components and existing code.
Enjoy the moments...😍
// in your main.js
Vue.use(require('vue-moment'));
// and use in component
{{'2019-10-03 14:02:22' | moment("calendar")}}
// or like this
{{created_at | moment("calendar")}}
I've read the solutions posted here and it seems to be more complex than my solution so I'm presenting this one, what I do is like this
The thing you need:
import moment from 'moment';
...
data() {
return {
moment: moment, // This is the one line of code that you need
}
}
So this is what it looks like
HTML (Now this works):
<h1>{{moment().format('MMMM Do YYYY, h:mm:ss a')}}</h1>
JS:
import moment from 'moment';
export default {
data() {
return {
moment: moment, // This is the one line of code that you need
}
}
}
I'd simply import the moment module, then use a computed function to handle my moment() logic and return a value that's referenced in the template.
While I have not used this and thus can not speak on it's effectiveness, I did find https://github.com/brockpetrie/vue-moment for an alternate consideration
TESTED
import Vue from 'vue'
Vue.filter('formatYear', (value) => {
if (!value) return ''
return moment(value).format('YYYY')
})
Install the moment module:
npm i moment
In your vue component:
import moment from 'moment';
export default {
data(){
},
methods:{
moment(date){ return moment(date) }
}
}
Inside the template:
<span>{{ moment().format('MMMM Do YYYY, h:mm:ss a') }}</span>

Resources