Vue storybook globalTypes not re-rendering preview - vue.js

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({
...

Related

TipTap Vue custom component selects the entire line, not the selected one

I will immediately introduce the custom extension Tag.js
import { mergeAttributes, Node } from "#tiptap/core";
import { VueNodeViewRenderer } from "#tiptap/vue-3";
import { markInputRule } from "#tiptap/core";
import { markPasteRule } from "#tiptap/core";
import Component from "~/components/Editor/Tag.vue";
const starInputRegex = /(?:^|\s)((?:\*)((?:[^*]+))(?:\*))$/;
const starPasteRegex = /(?:^|\s)((?:\*)((?:[^*]+))(?:\*))/g;
const underscoreInputRegex = /(?:^|\s)((?:_)((?:[^_]+))(?:_))$/;
const underscorePasteRegex = /(?:^|\s)((?:_)((?:[^_]+))(?:_))/g;
export default Node.create({
name: "vuetag",
group: "block",
content: "inline*",
selectable: true,
parseHTML() {
return [
{
tag: "tag",
},
];
},
renderHTML({ HTMLAttributes }) {
return ["tag", mergeAttributes(HTMLAttributes), 0];
},
addNodeView() {
return VueNodeViewRenderer(Component);
},
addInputRules() {
return [
markInputRule({
find: starInputRegex,
type: this.type,
}),
markInputRule({
find: underscoreInputRegex,
type: this.type,
}),
];
},
addPasteRules() {
return [
markPasteRule({
find: starPasteRegex,
type: this.type,
}),
markPasteRule({
find: underscorePasteRegex,
type: this.type,
}),
];
},
addCommands() {
return {
setTag:
() =>
({ commands }) => {
return commands.setNode(this.name);
},
};
},
});
Component Tag.vue
<template>
<node-view-wrapper>
<el-tag><node-view-content /></el-tag>
</node-view-wrapper>
</template>
<script>
import { NodeViewContent, nodeViewProps, NodeViewWrapper } from "#tiptap/vue-3";
export default {
components: {
NodeViewWrapper,
NodeViewContent,
},
props: nodeViewProps,
};
</script>
<style lang="scss"></style>
There is a text: Did you see that? That’s a Vue component. We are really living in the future.
Let's say I want the phrase Did you see that? specify as a tag. I highlight this phrase and click on the button, the event setTag() is triggered
The result I get is this:<tag>Did you see that? That’s a Vue component. We are really living in the future.</tag>
The problem is that here the whole one line becomes a tag, that is, inside the Tag component.Vue
And there should be such a result: <tag>Did you see that?</tag> That’s a Vue component. We are really living in the future.
As an el-tag, I took from https://element-plus.org/en-US/component/tag.html

Storybook JS how to trigger the dark mode "channel" from my Vue component

I've installed the Storybook Js addon, "storybook-dark-mode-vue" (I'm not sure if it makes any difference whether or not I just used the default "storybook-dark-mode" addon) but I'm not sure how to trigger the "channels" from my vue component story.
My example story is:
import BToggle from './Toggle.vue';
export default {
name: 'Components/Toggle',
component: BToggle,
// More on argTypes: https://storybook.js.org/docs/vue/api/argtypes
parameters: {
docs: {
description: {
component: 'Nothing to see here',
},
},
},
argTypes: {
label: {
control: { type: 'text' },
},
onToggle: {
action: 'changed',
},
},
};
const Template = (args, { argTypes }) => ({
components: { BToggle },
props: Object.keys(argTypes),
template: '<b-toggle v-bind="$props" #onToggle="onToggle"></b-toggle>',
});
export const Default = Template.bind({});
Default.args = {
label: 'default',
};
The "onToggle" event works, I see the action being triggered in the Storybook "actions" tag, so how do I make it trigger the Storybook "STORYBOOK_DARK_MODE_VUE" event in my preview.js file?
My preview.js file has:
const channel = addons.getChannel();
channel.on('STORYBOOK_DARK_MODE_VUE', () => {
console.log('activating dark mode');
});
channel.off('STORYBOOK_DARK_MODE_VUE', () => {
console.log('activating dark mode');
});

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
]

vue2 composition api why does watch not fire

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.

How to pass multiple mutations to update Vuex store?

