onMounted hook is not called by Vue - vuejs3

I have two components, one is managing data, the other is a vue template. I have simplified them here. The problem is that when the locations come in via the fetch, the locations in the vue template stays empty. I've checked with isRef() and that returns true, but it's just an empty array. Looking in the Vue dev tools panel, the locations does have elements in the array.
Locations.js
import {
ref,
isRef,
onMounted,
} from 'vue';
export default function useLocations () {
const locations = ref([]);
const loadImageData = (locId) => {
isRef(locations); // === true
// #FIXME locations.value is always empty here.
locations.value.forEach( (loc,key) => {
console.debug( loc.id, locId )
})
};
const getLocations = async () => {
const locs = await apiFetch({ path: '/wp/v2/tour-location'});
locations.value = locs;
};
onMounted( getLocations );
return {
locations,
getLocations,
loadImageData,
};
}
App.vue
<template>
<div class="location">
<h1>{{ location.name }}</h1>
<img :src="location.main_image" />
</div>
</template>
<script>
import useLocations from '#/composables/Locations';
export default {
name: 'Location',
props: [,'location'],
data () {return {}},
watch: {
location: {
// deep: true,
immediate: true,
handler: function(){
const { loadImageData } = useLocations();
loadImageData( location.id );
}
}
},
}
</script>
When loadImageData() is called from the Location.vue component, locations is always an empty array. Why doesn't it get updated in that function as it does in other places within the app?

onMounted is a hook registration function.
These lifecycle hook registration functions can only be used synchronously during setup(), since they rely on internal global state to locate the current active instance (the component instance whose setup() is being called right now). Calling them without a current active instance will result in an error.
[emphasis mine]
Docs
As you are using your useLocations composition function outside setup(), your getLocations function is never called and locations is always empty array
To explain it further. You do not have to call onMounted (or any other hook registration function) directly inside setup(). It is perfectly fine to place that call into separate composition function outside any component (as you did) but that function must then be used from inside the setup()

Related

Render dynamic Vue 3 slot content in StorybookJS

Using the example Storybook code at the bottom of this post, I expect to see a Primary button rendered containing the text, "Primary Button", but instead the button renders following code, verbatim:
(...args) => {
// If a user calls a compiled slot inside a template expression (#1745), it
// can mess up block tracking, so by default we disable block tracking and
// force bail out when invoking a compiled slot (indicated by the ._d flag).
// This isn't necessary if rendering a compiled `<slot>`, so we flip the
// ._d flag off when invoking the wrapped fn inside `renderSlot`.
if (renderFnWithContext._d) {
setBlockTracking(-1);
}
const prevInstance = setCurrentRenderingInstance(ctx);
const res = fn(...args);
setCurrentRenderingInstance(prevInstance);
if (renderFnWithContext._d) {
setBlockTracking(1);
}
if (true) {
devtoolsComponentUpdated(ctx);
}
return res;
}
How do I render Vue component slot content in Storybook?
The Storybook code:
// ./components/button/Button.stories.js
import AtxButton from './Button';
import ButtonTypes from './Button.types';
export default {
/* 👇 The title prop is optional.
* See https://storybook.js.org/docs/vue/configure/overview#configure-story-loading
* to learn how to generate automatic titles
*/
title: 'Button',
component: AtxButton,
argTypes: {
default: {
description: 'The default Vue slot',
control: 'text'
}
}
};
//👇 We create a “template” of how args map to rendering
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { AtxButton },
setup() {
//👇 The args will now be passed down to the template
return { args };
},
template: `
<AtxButton v-bind="args">{{ args.default }}
</AtxButton>
`
});
//👇 Each story then reuses that template
export const Primary = Template.bind({});
Primary.args = {
intensity: ButtonTypes.Intensity.Primary,
default: 'Primary Button'
};
export const Secondary = Template.bind({});
Secondary.args = {
intensity: ButtonTypes.Intensity.Secondary,
default: 'Secondary Button'
};
I had syntax error in my component code. The Button component is created using a render function, which includes slot content by accessing this.slots.default(). I was getting a console error claiming "this.slots.default() is not a function", so I changed it to this.slots.default, assuming it might have been a getter.
That wasn't the right solution, of course. I changed it back to this.slots.default(), and then ensured that slot content was always included in the Storybook story. Now it works perfectly!

