Create computed property dependent on variables within onMounted() lifecycle hook - vue.js

I have a component that needs to calculate some values using the DOM, and so I have the onMounted() lifecycle hook. In onMounted() I calculate some values (technically, they're computed properties) dependent on DOM elements.
I then use the values found in another computed property, leftOffset. I need to use leftOffset in template (it changes some CSS, not really relevant).
Below is my setup():
setup() {
let showContent = ref(false);
let windowWidth = ref(null);
let contentHider = ref(null);
let contentHiderXPos ;
let contentHiderWidth ;
let leftWidth;
onMounted(() => {
// The DOM element will be assigned to the ref after initial render
windowWidth = ref(window.innerWidth);
contentHiderXPos = computed(() => contentHider.value.getBoundingClientRect().left);
contentHiderWidth = computed(() => contentHider.value.getBoundingClientRect().width);
leftWidth = computed(() => contentHiderXPos.value + contentHiderWidth.value);
});
let leftOffset = computed(() => {
return -(windowWidth.value - leftWidth.value)
});
return {
contentHider,
leftOffset,
}
contentHider references a the DOM element of one of the divs defined in template.
My problem that leftOffest.value is undefined, because it tries to access windowWidth.value and leftWidth.value, which are also undefined. I've also tried putting leftOffset inside onMounted(), but then I can't access it from template (it's undefined).
How can I re-structure my code so that leftOffset can both be accessed from template and can also access the values found in onMounted()?
I've searched online, but couldn't find anything specific to the composition API.

Your usage of the ref is wrong. Follow the comments in the below snippet.
Also this is assuming that you do not want the window.innerwidth to be reactive.
// dont create another ref => you are trying to assign a new object here, thus breaking the reactivity
// windowWidth = ref(window.innerWidth);
// do this instead
windowWidth.value = window.innerWidth
If you want the innerWidth to be reactive, you have to use the native event listener like this;
const windowWidth = ref(window.innerWidth)
onMounted(() => {
window.addEventListener('resize', () => {windowWidth.value = window.innerWidth} )
})
onUnmounted(() => {
window.removeEventListener('resize', () => {windowWidth.value = window.innerWidth})
})

Related

Vue 3 / Pinia: how to get the updated value from the store when it's changed?

Using Vue 3 with Pinia in an Interia setup. I can see Pinia in my Dev Tools and I can console log out the (starting) value.
I have a component set up like:
<Comment>
<ChildComment>
In ChildComment, I have this function
const toggleEdit = () => {
commentStore.childEditing = !editMode.value
editMode.value = !editMode.value;
};
When I trigger this function, the value updates in the store:
In the <Comment> component, I have tried using various methods like so:
import { useStore } from "../../store/commentStore";
const commentStore = useStore();
// USING STATE DIRECTLY
// const isEditingChildComment = ref(commentStore.childEditing);
// const isEditingChildComment = reactive(commentStore.childEditing);
// const isEditingChildComment = computed(() => commentStore.childEditing);
// USING GETTER
// const isEditingChildComment = ref(commentStore.getChildEditing);
// const isEditingChildComment = reactive(commentStore.getChildEditing);
// const isEditingChildComment = computed(() => commentStore.getChildEditing);
But isEditingChildComment is never updated in any of the above 6 scenarios, it remains as false:
Am I missing something obvious?
If you use a computed property, you need to return a value. For example:
const isEditingChildComment = computed(() => {
return commentStore.childEditing);
}
It looks also if you are mutating the store state directly. I think you should use an Action for that, to guarantee the working of reactivity.
Try using the store.$subscribe function. Here's a link to the docs
Example from docs:
cartStore.$subscribe((mutation, state) => {
// import { MutationType } from 'pinia'
mutation.type // 'direct' | 'patch object' | 'patch function'
// same as cartStore.$id
mutation.storeId // 'cart'
// only available with mutation.type === 'patch object'
mutation.payload // patch object passed to cartStore.$patch()
// persist the whole state to the local storage whenever it changes
localStorage.setItem('cart', JSON.stringify(state))
})

At what point are props contents available (and are they reactive once they are)?

I pass data into a component via props:
<Comment :comment="currentCase.Comment" #comment="(c) => currentCase.Comment=c"></Comment>
currentCase is updated via a fetch call to an API during the setup of the component (the one that contains the line above)
The TS part of <Comment> is:
<script lang="ts" setup>
import { Comment } from 'components/helpers'
import { ref, watch } from 'vue'
const props = defineProps<{comment: Comment}>()
const emit = defineEmits(['comment'])
console.log(props)
const dateLastUpdated = ref<string>(props.comment?.DateLastUpdated as string)
const content = ref<string>(props.comment?.Content as string)
watch(content, () => emit('comment', {DateLastUpdated: dateLastUpdated, Content: content}))
</script>
... where Comment is defined in 'components/helpers' as
export class Comment {
DateLastUpdated?: string
Content?: string
public constructor(init?: Partial<Case>) {
Object.assign(this, init)
}
}
content is used in the template, but is empty when the component is rendered. I added a console.log() to check whether the props were known - and what is passed is undefined at that point:
▸ Proxy {comment: undefined}
When looking at the value of the props once the application is rendered, their content is correct:
{
"comment": {
"DateLastUpdated": "",
"Content": "comment 2 here"
}
}
My question: why is comment not updated when props are available (and when are their content available?)
I also tried to push the update later in the reactive cycle, but the result is the same:
const dateLastUpdated = ref<string>('')
const content = ref<string>('')
onMounted(() => {
console.log(props)
dateLastUpdated.value = props.comment?.DateLastUpdated as string
content.value = props.comment?.Content as string
watch(content, () => emit('comment', {DateLastUpdated: dateLastUpdated, Content: content}))
})
Vue lifecycle creates component instances from parent to child, then mounts them in the opposite order. Prop value is expected to be available in a child if it's available at this time in a parent. If currentCase is set asynchronously in a parent, the value it's set to isn't available on component creation, it's a mistake to access it early.
This disables the reactivity:
content.value = props.comment?.Content as string
props.comment?.Content === undefined at the time when this code is evaluated, it's the same as writing:
content.value = undefined;
Even if it weren't undefined, content wouldn't react to comment changes any way, unless props.comment is explicitly watched.
If content is supposed to always react to props.comment changes, it should be computed ref instead:
const content = computed(() => props.comment?.Content as string);
Otherwise it should be a ref and a watcher:
const content = ref();
const unwatch = watchEffect(() => {
if (props.comment?.Content) {
content.value = props.comment.Content;
unwatch();
...
}
});

Vue JS composition API can't access array

I am currently trying to learn Vue JS without ever having encountered Javascript.
All the brackets, arrows, etc. are driving me crazy.
With the Composition API, I come across a question that I can't successfully google.
That's working:
setup() {
const store = useStore ();
const packagingdata = ref ([]);
const loadpackagingdata = async () => {
await store.dispatch (Actions.LOADPACKAGING_LIST, 10);
packagingdata.value = store.getters.getPackagingData;
return {
packagingdata,
}
I can access {{packagingdata}}. That contains an array.
{{Packagingdata.products}}
does work
but {{packagingdata.products [0]}} doesn't work.
But when I add it to the setup () like this:
setup() {
const store = useStore ();
const packagingdata = ref ([]);
const getProducts = ref ([]);
const loadpackagingdata = async () => {
await store.dispatch (Actions.LOADPACKAGING_LIST, 10);
packagingdata.value = store.getters.getPackagingData;
getProducts.value = store.getters.getProducts;
};
return {
packagingdata,
getProducts
}
then {{ getProducts }} returns what I wanted even if the getter function only is like this:
get getAddress() {
return this.packagingdata["products"][0];
}
What is happening there?
What am I doing wrong? I would prefer to not create a ref() and getterfunction for every computed value.
Whats the solution with computed?
best regards
I found two methods to avoid the error.
add a v-if="mygetter.length" in the template
check in the getter whether the variable is set and otherwise return false
Both of these prevent an error already occurring when the page is loaded that [0] cannot be found

reactive object not updating on event emitted from watch

i'm building a complex form using this reactive obj
const formData = reactive({})
provide('formData', formData)
inside the form one of the components is rendered like this:
<ComboZone
v-model:municipality_p="formData.registry.municipality"
v-model:province_p="formData.registry.province"
v-model:region_p="formData.registry.region"
/>
this is the ComboZone render function:
setup(props: any, { emit }) {
const { t } = useI18n()
const { getters } = useStore()
const municipalities = getters['registry/municipalities']
const _provinces = getters['registry/provinces']
const _regions = getters['registry/regions']
const municipality = useModelWrapper(props, emit, 'municipality_p')
const province = useModelWrapper(props, emit, 'province_p')
const region = useModelWrapper(props, emit, 'region_p')
const updateConnectedField = (key: string, collection: ComputedRef<any>) => {
if (collection.value && collection.value.length === 1) {
console.log(`update:${key} => ${collection.value[0].id}`)
emit(`update:${key}`, collection.value[0].id)
} else {
console.log(`update:${key} =>undefined`)
emit(`update:${key}`, undefined)
}
}
const provinces = computed(() => (municipality.value ? _provinces[municipality.value] : []))
const regions = computed(() => (province.value ? _regions[province.value] : []))
watch(municipality, () => updateConnectedField('province_p', provinces))
watch(province, () => updateConnectedField('region_p', regions))
return { t, municipality, province, region, municipalities, provinces, regions }
}
useModelWrapper :
import { computed, WritableComputedRef } from 'vue'
export default function useModelWrapper(props: any, emit: any, name = 'modelValue'): WritableComputedRef<any> {
return computed({
get: () => props[name],
set: (value) => {
console.log(`useModelWrapper update:${name} => ${value}`)
emit(`update:${name}`, value)
}
})
}
problem is that the events emitted from useModelWrapper update the formData in the parent template correctly, the events emitted from inside the watch function are delayed by one render....
TL;DR;
Use watchEffect instead of watch
...with the caveat that I haven't tried to reproduce, my guess is that you're running into this issue because you're using watch which runs lazily.
The lazy nature is a result of deferring execution, which is likely why you're seeing it trigger on the next cycle.
watchEffect
Runs a function immediately while reactively tracking its dependencies and re-runs it whenever the dependencies are changed.
watch
Compared to watchEffect, watch allows us to:
Perform the side effect lazily;
Be more specific about what state should trigger the watcher to re-run;
Access both the previous and current value of the watched state.
found a solution, watchEffect wasn't the way.
Looks like was an issue with multiple events of update in the same tick, worked for me handling the flush of the watch with { flush: 'post' } as option of the watch function.
Try to use the key: prop in components. I think it will solve the issue.

How to get the last value emit from vue event bus?

I'm using vue event bus, for example in resize window:
resize.ts:
import Vue from 'vue';
export const windowResize = new Vue();
when I emit value:
const innerWidth = …;
const innerHeight = …;
resize.$emit('resize', { innerWidth, innerHeight });
when I received value:
resize.$on('resize', ({ innerWidth, innerHeight }) => { … });
My question is it possible to get the get the last value has been dispatched?
for example say in lazy component I want to have the width and height that calculate in the parents components? I think something like subject behavior (not BehaviorSubject) in rxjs…
In this lazy component I want to write
resize.$on('resize', ({ innerWidth, innerHeight }) => { … });
but I don't want to trigger the resize event.