If I mutate the state only once (by committing either of the two mutations shown below), there is no error and chart is updated correctly.
If I run both mutations:
commit('SET_COURSE_MATERIAL', data)
commit('SET_TOOLS_EQUIPMENT', data)
then I get Maximum call stack exceeded: RangeError.
If I comment out the code in the watch property of chart.vue, there are no errors and I can see the state with correct values in console.log
I am getting the error regarding maximum call stack only when I run "npm run dev". When I deploy it to Google Cloud, the site works as expected and I don't get any errors. I even re-checked this by editing some code and re-deploying it twice while also noticing the time in the build logs.
summary.vue
<v-card-text>
<chart
:chart-config.sync="this.$store.state.summary.courseMaterial"
/>
</v-card-text>
...
<v-card-text>
<chart
:chart-config.sync="this.$store.state.summary.toolsEquipment"
/>
</v-card-text>
chart.vue
<template>
<v-flex>
<no-ssr><vue-c3 :handler="handler"/></no-ssr>
</v-flex>
</template>
<script>
import Vue from 'vue'
export default {
name: 'chart',
props: ['chartConfig'],
data() {
return {
handler: new Vue()
}
},
watch: {
chartConfig: function(val) {
console.log('chart component > watch > chartConfig', val)
this.drawChart()
}
},
created() {
this.drawChart()
},
methods: {
drawChart() {
this.handler.$emit('init', this.chartConfig)
}
}
}
</script>
store/summary.js
import axios from 'axios'
import _ from 'underscore'
import Vue from 'vue'
import {
courseMaterialChartConfig,
toolsEquipmentChartConfig,
} from './helpers/summary.js'
axios.defaults.baseURL = process.env.BASE_URL
Object.filter = (obj, predicate) =>
Object.assign(
...Object.keys(obj)
.filter(key => predicate(obj[key]))
.map(key => ({ [key]: obj[key] }))
)
export const state = () => ({
courseMaterial: '',
toolsEquipment: '',
})
export const getters = {
courseMaterial(state) {
return state.courseMaterial
},
toolsEquipment(state) {
return state.toolsEquipment
}
}
export const actions = {
async fetchData({ state, commit, rootState, dispatch }, payload) {
axios.defaults.baseURL = process.env.BASE_URL
let { data: initialData } = await axios.post(
'summary/fetchInitialData',
payload
)
console.log('initialData', initialData)
let [counterData, pieChartData, vtvcData, guestFieldData] = initialData
//dispatch('setCourseMaterial', pieChartData.coureMaterialStatus)
//dispatch('setToolsEquipment', pieChartData.toolsEquipmentStatus)
},
setCourseMaterial({ commit }, data) {
commit('SET_COURSE_MATERIAL', courseMaterialChartConfig(data))
},
setToolsEquipment({ commit }, data) {
commit('SET_TOOLS_EQUIPMENT', toolsEquipmentChartConfig(data))
}
}
export const mutations = {
// mutations to set user in state
SET_COURSE_MATERIAL(state, courseMaterial) {
console.log('[STORE MUTATIONS] - SET_COURSEMATERIAL:', courseMaterial)
state.courseMaterial = courseMaterial
},
SET_TOOLS_EQUIPMENT(state, toolsEquipment) {
console.log('[STORE MUTATIONS] - SET_TOOLSEQUIPMENT:', toolsEquipment)
state.toolsEquipment = toolsEquipment
},
}
helpers/summary.js
export const courseMaterialChartConfig = data => {
return {
data: {
type: 'pie',
json: data,
names: {
received: 'Received',
notReceived: 'Not Received',
notReported: 'Not Reported'
}
},
title: {
text: 'Classes',
position: 'right'
},
legend: {
position: 'right'
},
size: {
height: 200
}
}
}
export const toolsEquipmentChartConfig = data => {
return {
data: {
type: 'pie',
json: data,
names: {
received: 'Received',
notReceived: 'Not Received',
notReported: 'Not Reported'
}
},
title: {
text: 'Job Role Units',
position: 'right'
},
legend: {
position: 'right'
},
size: {
height: 200
}
}
}
Deep copy the chart config.
methods: {
drawChart() {
this.handler.$emit('init', {...this.chartConfig})
}
}