How to test computed value inside setup function in Vue.js 3 with vue-test-utils & Jest

I am getting "TypeError: Cannot add property myData, object is not extensible" on setData
Hello.vue
<template>
<div v-if="isEditable" id="myEditDiv">
<button type="button"> Edit </button>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, reactive} from "vue"
export default defineComponent({
setup() {
const myObject = {myName:"", myNumber:""}
let myData = reactive({myObject})
const isEditable = computed(() => {
return myData.myObject.myName.startsWith('DNU') ? false : true
})
return {
isEditable
}
}
})
</script>
Hello.spec.ts
import { shallowMount } from '#vue/test-utils'
import Hello from '#/components/Hello.vue'
import { reactive } from 'vue'
describe('Hello.vue Test', () => {
it('is isEditable returns FALSE if NAME starts with DNU', async () => {
const myObject = {myName:"DNU Bad Name", myNumber:"12345"}
let myData = reactive({myObject})
const wrapper = shallowMount(Hello)
await wrapper.setData({'myData' : myData})
expect(wrapper.vm.isEditable).toBe(false)
})
})
I also tried to see if that DIV is visible by:
expect(wrapper.find('#myEditDiv').exists()).toBe(false)
still same error. I might be completely off the path, so any help would be appreciated.
Update
This is possible several different ways. There's two issues that need to be addressed.
The variable has to be made available. You can use vue's expose function in setup (but getting the value is really messy: wrapper.__app._container._vnode.component.subTree.component.exposed😱) or just include it in the return object (accessible through wrapper.vm).
change how you mutate the data in the test.
your test has
const myObject = {myName:"DNU Bad Name", myNumber:"12345"}
let myData = reactive({myObject})
const wrapper = shallowMount(Hello)
await wrapper.setData({'myData' : myData})
even if setData was able to override the internal, it would not work.
the problem is that the setup function has this
let myData = reactive({ myObject });
const isEditable = computed(() => {
return myData.myObject.myName.startsWith("DNU") ? false : true;
});
where editable is using a computed generated from that instance of myData. If you override myData with a separate reactive, the computed will still continue to use the old one. You need to replace the contents of the reactive and not the reactive itself
To update the entire content of the reactive, you can use:
Object.assign(myReactive, myNewData)
you can make that a method in your component, or just run that from the test. If you update any value within the reactive (like myData.myObject) you can skip the Object.asign
Here are several versions of how you can test it.
Component:
<template>
<div v-if="isEditable" id="myEditDiv">
<button type="button">Edit</button>
</div>
</template>
<script>
import { computed, defineComponent, reactive } from "vue";
export default defineComponent({
setup(_, { expose }) {
const myObject = { myName: "", myNumber: "" };
let myData = reactive({ myObject });
const isEditable = computed(() => {
return myData.myObject.myName.startsWith("DNU") ? false : true;
});
const updateMyData = (data) => Object.assign(myData, data);
expose({ updateMyData });
return {
isEditable,
updateMyData,
myData
};
},
});
</script>
the test
import { shallowMount } from "#vue/test-utils";
import MyComponent from "#/components/MyComponent.vue";
const data = { myObject: { myName: "DNU Bad Name" } };
describe("MyComponent.vue", () => {
it.only("sanity test", async () => {
const wrapper = shallowMount(MyComponent);
expect(wrapper.vm.isEditable).toBe(true);
});
it.only("myData", async () => {
const wrapper = shallowMount(MyComponent);
Object.assign(wrapper.vm.myData, data);
expect(wrapper.vm.isEditable).toBe(false);
});
it.only("myData", async () => {
const wrapper = shallowMount(MyComponent);
wrapper.vm.myData.myObject = data.myObject;
expect(wrapper.vm.isEditable).toBe(false);
});
it.only("updateMyData method via return", async () => {
const wrapper = shallowMount(MyComponent);
wrapper.vm.updateMyData(data);
expect(wrapper.vm.isEditable).toBe(false);
});
it.only("updateMyData method via expose🙄", async () => {
const wrapper = shallowMount(MyComponent);
wrapper.__app._container._vnode.component.subTree.component.exposed.updateMyData(
data
);
expect(wrapper.vm.isEditable).toBe(false);
});
});
It is not possible through setData
from the docs:
setData
Updates component internal data.
Signature:
setData(data: Record<string, any>): Promise<void>
Details:
setData does not allow setting new properties that are not defined in the component.
Also, notice that setData does not modify composition API setup() data.
It seems that updating internals with composition API is incompatible with setData. See the method name setData, refers to this.data and was likely kept in the vue test utils mostly for backwards compatibility.
I suspect the theory is that it's bad practice anyway to test, what would be considered, an implementation detail and the component test should focus on validating inputs an outputs only. Fundamentally though, this is a technical issue, because the setup function doesn't expose the refs and reactives created in the setup.
There is a MUCH easier way to do this.....
Put your composables in a separate file
Test the composables stand alone.
Here is the vue file:
<template>
<div>
<div>value: {{ counter }}</div>
<div>isEven: {{ isEven }}</div>
<button type="button" #click="increment">Increment</button>
</div>
</template>
<script setup lang='ts'>
import {sampleComposable} from "./sample.composable";
const {isEven, counter, increment} = sampleComposable();
</script>
Here is the composable:
import {computed, ref} from 'vue';
export function sampleComputed() {
const counter = ref(0);
function increment() {
counter.value++;
}
const isEven = computed(() => counter.value % 2 === 0);
return {counter, increment, isEven};
}
Here is the test:
import {sampleComposable} from "./sample.composable";
describe('sample', () => {
it('simple', () => {
const computed = sampleComposable();
expect(computed.counter.value).toEqual(0);
expect(computed.isEven.value).toEqual(true);
computed.increment();
expect(computed.counter.value).toEqual(1);
expect(computed.isEven.value).toEqual(false);
computed.increment();
expect(computed.counter.value).toEqual(2);
expect(computed.isEven.value).toEqual(true);
})
});
This just 'works'. You don't have to deal w/ mounting components or any other stuff, you are JUST TESTING JAVASCRIPT. It's faster and much cleaner. It seems silly to test the template anyway.
One way to make this easier to test is to put all of your dependencies as arguments to the function. For instance, pass in the props so it's easy to just put in dummy values as need. Same for emits.
You can tests watches as well. You just need to flush the promise after setting the value that is being watched:
composable.someWatchedThing.value = 6.5;
await flushPromises();
Here is my flushPromises (which I found here):
export function flushPromises() {
return new Promise(process.nextTick);
}

