How to make nested properties reactive in Vue - vue.js

I hava a component with complex nested props:
<template>
<div>
<a-tree :tree-data="data" :selected-keys="[selectedKey]" #select="onSelect" />
<div>
<a-input v-model="sheet[selectedKey].tableName" />
<ux-grid ref="previewTable">
<ux-table-column v-for="field in sheet[selectedKey].fields"
:key="field.name" :field="field.name">
<a-input slot="header" v-model="field.label" />
</ux-table-column>
</ux-grid>
</div>
</div>
</template>
<script>
export default {
props: {
previewData: { type: Array, default: () => [] }
},
data () {
return {
data: this.previewData,
selectedKey: '0-0-0',
sheet: { 'none': { tableName: null, fields: [] } }
}
},
created () {
this.data.forEach((file, fid) => {
file.sheets.forEach((sheet, sid) => {
this.$set(this.sheet, `0-${fid}-${sid}`, {
tableName: sheet.label,
fields: sheet.fields.map(field => ({ ...field }))
})
})
})
},
mounted () {
this.$refs.previewTable.reloadData(this.data[0].sheets[0].data)
},
methods: {
onSelect ([ key ], { node }) {
if (key !== undefined && 'fields' in node.dataRef) {
this.selectedKey = key
this.$refs.previewTable.reloadData(node.dataRef.data)
} else {
this.selectedKey = 'none'
this.$refs.previewTable.reloadData()
}
}
}
}
</script>
And previewData props is something like:
{
name: "example.xlsx",
filename: "80b8519f-f7f1-4524-9d63-a8b6c92152b8.xlsx",
sheets: [{
name: "example",
label: "example",
fields:[
{ label: "col1", name: "col1", type: "NUMBER" },
{ label: "col2", name: "col2", type: "STRING" }
]
}]
}
</script>
This component allows user to edit the label properties. I have to make Object sheet reactive to user input, and I tried $set and Object.assign, it works for sheets.label but fields[].label is still not reactive.
I wish to know what would be the declarative (and optimal) solution for it

You might need a watcher or computed property in React for previewData to be changed.

Related

Vue.js: Child Component mutates state, parent displays property as undefined

I have a parent component that lists all the tasks:
<template>
<div class="tasks-wrapper">
<div class="tasks-header">
<h4>{{ $t('client.taskListingTitle') }}</h4>
<b-button variant="custom" #click="showAddTaskModal">{{ $t('client.addTask') }}</b-button>
</div>
<b-table
striped
hover
:items="tasks"
:fields="fields"
show-empty
:empty-text="$t('common.noResultsFound')">
</b-table>
<AddTaskModal />
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import AddTaskModal from '#/components/modals/AddTaskModal'
import moment from 'moment'
export default {
name: 'TaskListing',
components: {
AddTaskModal
},
data () {
return {
tasks: [],
fields: [
{ key: 'createdOn', label: this.$t('tasks.tableFields.date'), formatter: 'formatDate' },
{ key: 'domain', label: this.$t('tasks.tableFields.task') },
{ key: 'comment', label: this.$t('tasks.tableFields.comment') },
{ key: 'status', label: this.$t('tasks.tableFields.status') }
]
}
},
computed: {
...mapGetters('users', ['user'])
},
methods: {
...mapActions('tasks', ['fetchTasks']),
...mapActions('users', ['fetchUserById']),
formatDate: function (date) {
return moment.utc(date).local().format('DD.MM.YYYY HH:mm')
},
showAddTaskModal () {
this.$bvModal.show('addTaskModal')
}
},
async mounted () {
const currUserId = this.$router.history.current.params.id
if (this.user || this.user.userId !== currUserId) {
await this.fetchUserById(currUserId)
}
if (this.user.clientNumber !== null) {
const filters = { clientReferenceNumber: { value: this.user.clientNumber } }
this.tasks = await this.fetchTasks({ filters })
}
}
}
</script>
Inside this component there is a child which adds a task modal.
<template>
<b-modal
id="addTaskModal"
:title="$t('modals.addTask.title')"
hide-footer
#show="resetModal"
#hidden="resetModal"
>
<form ref="form" #submit.stop.prevent="handleSubmit">
<b-form-group
:invalid-feedback="$t('modals.requiredFields')">
<b-form-select
id="task-type-select"
:options="taskTypesOptions"
:state="taskTypeState"
v-model="taskType"
required
></b-form-select>
<b-form-textarea
id="add-task-input"
:placeholder="$t('modals.enterComment')"
rows="3"
max-rows="6"
v-model="comment"
:state="commentState"
required />
</b-form-group>
<b-button-group class="float-right">
<b-button variant="danger" #click="$bvModal.hide('addTaskModal')">{{ $t('common.cancel') }}</b-button>
<b-button #click="addTask">{{ $t('modals.addTask.sendMail') }}</b-button>
</b-button-group>
</form>
</b-modal>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
name: 'AddTaskModal',
data () {
return {
comment: '',
commentState: null,
taskTypesOptions: [
{ value: null, text: this.$t('modals.addTask.taskType') },
{ value: 'OnBoarding', text: 'Onboarding' },
{ value: 'Accounts', text: 'Accounts' },
{ value: 'TopUp', text: 'Topup' },
{ value: 'Overdraft', text: 'Overdraft' },
{ value: 'Aml', text: 'Aml' },
{ value: 'Transfers', text: 'Transfers' },
{ value: 'Consultation', text: 'Consultation' },
{ value: 'TechnicalSupport', text: 'TechnicalSupport' },
{ value: 'UnblockPin', text: 'UnblockPin' },
{ value: 'Other', text: 'Other' }
],
taskType: null,
taskTypeState: null
}
},
computed: {
...mapGetters('users', ['user']),
...mapGetters('tasks', ['tasks'])
},
methods: {
...mapActions('tasks', ['addNewTask', 'fetchTasks']),
...mapActions('users', ['fetchUserById']),
async addTask (bvModalEvt) {
bvModalEvt.preventDefault()
if (!this.checkFormValidity()) { return }
const currUserId = this.$router.history.current.params.id
if (this.user || this.user.userId !== currUserId) {
await this.fetchUserById(currUserId)
}
const data = {
clientPhone: this.user.phoneNumber,
comment: this.comment,
clientReferenceNumber: this.user.clientNumber,
domain: this.taskType
}
await this.addNewTask(data)
if (this.user.clientNumber !== null) {
const filters = { clientReferenceNumber: { value: this.user.clientNumber } }
this.tasks = await this.fetchTasks({ filters })
// this.tasks may be useless here
}
console.log(this.tasks)
this.$nextTick(() => { this.$bvModal.hide('addTaskModal') })
},
checkFormValidity () {
const valid = this.$refs.form.checkValidity()
this.commentState = valid
this.taskTypeState = valid
return valid
},
resetModal () {
this.comment = ''
this.commentState = null
this.taskTypeState = null
}
}
}
</script>
When I add a task I call getalltasks to mutate the store so all the tasks are added. Then I want to render them. They are rendered but the property createdOn on the last task is InvalidDate and when I console log it is undefined.
The reason I need to call gettasks again in the modal is that the response on adding a task does not return the property createdOn. I do not want to set it on the front-end, I want to get it from the database.
I logged the store and all the tasks are added to the store.
Why is my parent component not rendering this particular createdOn property?
If I refresh the page everything is rendering fine.
If you add anything into a list of items that are displayed by v-for, you have to set a unique key. Based on your explanation, I assume that your key is the index and when you add a new item, you mess with the current indexes. Keys must be unique and unmutateable. What you need to do is to create a unique id for each element.
{
id: Math.floor(Math.random() * 10000000)
}
When you create a new task, use the same code to generate a new id, and use id as key. If this doesn't help, share your d-table and related vuex code too.

