RESOLVED - DevExpress controller in Vue3 Composition API - vue.js

i'm developing in Vue3 with DevExpress controllers. On DevExpress website all the Vue demos show the Options API mode but i'm using the Composition API, and initially i have some problems about the customization form on runtime because append/remove items worked just in Options API.
I've undestand my error depends on reactivity.
On demos DevExtreme components use SimpleItems generated by itemOptions that is an array of objects related the options (parameters) of SimpleItem component.
[{
name: 'trash',
location: 'after',
options: {
type: 'danger',
stylingMode: 'text',
icon: 'trash',
onClick: () => {
employee.Phone.splice(index, 1);
itemOptions = this.getPhonesOptions(this.payload.Phone);
},
},
}]
Initially in Composition API i was instanceted just the values to show on each SimpleItem
const employee = reactive({
Phone: ['123']
})
but it was a mistake! I've understood that is necessary to make reactive itemOptions too!
Below the correct examples based on demo
<script setup>
import { reactive } from 'vue'
import {
DxForm,
DxSimpleItem,
DxGroupItem,
DxButtonItem,
DxLabel,
} from 'devextreme-vue/form';
const employee = reactive({
Phone: ['123'],
phoneOptions: []
})
employee.itemOptions = reactive([])
var phoneOptions = getPhoneItems(employee.Phone)
var addPhoneButton = {
icon: 'plus',
type: 'green',
stylingMode: 'contained',
onClick: () => {
employee.Phone.push('');
phoneOptions = getPhoneItems(employee.Phone);
},
}
function getPhoneItems(items) {
const options = [];
for (let i = 0; i < items.length; i += 1) {
options.push(newPhoneOptions(i));
}
return options;
}
function newPhoneOptions(index) {
return {
buttons: [{
name: 'trash',
location: 'after',
options: {
type: 'danger',
stylingMode: 'text',
icon: 'trash',
onClick: () => {
employee.Phone.splice(index, 1);
phoneOptions = getPhoneItems(employee.Phone);
},
},
}],
};
}
</script>
<template>
<DxForm
id="form"
:form-data="employee"
>
<DxGroupItem
name="items-container"
>
<DxGroupItem
item-type="group"
name="phones"
>
<DxSimpleItem
v-for="(p, index) in phoneOptions"
:key="'Phone' + (index + 1)"
:data-field="'Phone[' + index + ']'"
:editor-options="p"
>
<DxLabel :text="'ITEM ' + (index + 1)"/>
</DxSimpleItem>
</DxGroupItem>
<DxButtonItem
:button-options="addPhoneButton"
css-class="add-phone-button"
horizontal-alignment="left"
/>
</DxGroupItem>
</DxForm>
</template>
Regards
Davide

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

Testing vue watchers with vue-testing-library

