it's the first time I use Vue (v2 not v3) and I'm stucked trying to use a variable (defined inside a methods) inside the template.
My semplified code:
<template>
<div class="container" #mouseover="isHovered = true" #mouseleave="isHovered = false">
<div class="c-container">
<div ref="topCContainerRef" class="top-c-container">
<div
:class="['top-c', ...]"
:style="{ height: `${isHovered ? 0 : this.scaledHeight}` }" // <-- HERE I need `scaledHeight`
>
</div>
</div>
</div>
</div>
</template>
<script>
import { scaleLinear } from 'd3-scale'
export default {
name: 'MyComponent',
components: { },
props: {
...,
datum: {
type: Number,
required: true,
},
...
},
data: function () {
return {
isHovered: false,
scaledHeight: {},
}
},
mounted() {
this.matchHeight()
},
methods: {
matchHeight() {
const topCContainerHeight = this.$refs.topCContainerRef.clientHeight
const heightScale = scaleLinear([0, 100], [20, topCContainerHeight])
const scaledHeight = heightScale(this.datum)
this.scaledHeight = scaledHeight // I want to use this value inside the template
},
},
}
</script>
How can I get the value of scaledHeight inside the template section?
If I didn't use this, I get no error but the height value is always 0, like scaledHeight is ignored..
I read the documentation but it doesn't help me
I encountered and solved this problem today.
You can change your styles like below.
<div
:class="['top-c', ...]"
:style="{ height: isHovered ? 0 : scaledHeight }"
>
It works fine for me, and hope it will help you~~
Fixed using computed
computed: {
computedHeight: function () {
return this.isHovered ? 0 : this.matchHeight()
},
},
methods: {
matchHeight() {
const topCContainerHeight = this.$refs.topCContainerRef.clientHeight
const heightScale = scaleLinear([0, 100], [20, topCContainerHeight])
return heightScale(this.datum)
},
},
Related
Here's the problem I'm having. I have a Leads page, that is my Leads.vue template. It loads my leads and then passes the leads data to other components via props.
The LeadSources component receives a computed method as it's property.
You can see that on the Leads.vue page, the LeadSources component calls the getSourceData() method for it's property data.
When I check the value of the props for LeadSources.vue in the setup() the value for chartData initially is an empty array. If the page hot-reloads then the LeadSources apexchart will populate with the :series data but otherwise I cannot get it to work.
Essentially it works like this.
Leads.vue passes getSourceData() to the LeadsSources.vue component which on the setup() sets it as the variable series and tries to load the apexchart with it.
It will not work if I refresh my page but if I save something in my IDE, the hot-reload will load the updated apexchart and the data appears. It seems like the prop values don't get set in the setup() function the first time around. How do I architecturally get around this? What's the proper way to set this up? I can't tell if the issue is on the leads.vue side of things or with the way that the LeadSources.vue component is being put together.
Any help would be appreciated, I've spent way too long trying to get this to work properly.
Leads.vue
<template>
<!--begin::Leads-->
<div class="row gy-5 g-xl-8 mb-8">
<div class="col-xxl-12">
<LeadTracker
:lead-data="leadData"
:key="componentKey"
/>
</div>
</div>
<div class="row gy-5 g-xl-8 mb-8">
<div class="col-xxl-12">
<LeadSources
chart-color="primary"
chart-height="500"
:chart-data="getSourceData"
:chart-threshold="sourceThreshold"
widget-classes="lead-sources"
></LeadSources>
</div>
</div>
<!--end::Leads-->
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent, onMounted } from "vue";
import { setCurrentPageTitle } from "#/core/helpers/breadcrumb";
import LeadSources from "#/components/leads/sources/LeadSources.vue";
import LeadTracker from "#/components/leads/tracker/LeadTracker.vue";
import LeadService from "#/core/services/LeadService";
import ApiService from "#/core/services/ApiService";
import {Lead} from "#/core/interfaces/lead";
export default defineComponent({
name: "leads",
components: {
LeadTracker,
LeadSources
},
data() {
return {
leadData: [] as Lead[],
}
},
beforeCreate: async function() {
this.leadData = await new LeadService().getLeads()
},
setup() {
onMounted(() => {
setCurrentPageTitle("Lead Activity");
});
const sourceThreshold = 5;
return {
sourceThreshold,
componentKey: 0
};
},
computed: {
getSourceData() {
interface SingleSource {
source: string;
value: number;
}
const sourceData: Array<SingleSource> = [];
// Make array for source names
const sourceTypes = [];
this.leadData.filter(lead => {
if (!sourceTypes.includes(lead.source)) sourceTypes.push(lead.source);
});
// Create objects for each form by name, push to leadSourceData
sourceTypes.filter(type => {
let totalSourceLeads = 1;
this.leadData.filter(form => {
if (form.source == type) totalSourceLeads++;
});
const leadSourceData = {
source: type,
value: totalSourceLeads
};
sourceData.push(leadSourceData);
});
// Sort by source popularity
sourceData.sort(function(a, b) {
return a.value - b.value;
});
return sourceData;
}
}
});
</script>
LeadSources.vue
<template>
<!--begin::Lead Sources Widget-->
<div :class="widgetClasses" class="card card-footprint">
<!--begin::Body-->
<div
class="card-body p-0 d-flex justify-content-between flex-column overflow-hidden"
>
<div class="d-lg-flex flex-stack flex-grow-1 px-9 py-6">
<!--begin::Text-->
<div class="d-flex flex-column text-start col-lg-10">
<span class="card-title">Lead Sources</span>
<span class="card-description">Where your leads are coming from.</span>
<!--begin::Chart-->
<div class="d-flex flex-column">
<apexchart
class="mixed-widget-10-chart lead-sources-donut"
:options="chartOptions"
:series="series"
type="donut"
:height="chartHeight"
:threshold="chartThreshold"
></apexchart>
</div>
<!--end::Chart-->
</div>
<!--begin::Unused Data-->
<div class="d-flex flex-row flex-lg-column lg-col-2 justify-content-between unused-data">
<div class="alt-sources flex-fill">
<div><span class="alt-header">Other Sources:</span></div>
<div v-for="item in otherSources" :key="item.source">
<span>{{ item.source }}</span>
<span>{{ item.value }}%</span>
</div>
</div>
<div class="alt-sources flex-fill">
<div><span class="alt-header">Sources Not Utilized:</span></div>
<div v-for="item in unusedSources" :key="item.source">
<span>{{ item.source }}</span>
</div>
</div>
</div>
<!--end::Unused Data-->
</div>
</div>
</div>
<!--end::Lead Sources Widget-->
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "LeadSource",
props: {
widgetClasses: String,
chartColor: String,
chartHeight: String,
chartLabels: Array,
chartData: Array,
chartThreshold: Number
},
setup(props) {
const sum = (data) => {
let total = 0;
data?.map(function(v) {
total += v;
});
return total;
}
const chartData = ref(props.chartData).value;
const threshold = ref(props.chartThreshold).value;
const usedSourcesLabel: string[] = [];
const usedSourcesData: number[] = [];
const otherSources: any = [];
const unusedSources: any = [];
const splitData = (data, max) => {
// set used, other sources < 5%, unused sources
data.filter((item) => {
if (item.value > max) {
usedSourcesLabel.push(item.source);
usedSourcesData.push(item.value);
} else if (item.value < max && item.value != 0 && item.value !== null) {
otherSources.push(item);
} else if (item.value == 0 || item.value === null) {
unusedSources.push(item);
}
});
};
splitData(chartData, threshold);
const chartOptions = {
chart: {
width: 380,
type: "donut"
},
colors: [
"#1C6767",
"#CD2E3B",
"#154D5D",
"#F1D67E",
"#4F9E82",
"#EF8669",
"#393939",
"#30AEB4"
],
plotOptions: {
pie: {
startAngle: -90,
endAngle: 270
}
},
dataLabels: {
enabled: false
},
fill: {
type: "gradient",
gradient: {
type: "horizontal",
shadeIntensity: 0.5,
opacityFrom: 1,
opacityTo: 1,
stops: [0, 100],
}
},
legend: {
show: true,
position: "left",
fontSize: "16px",
height: 220,
onItemClick: {
toggleDataSeries: false
},
onItemHover: {
highlightDataSeries: false
},
formatter: function (val, opts) {
return val + " - " + opts.w.globals.series[opts.seriesIndex];
}
},
title: {
text: undefined
},
tooltip: {
style: {
fontSize: "14px"
},
marker: {
show: false
},
y: {
formatter: function(val) {
return val + "%";
},
title: {
formatter: (seriesName) => seriesName,
},
}
},
labels: usedSourcesLabel,
annotations: {
position: "front",
yaxis: [{
label: {
text: "text annotation"
}
}],
xaxis: [{
label: {
text: "text xaxis annotation"
}
}],
},
responsive: [{
breakpoint: 480,
options: {
legend: {
position: "bottom",
horizontalAlign: "left"
}
}
}]
};
const series = usedSourcesData;
return {
chartOptions,
series,
otherSources,
unusedSources
};
}
});
</script>
Edited
I will attach the LeadService.ts class as well as the ApiService.ts class so you can see where the data is coming from
LeadService.ts
import ApiService from "#/core/services/ApiService";
import {Lead} from "#/core/interfaces/lead";
export default class LeadService {
getLeads() {
const accountInfo = JSON.parse(localStorage.getItem('accountInfo') || '{}');
ApiService.setHeader();
return ApiService.query("/leads", {params: {client_id : accountInfo.client_id}})
.then(({ data }) => {
let leadData: Lead[] = data['Items'];
return leadData;
})
.catch(({ response }) => {
return response;
});
}
}
ApiService.ts
import { App } from "vue";
import axios from "axios";
import VueAxios from "vue-axios";
import JwtService from "#/core/services/JwtService";
import { AxiosResponse, AxiosRequestConfig } from "axios";
import auth from "#/core/helpers/auth";
/**
* #description service to call HTTP request via Axios
*/
class ApiService {
/**
* #description property to share vue instance
*/
public static vueInstance: App;
/**
* #description initialize vue axios
*/
public static init(app: App<Element>) {
ApiService.vueInstance = app;
ApiService.vueInstance.use(VueAxios, axios);
ApiService.vueInstance.axios.defaults.baseURL = "https://api.domain.com/";
}
/**
* #description set the default HTTP request headers
*/
public static setHeader(): void {
ApiService.vueInstance.axios.defaults.headers.common[
"Authorization"
] = `Bearer ${auth.getSignInUserSession().getIdToken().jwtToken}`;
ApiService.vueInstance.axios.defaults.headers.common[
"Content-Type"
] = "application/json application/vnd.api+json";
}
/**
* #description send the GET HTTP request
* #param resource: string
* #param params: AxiosRequestConfig
* #returns Promise<AxiosResponse>
*/
public static query(
resource: string,
params: AxiosRequestConfig
): Promise<AxiosResponse> {
return ApiService.vueInstance.axios.get(resource, params).catch(error => {
// #TODO log out and send home if response is 401 bad auth
throw new Error(`[KT] ApiService ${error}`);
});
}
}
export default ApiService;
I think the issue is caused when you're calling the data from the api.
This code:
beforeCreate: async function() {
this.leadData = await new LeadService().getLeads()
},
I'd refactor to
async created () {
const service = new LeadService()
const value = await service.getLeads()
}
Also it would be nice to be able to see how you're fetching your data.
Sometimes this code: const value = await axios.get('/api/getStuff').data can be problematic because of paratheses issues. Which causes the same issue you described of, hot reloading working, but fresh not. I suspect the same sort of issue relies of the code executing like => (await new LeadService()).getLeads() Where you're probably awaiting the class, rather than the actual async code.
So I can do this (Pug & CoffeeScript):
input(placeholder="0", v-model.number="order[index]" v-on:change="adjustInput")
...
adjustInput: ->
event.target.style.width = event.target.value.length + 'ch'
... but it only works if I change the input in the browser, by hand. The input does not change its width if the v-model changes.
How can I make it so that the input width changes even if the change is due to Vue reactivity?
Check this one, but you must check the font-size on input and on fake_div
var app = new Vue({
el: '#app',
data() {
return {
order: 1, // your value
fakeDivWidth: 10, // width from start, so input width = 10px
};
},
watch: {
order: { // if value from input changing
handler(val) {
this.inputResize();
},
},
},
methods: {
inputResize() {
setTimeout(() => {
this.fakeDivWidth = this.$el.querySelector('.fake_div').clientWidth;
}, 0);
},
},
})
.fake_div {
position: absolute;
left: -100500vw;
top: -100500vh;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input placeholder="0" v-model.number="order" v-bind:style="{width: fakeDivWidth + 'px'}" >
<div class="fake_div">{{ order }}</div> // fake_div needed to see this div width
// этот блок фейковый и нужен только для того, что бы наш input становился такого же размера как этот div
</div>
Try this
input(placeholder="0", v-model.number="order[index]" v-on:change="adjustInput" :style="{width: reactiveWidth}")
// Your secret Vue code
data() {
return function() {
reactiveWidth: 100px; // (some default value)
}
},
methods: {
adjustInput() {
this.reactiveWidth = event.target.value.length + 'ch'
}
},
computed: {
reactiveWidth() {
return this.number + 'ch';
}
}
Since I don't know all parts of the code, you might need to tweak this a bit. With just binding this.number to a order[index] you are not affecting the width of the input in any way. The computed property listens to changes in number
The easiest workaround to me is to replace the input field with a contenteditable span which will wrap around the text:
<script setup>
import {reactive} from 'vue'
const state = reactive({
input: 'My width will adapt to the text'
})
</script>
<template>
<span class="input" #input="e => state.input = e.target.innerText" contenteditable>{{state.input}}</span>
</template>
This works like v-model=state.input
I am having a hard time to understand this, so I have a component which is already complied which is a grid, now when I click on a button a modal pops-up and display another grid inside the modal at this point my code looks like this for the modal pop-up
<template>
<transition v-if="this.modalVisible" v-bind:title.sync="this.modalVisible" name="modal">
<div class="modal-mask">
<div class="modal-wrapper">
<div class="modal-container">
<div class="modal-header">
{{ modalHeaderName }}
</div>
<div class="modal-body">
//this is another component
<grid-data :grid-values="dummy" :tool-bar="false"></grid-data>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>
</transition>
</template>
<script>
import DataTable from './core/gridTable.vue';
export default {
components:{
JqxButton,
'grid-data' : DataTable,
},
props : {
modalHeaderName : String,
modalVisible : Boolean
},
data: function () {
return {
buttonWidth: 120,
buttonHeight: '100%',
value: this.buttonName,
dummy : [
{ name: 'ProductName', type: 'string' },
{ name: 'QuantityPerUnit', type: 'int' },
{ name: 'UnitPrice', type: 'float' },
{ name: 'UnitsInStock', type: 'float' },
{ name: 'Discontinued', type: 'bool' }
],
}
}
}
</script>
Now, the grid is a vue component which was already complied and rendered, now will I import it again it says
[Vue warn]: Failed to mount component: template or render function not defined.
<template>
<div>
<!-- sync here is, getting the value from the updated modal-->
<custom-modal :modal-visible="this.showModal" v-bind:showModal.sync="showModal" :modal-header-name="this.modalHeaderName"></custom-modal>
<JqxGrid :width="width" :source="dataAdapter" :columns="gridValues"
:pageable="true" :autoheight="true" :sortable="true"
:altrows="true" :enabletooltip="true" :editable="true"
:selectionmode="'multiplecellsadvanced'" :showtoolbar="this.toolBar" :rendertoolbar="rendertoolbar">
</JqxGrid>
</div>
</template>
<script>
import JqxGrid from "../jqx-vue/vue_jqxgrid.vue";
import CustomModal from "../customModal";
export default {
components: {
JqxGrid,
'custom-modal' : CustomModal
},
// added the name here
name: 'jqx-grid',
props : {
gridValues : Array,
toolBar : Boolean
},
data: function () {
return {
showModal : false,
modalHeaderName : '',
width: '100%',
dataAdapter: new jqx.dataAdapter({
datatype: 'xml',
datafields : this.gridValues,
url: ''
}),
columns: []
}
},
mounted: function () {
this.createButtons();
},
methods: {
rendertoolbar: function (toolbar) {
let buttonsContainer = document.createElement('div');
buttonsContainer.style.cssText = 'overflow: hidden; position: relative; margin: 5px;';
let addButtonContainer = document.createElement('div');
let deleteButtonContainer = document.createElement('div');
addButtonContainer.id = 'addButton';
deleteButtonContainer.id = 'deleteButton';
addButtonContainer.style.cssText = 'float: left; margin-left: 5px;padding-bottom:25px;';
deleteButtonContainer.style.cssText = 'float: left; margin-left: 5px;padding-bottom:25px;';
buttonsContainer.appendChild(addButtonContainer);
buttonsContainer.appendChild(deleteButtonContainer);
toolbar[0].appendChild(buttonsContainer);
},
createButtons: function () {
let addButtonOptions = {
height: 25, value: ' <i class="fa fa-plus" style="padding-top:3px"></i> Add Items ',
};
let addButton = jqwidgets.createInstance('#addButton', 'jqxButton', addButtonOptions);
let deleteButtonOptions = {
height: 25, value: ' <i class="fa fa-ban" style="padding-top:3px"></i> Remove All ',
};
let deleteButton = jqwidgets.createInstance('#deleteButton', 'jqxButton', deleteButtonOptions);
// add new row.
addButton.addEventHandler('click', (event) => {
this.showModal = true;
this.modalHeaderName = 'Bulk Add Items';
});
// delete selected row.
deleteButton.addEventHandler('click', (event) => {
// alert('delete')
});
},
cellsrenderer: function (row, columnsfield, value, defaulthtml, columnproperties, rowdata) {
if (value < 20) {
return '<span style="margin: 4px; float: ' + columnproperties.cellsalign + '; color: #ff0000;">' + value + '</span>';
}
else {
return '<span style="margin: 4px; float: ' + columnproperties.cellsalign + '; color: #008000;">' + value + '</span>';
}
}
}
}
</script>
How can I overcome this issue?
I have seen question like this which says the component grid is trying to compile again and hence the error but I am not sure of that, so we should be using the complied version of the grid component.
NOTE: Using Vue with Laravel 5.4
Error Image
I didn't see an obvious error when you first posted the code. Right now I see JqxButton inside components of the upper code block, which would be undefined. In your code, you always import some components for which we can't see the code.
Generally, when I'm in a situation like this and everything seems to be looking okay, I remove all sub-components and see if the error goes away. Then, I re-add one component after each other until I hit the error again and try to debug it there.
From your description, I suspect you have some kind of cycle in your dependencies and you might find the documentation about circular references helpful.
Vue needs a lazy import for circular dependencies:
components: {
"my-circular-dependency": () => import("./my-circular-dependency.vue");
}
I've created a simple component named DefaultButton.
It bases on properties, that are being set up whenever this component is being created.
The point is that after mounting it, It does not react on changes connected with "defaultbutton", that is an object located in properties
<template>
<button :class="buttonClass" v-if="isActive" #click="$emit('buttonAction', defaultbutton.id)" >
{{ this.defaultbutton.text }}
</button>
<button :class="buttonClass" v-else disabled="disabled">
{{ this.defaultbutton.text }}
</button>
</template>
<script>
export default {
name: "defaultbutton",
props: {
defaultbutton: Object
},
computed: {
buttonClass() {
return `b41ngt ${this.defaultbutton.state}`;
},
isActive() {
return (this.defaultbutton.state === "BUTTON_ACTIVE" || this.defaultbutton.state === "BUTTON_ACTIVE_NOT_CHOSEN");
}
}
};
</script>
Having following component as a parent one:
<template>
<div v-if="state_items.length == 2" class="ui placeholder segment">
{{ this.state_items[0].state }}
{{ this.state_items[1].state }}
{{ this.current_active_state }}
<div class="ui two column very relaxed stackable grid">
<div class="column">
<default-button :defaultbutton="state_items[0]" #buttonAction="changecurrentstate(0)"/>
</div>
<div class="middle aligned column">
<default-button :defaultbutton="state_items[1]" #buttonAction="changecurrentstate(1)"/>
</div>
</div>
<div class="ui vertical divider">
Or
</div>
</div>
</template>
<script type="text/javascript">
import DefaultButton from '../Button/DefaultButton'
export default {
name: 'changestatebox',
data() {
return {
current_active_state: 1
}
},
props: {
state_items: []
},
components: {
DefaultButton
},
methods: {
changecurrentstate: function(index) {
if(this.current_active_state != index) {
this.state_items[this.current_active_state].state = 'BUTTON_ACTIVE_NOT_CHOSEN';
this.state_items[index].state = 'BUTTON_ACTIVE';
this.current_active_state = index;
}
},
},
mounted: function () {
this.state_items[0].state = 'BUTTON_ACTIVE';
this.state_items[1].state = 'BUTTON_ACTIVE_NOT_CHOSEN';
}
}
</script>
It clearly shows, using:
{{ this.state_items[0].state }}
{{ this.state_items[1].state }}
{{ this.current_active_state }}
that the state of these items are being changed, but I am unable to see any results on the generated "DefaultButtons". Classes of objects included in these components are not being changed.
#edit
I've completely changed the way of delivering the data.
Due to this change, I've abandoned the usage of an array; instead I've used two completely not related object.
The result is the same - class of the child component's object is not being
DefaulButton.vue:
<template>
<button :class="buttonClass" v-if="isActive" #click="$emit('buttonAction', defaultbutton.id)" >
{{ this.defaultbutton.text }}
</button>
<button :class="buttonClass" v-else disabled="disabled">
{{ this.defaultbutton.text }}
</button>
</template>
<style lang="scss">
import './DefaultButton.css';
</style>
<script>
export default {
name: "defaultbutton",
props: {
defaultbutton: {
type: Object,
default: () => ({
id: '',
text: '',
state: '',
})
}
},
computed: {
buttonClass() {
return `b41ngt ${this.defaultbutton.state}`;
},
isActive() {
return (this.defaultbutton.state === "BUTTON_ACTIVE" ||
this.defaultbutton.state === "BUTTON_ACTIVE_NOT_CHOSEN");
}
}
};
</script>
ChangeStateBox.vue:
<template>
<div class="ui placeholder segment">
{{ this.state_first.state }}
{{ this.state_second.state }}
{{ this.current_active_state }}
<div class="ui two column very relaxed stackable grid">
<div class="column">
<default-button :defaultbutton="state_first" #buttonAction="changecurrentstate(0)"/>
</div>
<div class="middle aligned column">
<default-button :defaultbutton="state_second" #buttonAction="changecurrentstate(1)"/>
</div>
</div>
<div class="ui vertical divider">
Or
</div>
</div>
</template>
<script type="text/javascript">
import DefaultButton from '../Button/DefaultButton'
export default {
name: 'changestatebox',
data() {
return {
current_active_state: 1
}
},
props: {
state_first: {
type: Object,
default: () => ({
id: '',
text: ''
})
},
state_second: {
type: Object,
default: () => ({
id: '',
text: ''
})
},
},
components: {
DefaultButton
},
methods: {
changecurrentstate: function(index) {
if(this.current_active_state != index) {
if(this.current_active_state == 1){
this.$set(this.state_first, 'state', "BUTTON_ACTIVE_NOT_CHOSEN");
this.$set(this.state_second, 'state', "BUTTON_ACTIVE");
} else {
this.$set(this.state_first, 'state', "BUTTON_ACTIVE");
this.$set(this.state_second, 'state', "BUTTON_ACTIVE_NOT_CHOSEN");
}
this.current_active_state = index;
}
},
},
created: function () {
this.state_first.state = 'BUTTON_ACTIVE';
this.state_second.state = 'BUTTON_ACTIVE_NOT_CHOSEN';
}
}
</script>
You're declaring props wrong. It is either an array of prop names or it is an object with one entry for each prop declaring its type, or it is an object with one entry for each prop declaring multiple properties.
You have
props: {
state_items: []
},
but to supply a default it should be
props: {
state_items: {
type: Array,
default: []
}
},
But your problem is most likely that you're mutating state_items in such a way that Vue can't react to the change
Your main problem is the way you are changing the button state, according with Array change detection vue can't detect mutations by indexing.
Due to limitations in JavaScript, Vue cannot detect the following
changes to an array:
When you directly set an item with the index, e.g.
vm.items[indexOfItem] = newValue When you modify the length of the
array, e.g. vm.items.length = newLength
In case someone will be having the same issue:
#Roy J as well as #DobleL were right.
The reason behind this issue was related with the wrong initialization of state objects.
According to the documentation:
Vue cannot detect property addition or deletion.
Since Vue performs the getter/setter conversion process during instance
initialization, a property must be present in the
data object in order for Vue to convert it and make it reactive.
Before reading this sentence, I used to start with following objects as an initial data:
var local_state_first = {
id: '1',
text: 'Realized',
};
var local_state_second = {
id: '2',
text: 'Active'
};
and the correct version of it looks like this:
var local_state_first = {
id: '1',
text: 'Realized',
state: 'BUTTON_ACTIVE'
};
var local_state_second = {
id: '2',
text: 'Active',
state: 'BUTTON_ACTIVE'
};
whereas declaring the main component as:
<change-state-box :state_first="local_state_first" :state_second="local_state_second" #buttonAction="onbuttonAction"/>
Rest of the code remains the same ( take a look at #edit mark in my main post )
I have this 3 components in VueJS. The problem i want to solve is: When i click at vehicle component, it needs to be selected (selected = true) and other vehicles unselected.
What i need to do for two-way data binding? Because i'm changing this selected property in VehiclesList.vue component and it also need to be changed in Monit.vue (which is a parent) and 'Vehicle.vue' need to watch this property for change class.
Also problem is with updating vehicles. In Monit.vue i do not update full object like this.vehicles = response.vehicles, but i do each by each one, and changing only monit property.
Maybe easier would be use a store for this. But i want to do this in components.
EDITED:Data sctructure
{
"m":[
{
"id":"v19",
"regno":"ATECH DOBLO",
"dt":"2017-10-09 13:19:01",
"lon":17.96442604,
"lat":50.66988373,
"v":0,
"th":0,
"r":0,
"g":28,
"s":"3",
"pow":1
},
{
"id":"v20",
"regno":"ATECH DUCATO_2",
"dt":"2017-10-10 01:00:03",
"lon":17.96442604,
"lat":50.6698494,
"v":0,
"th":0,
"r":0,
"g":20,
"s":"3"
},
]
}
Monit.vue
<template>
<div class="module-container">
<div class="module-container-widgets">
<vehicles-list :vehicles="vehicles"></vehicles-list>
</div>
</div>
</template>
<script>
import VehiclesList from '#/components/modules/monit/VehiclesList.vue';
export default {
name: "Monit",
data (){
return {
vehicles: null
}
},
components: {
VehiclesList
},
methods: {
getMonitData(opt){
let self = this;
if (this.getMonitDataTimer) clearTimeout(this.getMonitDataTimer);
this.axios({
url:'/monit',
})
.then(res => {
let data = res.data;
console.log(data);
if (!data.err){
self.updateVehicles(data.m);
}
self.getMonitDataTimer = setTimeout(()=>{
self.getMonitData();
}, self.getMonitDataDelay);
})
.catch(error => {
})
},
updateVehicles(data){
let self = this;
if (!this.vehicles){
this.vehicles = {};
data.forEach((v,id) => {
self.vehicles[v.id] = {
monit: v,
no: Object.keys(self.vehicles).length + 1
}
});
} else {
data.forEach((v,id) => {
if (self.vehicles[v.id]) {
self.vehicles[v.id].monit = v;
} else {
self.vehicles[v.id] = {
monit: v,
no: Object.keys(self.vehicles).length + 1
}
}
});
}
},
},
mounted: function(){
this.getMonitData();
}
};
</script>
VehiclesList.vue
<template>
<div class="vehicles-list" :class="{'vehicles-list--short': isShort}">
<ul>
<vehicle
v-for="v in vehicles"
:key="v.id"
:data="v"
#click.native="select(v)"
></vehicle>
</ul>
</div>
</template>
<script>
import Vehicle from '#/components/modules/monit/VehiclesListItem.vue';
export default {
data: function(){
return {
isShort: true
}
},
props:{
vehicles: {}
},
methods:{
select(vehicle){
let id = vehicle.monit.id;
console.log("Select vehicle: " + id);
_.forEach((v, id) => {
v.selected = false;
});
this.vehicles[id].selected = true;
}
},
components:{
Vehicle
}
}
</script>
Vehicle.vue
<template>
<li class="vehicle" :id="data.id" :class="classes">
<div class="vehicle-info">
<div class="vehicle-info--regno font-weight-bold"><span class="vehicle-info--no">{{data.no}}.</span> {{ data.monit.regno }}</div>
</div>
<div class="vehicle-stats">
<div v-if="data.monit.v !== 'undefined'" class="vehicle-stat--speed" data-name="speed"><i class="mdi mdi-speedometer"></i>{{ data.monit.v }} km/h</div>
</div>
</li>
</template>
<script>
export default {
props:{
data: Object
},
computed:{
classes (){
return {
'vehicle--selected': this.data.selected
}
}
}
}
</script>
Two-way component data binding was deprecated in VueJS 2.0 for a more event-driven model: https://v2.vuejs.org/v2/guide/components.html#One-Way-Data-Flow
This means, that changes made in the parent are still propagated to the child component (one-way). Changes you make inside the child component need to be explicitly send back to the parent via custom events: https://v2.vuejs.org/v2/guide/components.html#Custom-Events or in 2.3.0+ the sync keyword: https://v2.vuejs.org/v2/guide/components.html#sync-Modifier
EDIT Alternative (maybe better) approach:
Monit.vue:
<template>
<div class="module-container">
<div class="module-container-widgets">
<vehicles-list :vehicles="vehicles" v-on:vehicleSelected="onVehicleSelected"></vehicles-list>
</div>
</div>
</template>
<script>
import VehiclesList from '#/components/modules/monit/VehiclesList.vue';
export default {
name: "Monit",
data (){
return {
vehicles: null
}
},
components: {
VehiclesList
},
methods: {
onVehicleSelected: function (id) {
_.forEach((v, id) => {
v.selected = false;
});
this.vehicles[id].selected = true;
}
...other methods
},
mounted: function(){
this.getMonitData();
}
};
</script>
VehicleList.vue:
methods:{
select(vehicle){
this.$emit('vehicleSelected', vehicle.monit.id)
}
},
Original post:
For your example this would probably mean that you need to emit changes inside the select method and you need to use some sort of mutable object inside the VehicleList.vue:
export default {
data: function(){
return {
isShort: true,
mutableVehicles: {}
}
},
props:{
vehicles: {}
},
methods:{
select(vehicle){
let id = vehicle.monit.id;
console.log("Select vehicle: " + id);
_.forEach((v, id) => {
v.selected = false;
});
this.mutableVehicles[id].selected = true;
this.$emit('update:vehicles', this.mutableVehicles);
},
vehilcesLoaded () {
// Call this function from the parent once the data was loaded from the api.
// This ensures that we don't overwrite the child data with data from the parent when something changes.
// But still have the up-to-date data from the api
this.mutableVehilces = this.vehicles
}
},
components:{
Vehicle
}
}
Monit.vue
<template>
<div class="module-container">
<div class="module-container-widgets">
<vehicles-list :vehicles.sync="vehicles"></vehicles-list>
</div>
</div>
</template>
<script>
You still should maybe think more about responsibilities. Shouldn't the VehicleList.vue component be responsible for loading and managing the vehicles? This probably would make thinks a bit easier.
EDIT 2:
Try to $set the inner object and see if this helps:
self.$set(self.vehicles, v.id, {
monit: v,
no: Object.keys(self.vehicles).length + 1,
selected: false
});