Avoid mutating a prop directly (render function)

just trying out creating components with the render function, but I am getting a weird warning:
Following component:
import Vue, { CreateElement, VNode } from 'vue'
export default Vue.extend({
name: 'p-form-input',
props: {
type: String,
name: String,
value: {
type: [ String, Number ]
},
placeholder: String,
disabled: Boolean
},
data() {
return {
localValue: ''
}
},
watch: {
value(value) {
this.localValue = value
}
},
mounted() {
this.localValue = this.$props.value
},
methods: {
onInput(e: any) {
this.localValue = e.target.value
this.$emit('input', this.localValue)
},
onChange(e: any) {
this.localValue = e.target.value
this.$emit('change', this.localValue)
}
},
render(h: CreateElement): VNode {
return h('input', {
class: 'form-control',
domProps: {
disabled: this.$props.disabled,
type: this.$props.type,
name: this.$props.name,
placeholder: this.$props.placeholder,
value: this.localValue
},
on: {
input: this.onInput,
change: this.onChange
}
})
}
})
A v-model="inputValue" on the component does trigger a input/change on inputValue but im getting the warning?
Using vue 2.6.11!
Edit:
Don't mind the ts-ignore, it complains about no types found for it, so that's more cosmetic!!!
<template>
<div id="app">
<p-form-input type="text" name="some_input" v-model="inputValue" /> {{ inputValue }}
</div>
</template>
<script lang="ts">
import Vue from 'vue'
// #ts-ignore
import PFormInput from 'vue-components/esm/components/form-input'
export default Vue.extend({
name: 'App',
components: {
PFormInput,
},
data() {
return {
inputValue: 'fdsdsfdsf'
}
}
});
</script>
You have a prop named "value' and then you are using a variable named "value" in your method:
onInput(e: any) {
const value = e.target.value
this.localValue = value
this.$emit('input', value)
},
Don't reuse the name "value". In fact, you don't even need that variable:
onInput(e: any) {
this.localValue = e.target.value
this.$emit('input', this.localValue)
},
Same thing for onChange:
onChange(e: any) {
this.localValue = e.target.value
this.$emit('change', this.localValue)
}

How to get value altLanguages and put that dynamically into a JSON, so it can be loaded into the Head