Vue 3 Composition API: Array.length Not Reactive

I'm struggling to figure out how to make properties of an array reactive. Is this possible? In the example below, the filteredResults array itself is reactive, and working, but neither the resultCountRef (wrapped in reactive()) nor the resultCount fields are reactive. In otherwords, if I click the Filter for Apples button, the filteredResults changes to just the one item, but the two count fields remain at 3. Note that using {{filteredResults.length}} in the template does work as expected. Here is a working sample.
And here is the code (a Search.vue composition API component, and a useFilter composition function):
Search.vue:
<template>
<div>resultCountRef: {{resultCountRef}}</div>
<div>resultCount: {{resultCount}}</div>
<div>filteredResults.length: {{filteredResults.length}}</div>
<div>filteredResults: {{filteredResults}}</div>
<div>filters: {{filters}}</div>
<div><button #click="search()">Search</button></div>
<div><button #click="applyFilter('apples')">Filter for Apples</button></div>
</template>
<script>
import { reactive } from 'vue';
import useFilters from './useFilters.js';
export default {
setup(props) {
const products = reactive(['apples', 'oranges', 'grapes']);
const { filters, applyFilter, filteredResults } = useFilters(products);
const resultCountRef = reactive(filteredResults.value.length);
const resultCount = filteredResults.value.length;
return {
resultCountRef,
resultCount,
filteredResults,
filters,
applyFilter,
};
},
};
</script>
useFilters.js:
import { ref, onMounted } from 'vue';
function filterResults(products, filters) {
return products.filter((product) => filters.every(
(filter) => {
return product.includes(filter);
},
));
}
export default function useFilters(products) {
const filters = ref([]);
const filteredResults = ref([...products]);
const applyFilter = (filter) => {
filters.value.push(filter);
filteredResults.value.splice(0);
filteredResults.value.splice(0, 0, ...filterResults(products, filters.value));
};
return { filters, applyFilter, filteredResults };
}
UPDATED
const resultCountRef = reactive(filteredResults.value.length);
The reason why reactive is not working is because filteredResults.value.length returned a simple number and not referencing to anything.
From #JimCopper 's comment:
The returned object is simply providing an observable object that wraps the object/value/array passed in (in my case the filteredResults.length value). That returned object resultCountRef is the object that is tracked and can be then modified, but that's useless in my case and it's why reactive didn't work. )
As the resultCount depends on filteredResults, you can use computed to monitor the change
Here is the modified playground
setup(props) {
const products = reactive(['apples', 'oranges', 'grapes']);
const { filters, applyFilter, filteredResults } = useFilters(products);
// const resultCountRef = reactive(filteredResults.length);
const resultCount = computed(() => filteredResults.length) // use computed to react to filteredResults changes
return {
// resultCountRef,
resultCount,
filteredResults,
filters,
applyFilter,
};
},
The new doc does not have a very nice explanation on computed so i just quote it from the old doc explanation
A computed property is used to declaratively describe a value that depends on other values. When you data-bind to a computed property inside the template, Vue knows when to update the DOM when any of the values depended upon by the computed property has changed.

