Vuejs - Resolve deep nested v-model property at runtime - vue.js

I have a dynamic form where the v-model of the input control is resolved at runtime. It works for simple 0 or 1 level deep objects. But I do not know how to get it working for nested properties that are more than 1 level deep.
My HTML is like:
<div v-for="element in elements" v-bind:key="element.name">
<q-input v-model="inputdata[element.model]"></q-input>
</div>
Javascript
<script>
export default {
data () {
return {
inputdata: {
account: {
name: '',
address: {
street: ''
}
},
},
}
},
}
</script>
Array with data:
elements: [
{
type: 'text',
hint: 'Address',
label: 'Street',
model: 'account.address.street', // does not work. i want to be able to set any level deep property
name: 'street'
}
]
As long as I try to set the property at 0 or 1st level (inputdata or inputdata.account), it works.
How to get a property as deep as inputdata.account.name or inputdata.account.address.street to work?

maybe you can use custom iterative methods instead of v-model
const getValueByModel = (model, data) => {
if(model.includes('.')){
model = model.split('.');
let key = model.shift();
return getValueByModel(model.join('.'), data[key]);
}
else{
return data[model];
}
}
const setValueByModel = (model, oldObject, newValue) => {
if(model.includes('.')){
model = model.split('.');
let key = model.shift();
oldObject[key] = setValueByModel(model.join('.'), oldObject[key], newValue);
}
else{
oldObject[model] = newValue;
}
return oldObject;
}
const getValueByModel = (model, data) => {
if(model.includes('.')){
model = model.split('.');
let key = model.shift();
return getValueByModel(model.join('.'), data[key]);
}
else{
return data[model];
}
}
const setValueByModel = (model, oldObject, newValue) => {
if(model.includes('.')){
model = model.split('.');
let key = model.shift();
oldObject[key] = setValueByModel(model.join('.'), oldObject[key], newValue);
}
else{
oldObject[model] = newValue;
}
return oldObject;
}
new Vue({
el: '#app',
data () {
return {
inputdata: {
account: {
name: '',
address: {
street: ''
}
},
},
elements: [
{
type: 'text',
hint: 'Name',
label: 'Name',
model: 'account.name',
name: 'name'
},
{
type: 'text',
hint: 'Address',
label: 'Street',
model: 'account.address.street',
name: 'street'
},
]
}
},
methods: {
getInputValue(model){
return getValueByModel(model, this.inputdata);
},
updateInputValue(model, event){
let newValue = event.target.value;
this.inputdata = {...setValueByModel(model, this.inputdata, newValue)};
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<main id="app">
<div v-for="element in elements" v-bind:key="element.name">
<input :value="getInputValue(element.model)"
#input="updateInputValue(element.model, $event)"
:placeholder="element.name"/>
</div>
{{ inputdata }}
</main>

Related

How do I seperate an array so the data shows up as individual list items?

I am struggling re-writing some older code I made in vue. I have a form which allows the user to enter multiple email recipients. The problem is that the array in which these emails are stored outputs them as one single item.
How do I seperate these email addresses?
This is my code. I know that Vue has something called Split, but I am not sure where I could use it here (or if it would change anything).
<template>
<b-form-group :label="label">
<vue-tags-input
v-model="inputValue"
:tags="tags"
:autocomplete-min-length="0"
:add-on-key="[',', 13]"
:autocomplete-items="filteredAutocompleteItems"
:placeholder="$t('addEmailAddressesHere')"
data-test-id="send-doc-email-to"
#before-adding-tag="addTag"
#tags-changed="tagsChanged"
class="mw-100"
/>
</b-form-group>
</template>
<script>
import {propertyService} from '#/services/property';
import {tenancyService} from '#/services/tenancy';
import {unitService} from '#/services/unit';
import {userService} from '#/services/user';
import VueTagsInput from '#johmun/vue-tags-input';
export default {
components: {
VueTagsInput
},
props: {
value: Array,
label: String,
entityId: String,
entityType: String,
prefill: {
type: Boolean,
default: false
},
asNotification: {
type: Boolean,
default: false
},
includeUser: {
type: Boolean,
default: false
}
},
data() {
return {
inputValue: '',
autocompleteItems: []
};
},
computed: {
tags() {
return (this.value || []).map(this.setText);
},
filteredAutocompleteItems() {
return this.autocompleteItems.filter(autocompleteItem =>
autocompleteItem.text.toUpperCase().includes(this.inputValue.toUpperCase()));
}
},
methods: {
addTag({tag, addTag}) {
if (!tag.recipients) {
tag.recipients = [{emailAddress: tag.text}];
}
addTag(tag);
},
setText(tag) {
tag.text = [tag.description, tag.recipients.map(recipient => recipient.emailAddress).join(', ')].filter(Boolean).join(' | ');
return tag;
},
tagsChanged(newTags) {
this.$emit('input', newTags);
},
load() {
switch (this.entityType) {
case 'TENANCY':
userService.getCurrentUser().then(userResult => {
tenancyService.getTenants(this.entityId).then(result => {
const defaultTags = [];
const recipients = result.data
.map(tenant => tenant.legalEntity)
.filter(legalEntity => legalEntity.email || (!legalEntity.email && this.asNotification ? legalEntity.name : null))
.map(legalEntity => ({
emailAddress: legalEntity.email || (!legalEntity.email && this.asNotification ? legalEntity.name.concat(' ', `(${this.$t('letterMail').toLowerCase()})`) : null),
legalEntityId: legalEntity.id
}));
if (recipients.length) {
defaultTags.push(this.setText({description: this.$t('tenants'), recipients}));
}
this.autocompleteItems.push(...defaultTags);
if (this.includeUser) {
defaultTags.push(this.setText({
description: this.$t('user'),
recipients: [{emailAddress: userResult.data.email}]
}));
}
if (this.prefill) {
this.tagsChanged(defaultTags);
}
tenancyService.getUnits(this.entityId).then(result =>
result.data.forEach(unitTenancy => this.addPropertyContactsToAutocompleteItems(unitTenancy.unit.propertyId)));
});
});
break;
case 'UNIT':
unitService.get(this.entityId).then(result =>
this.addPropertyContactsToAutocompleteItems(result.data.propertyId));
break;
case 'PROPERTY':
this.addPropertyContactsToAutocompleteItems(this.entityId);
break;
}
},
addPropertyContactsToAutocompleteItems(propertyId) {
propertyService.listContacts(propertyId).then(result => {
this.autocompleteItems.push(...result.data
.filter(contact => contact.email)
.map(contact => this.setText({
description: contact.profession ? this.$t(`model.contact.professions.${contact.profession}`) : null,
recipients: [{emailAddress: contact.email, legalEntityId: contact.id}]
}))
);
});
}
},
created() {
this.load();
}
};
</script>

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.

set data of parent component in child

In parent component, send a variable as prop to child(DirectionsRenderer)
In child there is function(preparePoints function in DirectionsRenderer.js) set this;
but couldnt access 'this' reference inside that function
Parent:
<template>
<div>
<div>
<h2>Start</h2>
<label>
<gmap-autocomplete #place_changed="setStartPlace"></gmap-autocomplete>
<button #click="addStartMarker">Add</button>
</label>
<br />
</div>
<div>
<h2>End</h2>
<label>
<gmap-autocomplete #place_changed="setEndPlace"></gmap-autocomplete>
<button #click="addEndMarker">Add</button>
</label>
<br />
</div>
<br />
<gmap-map ref="xyz" :center="center" :zoom="7" style="width:100%; height: 400px;">
<gmap-marker
:key="index"
v-for="(m, index) in markers"
:position="m.position"
#click="center=m.position"
></gmap-marker>
<DirectionsRenderer
:v-model="pointList" //this is what I want filled by child component
travelMode="DRIVING"
:origin="origin"
:destination="destionation"
/>
{{pointList}}
</gmap-map>
</div>
</template>
<script>
import DirectionsRenderer from "./DirectionsRenderer.js";
export default {
components: {
DirectionsRenderer,
},
name: "GoogleMap",
data() {
return {
center: { lat: 41.85, lng: -87.65 },
pointList: [],
markers: [],
places: [],
path: [],
currentPlace: null,
startPoint: {},
endPoint: {},
};
},
computed: {
origin() {
if (!this.startPoint) return null;
return this.startPoint;
},
destionation() {
if (!this.endPoint) return null;
return this.endPoint;
},
},
mounted() {
this.geolocate();
},
methods: {
getPoints() {
return this.pointList;
},
setStartPlace(place) {
this.currentPlace = place;
},
setEndPlace(place) {
this.currentPlace = place;
},
addStartMarker() {
if (this.currentPlace) {
const marker = {
lat: this.currentPlace.geometry.location.lat(),
lng: this.currentPlace.geometry.location.lng(),
};
this.startPoint = marker;
this.markers[0] = { position: marker };
this.places.push(this.currentPlace);
this.center = marker;
this.currentPlace = null;
}
},
addEndMarker() {
if (this.currentPlace) {
const marker = {
lat: this.currentPlace.geometry.location.lat(),
lng: this.currentPlace.geometry.location.lng(),
};
this.endPoint = marker;
this.markers[1] = { position: marker };
this.places.push(this.currentPlace);
this.center = marker;
this.currentPlace = null;
}
},
geolocate: function () {
navigator.geolocation.getCurrentPosition((position) => {
this.center = {
lat: position.coords.latitude,
lng: position.coords.longitude,
};
});
},
},
};
</script>
Child(DirectionsRenderer.js):
export default MapElementFactory({
name: "directionsRenderer",
ctr() {
return window.google.maps.DirectionsRenderer;
},
events: [],
mappedProps: {},
props: {
pointList: { type: Array },
origin: { type: Object },
destination: { type: Object },
travelMode: { type: String }
},
methods: {
preparePoints: (array) => {
var result = []
array.forEach(element => {
result.push({ lat: element.lat(), lng: element.lng() })
});
debugger;
this.pointList = result;//**Throws exception cant read 'pointList' of undefined
}
},
afterCreate(directionsRenderer) {
let directionsService = new window.google.maps.DirectionsService();
this.$watch(
() => [this.origin, this.destination, this.travelMode],
() => {
let { origin, destination, travelMode } = this;
if (!origin || !destination || !travelMode) return;
var self = this;
directionsService.route(
{
origin,
destination,
travelMode
},
(response, status) => {
self.preparePoints(response.routes[0].overview_path);
}
);
}
);
},
});
How to retrieve data from child properly?
There is array in child all I need to do set 'pointList' in child and use it in parent.
You cannot use events.
The problem is, that you do not have a child / parent relationship, but a deeper hierarchy. You can use provide/inject for this.
In your parent, use
export default {
... all your old code ...
provide() {
return {
// Notice that this is a lambda => this gets bound to the this in provide, which is the vue instance
pointsCallback: (points) => this.pointList = points
}
}
}
In your child, use this:
export default {
inject: ['pointsCallback'],
// Your other code ...
// { {
// Somewhere after self.preparePoints(response.routes[0].overview_path);
this.pointsCallback(theNewPointsListYouComputed);
// This will call the pointsCallback you defined in your parent.
// } } and so on
}

Vuejs2: Vue Tables 2 - Multiple Custom filters

I'm trying to use a Vue table 2 filter to filter data by date, unfortunately it is not wroking and I am not able to find the reason. Has anyone tried such multiple filters with Vue table 2?
I went through the documentation but cannot find a solution.
https://matanya.gitbook.io/vue-tables-2/custom-filters
Html code to filter the data by date
<div class="col-md-4">
<div class="form-group">
<label for="sel1">Start Date:</label>
<input type="text" class="form-control" #keyup="applyFilterSearchText(searchText)" v-model="searchText" placeholder="End date" />
</div>
</div>
import { Event } from "vue-tables-2";
import axios from "axios";
export default {
title: "HelloWorld",
props: {
msg: String
},
data() {
return {
letters: ["Filled", "Unfilled", "Expired"],
selectedLetter: "",
searchText: "",
columns: ["refNumber", "vacancyTitle", "sector", "startDate", "endDate", "vacancyStatus"],
//data: getdetails(),
options: {
headings: {
refNumber: "Ref",
vacancyTitle: "Title",
sector: "Sector",
startDate: "Start",
endDate: "End",
vacancyStatus: "Status"
},
customFilters: [
{
name: "alphabet",
callback: function(row, query) {
return row.vacancyStatus == query;
}
},
{
name: "datefilter",
callback: function(row, query) {
return row.startDate == query;
}
}
],
// filterAlgorithm: {
// textsearch(row, query) {
// return (row.title).includes(query);
// }
// },
sortable: ["startDate", "vacancyTitle","endDate"],
sortIcon: {
base: "fa",
is: "fa-sort",
up: "fa-sort-asc",
down: "fa-sort-desc"
},
texts: {
filter: "Search by text:"
}
},
tableData:[],
};
},
methods: {
applyFilter(event) {
this.selectedLetter = event.target.value;
Event.$emit("vue-tables.filter::alphabet", this.selectedLetter);
},
applyFilterSearchText() {
console.log(this.searchText,"heiiiiiiiiiiii");
Event.$emit("vue-tables.filter::datefilter", this.searchText);
},
getdetails(){
axios.get("https://localhost:44399/api/Vacancy")
.then((res) => {
console.log(res.data,"ressssssss");
this.tableData = res.data;
})
.catch(function(error) {
console.log("Error: ", error);
});
}
},
mounted() {
this.getdetails();
}
};
A potential solution to that is to sort the data before using it in your data-table. Start by creating a computed of your data but with all your potential filters in it and create data variables with "parameters" (sort direction, sort column...)
export default {
title: "HelloWorld",
props: {
msg: String
},
data () {
return {
yourData: [],
sortBy: 'name',
sortDir: 'asc',
filterSearch: ''
}
},
computed: {
filteredData () {
if (filterSearch != '') {
let _this = this;
return this.sortedData.filter(item => {
return item.name.toLowerCase().includes(_this.filterSearch.toLowerCase());
})
} else {
return this.sortedData;
}
},
sortedData() {
return this.yourData.sort((a, b) => {
let modifier = 1;
if (this.sortBy == "order") {
if (this.sortDir === 'asc') {
return a[this.sortBy] - b[this.sortBy]
} else if (this.sortDir === 'desc') {
return b[this.sortBy] - a[this.sortBy]
}
} else {
if (this.sortDir === 'desc') modifier = -1;
if (a[this.sortBy] < b[this.sortBy]) return -1 * modifier;
if (a[this.sortBy] > b[this.sortBy]) return modifier;
return 0;
}
});
}
}
}
With that you just have to replace the props value you use to pass data to your VueTables

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.