Make computations in a child component after axios requests in a parent component - vue.js

Here is my problem : I have a parent component Edit that makes several axios requests, the results of these requests are passed down to a child component (Menu in my example).
<template>
<div>
<p>
Order Id : {{ this.$route.params.id }}
</p>
<head-fields :menu="menu"></head-fields>
<div>
<b-tabs content-class="mt-3">
<b-tab title="Menu" active>
<Menu :menuItems="items" :nutritionalValues="nutritionalValues"></Menu>
</b-tab>
<b-tab title="Specifications">
<specifications :specifications="specifications">
</specifications>
</b-tab>
<b-tab title="Redundancies"><p>I'm the redundancies tab!</p></b-tab>
</b-tabs>
</div>
</div>
</template>
<script>
import HeadFields from "./HeadFields";
import Menu from "./Menu";
import Specifications from "./Specifications";
export default {
name: "Edit",
components: {HeadFields, Menu, Specifications},
data(){
return{
menu: {},
loaded: false,
items: {},
nutritionalValues: {},
specifications: {},
error:{}
}
},
created(){
this.find(this.$route.params.id);
},
methods:{
find(id){
axios.get('/menusV2/'+id)
.then(response => {
this.loading = false;
this.menu = response.data[0];
this.fetchMenu(this.menu.orderId);
this.fetchSpecifications(this.menu.orderId);
});
return this.menu;
},
fetchMenu(orderId){
// console.log(orderId);
axios
.get('/menusV2/'+orderId+'/menu')
.then(response => {
this.loading = false;
this.items = response.data.items;
this.nutritionalValues = response.data.nutritionalValues;
})
.catch(error => {
this.loading = false;
this.error = error.response.data.message || error.message;
})
},
fetchSpecifications(orderId){
axios
.get('/menusV2/'+orderId+'/specifications')
.then(response => {
this.loading = false;
this.specifications = response.data;
// this.checkSpecifications();
})
.catch(error => {
this.loading = false;
// this.error = error.response.data.message || error.message;
})
}
}
}
</script>
The data is passed down to the child component "Menu" as a prop :
<template>
<div class="panel panel-default">
<b-table
striped hover
:items="menuItems"
:fields="fields"
:primary-key="menuItems.pivotId"
>
</b-table>
</div>
</template>
<script>
export default {
name: "Menu",
props: ['menuItems', 'nutritionalValues'],
data() {
return {
loading: true,
perPage: ['10', '25', '50'],
rowSelected: true,
fields: [
{key: "meal", label: "Meal", sortable: true},
{key: "category", label: "Category", sortable: true},
{key: "itemName", label: "Name", sortable: true},
{key: "noOfServing", label: "Serving", sortable: true},
{key: "weight", label: "Weight", sortable: true},
{key: "calories", label: "Calories", sortable: true},
{key: "carbs", label: "Carbs", sortable: true},
{key: "proteins", label: "Proteins", sortable: true},
{key: "fats", label: "Fats", sortable: true},
]
}
},
mounted(){
this.checkSpecifications();
},
methods:{
searchIngredientSpecification(itemId, itemName, specifications){
//Checking of the ingredients name
for (var i=0; i < specifications.length; i++) {
if (specifications[i].itemName === itemName) {
console.log("Specification ! "+itemName);
}
}
//Checking of the nutritional properties
var ingredientNutritionalProperties = {};
axios
.get('/menusV2/'+itemId+'/ingredient/nutritionalProperties')
.then(response => {
ingredientNutritionalProperties = response.data;
});
console.log("Ingredient : " + itemName);
console.log(ingredientNutritionalProperties);
},
searchDishSpecification(itemId, itemName, specifications){
//Checking of the ingredients name
for (var i=0; i < specifications.length; i++) {
if (specifications[i].itemName === itemName) {
console.log("Specification ! "+itemName);
}
}
//Checking of the nutritional properties
var dishNutritionalProperties = {};
axios
.get('/menusV2/'+itemId+'/dish/nutritionalProperties')
.then(response => {
dishNutritionalProperties = response.data;
});
console.log("Dish : " + itemName);
console.log(dishNutritionalProperties);
var ingredientsDish = {};
var ingredientsNutritionalProperties = {};
axios
.get('/menusV2/'+itemId+'/getIngredients')
.then(response => {
ingredientsDish = response.data.ingredients;
ingredientsNutritionalProperties = response.data.nutritionalProperties;
});
console.log("Dish : " + itemName);
console.log(ingredientsDish);
console.log(ingredientsNutritionalProperties);
},
checkSpecifications(){
console.log("Check Specifications launched !");
console.log(this.menuItems);
var items = this.menuItems;
items.forEach(
element => {
switch(element.type){
case 'Ingredient':
this.searchIngredientSpecification(element.itemId,element.itemName,this.specifications);
break;
case 'Dish':
this.searchDishSpecification(element.itemId,element.itemName,this.specifications);
break;
}
}
);
},
}
}
</script>
The problem I have is around the methods in the child component that are fired before the menuItems prop is filled with data from the axios request.
I think that a possible fix to this problem would be to use computed properties or watchers but I don't really know if it will help me..
Here is the error that is thrown :
Thanks for your help !