Anyone know how I would test a watcher in a component with vue-testing-library?
Here is my component. I want to test that the method is called when the brand vuex state is updated. With vue test utils it would be easy but I have not found a good way to do this with vue testing library. Has anyone did this before using vue testing library.
<template>
<v-data-table
data-testid="builds-table"
:headers="headers"
:items="builds"
:items-per-page="10"
class="elevation-1"
:loading="loading"
>
<template v-slot:[getItemStatus]="{ item }">
<v-chip :color="getStatusColor(item.status)" dark>
{{ item.status }}
</v-chip>
</template>
</v-data-table>
</template>
<script>
import { mapState } from "vuex";
import { getScheduledBuilds } from "../services/buildActivationService";
import { getStatusColor } from "../utils/getStatusColor";
export default {
name: "BuildsTable",
data() {
return {
loading: false,
headers: [
{
text: "Activation Time",
align: "start",
value: "buildActivationTime",
},
{ text: "Build ID", value: "buildId" },
{ text: "Build Label", value: "buildLabel" },
{ text: "Status", value: "status" },
],
error: "",
};
},
async mounted() {
this.getBuilds();
},
computed: {
...mapState(["brand", "builds"]),
getItemStatus() {
return `item.status`;
},
},
watch: {
brand() {
this.getBuilds();
},
},
methods: {
getStatusColor(status) {
return getStatusColor(status);
},
async getBuilds() {
try {
this.loading = true;
const builds = await getScheduledBuilds(this.$store.getters.brand);
this.$store.dispatch("setBuilds", builds);
this.items = this.$store.getters.builds;
this.loading = false;
} catch (error) {
this.loading = false;
this.error = error.message;
this.$store.dispatch("setBuilds", []);
}
},
},
};
</script>
Vue Testing Library is just a wrapper for Vue Test Utils, so the same call verification techniques apply.
Here's how to verify the call with Jest and Vue Testing Library:
Spy on the component method definition before rendering the component:
import { render } from '#testing-library/vue'
import BuildsTable from '#/components/BuildsTable.vue'
const getBuilds = jest.spyOn(BuildsTable.methods, 'getBuilds')
render(BuildsTable)
Render the component with a given store and a callback to capture the Vuex store instance under test:
let store = {
state: {
brand: '',
builds: [],
}
}
const storeCapture = (_, vuexStore) => store = vuexStore
render(BuildsTable, { store }, storeCapture)
Update the store's brand value, and wait a macro tick for the watcher to take effect, then verify the getBuilds spy is called twice (once in mounted() and again in the brand watcher):
store.state.brand = 'foo'
await new Promise(r => setTimeout(r)) // wait for effect
expect(getBuilds).toHaveBeenCalledTimes(2)
The full test would look similar to this:
import { render } from '#testing-library/vue'
import BuildsTable from '#/components/BuildsTable.vue'
describe('BuildsTable.vue', () => {
it('calls getBuilds when brand changes', async() => {
const getBuilds = jest.spyOn(BuildsTable.methods, 'getBuilds')
let store = {
state: {
brand: '',
builds: [],
}
}
const storeCapture = (_, vuexStore) => store = vuexStore
render(BuildsTable, { store }, storeCapture)
store.state.brand = 'foo'
await new Promise(r => setTimeout(r)) // wait for effect
expect(getBuilds).toHaveBeenCalledTimes(2)
})
})

chartJS pie chart not updating after axios GET