Currently i am trying to get the value of altLanguages and output that dynamically in a JSON, so it can be injected into the head. The altLanguages are the meta attribute values that should be added before rendering the page to avoid the error (altLanguages is undefined). Anyone know how to do that.
<template>
<header class="site-header">
<router-link to="/" class="logo">Example Site</router-link>
<nav>
<ul>
<li v-for="menuLink in menuLinks" :key="menuLink.id">
<prismic-link :field="menuLink.link">{{ $prismic.richTextAsPlain(menuLink.label) }}</prismic-link>
</li>
</ul>
</nav>
<alternate-languages :altLanguages="altLanguages" />
<!-- <alternate-content :altLanguages="altLanguages" /> -->
</header>
</template>
<script>
export default {
props: {
id: { type: String, default: "" },
altLanguages: { type: Array, default: () => ([]) }
},
data() {
return {
menuContent: [],
menuLinks: [],
// altLanguages: []
};
},
methods: {
async getMenu(lang) {
//Query to get menu content
const menuContent = await this.$prismic.client.getSingle("menu", {
lang: lang
});
this.menuContent = menuContent;
this.menuLinks = menuContent.data.menu_links;
}
},
created() {
// this.getLanguages(this.id);
this.getMenu(this.$route.params.lang);
},
watch: {
$route(to, from) {
this.getMenu(to.params.lang);
}
}
// beforeRouteUpdate(to, from, next) {
// console.log("new");
// this.getMenu(to.params.lang);
// next();
// }
};
</script>
//expected output
export default {
data: function () {
return {
title: 'My Title'
}
},
// Usage with context the component
head: {
// To use "this" in the component, it is necessary to return the object through a function
title: function () {
return {
inner: this.title
}
},
meta: [
// altLanguages should be output in here.....
{ name: 'description', content: 'My description', id: 'desc' }
]
}
...
}
}

Display Boolean on Vuetify dataTable

I made a data table with vue vuetify and I fetch my data from my api server.
In my table, everything is display except boolean.
Can you help me to see clearer
Table component
<template>
<v-data-table
:headers="headers"
:items="fixture"
:items-per-page="5"
class="elevation-10"
>
</v-data-table>
</template>
<script>
export default {
name: 'my_fixtures',
props: ['fixture'],
data () {
return {
headers: [
{ text: 'name',value: 'name'},
{ text: 'State', value: 'on' },
{ text: 'Group', value: 'group' },
{ text: 'Recipe', value: 'recipe' },
{ text: 'start', value: 'startDate' },
{ text: 'end', value: 'endDate' },
{ text: 'Action', value: 'action' },
],
items: this.fixtures
}
}
}
</script>
In the object that I receive , the key 'on' is a Boolean.
I have all display , but nothing in the 'on' column
and this is what I do with props
<template>
<v-container>
<my_fixtures v-bind:fixture="fixtures"></my_fixtures>
<router-view></router-view>
</v-container>
</template>
<script>
import my_fixtures from "./greenhouse/fixtures";
export default {
name: "my_client",
data: function (){
return {fixtures: []}
},
components: {my_fixtures},
mounted() {
http.get('fixture/client')
.then(result => {
this.fixtures = result;
})
.catch(error => {
console.error(error);
});
}
}
</script>
Process and print data using methods.
try this.
<td class="text-xs-right">{{ computedOn(props.fixture.on) }}</td>
export default {
methods: {
computedOn (value) {
return String(value)
}
}
}
UPDATE
Modifying original data due to vuetify bug
https://github.com/vuetifyjs/vuetify/issues/8554
export default {
mounted() {
http.get('fixture/client')
.then(result => {
this.fixtures = result.map(value => {
value.on = String(value.on)
})
})
.catch(error => {
console.error(error);
});
}
}

Vue/AgGrid: Floating Filter Component, Array of Objects Cell Data

In my AgGrid data, I have a column which has a value of array of objects like this:
...
media:
[
{ title: 'Facebook', site: 'http://www.facebook.com'},
{ title: 'Twitter', site: 'http://www.twitter.com'}
{ title: 'Instagram', site: 'http://www.instagram.com'}
]
...
I managed to show in by cellRenderer like this
'Facebook, Twitter, Instagram'
But my problem is my floating filter , I don't know how to filter all columns which has for example has 'Facebook' media.
Here's my custom floating filter component:
import Vue from 'vue'
export default Vue.extend({
template: `
<input type="text" #change="valueChanged($event)"/>
`,
data: function() {
return {
currentValue: ''
}
},
beforeMount() {},
mounted() {},
methods: {
valueChanged(event) {
this.currentValue = event.target.value
this.params.onFloatingFilterChanged({
model: this.buildModel()
})
},
onParentModelChanged(parentModel) {
this.currentValue = !parentModel ? 0 : parentModel.filter
},
buildModel() {
if (this.currentValue === 0) {
return null
}
return {
filterType: 'text',
type: 'equalsTo',
filter: this.currentValue,
filterTo: null
}
}
}
});