vue2 composition api why does watch not fire - vuejs2

I hope this is simple.
I have this component called "brands", it's code looks like this:
import {
computed,
defineComponent,
getCurrentInstance,
ref,
toRefs,
watch,
} from "#vue/composition-api";
import { useTrackProductImpressions } from "#logic/track-product-impressions";
import BrandComponent from "#components/brand/brand.component.vue";
import { Brand } from "#/_core/models";
export default defineComponent({
name: "Brands",
components: { BrandComponent },
emits: ["onMore"],
props: {
brands: {
type: Array,
required: false,
default: () => [],
},
hasMoreResults: {
type: Boolean,
required: false,
},
itemsToShow: {
type: Number,
required: false,
},
loading: {
type: Boolean,
required: false,
},
total: {
type: Number,
required: false,
},
},
setup(props) {
const instance = getCurrentInstance();
const { itemsToShow, brands } = toRefs(props);
const count = ref(0);
const skip = computed(() => {
if (!itemsToShow.value) return 0;
return count.value * itemsToShow.value;
});
const more = () => {
count.value++;
instance.proxy.$emit("onMore");
};
console.log(brands);
watch(brands, (impressions: Brand[]) => {
console.log(impressions);
if (!impressions.length) return;
const route = instance.proxy.$route;
useTrackProductImpressions(impressions, route);
});
return {
skip,
more,
};
},
});
As you can see, I have set up a watch on the brands property, so that when it's defined it will track it. The problem is, it never gets called.
The first console log, I can see the brands ref and can see it's value is an array, but in the watch method, it never gets invoked.
The component is never initialised without brands:
<brands
:brands="brands"
:hasMoreResults="brandsHasMoreResults"
:itemsToShow="brandsItemsToShow"
:total="brandsTotal"
#onMore="brandsFetchMore()"
v-if="brands.length"
/>
Any help would be appreciated.

Related

Storybook.js (Vue) Docs Template Output