You are getting the error because when the checkSpecifications method is run on mount, this.menuItems is not an array and forEach must only be used on arrays.
One option is to add a watcher to menuItems and only once it has been filled with a value (making sure it's an array) then run the checkSpecifications method.
Alternatively, you could define menuItems as an array and provide a default value.
props: {
menuItems: {
type: Array,
default: []
},
nutritionalValues: {
type: Array,
default: []
}
It's always good practice to define the type of your props.

Related

How to make nested properties reactive in Vue

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.

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.

Change radio button value to 'checked' (Vue.js, Vuetify)

I'm a little new in Vue.js and currently I am having a problem with radios.
I have a form with different inputs. When I'm submiting the form I create a JSON file with all the form answers:
Instead of this output I want the value of the selected radio to be 'Checked'.
{
"pos_r_1":"Radio 1"
"pos_r_2":"",
"pos_r_3":"",
"pos_r_4":"",
"pos_t_5":"this a test",
}
LIKE THAT:
{
"pos_r_1":"Checked"
"pos_r_2":"",
"pos_r_3":"",
"pos_r_4":"",
"pos_t_5":"this a test",
}
How can I change the value of the radio to 'checked'?
HTML
<v-form class="text-left" name="form" id="form">
<v-radio-group
v-model="checked"
hide-details="auto"
row
>
<v-radio
v-for="radio in group"
:key="radio.id"
:id="radio.id"
:name="radio.id"
:label="radio.text"
:value="radio.text"
/>
</v-radio-group>
<v-text-field
id="pos_t_5"
name="pos_t_5"
label="Text"
v-model="textfield"
/>
<v-btn
class="p-2"
color="primary"
elevation="11"
#click="onSubmit"
>click me</v-btn>
</v-form>
Script
export default Vue.extend({
name: 'Test',
data: function () {
return {
checked: '',
textfield: '',
group: [
{id: 'pos_r_1', text: 'Radio 1'},
{id: 'pos_r_2', text: 'Radio 2'},
{id: 'pos_r_3', text: 'Radio 3'},
{id: 'pos_r_4', text: 'Radio 4'},
],
}
},
onSubmit() {
this.loading = true
const form = document.querySelector('form');
const data = new FormData(form);
let currentObj = this;
let url = '/report/form/store';
axios({
url,
method: 'post',
data,
}) .then(function (response) {
currentObj.output = response.data;
}) .catch( (error) => {
if (error.response && error.response.status === 422){
this.errors = error.response.data.errors;
}
});
}
}
})
I've tried to change the value of the Radios to 'checked' but that doesn't work because then when I click one radio all getting checked.
This is only an example of the form. The form will be big with more that 20 different questions.
Update
This is not the way to do it. I have to manipulate the value of the radios buttons inside a Form Model, where I will save all the form answers. Then I can change the value of the radio to 'checked' easier.
So, radio buttons works as it should to https://developer.mozilla.org/ru/docs/Web/HTML/Element/Input/radio.
If you need to send a request like pos_r_3: Radio 3, you have to do some transformation on it. I'd suggest the code like:
export default Vue.extend({
name: 'Test',
data: function () {
return {
checked: '',
group: [
{id: 'pos_r_1', text: 'Radio 1'},
{id: 'pos_r_2', text: 'Radio 2'},
{id: 'pos_r_3', text: 'Radio 3'},
{id: 'pos_r_4', text: 'Radio 4'},
],
}
},
onSubmit() {
this.loading = true
const form = document.querySelector('form');
const data = new FormData(form);
for (let i = 0; i < this.group.length; i++){
const currentGroup = this.group[i];
data.set(currentGroup.id, currentGroup.text === this.checked ? 'Checked' : '');
}
let currentObj = this;
let url = '/report/form/store';
axios({
url,
method: 'post',
data,
}) .then(function (response) {
currentObj.output = response.data;
}) .catch( (error) => {
if (error.response && error.response.status === 422){
this.errors = error.response.data.errors;
}
});
}
}

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);
});
}
}

Vuejs Reactivity on Set Value

