I'm making a storyBook out of a component that we'll call ParentComponent, it has a child called ChildComponent.
Inside ChildComponent there is a function that makes a call to the code of another repository so this is where the storybook fails.
components: {
DataProvider: () => import("common-data-provider"),// --> problem this line
}
How can I mock ChildComponent and not make this request?
StoryBook of Parent Component
import articlesFunds from "../__fixtures__/articles.json";
export default {
component: ParentComponent,
argTypes: {
getConfigFilesDataProvider: {
control: "object",
},
},
args: {
getConfigFilesDataProvider: () => new Promise((resolve) => setTimeout(() => resolve({ data: articlesFunds }))),
},
};
export const Default = (args) => ({
components: { ParentComopnent },
props: Object.keys(args),
template: `
<ParentComopnent
:data-provider="getConfigFilesDataProvider"
/>
`,
});
ChildComponent
<template>
<DataProvider :data="articles" />
</template>
<script>
export default {
name: "ChildComponent",
components: {
DataProvider: () => import("common-data-provider"),// --> problem this line
ComponentB
},
props: {
articles: {
type: Array,
required: true,
},
}
};
</script>
Storybook doesn't specifically provide the support for module mocking, but it's possible to use Webpack aliases for that.
Dependency injection of any kind can be used to modify the behaviour of a component in uncommon conditions, e.g. the ability to provide custom component:
props: {
articles: {
type: Array,
required: true,
},
dataProvider: {
type: null,
default: () => import("common-data-provider"),
}
}
And used like dynamic component:
<component :is="dataProvider" :data="articles"/>
Then custom implementation can be provided through dataProvider prop.
Related
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');
});
<template>
<component :is="myComponent" />
</template>
<script>
export default {
props: {
component: String,
},
data() {
return {
myComponent: '',
};
},
computed: {
loader() {
return () => import(`../components/${this.component}`);
},
},
created() {
this.loader().then(res => {
// components can be defined as a function that returns a promise;
this.myComponent = () => this.loader();
},
},
}
</script>
Reference:
https://medium.com/scrumpy/dynamic-component-templates-with-vue-js-d9236ab183bb
Vue js import components dynamically
Console throw error "this.loader() is not a function" or "this.loader().then" is not a function.
Not sure why you're seeing that error, as loader is clearly defined as a computed prop that returns a function.
However, the created hook seems to call loader() twice (the second call is unnecessary). That could be simplified:
export default {
created() {
// Option 1
this.loader().then(res => this.myComponent = res)
// Option 2
this.myComponent = () => this.loader()
}
}
demo 1
Even simpler would be to rename loader with myComponent, getting rid of the myComponent data property:
export default {
//data() {
// return {
// myComponent: '',
// };
//},
computed: {
//loader() {
myComponent() {
return () => import(`../components/${this.component}`);
},
},
}
demo 2
Buefy's Dialog component expects a message prop - string. According to a documentation, that string can contain HTML. I would like to use template values in the string, but of course is should be XSS safe.
Current unsafe example
This is unsafe, as this.name is unsafe. I could use a NPM package to html encode the name, but I really prefer to use Vue.
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
name: { type: String, required: false },
},
methods: {
showModal() {
this.$buefy.dialog.confirm({
title: 'myTitle',
message: `<p>Hello ${this.name}</p>`, // unsafe - possible XSS!
cancelText: 'Cancel',
confirmText: 'OK',
type: 'is-success',
onConfirm: async () => {
// something
},
});
},
},
});
</script>
This is an issue of the used Buefy component, as documented here:
Desired Setup
I've created a new component, in this example I call it ModalMessage.Vue
<template>
<p>Hello {{name}}</p>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
name: { type: String, required: true },
},
});
</script>
Then I like to render the ModalMessage.Vue to a string in Typescript:
<script lang="ts">
import Vue from 'vue';
import ModalMessage from 'ModalMessage.vue';
export default Vue.extend({
props: {
name: { type: String, required: false },
},
methods: {
showModal() {
this.$buefy.dialog.confirm({
title: 'myTitle',
message:, // todo render ModalMessage and pass name prop
cancelText: 'Cancel',
confirmText: 'OK',
type: 'is-success',
onConfirm: async () => {
// something
},
});
},
},
});
</script>
Question
How could I render the ModalMessage.Vue, and passing the name prop, to a string?
I'm pretty sure this is possible - I have seen it in the past. Unfortunately I cannot find it on the web or StackOverflow. I could only find questions with rendering a template from string, but I don't need that - it needs to be to string.
Imho your real question is "How to use Buefy's Dialog component with user provided content and be safe in terms of XSS"
So what you want is to create some HTML, include some user provided content (this.name) within that HTML content and display it in a Dialog. You are right that putting unfiltered user input into a message parameter of Dialog is not safe (as clearly noted in Buefy docs)
But your "Desired Setup" seems unnecessary complicated. Imho the easiest way is to use (poorly documented) fact that message parameter of Buefy Dialog configuration objects can be an Array of VNode's instead of a string. It is poorly documented but it is very clear from the source here and here that if you pass an array of VNode's, Buefy puts that content into Dialogs default slot instead of rendering it using v-html (which is the dangerous part)
And easiest way to get Array of VNode in Vue is to use slots...
So the component:
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
methods: {
showModal() {
this.$buefy.dialog.confirm({
title: 'myTitle',
message: this.$slots.default, // <- this is important part
cancelText: 'Cancel',
confirmText: 'OK',
type: 'is-success',
onConfirm: async () => {
// something
},
});
},
},
});
</script>
and it's usage:
<MyDialog>
<p>Hello {{name}}</p>
</MyDialog>
or
<MyDialog>
<ModalMessage :name="name" />
</MyDialog>
In both cases if the name contains any HTML, it will be encoded by Vue
Here is a simple demo of the technique described above (using plain JS - not TS)
Try this.
<script lang="ts">
import Vue from 'vue';
import ModalMessage from 'ModalMessage.vue';
export default Vue.extend({
props: {
name: { type: String, required: false },
},
methods: {
showModal() {
const message = new Vue({
components: { ModalMessage },
render: h => h('ModalMessage', { name: this.name })
})
message.$mount()
const dialog = this.$buefy.dialog.confirm({
title: 'myTitle',
message: [message._vnode],
cancelText: 'Cancel',
confirmText: 'OK',
type: 'is-success',
onConfirm: async () => {
// something
},
});
dialog.$on('hook:beforeDestroy', () => {
message.$destroy()
});
},
},
});
</script>
Source codeļ¼
Demo:
I need a single file component to load its template via AJAX.
I search a while for a solution and found some hints about dynamic components.
I crafted a combination of a parent component which imports a child component and renders the child with a dynamic template.
Child component is this:
<template>
<div>placeholder</div>
</template>
<script>
import SomeOtherComponent from './some-other-component.vue';
export default {
name: 'child-component',
components: {
'some-other-component': SomeOtherComponent,
},
};
</script>
Parent component is this
<template>
<component v-if='componentTemplate' :is="dynamicComponent && {template: componentTemplate}"></component>
</template>
<script>
import Axios from 'axios';
import ChildComponent from './child-component.vue';
export default {
name: 'parent-component',
components: {
'child-component': ChildComponent,
},
data() {
return {
dynamicComponent: 'child-component',
componentTemplate: null,
};
},
created() {
const self = this;
this.fetchTemplate().done((htmlCode) => {
self.componentTemplate = htmlCode;
}).fail((error) => {
self.componentTemplate = '<div>error</div>';
});
},
methods: {
fetchTemplate() {
const formLoaded = $.Deferred();
const url = '/get-dynamic-template';
Axios.get(url).then((response) => {
formLoaded.resolve(response.data);
}).catch((error) => {
formLoaded.reject(error);
}).then(() => {
formLoaded.reject();
});
return formLoaded;
},
},
};
</script>
The dynamic template code fetched is this:
<div>
<h1>My dynamic template</h1>
<some-other-component></some-other-component>
</div>
In general the component gets its template as expected and binds to it.
But when there are other components used in this dynamic template (some-other-component) they are not recognized, even if they are correctly registered inside the child component and of course correctly named as 'some-other-component'.
I get this error: [Vue warn]: Unknown custom element: some-other-component - did you register the component correctly? For recursive components, make sure to provide the "name" option.
Do I miss something or is it some kind of issue/bug?
I answer my question myself, because I found an alternative solution after reading a little bit further here https://forum.vuejs.org/t/load-html-code-that-uses-some-vue-js-code-in-it-via-ajax-request/25006/3.
The problem in my code seems to be this logical expression :is="dynamicComponent && {template: componentTemplate}". I found this approach somewhere in the internet.
The original poster propably assumed that this causes the component "dynamicComponent" to be merged with {template: componentTemplate} which should override the template option only, leaving other component options as defined in the imported child-component.vue.
But it seems not to work as expected since && is a boolean operator and not a "object merge" operator. Please somebody prove me wrong, I am not a JavaScript expert after all.
Anyway the following approach works fine:
<template>
<component v-if='componentTemplate' :is="childComponent"></component>
</template>
<script>
import Axios from 'axios';
import SomeOtherComponent from "./some-other-component.vue";
export default {
name: 'parent-component',
components: {
'some-other-component': SomeOtherComponent,
},
data() {
return {
componentTemplate: null,
};
},
computed: {
childComponent() {
return {
template: this.componentTemplate,
components: this.$options.components,
};
},
},
created() {
const self = this;
this.fetchTemplate().done((htmlCode) => {
self.componentTemplate = htmlCode;
}).fail((error) => {
self.componentTemplate = '<div>error</div>';
});
},
methods: {
fetchTemplate() {
const formLoaded = $.Deferred();
const url = '/get-dynamic-template';
Axios.get(url).then((response) => {
formLoaded.resolve(response.data);
}).catch((error) => {
formLoaded.reject(error);
}).then(() => {
formLoaded.reject();
});
return formLoaded;
},
},
};
</script>
I am trying to map an action to a component using mapActions helper from vuex. Here is my labels.js vuex module:
export const FETCH_LABELS = 'FETCH_LABELS'
export const FETCH_LABEL = 'FETCH_LABEL'
const state = () => ({
labels: [
{ name: 'Mord Records', slug: 'mord', image: '/images/labels/mord.jpg'},
{ name: 'Subsist Records', slug: 'subsist', image: '/images/labels/subsist.jpg'},
{ name: 'Drumcode Records', slug: 'drumcode', image: '/images/labels/drumcode.png'},
],
label: {} // null
})
const mutations = {
FETCH_LABEL: (state, { label }) => {
state.label = label
},
}
const actions = {
fetchLabel({commit}, slug) {
let label = state.labels.filter((slug, index) => {
return slug == state.labels[index]
})
commit(FETCH_LABEL, { label })
},
}
const getters = {
labels: state => {
return state.labels
},
label: (state, slug) => {
}
}
export default {
state,
mutations,
actions,
getters
}
Here is my component _slug.vue page where I want to map the fetchLabel action:
<template>
<div class="container">
<div class="section">
<div class="box">
<h1>{{ $route.params.slug }}</h1>
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
export default {
data() {
return {
title: this.$route.params.slug
};
},
computed: {
// Research
// labels() {
// return this.$store
// }
...mapGetters({
labels: "modules/labels/labels"
})
},
components: {},
methods: {
...mapActions({
fetchLabel: 'FETCH_LABEL' // map `this.add()` to `this.$store.dispatch('increment')`
})
},
created() {
console.log('created')
this.fetchLabel(this.$route.params.slug)
},
head() {
return {
title: this.title
}
},
layout: "app",
}
</script>
<style>
</style>
However inside the created() lifecycle hook at this.fetchLabel(this.$route.params.slug) it throws the following error in the console:
[vuex] unknown action type: FETCH_LABEL
What am I missing or doing wrong? Please help me solve this.
Note that in Nuxt.js:
Modules: every .js file inside the store directory is transformed as a namespaced module (index being the root module).
You are using:
Here is my labels.js vuex module:
with labels.js as you stated above so you'll need to access everything as namespaced modules so your mapAction helper should be like as such:
methods: {
...mapActions({
nameOfMethod: 'namespace/actionName'
})
}
So you would have this:
...mapActions({
fetchLabel: 'labels/fetchLabel'
})
You could also clean it up by doing so for when you'd like to retain the name of your action as your method name.
...mapActions('namespace', ['actionName']),
...
So you would have this:
...mapActions('labels', ['fetchLabel']),
...
In both cases the computed prop should work without a problem.
Your action name is fetchLabel and not FETCH_LABEL (which is a mutation). In mapActions change to
methods: {
...mapActions({
fetchLabel
})
},