In my Vue app I'm loading a page and it grabs some data from my Flask backend. I draw a bunch of elements and I also draw a pie chart based on an array of 3 values returned from the backend. When I get the response I update this.pie_data and I thought this would update in my template to reflect the pie chart. It renders the elements that get set in this.nvrs so not sure why it doesn't work for my pie chart.
Appreciate the help.
Template
<div class="box">
<p class="title">System Overview</p>
<chart :type="'pie'": data=chartData></chart>
</div>
Script
import axios from 'axios'
import Chart from 'vue-bulma-chartjs'
export default {
name: 'NVR_Overview',
components: {
Chart,
},
data: () => ({
nvrs: [],
// Health, Down, Warn
pie_data: [],
}),
methods: {
goToNVR (nvrId)
{
let wpsId = this.$route.params['wpsId']
let path = '/wps/' + wpsId + '/nvr/' + nvrId
this.$router.push(path)
},
},
created ()
{
axios
.get('http://localhost:5000/wps/' + this.$route.params['wpsId'])
.then(response =>
{
this.nvrs = response.data['nvr_list']
this.pie_data = response.data['pie_data']
console.log(this.pie_data)
})
.catch(e =>
{
console.log(e)
})
},
computed: {
chartData ()
{
return {
labels: ['Healthy', 'Down', 'Warning'],
datasets: [
{
data: this.pie_data,
backgroundColor: ['#41B482', '#ff4853', '#FFCE56'],
},
],
}
},
},
}
Solution 1:
Copy old chart data reference when you make the change (old data have Observer, so don't make completed new object), instead of using computed value, use watch:
<template>
<div class="box">
<p class="title">System Overview</p>
<chart :type="'pie'" :data="chartData"></chart>
</div>
</template>
<script>
import axios from 'axios'
import Chart from 'vue-bulma-chartjs'
export default {
name: 'NVR_Overview',
components: {
Chart,
},
data: () => ({
nvrs: [],
// Health, Down, Warn
pie_data: [],
chartData: {
labels: ['Healthy', 'Down', 'Warning'],
datasets: [
{
data: [],
backgroundColor: ['#41B482', '#ff4853', '#FFCE56'],
},
]
}
}),
methods: {
goToNVR (nvrId)
{
let wpsId = this.$route.params['wpsId']
let path = '/wps/' + wpsId + '/nvr/' + nvrId
this.$router.push(path)
},
},
created ()
{
axios
.get('http://localhost:5000/wps/' + this.$route.params['wpsId'])
.then(response =>
{
this.nvrs = response.data['nvr_list']
this.pie_data = response.data['pie_data']
console.log(this.pie_data)
})
.catch(e =>
{
console.log(e)
})
},
watch: {
pie_data (newData) {
const data = this.chartData
data.datasets[0].data = newData
this.chartData = {...data}
}
},
}
</script>
You can check this problem on vue-bulma-chartjs repository https://github.com/vue-bulma/chartjs/pull/24
Solution 2:
Add ref to chart
<chart ref="chart" :type="'pie'" :data="data"</chart>
then in script after you assign data:
this.$nextTick(() => {
this.$refs.chart.resetChart();
})

Getting state, getters, actions of vuex module in vue component

I tried the syntax given in vuex doc.
store.state.a // -> moduleA's state
store.state.b // -> moduleB's state
app.js
/**
* First we will load all of this project's JavaScript dependencies which
* includes Vue and other libraries. It is a great starting point when
* building robust, powerful web applications using Vue and Laravel.
*/
require('./bootstrap');
/**
* Next, we will create a fresh Vue application instance and attach it to
* the page. Then, you may begin adding components to this application
* or customize the JavaScript scaffolding to fit your unique needs.
*/
Vue.component('task-index', require('./components/TaskList.vue'));
Vue.component('task-show', require('./components/TaskShow.vue'));
Vue.component('note-index', require('./components/NoteList.vue'));
Vue.component('note-show', require('./components/NoteShow.vue'));
const notes = {
state: {
edit: false,
list:[],
note: {
note : '',
id : ''
}
},
mutations: {
SET_EDIT: (state, data) => {
state.edit = data
},
SET_LIST: (state, data) => {
state.list = data
},
SET_NOTE: (state, data) => {
state.note.id = data.id;
state.note.note = data.note;
},
SET_EMPTY: (state) => {
state.note.note = '';
}
},
getters: {
noteCount: (state) => state.list.length
},
actions : {
getNote: ({commit,state}) => {
axios.get('/api/note/list')
.then((response) => {
commit('SET_LIST', response.data);
commit('SET_EDIT',false);
commit('SET_EMPTY');
})
},
}
};
const tasks = {
state: {
edit: false,
list:[],
task: {
body : '',
id : ''
}
},
mutations: {
SET_EDIT: (state, data) => {
state.edit = data
},
SET_LIST: (state, data) => {
state.list = data
},
SET_TASK: (state, data) => {
state.task.id = data.id;
state.task.body = data.body;
},
SET_EMPTY: (state) => {
state.task.body = '';
}
},
getters: {
taskCount: (state) => state.list.length
},
actions : {
getTask: ({commit,state}) => {
axios.get('/api/task/list')
.then((response) => {
commit('SET_LIST', response.data);
commit('SET_EDIT',false);
commit('SET_EMPTY');
})
},
}
};
const store = new Vuex.Store({
modules : {
task : tasks,
note : notes
}
});
const app = new Vue({
el: '#app',
store
});
TaskList.vue
<template>
<div >
<h4>{{count}} Task(s)</h4>
<ul class="list-group">
<li class="list-group-item" v-for="item in list">
{{item.body}}
<button class="btn btn-primary btn-xs" #click="showTask(item.id)">Edit</button>
<button class="btn btn-danger btn-xs" #click="deleteTask(item.id)">Delete</button>
</li>
</ul>
</div>
</template>
<script>
export default{
computed :{
list() {
return this.$store.state.task.list;
},
count(){
return this.$store.getters.taskCount;
}
},
mounted(){
this.$store.dispatch('getTask');
},
methods : {
showTask: function(id){
axios.get('/api/task/'+ id)
.then(response => {
this.$store.commit('SET_TASK',response.data);
this.$store.commit('SET_EDIT',true);
});
},
deleteTask: function(id){
axios.delete('/api/task/delete/' + id)
this.$store.dispatch('getTask');
}
}
}
</script>
I'am getting "Uncaught TypeError: Cannot read property 'task' of undefined " in this line of code 'return this.$store.state.task.list;'
acoording to documentation of vuex
By default, actions, mutations and getters inside modules are still
registered under the global namespace
so you can only use getters in vuex root context.
Well, the state you're trying to retrieve doesn't match the structure of your state:
state: {
edit: false,
list:[],
note: {
note : '',
id : ''
}
},
If you change this.$store.state.task.list to this.$store.state.list then you should be all patched up.