Vue3 Wait till Child Component is Mounted

I want to wait for a child component to mount before rendering a tooltip. While waiting, I will have a placeholder to display to the user.
An example component would be:
<template>
<div v-if="!mounted">Loading...</div>
<child-component></child-component>
</template>
<script>
export default defineComponent({
setup() {
const mounted = ref(false);
return {
mounted
}
}
});
</script>
After doing research, it looks like Vue2 supports life-cycle hooks on the components, which would change the above code to:
<template>
<div v-if="!mounted">Loading...</div>
<child-component #hook:mounted="mountedCheck"></child-component>
</template>
<script>
export default {
export default defineComponent({
setup() {
const mounted = ref(false);
const mountedCheck = () = {
mounted.value = true;
}
return {
mounted, mountedCheck
}
}
});
}
However, I cant seem to get the #hook:mounted to work. Is there something similar in Vue3, or am I missing something?
The syntax has changed in Vue3. Now you need to use #vue:mounted instead of #hook:mounted.
See Vue 3 Migration Guide - VNode Lifecycle Events for details
Also, keep in mind that some event names like destroyed and beforeDestroy have been renamed to unmounted and beforeUnmount respectively in Vue3
The child component needs to emit that hook
child:
<script>
export default defineComponent({
setup() {
onMounted(() => {
const { emit } = getCurrentInstance();
emit('mounted');
})
return {};
}
});
</script>
parent
<template>
<div v-if="!mounted">Loading...</div>
<child-component #mounted="mountedCheck"></child-component>
</template>

Redux pattern for populating the state, and then building components from that state?

I've got a component that builds search/sort filters that can be selected. I want the selected state of those filters to be tracked in redux so that the search builder can subscribe and see when they change and update appropriately. the thing I'm trying to figure out how to do (in a way that doesn't feel weird) is populate the filter objects into the state. Eg, right now in the <Search /> component I have something like:
<OptionPicker
group={'searchFilters'}
options={{word: 'price', active: true},
{word: 'distance', active: false},
{word: 'clowns', active: false}}
/>
So how to get those props into state to be used without triggering multiple element renders. I'm also rendering the app on the server as well, so for the initial attachment render, the state already has the options.
In the OptionPicker component I've got:
class OptionPicker extends Component {
constructor(props) {
super(props)
if (!props.optionstate) {
this.props.addOptionState(props)
}
}
render() {
return {this.props.optionstate.word.map((word) => <Option ... />)}
}
}
function mapStateToProps(state, props) {
return {
optionstate: state.optionstate[props.group],
};
}
function mapDispatchToProps(dispatch) {
return {
addOptionState: (props) => {
dispatch(addOptionState(props));
},
optionToggled: (group, word) => {
dispatch(updateOptionState(group, word));
}
};
}
export default connect(mapStateToProps,mapDispatchToProps)(OptionGroup);
This kinda works, but there exists a time when render is called before the redux state has been populated, which throws an error. I could guard against that, but none of this feels "right". Shouldn't that prop always be there? Is there a pattern for this that I'm missing?
I agree with you in that the prop should always be there. The pattern I use for this is to set up an initial state and to pass it to the reducer function:
export const INITIAL_STATE = {
optionstate: { /* all filters are present but deactivated */ }
};
export default function (state = INITIAL_STATE, action) {
// reduce new actions into the state
};

Resources