How I can get Vue component data while using script setup - vue.js

I need to rewrite
this code
export default Vue.extend<Props>({
functional: true,
render(h, { props, slots, data }) {
const { href, external, target, type, button } = props;
const slot = slots().default;
console.log(data);
....
to Vue 3 Composition script setup,
So I managed to get
<script setup lang="ts">
import {h, useSlots} from 'vue';
const props = defineProps<Props>();
const slots = useSlots();
...
But how I can get data ?
from this part -> render(h, { props, slots, data }) {
data should contain domProps if have such.. etc
console.log(data);
{
{
attrs: {
target: '_self',
href: 'https://example.com',
button: true
},
class: [
'X-button',
{
'X-button--': false,
'X-button--size-auto': true
}
]
}
Thanks in advance

If you still need this,
useSlots().default() returns the data for the slots, including the DOM props.

Related

Duplicated key error on Vue composition API's return statement

Since Vuetify's support for Vue 3 is still in beta, I'm trying to use Composition API in Vue 2. I'm using it like this:
<script>
import { defineComponent } from '#vue/composition-api'
import { computed, toRef } from 'vue'
import fetchOtherImages from 'some-library'
export default defineComponent({
name: 'PhotoGallery',
props: {
images: {
type: Array,
required: true
}
},
setup(props) {
const { images: imagesFromProps } = toRef(props)
const images = computed(() => [
...imagesFromProps.value.map(image => image.getUrl()),
...fetchOtherImages()
])
return { images }
},
})
</script>
The problem is, it throws vue/no-dupe-keys in that return statement. Am I doing this properly? I'm new to the whole Composition thing and Vue 2's documentation on the subject is not helping.
https://eslint.vuejs.org/rules/no-dupe-keys.html
There are a data named images (returned by setup) and a prop named images, which is against rule vue/no-dupe-keys.
You can rename the data returned by setup:
setup(props) {
const { images: imagesFromProps } = toRef(props)
const images = computed(() => [
...imagesFromProps.value.map(image => image.getUrl()),
...fetchOtherImages()
])
return { imagesComputed: images }
},
By the way, highly recommend Vue 2.7 instead of Vue 2.6 + #vue/composition-api!

Vue useStorage (usevue) always starting clean rather than importing state in Pinia store

I built my app on top of vitesse-nuxt3, and all is going well except for trying to use LocalStorage via vueuse.
Component:
<script setup lang="ts">
const { test } = useTestStore()
</script>
<template>
<div>
<pre>{{ test }}</pre>
<hr>
<input
:id="slug"
v-model="value"
type="text"
>
</div>
</template>
Pinia Store:
import { acceptHMRUpdate, defineStore } from 'pinia'
import { useStorage } from '#vueuse/core'
export const useTestStore = defineStore('test', () => {
const test = ref(
useStorage('test', {
initials: 'It is initials',
}),
)
return ({
test,
})
})
if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(useTestStore, import.meta.hot))
I watch it set the data (in Chrome's dev tools), but it always reloads the default data instead rather than persisting between refreshes.
Thank you.
The problem in your demo is that the component is being rendered server-side, which has no Local Storage, so useStorage() defaults to the given initial value.
One workaround is to render the component on the client only, using the <client-only> component:
<client-only>
<component-that-uses-local-storage />
</client-only>
demo
for store like this like #tony19 said
export const useAuthStore = defineStore({
id: 'auth.store',
state: () => {
token: {
accessToken: useStorage('accessToken', [XXXX], undefined, { serializer: StorageSerializers.object }),
refreshToken: useStorage('refreshToken', [YYYY], undefined, { serializer: StorageSerializers.object })
},
},
})
[XXXX] [YYYY] is default value
after ssr pinia.state.value become to
window.__INITIAL_SSR_CONTEXT__ = {
state: {
"auth.store": {
"token":{
"accessToken":[XXXX],
"refreshToken":[YYYY]
}
}
}
}
on client side reasign the json object to store like this
const ssr_state = (window as any)['__INITIAL_SSR_CONTEXT__']?.['state']
if (ssr_state) {
pinia.state.value = ssr_state
}
so the accessToken, refreshToken property changes to plan object on client side, you can change it but the storage don't update.
my solution:
add one action to store
actions:{
// ...
// call this once when isSSR is true on client side entry
reasignToken() {
this.token = {
accessToken: useStorage('accessToken', this.token.accessToken, undefined, {
serializer: StorageSerializers.object
}),
refreshToken: useStorage('refreshToken', this.token.refreshToken, undefined, {
serializer: StorageSerializers.object
})
}
},
// regular call on server side and client side
setToken(token) {
//...
}
}

Vue3 reactive components on globalProperties

In vuejs 2 it's possible to assign components to global variables on the main app instance like this...
const app = new Vue({});
Vue.use({
install(Vue) {
Vue.prototype.$counter = new Vue({
data: () => ({ value: 1 }),
methods: {
increment() { this.value++ },
}
});
}
})
app.$mount('#app');
But when I convert that to vue3 I can't access any of the properties or methods...
const app = Vue.createApp({});
app.use({
install(app) {
app.config.globalProperties.$counter = Vue.createApp({
data: () => ({ value: 1 }),
methods: {
increment() { this.value++ }
}
});
}
})
app.mount('#app');
Here is an example for vue2... https://jsfiddle.net/Lg49anzh/
And here is the vue3 version... https://jsfiddle.net/Lathvj29/
So I'm wondering if and how this is still possible in vue3 or do i need to refactor all my plugins?
I tried to keep the example as simple as possible to illustrate the problem but if you need more information just let me know.
Vue.createApp() creates an application instance, which is separate from the root component of the application.
A quick fix is to mount the application instance to get the root component:
import { createApp } from 'vue';
app.config.globalProperties.$counter = createApp({
data: () => ({ value: 1 }),
methods: {
increment() { this.value++ }
}
}).mount(document.createElement('div')); 👈
demo 1
However, a more idiomatic and simpler solution is to use a ref:
import { ref } from 'vue';
const counter = ref(1);
app.config.globalProperties.$counter = {
value: counter,
increment() { counter.value++ }
};
demo 2
Not an exact answer to the question but related. Here is a simple way of sharing global vars between components.
In my main app file I added the variable $navigationProps to global scrope:
let app=createApp(App)
app.config.globalProperties.$navigationProps = {mobileMenuClosed: false, closeIconHidden:false };
app.use(router)
app.mount('#app')
Then in any component where I needed that $navigationProps to work with 2 way binding:
<script>
import { defineComponent, getCurrentInstance } from "vue";
export default defineComponent({
data: () => ({
navigationProps:
getCurrentInstance().appContext.config.globalProperties.$navigationProps,
}),
methods: {
toggleMobileMenu(event) {
this.navigationProps.mobileMenuClosed =
!this.navigationProps.mobileMenuClosed;
},
hideMobileMenu(event) {
this.navigationProps.mobileMenuClosed = true;
},
},
Worked like a charm for me.
The above technique worked for me to make global components (with only one instance in the root component). For example, components like Loaders or Alerts are good examples.
Loader.vue
...
mounted() {
const currentInstance = getCurrentInstance();
if (currentInstance) {
currentInstance.appContext.config.globalProperties.$loader = this;
}
},
...
AlertMessage.vue
...
mounted() {
const currentInstance = getCurrentInstance();
if (currentInstance) {
currentInstance.appContext.config.globalProperties.$alert = this;
}
},
...
So, in the root component of your app, you have to instance your global components, as shown:
App.vue
<template>
<v-app id="allPageView">
<router-view name="allPageView" v-slot="{Component}">
<transition :name="$router.currentRoute.name">
<component :is="Component"/>
</transition>
</router-view>
<alert-message/> //here
<loader/> //here
</v-app>
</template>
<script lang="ts">
import AlertMessage from './components/Utilities/Alerts/AlertMessage.vue';
import Loader from './components/Utilities/Loaders/Loader.vue';
export default {
name: 'App',
components: { AlertMessage, Loader }
};
</script>
Finally, in this way you can your component in whatever other components, for example:
Login.vue
...
async login() {
if (await this.isFormValid(this.$refs.loginObserver as FormContext)) {
this.$loader.activate('Logging in. . .');
Meteor.loginWithPassword(this.user.userOrEmail, this.user.password, (err: Meteor.Error | any) => {
this.$loader.deactivate();
if (err) {
console.error('Error in login: ', err);
if (err.error === '403') {
this.$alert.showAlertFull('mdi-close-circle', 'warning', err.reason,
'', 5000, 'center', 'bottom');
} else {
this.$alert.showAlertFull('mdi-close-circle', 'error', 'Incorrect credentials');
}
this.authError(err.error);
this.error = true;
} else {
this.successLogin();
}
});
...
In this way, you can avoid importing those components in every component.

vue.js 2 single file component with dynamic template

I need a single file component to load its template via AJAX.
I search a while for a solution and found some hints about dynamic components.
I crafted a combination of a parent component which imports a child component and renders the child with a dynamic template.
Child component is this:
<template>
<div>placeholder</div>
</template>
<script>
import SomeOtherComponent from './some-other-component.vue';
export default {
name: 'child-component',
components: {
'some-other-component': SomeOtherComponent,
},
};
</script>
Parent component is this
<template>
<component v-if='componentTemplate' :is="dynamicComponent && {template: componentTemplate}"></component>
</template>
<script>
import Axios from 'axios';
import ChildComponent from './child-component.vue';
export default {
name: 'parent-component',
components: {
'child-component': ChildComponent,
},
data() {
return {
dynamicComponent: 'child-component',
componentTemplate: null,
};
},
created() {
const self = this;
this.fetchTemplate().done((htmlCode) => {
self.componentTemplate = htmlCode;
}).fail((error) => {
self.componentTemplate = '<div>error</div>';
});
},
methods: {
fetchTemplate() {
const formLoaded = $.Deferred();
const url = '/get-dynamic-template';
Axios.get(url).then((response) => {
formLoaded.resolve(response.data);
}).catch((error) => {
formLoaded.reject(error);
}).then(() => {
formLoaded.reject();
});
return formLoaded;
},
},
};
</script>
The dynamic template code fetched is this:
<div>
<h1>My dynamic template</h1>
<some-other-component></some-other-component>
</div>
In general the component gets its template as expected and binds to it.
But when there are other components used in this dynamic template (some-other-component) they are not recognized, even if they are correctly registered inside the child component and of course correctly named as 'some-other-component'.
I get this error: [Vue warn]: Unknown custom element: some-other-component - did you register the component correctly? For recursive components, make sure to provide the "name" option.
Do I miss something or is it some kind of issue/bug?
I answer my question myself, because I found an alternative solution after reading a little bit further here https://forum.vuejs.org/t/load-html-code-that-uses-some-vue-js-code-in-it-via-ajax-request/25006/3.
The problem in my code seems to be this logical expression :is="dynamicComponent && {template: componentTemplate}". I found this approach somewhere in the internet.
The original poster propably assumed that this causes the component "dynamicComponent" to be merged with {template: componentTemplate} which should override the template option only, leaving other component options as defined in the imported child-component.vue.
But it seems not to work as expected since && is a boolean operator and not a "object merge" operator. Please somebody prove me wrong, I am not a JavaScript expert after all.
Anyway the following approach works fine:
<template>
<component v-if='componentTemplate' :is="childComponent"></component>
</template>
<script>
import Axios from 'axios';
import SomeOtherComponent from "./some-other-component.vue";
export default {
name: 'parent-component',
components: {
'some-other-component': SomeOtherComponent,
},
data() {
return {
componentTemplate: null,
};
},
computed: {
childComponent() {
return {
template: this.componentTemplate,
components: this.$options.components,
};
},
},
created() {
const self = this;
this.fetchTemplate().done((htmlCode) => {
self.componentTemplate = htmlCode;
}).fail((error) => {
self.componentTemplate = '<div>error</div>';
});
},
methods: {
fetchTemplate() {
const formLoaded = $.Deferred();
const url = '/get-dynamic-template';
Axios.get(url).then((response) => {
formLoaded.resolve(response.data);
}).catch((error) => {
formLoaded.reject(error);
}).then(() => {
formLoaded.reject();
});
return formLoaded;
},
},
};
</script>

Update VueJs component on route change

Is there a way to re-render a component on route change? I'm using Vue Router 2.3.0, and I'm using the same component in multiple routes. It works fine the first time or if I navigate to a route that doesn't use the component and then go to one that does. I'm passing what's different in props like so
{
name: 'MainMap',
path: '/',
props: {
dataFile: 'all_resv.csv',
mapFile: 'contig_us.geo.json',
mapType: 'us'
},
folder: true,
component: Map
},
{
name: 'Arizona',
path: '/arizona',
props: {
dataFile: 'az.csv',
mapFile: 'az.counties.json',
mapType: 'state'
},
folder: true,
component: Map
}
Then I'm using the props to load a new map and new data, but the map stays the same as when it first loaded. I'm not sure what's going on.
The component looks like this:
data() {
return {
loading: true,
load: ''
}
},
props: ['dataFile', 'mapFile', 'mapType'],
watch: {
load: function() {
this.mounted();
}
},
mounted() {
let _this = this;
let svg = d3.select(this.$el);
d3.queue()
.defer(d3.json, `static/data/maps/${this.mapFile}`)
.defer(d3.csv, `static/data/stations/${this.dataFile}`)
.await(function(error, map, stations) {
// Build Map here
});
}
You may want to add a :key attribute to <router-view> like so:
<router-view :key="$route.fullPath"></router-view>
This way, Vue Router will reload the component once the path changes. Without the key, it won’t even notice that something has changed because the same component is being used (in your case, the Map component).
UPDATE --- 3 July, 2019
I found this thing on vue-router documentation, it's called In Component Guards. By the description of it, it really suits your needs (and mine actually). So the codes should be something like this.
export default () {
beforeRouteUpdate (to, from, next) {
// called when the route that renders this component has changed,
// but this component is reused in the new route.
// For example, for a route with dynamic params `/foo/:id`, when we
// navigate between `/foo/1` and `/foo/2`, the same `Foo` component instance
// will be reused, and this hook will be called when that happens.
// has access to `this` component instance.
const id = to.params.id
this.AJAXRequest(id)
next()
},
}
As you can see, I just add a next() function. Hope this helps you! Good luck!
Below is my older answer.
Only saved for the purpose of "progress"
My solution to this problem was to watch the $route property.
Which will ended up you getting two values, that is to and from.
watch: {
'$route'(to, from) {
const id = to.params.id
this.AJAXRequest(id)
}
},
The alternate solution to this question handles this situation in more cases.
First, you shouldn't really call mounted() yourself. Abstract the things you are doing in mounted into a method that you can call from mounted. Second, Vue will try to re-use components when it can, so your main issue is likely that mounted is only ever fired once. Instead, you might try using the updated or beforeUpdate lifecycle event.
const Map = {
data() {
return {
loading: true,
load: ''
}
},
props: ['dataFile', 'mapFile', 'mapType'],
methods:{
drawMap(){
console.log("do a bunch a d3 stuff")
}
},
updated(){
console.log('updated')
this.drawMap()
},
mounted() {
console.log('mounted')
this.drawMap()
}
}
Here's a little example, not drawing the d3 stuff, but showing how mounted and updated are fired when you swap routes. Pop open the console, and you will see mounted is only ever fired once.
you can use just this code:
watch: {
$route(to, from) {
// react to route changes...
}
}
Yes, I had the same problem and solved by following way;
ProductDetails.vue
data() {
return {
...
productId: this.$route.params.productId,
...
};
},
methods: {
...mapActions("products", ["fetchProduct"]),
...
},
created() {
this.fetchProduct(this.productId);
...
}
The fetchProduct function comes from Vuex store. When an another product is clicked, the route param is changed by productId but component is not re-rendered because created life cycle hook executes at initialization stage.
When I added just key on router-view on parent component app.vue file
app.vue
<router-view :key="this.$route.path"></router-view>
Now it works well for me. Hopefully this will help Vue developers!
I was having the same issue, but slightly different. I just added a watch on the prop and then re-initiated the fetch method on the prop change.
import { ref, watch } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import Page from './content/Page.vue';
import Post from './content/Post.vue';
const props = defineProps({ pageSlug: String });
const pageData = ref(false);
const pageBodyClass = ref('');
function getPostContent() {
let postRestEndPoint = '/wp-json/vuepress/v1/post/' + props.pageSlug;
fetch(postRestEndPoint, { method: 'GET', credentials: 'same-origin' })
.then(res => res.json())
.then(res => {
pageData.value = res;
})
.catch(err => console.log(err));
}
getPostContent();
watch(props, (curVal, oldVal) => {
getPostContent();
});
watch(pageData, (newVal, oldVal) => {
if (newVal.hasOwnProperty('data') === true && newVal.data.status === 404) {
pageData.value = false;
window.location.href = "/404";
}
});
router - index.js
{
path: "/:pageSlug",
name: "Page",
component: Page,
props: true,
},
{
path: "/product/:productSlug",
name: "Product",
component: Product,
},
{
path: "/404",
name: "404",
component: Error404,
}