Using StoryBook.js, when I navigate to a component, view its "Docs" and click the "Show Code" button, why do I get code that looks like this...
(args, { argTypes }) => ({
components: { Button },
props: Object.keys(argTypes),
template: '<Button v-bind="$props" />',
})
...as opposed to this...
<Button type="button" class="btn btn-primary">Label</Button>
Button.vue
<template>
<button
:type="type"
:class="'btn btn-' + (outlined ? 'outline-' : '') + variant"
:disabled="disabled">Label</button>
</template>
<script>
export default {
name: "Button",
props: {
disabled: {
type: Boolean,
default: false,
},
outlined: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'button',
},
variant: {
type: String,
default: 'primary',
validator(value) {
return ['primary', 'success', 'warning', 'danger'].includes(value)
}
}
}
}
</script>
Button.stories.js
import Button from '../components/Button'
export default {
title: 'Button',
component: Button,
parameters: {
componentSubtitle: 'Click to perform an action or submit a form.',
},
argTypes: {
disabled: {
description: 'Make a button appear to be inactive and un-clickable.',
},
outlined: {
description: 'Add a border to the button and remove the fill colour.',
},
type: {
options: ['button', 'submit'],
control: { type: 'inline-radio' },
description: 'Use "submit" when you want to submit a form. Use "button" otherwise.',
},
variant: {
options: ['primary', 'success'],
control: { type: 'select' },
description: 'Bootstrap theme colours.',
},
},
}
const Template = (args, { argTypes }) => ({
components: { Button },
props: Object.keys(argTypes),
template: '<Button v-bind="$props" />',
})
export const Filled = Template.bind({})
Filled.args = { disabled: false, outlined: false, type: 'button', variant: 'primary' }
export const Outlined = Template.bind({})
Outlined.args = { disabled: false, outlined: true, type: 'button', variant: 'primary' }
export const Disabled = Template.bind({})
Disabled.args = { disabled: true, outlined: false, type: 'button', variant: 'primary' }
I thought I followed their guides to the letter, but I just can't understand why the code output doesn't look the way I expect it to.
I simply want any of my colleagues using this to be able to copy the code from the template and paste it into their work if they want to use the component without them having to be careful what they select from the code output.
For anyone else who encounters this issue, I discovered that this is a known issue for StoryBook with Vue 3.
As mine is currently a green-field project at the time of writing this, I put a temporary workaround in place by downgrading Vue to ^2.6.
This is OK for me. I'm using the options API to build my components anyway so I'll happily upgrade to Vue ^3 when Storybook resolve the above linked issue.
One of possible options is to use current workaround that I found in the GH issue mentioned by Simon K https://github.com/storybookjs/storybook/issues/13917:
Create file withSource.js in the .storybook folder with following content:
import { addons, makeDecorator } from "#storybook/addons";
import kebabCase from "lodash.kebabcase"
import { h, onMounted } from "vue";
// this value doesn't seem to be exported by addons-docs
export const SNIPPET_RENDERED = `storybook/docs/snippet-rendered`;
function templateSourceCode (
templateSource,
args,
argTypes,
replacing = 'v-bind="args"',
) {
const componentArgs = {}
for (const [k, t] of Object.entries(argTypes)) {
const val = args[k]
if (typeof val !== 'undefined' && t.table && t.table.category === 'props' && val !== t.defaultValue) {
componentArgs[k] = val
}
}
const propToSource = (key, val) => {
const type = typeof val
switch (type) {
case "boolean":
return val ? key : ""
case "string":
return `${key}="${val}"`
default:
return `:${key}="${val}"`
}
}
return templateSource.replace(
replacing,
Object.keys(componentArgs)
.map((key) => " " + propToSource(kebabCase(key), args[key]))
.join(""),
)
}
export const withSource = makeDecorator({
name: "withSource",
wrapper: (storyFn, context) => {
const story = storyFn(context);
// this returns a new component that computes the source code when mounted
// and emits an events that is handled by addons-docs
// this approach is based on the vue (2) implementation
// see https://github.com/storybookjs/storybook/blob/next/addons/docs/src/frameworks/vue/sourceDecorator.ts
return {
components: {
Story: story,
},
setup() {
onMounted(() => {
try {
// get the story source
const src = context.originalStoryFn().template;
// generate the source code based on the current args
const code = templateSourceCode(
src,
context.args,
context.argTypes
);
const channel = addons.getChannel();
const emitFormattedTemplate = async () => {
const prettier = await import("prettier/standalone");
const prettierHtml = await import("prettier/parser-html");
// emits an event when the transformation is completed
channel.emit(
SNIPPET_RENDERED,
(context || {}).id,
prettier.format(`<template>${code}</template>`, {
parser: "vue",
plugins: [prettierHtml],
htmlWhitespaceSensitivity: "ignore",
})
);
};
setTimeout(emitFormattedTemplate, 0);
} catch (e) {
console.warn("Failed to render code", e);
}
});
return () => h(story);
},
};
},
});
And then add this decorator to preview.js:
import { withSource } from './withSource'
...
export const decorators = [
withSource
]

Vue storybook globalTypes not re-rendering preview

