I have the following component, it is a very simple countdown to 0. When the countdown reaches 0 it fires a redirect. When I put this component on my page itself, the onMounted function fires the interval(counterFunc()). When I isolate it into a component the interval(counterFunc()) does not fire. How can I fix this?
<template>
{{ counter }}
</template>
<script setup lang="ts">
const props = defineProps<{
time: number;
}>();
let counter = toRef(props, 'time')
const counterFunc = () => {
setInterval(() => {
counter.value--;
if (counter.value == 0) {
clearError({redirect: '/'})
}
}, 1000);
};
onMounted(() => {
counterFunc();
});
Using it in a template:
<AppCountdown :time=10></AppCountdown>
Here is a working example:
<script setup lang="ts">
import { defineProps, ref, onMounted, toRef } from "vue";
const props = defineProps<{
time: number;
}>();
const counter = ref(props.time); // set a proper ref that can be mutated
const counterFunc = () => {
setInterval(() => {
counter.value = counter.value - 1; // mutate the ref as expected
if (counter.value == 0) {
console.log("clearError"); // replace with your clear error function
}
}, 1000);
};
onMounted(counterFunc);
</script>
<template>
{{ counter }}
</template>
You can try it here: https://codesandbox.io/s/heuristic-stallman-niiqzl?file=/src/App.vue:0-419
Related
I am building a project with Nuxt and I need to know the size of the wrapper to adjust the grid setting
(I want a single line, I could still do this in pure CSS probably by hiding the items)
It's my first time using composition API & script setup
<script setup>
const props = defineProps({
name: String
})
const width = ref(0)
const wrapper = ref(null)
const maxColumns = computed(() => {
if (width.value < 800) return 3
if (width.value < 1000) return 4
return 5
})
onMounted(() => {
width.value = wrapper.value.clientWidth
window.onresize = () => {
width.value = wrapper.value.clientWidth
console.log(width.value);
};
})
</script>
<template>
<div class="category-preview" ref="wrapper">
...
</div>
</template>
The console log is working properly, resizing the window and refreshing the page will return 3, 4 or 5 depending on the size, but resizing won't trigger the computed value to change
What am I missing ?
In my test enviroment I had to rename your ref 'width' into something else. After that it did worked for me with a different approach using an event listener for resize events.
You can do something like this:
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'
const wrapperWidth = ref(0)
const wrapper = ref(null)
// init component
onMounted(() => {
getDimensions()
window.addEventListener('resize', debounce(() => getDimensions(), 250))
})
// remove event listener after destroying the component
onUnmounted(() => {
window.removeEventListener('resize', debounce)
})
// your computed property
const maxColumns = computed(() => {
if (wrapperWidth.value < 800) {
return 3
} else if (wrapperWidth.value < 1000) {
return 4
} else {
return 5
}
})
// get template ref dimensions
function getDimensions () {
const { width } = wrapper.value.getBoundingClientRect()
wrapperWidth.value = width
}
// wait to call getDimensions()
// it's just a function I have found on the web...
// there is no need to call getDimensions() after every pixel have changed
const debounce = (func, wait) => {
let timeout
return function executedFunction (...args) {
const later = () => {
timeout = null
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}
</script>
<template>
<div ref="wrapper">
{{ maxColumns }} // will change after resize events
</div>
</template>
New to Vue and Vite but trying to get dynamic layouts working properly here. I believe I have what is needed but the issue it the meta seems to always come up as an empty object or undefined.
AppLayout.vue
<script setup lang="ts">
import AppLayoutDefault from './stub/AppLayoutDefault.vue'
import { markRaw, watch } from 'vue'
import { useRoute } from 'vue-router'
const layout = markRaw(AppLayoutDefault)
const route = useRoute()
console.log('Current path: ', route.path)
console.log('Route meta:', route.meta)
watch(
() => route.meta,
async (meta) => {
try {
const component = await import(`./stub/${meta.layout}.vue`)
layout.value = component?.default || AppLayoutDefault
} catch (e) {
layout.value = AppLayoutDefault
}
},
{ immediate: true }
)
</script>
<template>
<component :is="layout"> <router-view /> </component>
</template>
App.vue
<script setup lang="ts">
import AppLayout from '#/layouts/AppLayout.vue'
</script>
<template>
<AppLayout>
<router-view />
</AppLayout>
</template>
Each and every route has the appropriate meta set with a property called layout.
I just can't seem to get he layout applied correctly on the first load or any click of a link in the navbar(which are just router-link) for that matter.
The main problem is layout is initialized as markRaw(), which is not a reactive ref:
const layout = markRaw(AppLayoutDefault) // ❌ not reactive
Solution
Initialize layout as a ref.
Instead of watching the full route object, watch route.meta?.layout because that's the only relevant field for the handler.
Wrap layout's new values with markRaw() to avoid reactivity on the component definition.
const layout = ref() 1️⃣
watch(
() => route.meta?.layout as string | undefined, 2️⃣
async (metaLayout) => {
try {
const component = metaLayout && await import(/* #vite-ignore */ `./${metaLayout}.vue`)
layout.value = markRaw(component?.default || AppLayoutDefault) 3️⃣
} catch (e) {
layout.value = markRaw(AppLayoutDefault) 3️⃣
}
},
{ immediate: true }
)
demo
The solution from Tony is great. But you need to add computed inside the watch because it will prevent code execution for the very first "layout" variable initialized which is still undefined and it will cause unnecessary rendering. So, please add the computed function inside the watch function. Also you need to watch "route.path" not the "route.meta?.layout".
import DefaultLayout from '#/layouts/Default.vue'
import { markRaw, ref } from '#vue/reactivity'
import { computed, watch } from '#vue/runtime-core'
import { useRoute } from 'vue-router'
const layout = ref()
const route = useRoute()
watch(
computed(() => route.path), async () => {
let metaLayout = route.meta?.layout
try {
const metaLayoutComponent = metaLayout && await import(`./layouts/${metaLayout}.vue`)
layout.value = markRaw(metaLayoutComponent?.default || DefaultLayout)
} catch (error) {
layout.value = markRaw(DefaultLayout)
}
}
);
In vuejs3 app
I read data with axios request from backend API. I see that data are passed to internal
component, but I do not see content of the child component is rendered on the page.
Parent component:
<template>
<div class="row m-0 p-0" v-show="forumCategories.length && isPageLoaded">
<div v-for="(nextActiveForumCategory, index) in forumCategories" :key="nextActiveForumCategory.id" class="col-sm-12 col-md-6 p-2 m-0">
index::{{ index}}
<forum-category-block
:currentLoggedUser="currentLoggedUser"
:nextActiveForumCategory="nextActiveForumCategory"
:index="index"
:is_show_location="true"
></forum-category-block>
</div>
</div>
</template>
<script>
import ForumCategoryBlock from '#/views/forum/ForumCategoryBlock.vue'
import { useStore } from 'vuex'
export default {
name: 'forumsByCategoryPage',
components: {
ForumCategoryBlock,
},
setup () {
const store = useStore()
const orderBy = ref('created_at')
const orderDirection = ref('desc')
const forumsPerPage = ref(20)
const currentPage = ref(1)
let forumsTotalCount = ref(0)
let forumCategories = ref([])
let isPageLoaded = ref(false)
let credentialsConfig = settingCredentialsConfig
const currentLoggedUserToken = computed(
() => {
return store.getters.token
}
)
const currentLoggedUser = computed(
() => {
return store.getters.user
}
)
const forumsByCategoryPageInit = async () => {
loadForums()
}
function loadForums() {
isPageLoaded = false
let credentials = getClone(credentialsConfig)
credentials.headers.Authorization = 'Bearer ' + currentLoggedUserToken.value
let filters = { current_page: currentPage.value, order_by: orderBy.value, order_direction: orderDirection.value }
const apiUrl = process.env.VUE_APP_API_URL
axios.get(apiUrl + '/forums-by-category', filters, credentials)
.then(({ data }) => {
console.log('/forums-by-category data::')
console.log(data)
forumCategories.value = data.forumCategories
forumsTotalCount.value = data.forumsTotalCount
isPageLoaded = true
console.log('++forumCategories::')
console.log(forumCategories)
})
.catch(error => {
console.error(error)
isPageLoaded = true
})
} // loadForums() {
onMounted(forumsByCategoryPageInit)
return {
currentPage, orderBy, orderDirection, isPageLoaded, loadForums, forumCategories, getHeaderIcon, pluralize, forumsTotalCount, forumCategoriesTitle, currentLoggedUser
}
} // setup
</script>
and ForumCategoryBlock.vue:
<template>
<div class="">
<h1>INSIDE</h1>
<fieldset class="bordered" >
<legend class="blocks">Block</legend>
nextActiveForumCategory::{{ nextActiveForumCategory}}<br>
currentLoggedUser::{{ currentLoggedUser}}<br>
index::{{ index }}<br>
</fieldset>
</div>
</template>
<script>
import { computed } from 'vue'
export default {
name: 'forumCategoryBlock',
props: {
currentLoggedUser: {
type: Object,
default: () => {}
},
nextActiveForumCategory: {
type: Object,
default: () => {}
},
index: {
type: Number,
default: () => {}
}
},
setup (props) {
console.log('setup props::')
console.log(props)
const nextActiveForumCategory = computed({
get: () => props.value.nextActiveForumCategory
})
const currentLoggedUser = computed({
get: () => props.value.currentLoggedUser
})
const index = computed({
get: () => props.index
})
return { /* currentLoggedUser, nextActiveForumCategory, index */ }
}
}
</script>
What I see in browser : https://prnt.sc/vh7db9
What is wrong abd how to fix it ?
MODIFIED :
I understood WHERE the error :
<div class="row m-0 p-0" v-show="forumCategories.length && isPageLoaded" style="border: 2px dotted red;">
if to remove 2nd condition && isPageLoaded in a line above I see content.
But looks like that var isPageLoaded is not reactive and I do not see why?
If is declared with ref and is declared in return of setup method.
But looks like as I modify it in loadForums method it does not work in template...
Thanks!
isPageLoaded is losing its reactivity because loadForums() is changing its type from ref to Boolean:
isPageLoaded = true // ❌ no longer a ref
isPageLoaded is a ref, so your code has to access it through its value property. It's probably best to use const instead of let here to avoid this mistake:
const isPageLoaded = ref(false)
isPageLoaded.value = true // ✅
Is there a way to bind (and expose in the component itself) "data"/"methods" using the Composition API in using a "render" function (and not a template) in Vue.js?
(Previously, in the Options API, if you use a render method in the options configuration, all of the data/props/methods are still exposed in the component itself, and can be still accessed by componentInstance.someDataOrSomeMethod)
Templated Component:
<template>
<div #click="increment">{{ counter }}</div>
</template>
<script lang="ts">
import { defineComponent, Ref, ref, computed } from '#vue/composition-api'
export default defineComponent({
name: 'TranslationSidebar',
setup () {
const counter: Ref<number> = ref(0)
const increment = () => {
counter.value++
}
return {
counter: computed(() => counter.value),
increment
} // THIS PROPERTY AND METHOD WILL BE EXPOSED IN THE COMPONENT ITSELF
}
})
</script>
Non-Templated "Render" Component:
<script lang="ts">
import { defineComponent, Ref, ref, createElement } from '#vue/composition-api'
export default defineComponent({
name: 'TranslationSidebar',
setup () {
const counter: Ref<number> = ref(0)
const increment = () => {
counter.value++
}
return () => {
return createElement('div', { on: { click: increment } }, String(counter.value))
} // THE COUNTER PROP AND INCREMENT ARE BOUND, BUT NOT EXPOSED IN THE COMPONENT ITSELF
}
})
</script>
Options API using the render option:
<script lang="ts">
export default {
name: 'Test',
data () {
return {
counter: 0
}
},
mounted () {
console.log('this', this)
},
methods: {
increment () {
this.counter++
}
},
render (h) {
return h('div', { on: { click: this.increment } }, this.counter)
}
}
</script>
I can't make any claim that this is the proper way as it seems pretty hacky and not really ideal, but it does do what I need to do for now. This solution uses the provide method which grants access to properties/methods provided via componentInstance._provided. Not really ecstatic doing it this way though:
<script lang="ts">
import { defineComponent, Ref, ref, createElement, provide, computed } from '#vue/composition-api'
export default defineComponent({
name: 'TranslationSidebar',
setup () {
const counter: Ref<number> = ref(0)
const increment = () => {
counter.value++
}
provide('increment', increment)
provide('counter', computed(() => counter.value))
return () => {
return createElement('div', { on: { click: increment } }, String(counter.value))
}
}
})
</script>
Here is a working Vue2 example:
<template>
<div>
<h1>O_o</h1>
<component :is="name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
export default {
data: () => ({
isShow: false
}),
computed: {
name() {
return this.isShow ? () => import('./DynamicComponent') : '';
}
},
methods: {
onClick() {
this.isShow = true;
}
},
}
</script>
Redone under Vue3 option does not work. No errors occur, but the component does not appear.
<template>
<div>
<h1>O_o</h1>
<component :is="state.name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
import {ref, reactive, computed} from 'vue'
export default {
setup() {
const state = reactive({
name: computed(() => isShow ? import('./DynamicComponent.vue') : '')
});
const isShow = ref(false);
const onClick = () => {
isShow.value = true;
}
return {
state,
onClick
}
}
}
</script>
Has anyone studied the vue2 beta version? Help me please. Sorry for the clumsy language, I use Google translator.
Leave everything in the template as in Vue2
<template>
<div>
<h1>O_o</h1>
<component :is="name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
Change only in "setup" using defineAsyncComponent
You can learn more about defineAsyncComponent here
https://labs.thisdot.co/blog/async-components-in-vue-3
const isShow = ref(false);
const name = computed (() => isShow.value ? defineAsyncComponent(() => import("./DynamicComponent.vue")) : '')
const onClick = () => {
isShow.value = true;
}
Try this
import DynamicComponent from './DynamicComponent.vue'
export default {
setup() {
const state = reactive({
name: computed(() => isShow ? DynamicComponent : '')
});
...
return {
state,
...
}
}
}
The issue with this seems to be to do with the way we register components when we use the setup script - see the official docs for more info. I've found that you need to register the component globally in order to reference it by string in the template.
For example, for the below Vue component:
<template>
<component :is="item.type" :item="item"></component>
</template>
<script setup lang="ts">
// Where item.type contains the string 'MyComponent'
const props = defineProps<{
item: object
}>()
</script>
We need to register the component in the main.ts, as such:
import { createApp } from 'vue'
import App from './App.vue'
import MyComponent from './MyComponent.vue'
var app = createApp(App);
app.component('MyComponent', MyComponent)
app.mount('#app')
Using 'watch' everything works.
<template>
<component :is="componentPath"/>
</template>
<script lang="ts">
import {defineComponent, ref, watch, SetupContext} from "vue";
export default defineComponent({
props: {
path: {type: String, required: true}
},
setup(props: { path: string }, context: SetupContext) {
const componentPath = ref("");
watch(
() => props.path,
newPath => {
if (newPath !== "")
import("#/" + newPath + ".vue").then(val => {
componentPath.value = val.default;
context.emit("loaded", true);
});
else {
componentPath.value = "";
context.emit("loaded", false);
}
}
);
return {componentPath};
}
});
</script>