I have a custom table with actions via a modal popup that set values on Rows. Things are mostly working great (Updates to Foo and Bar get sent to the backend and are set in a database, reload of the page pulls the data from the database and shows foo/bar were correctly set). The only not-great part is on setting of Foo, the value in the table does not get updated. Bar gets updated/is reactive. (the template uses foo.name and bar.id). Does anyone have any ideas on how to get Foo to update in the table? I've changed the moustache template to use {{ foo.id }} and it updates, but I need foo.name.
<template>
<div>
<c-dialog
v-if="foo_modal"
title="Set Foo"
:actions="foo_modal_options.actions"
#cancel="foo_modal = null">
<slot>
<div class="form-group">
<label>Foo:</label>
<select class="form-control" v-model="foo_modal.thing.foo.id">
<option v-for="foo in foos" :key="foo.id" :value="foo.id">{{ foo.name }}</option>
</select>
</div>
</slot>
</c-dialog>
<c-dialog
v-if="bar_modal"
title="Set Rod/Stick"
:actions="bar_modal_options.actions"
#cancel="bar_modal = null">
<slot>
<div class="form-group">
<label>Rod:</label>
<select class="form-control" v-model="bar_modal.thing.rod.id">
<option v-for="bar in bars" :key="bar.id" :value="bar.id" v-if="bar.type === 'rod'">{{ bar.id }}</option>
</select>
</div>
<div class="form-group">
<label>Stick:</label>
<select class="form-control" v-model="bar_modal.thing.stick.id">
<option v-for="bar in bars" :key="bar.id" :value="bar.id" v-if="bar.type === 'stick'">{{ bar.id }}</option>
</select>
</div>
</slot>
</c-dialog>
<c-table-paginated
class="c-table-clickable"
:rows="grid.data"
:columns="grid.columns"
:actions="grid.actions"
:key="componentKey">
</c-table-paginated>
</div>
</template>
<script>
import fooModal from '../../components/fooModal.vue';
import barModal from '../../components/barModal.vue';
import CTablePaginated from "../../components/custom/cTable/cTablePaginated";
import cTooltip from '../../components/custom/cTooltip/cTooltip.vue';
import cDialog from '../../components/custom/cDialog/cDialog.vue';
import moment from 'moment';
export default {
components: { CTablePaginated, cTooltip, cDialog },
methods: {
loadData() {
let that = this;
that.$http.get('/things', { params: that.param || {} })
.then(function (things) {
that.things = things.data;
that.grid.data = that.things;
});
},
setBar(thing_id, options, cb) {
let that = this;
this.$http.patch(`/things/${thing_id}`, { rod_id: options.rod, stick_id: options.stick })
.then(function () {
cb();
});
},
setFoo(thing_id, options, cb) {
let that = this;
this.$http.patch(`/things/${thing_id}`, { foo_id: options.foo_id })
.then(function () {
cb();
})
},
},
data() {
return {
componentKey: 0,
things: null,
foos: [],
bars: [],
foo_modal: null,
foo_modal_options: {
actions: [
{
label: "Save",
class: "btn-primary",
action: (function (ctx) {
return function () {
const thing = ctx.foo_modal.thing;
const options = {
foo_id: thing.foo.id,
};
ctx.setFoo(thing.id, options, function () {
ctx.foo_modal = null;
});
}
})(this)
},
{
label: "Cancel",
action: (function (ctx) {
return function () {
ctx.foo_modal = null;
}
})(this)
}
]
},
bar_modal: null,
bar_modal_options: {
actions: [
{
label: "Save",
class: "btn-primary",
action: (function (ctx) {
return function () {
const thing = ctx.bar_modal.thing;
const options = {
rod: thing.rod.id,
stick: thing.stick.id
};
ctx.setBar(thing.id, options, function () {
ctx.bar_modal = null;
});
}
})(this)
},
{
label: "Cancel",
action: (function (ctx) {
return function () {
ctx.bar_modal = null;
}
})(this)
}
]
},
grid: {
data: [],
columns: [
{
label: "Foo",
value: function (row) {
if (!row.foo) return "No foo set";
return `${row.foo.name }`;
}
},
{
label: "Rod/Stick",
value: function (row) {
if (!row.rod && !row.stick) return "No rod/stick set";
if (!row.rod) return `No rod set/${row.stick.id}`;
if (!row.stick) return `${row.rod.id}/no stick set`;
return `${row.rod.id}/${row.stick.id}`;
}
}
],
actions: [
{
label: "Set Foo",
visible: function (thing) {
return !thing.active;
},
action: (function (ctx) {
return function (thing) {
if (!thing.foo) thing.foo = {};
ctx.foo_modal = {
thing: thing
};
}
})(this)
},
{
label: "Set Bar",
visible: function (thing) {
return !thing.active;
},
action: (function (ctx) {
return function (thing) {
if (!thing.rod) thing.rod = {};
if (!thing.stick) thing.stick = {};
ctx.bar_modal = {
thing: thing
};
}
})(this)
},
],
}
};
},
props: {
title: {
type: String
},
param: {
type: Object,
required: true
},
events: {
type: Object,
required: true
}
},
created() {
let that = this;
this.loadData();
this.$http.get('/bars')
.then(function (bars) {
that.bars = bars.data;
});
this.$http.get('/foos')
.then(function (foos) {
that.foos = foos.data;
});
},
}
</script>
There are two possibilities you can try both if any one of them can help you.
You can set value by using Vuejs this.$set method for deep reactivity. Click here.
You can use this.$nextTick(()=>{ // set your variable here }). Click here.