Hello all i am currently in the process to integrate Storybook-Vue into my own pattern-lib.
So far everything worked like a charm. Expect for one thing and that is adding a globalType in the preview.js and then using it inside a decorators. Registering the new Global type works, i see it in the toolbar but when i change the selected value it does not re-render the preview.
My first guess is that the context is not an observable object so Vue never knows when this object actually gets an update. But i am not sure how i could change this.
// preview.js
import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n);
export const globalTypes = {
locale: {
name: 'Locale',
description: 'Internationalization locale',
defaultValue: 'en-US',
toolbar: {
icon: 'globe',
items: [
{ value: 'de-DE', right: '🇩🇪', title: 'German' },
{ value: 'en-US', right: '🇺🇸 ', title: 'English' },
{ value: 'cs-CZ', right: '🇨🇿', title: 'Czech' },
{ value: 'zh-CN', right: '🇨🇳', title: 'Chinese' },
],
},
},
};
const localeSelect = (story, context) => {
const wrapped = story(context)
const locale = context.globals.locale
return Vue.extend({
components: { wrapped, BiToast },
data () {
return {
locale
}
},
watch: {
locale: {
deep: true,
handler (val) {
this.$i18n.locale = val
}
}
},
template: `
<div>
{{ locale }}
<wrapped/>
</div>
`
})
}
export const decorators = [localeSelect]
Finally got it working. I just had to use useGlobals and set i18n outsite of the returned object.
import { useGlobals } from '#storybook/client-api';
const localeSelect = (story, context) => {
const [{locale}] = useGlobals();
i18n.locale = locale
return Vue.extend({
...

Vue 3 Composition API - How to specify a prop in setup()

I wrote a "loading state" mixin for Vue 2:
export default {
props: {
loading: {
type: Boolean,
default: false
},
},
data () {
return {
innerLoading: false,
}
},
mounted () {
this.innerLoading = !!this.loading
},
methods: {
startLoading () {
this.$emit('update:loading', this.innerLoading = true)
},
stopLoading () {
this.$emit('update:loading', this.innerLoading = false)
},
},
computed: {
isLoading () {
return !!this.innerLoading
},
isNotLoading () {
return !this.innerLoading
},
},
watch: {
loading (loading) {
this.innerLoading = !!loading
},
}
}
I use this mixin for other components to hold the loading state. For example for forms, buttons, tables etc.
Now, Im trying to rewrite this mixin to composition API style for Vue 3. Ideally, I would like to use my loading composable like this:
// components/Table.vue
import 'useLoading' from 'src/composables/loading'
export default defineComponent({
setup () {
const { startLoading, stopLoading, innerLoading } = useLoading()
// ...
return { startLoading, stopLoading, innerLoading, ... }
}
})
My question:
// How can I define the loading prop inside the setup() function?
props: {
loading: {
type: Boolean,
default: false
},
},
Of course I can define my component like this:
import 'useLoading' from 'src/composables/loading'
export default defineComponent({
props: {
loading: {
type: Boolean,
default: false
},
},
setup () {
const { startLoading, stopLoading, innerLoading } = useLoading();
}
})
But imagine, I have 20 components using this mixin/composable. So I want to define that loading prop only ONCE (like I did in mixin).
Is there a way how to do it with composition API?
you may be able to do something like this
import {withProps, useLoading} from "src/composables/loading";
export default defineComponent({
props: {
...withProps()
},
setup () {
const { startLoading, stopLoading, innerLoading } = useLoading();
}
})
where withProps is a function that would have your definitions
export const withProps = () => ({
loading: {
type: Boolean,
default: false
},
})
of course it doesn't need to be a function, but in some cases it may be helpful and preemptively making it a function can make api consistent.
Define an Object called loadingProps in separate file called makeLoadingProps:
export const loadingProps = {
loading: {
type: Boolean,
default: false
}
}
then import it inside your component defined using the script setup syntax:
<script setup lang="ts">
import {defineProps} from 'vue'
import { loadingProps } from 'src/composables/makeLoadingProps';
const props = defineProps({
...loadingProps,
//other props
})
const { startLoading, stopLoading, innerLoading } = useLoading(props)
</script>

Nuxt asyncData result is undefined if using global mixin head() method

I'm would like to get titles for my pages dynamically in Nuxt.js in one place.
For that I've created a plugin, which creates global mixin which requests title from server for every page. I'm using asyncData for that and put the response into storage, because SSR is important here.
To show the title on the page I'm using Nuxt head() method and store getter, but it always returns undefined.
If I place this getter on every page it works well, but I would like to define it only once in the plugin.
Is that a Nuxt bug or I'm doing something wrong?
Here's the plugin I wrote:
import Vue from 'vue'
import { mapGetters } from "vuex";
Vue.mixin({
async asyncData({ context, route, store, error }) {
const meta = await store.dispatch('pageMeta/setMetaFromServer', { path: route.path })
return {
pageMetaTitle: meta
}
},
...mapGetters('pageMeta', ['getTitle']),
head() {
return {
title: this.getTitle, // undefined
// title: this.pageMetaTitle - still undefined
};
},
})
I would like to set title in plugin correctly, now it's undefined
Update:
Kinda solved it by using getter and head() in global layout:
computed: {
...mapGetters('pageMeta', ['getTitle']),
}
head() {
return {
title: this.getTitle,
};
},
But still is there an option to use it only in the plugin?
Update 2
Here's the code of setMetaFromServer action
import SeoPagesConnector from '../../connectors/seoPages/v1/seoPagesConnector';
const routesMeta = [
{
path: '/private/kredity',
dynamic: true,
data: {
title: 'TEST 1',
}
},
{
path: '/private/kreditnye-karty',
dynamic: false,
data: {
title: 'TEST'
}
}
];
const initialState = () => ({
title: 'Юником 24',
description: '',
h1: '',
h2: '',
h3: '',
h4: '',
h5: '',
h6: '',
content: '',
meta_robots_content: '',
og_description: '',
og_image: '',
og_title: '',
url: '',
url_canonical: '',
});
export default {
state: initialState,
namespaced: true,
getters: {
getTitle: state => state.title,
getDescription: state => state.description,
getH1: state => state.h1,
},
mutations: {
SET_META_FIELDS(state, { data }) {
if (data) {
Object.entries(data).forEach(([key, value]) => {
state[key] = value;
})
}
},
},
actions: {
async setMetaFromServer(info, { path }) {
const routeMeta = routesMeta.find(route => route.path === path);
let dynamicMeta;
if (routeMeta) {
if (!routeMeta.dynamic) {
info.commit('SET_META_FIELDS', routeMeta);
} else {
try {
dynamicMeta = await new SeoPagesConnector(this.$axios).getSeoPage({ path })
info.commit('SET_META_FIELDS', dynamicMeta);
return dynamicMeta && dynamicMeta.data;
} catch (e) {
info.commit('SET_META_FIELDS', routeMeta);
return routeMeta && routeMeta.data;
}
}
} else {
info.commit('SET_META_FIELDS', { data: initialState() });
return { data: initialState() };
}
return false;
},
}
}

VueJS - vue-charts.js

I am trying to pass data I fetch from API to vue-chartjs as props, I am doing as in the documentation but it does not work.
Main component
<monthly-price-chart :chartdata="chartdata"/>
import MonthlyPriceChart from './charts/MonthlyPriceChart'
export default {
data(){
return {
chartdata: {
labels: [],
datasets: [
{
label: 'Total price',
data: []
}
]
},
options: {
responsive: true,
maintainAspectRatio: false
}
}
},
components: {
MonthlyPriceChart
},
created() {
axios.get('/api/stats/monthly')
.then(response => {
let rides = response.data
forEach(rides, (ride) => {
this.chartdata.labels.push(ride.month)
this.chartdata.datasets[0].data.push(ride.total_price)
})
})
.catch(error => {
console.log(error)
})
}
}
In response I have an array of obejcts, each of which looks like this:
{
month: "2018-10",
total_distance: 40,
total_price: 119.95
}
Then I want to send the data somehow to the chart so I push the months to chartdata.labels and total_price to chartdata.datasets[0].data.
chart component
import { Bar } from 'vue-chartjs'
export default {
extends: Bar,
props: {
chartdata: {
type: Array | Object,
required: false
}
},
mounted () {
console.log(this.chartdata)
this.renderChart(this.chartdata, this.options)
}
}
console.log(this.chartdata) outputs my chartsdata object from my main component and the data is there so the data is passed correctly to chart but nothing is rendered on the chart.
The documentation says this:
<script>
import LineChart from './LineChart.vue'
export default {
name: 'LineChartContainer',
components: { LineChart },
data: () => ({
loaded: false,
chartdata: null
}),
async mounted () {
this.loaded = false
try {
const { userlist } = await fetch('/api/userlist')
this.chartData = userlist
this.loaded = true
} catch (e) {
console.error(e)
}
}
}
</script>
I find this documentation a bit vague because it does not explain what I need to pass in chartdatato the chart as props. Can you help me?
Your issue is that API requests are async. So it happens that your chart will be rendered, before your API request finishes. A common pattern is to use a loading state and v-if.
There is an example in the docs: https://vue-chartjs.org/guide/#chart-with-api-data