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.
Related
I have a Vue app that can either randomize a title and subtitle OR manually edit these two values through a custom input component. When a user decides to edit, their input should then display on save those results on the parent component.
I have the randomizer and child component emitting the updated headings working, but having a troubled time updating the parents and state to display the custom input title and subtitle on save and getting a "undefined" error for both title and subtitle when I placed console logs in updateTitleAndSubtitle() in the actions section of the store.
The objective of this code challenging is to return the new values to the store and be able to display the custom inputs while having the randomizer handy whenever a user decides to use that instead.
Any direction on what I'm doing wrong or missing would be much appreciated. I've been reading article after article around Vuex and Vue2 for 3 days now with 2 months of experience using Vue.
Custom Input Child Component:
<template>
<div>
<label for="title">Edit Title: </label>
<input
type="text"
id="title"
:updateTitle="updateTitle"
v-model="inputTitle"
/>
<label for="title">Edit Subtitle: </label>
<input
type="text"
id="subtitle" :updateSubtitle="updateSubtitle"
v-model="inputSubtitle"
/>
</div>
</template>
<script>
export default {
name: 'CustomInput',
props: {
title: String,
subtitle: String,
},
computed: {
updateTitle() {
console.log('updateTitle: ', this.title);
return this.title;
},
updateSubtitle() {
console.log('updateSubtitle: ', this.subtitle);
return this.subtitle;
},
inputTitle: {
get() {
console.log('set title: ', this.title);
return this.title;
},
set(title) {
console.log('set title: ', title);
this.$emit('input', title);
},
},
inputSubtitle: {
get() {
return this.subtitle;
},
set(subtitle) {
console.log('set subtitle: ', subtitle);
this.$emit('input', subtitle);
},
},
},
};
</script>
Parent component:
<template>
<main class="home-page page">
<div v-if="!editMode" class="display-information">
<div class="title">
<span class="bold">Title: </span>{{title}}
</div>
<div class="subtitle">
<span class="bold">Subtitle: </span>{{subtitle}}
</div>
<div class="controls">
<button id="randomize-button" class="control-button" #click="randomizeTitleAndSubtitle">
Randomize
</button>
<button id="edit-button" class="control-button" #click="onEdit">Edit</button>
</div>
</div>
<div v-else class="edit-controls">
<CustomInput
:title="title"
:subtitle="subtitle"
#update="v => onSave(v)"
/>
<div class="controls">
<button id="cancel-button" class="control-button" #click="onCancel">Cancel</button>
<button id="save-button" class="control-button" #click="onSave">Save</button>
</div>
</div>
</main>
</template>
<script>
// # is an alias to /src
import CustomInput from '#/components/CustomInput.vue';
import { mapState, mapActions } from 'vuex';
export default {
name: 'Home',
components: {
CustomInput,
},
data() {
return {
editMode: false,
};
},
computed: {
...mapState(['title', 'subtitle']),
},
methods: {
...mapActions(['randomizeHeadings', 'updateHeadings']),
onEdit() {
this.editMode = true;
},
onCancel() {
this.editMode = false;
},
onSave(v) {
this.editMode = false;
this.title = v.title;
this.subtitle = v.subtitle;
this.updateTitleAndSubtitle(v);
},
},
mounted() {
this.randomizeHeadings();
},
};
Vuex Store:
import randomWords from 'random-words';
export default new Vuex.Store({
state: {
title: '',
subtitle: '',
},
mutations: {
UPDATE_TITLE(state, value) {
state.title = value;
},
UPDATE_SUBTITLE(state, value) {
state.subtitle = value;
},
},
actions: {
randomizeTitle({ commit }) {
const newTitle = randomWords();
commit('UPDATE_TITLE', newTitle);
},
randomizeSubtitle({ commit }) {
const newSubtitle = randomWords();
commit('UPDATE_SUBTITLE', newSubtitle);
},
randomizeTitleAndSubtitle({ dispatch }) {
dispatch('randomizeTitle');
dispatch('randomizeSubtitle');
},
updateTitleAndSubtitle({ commit }) {
const payload = {
title: this.title || null,
subtitle: this.subtitle || null,
};
commit('UPDATE_TITLE', payload);
commit('UPDATE_SUBTITLE', payload);
},
},
modules: {
},
});
I tested your code in my local development environment and find out that you need a lot of changes in your codes to work better. Here is the new vuex store code:
vuex store:
export default new Vuex.Store({
state: {
title: '',
subtitle: '',
},
mutations: {
UPDATE_TITLE(state, value) {
state.title = value;
},
UPDATE_SUBTITLE(state, value) {
state.subtitle = value;
},
},
actions: {
randomizeTitle({ commit }) {
const newTitle = randomWords();
commit('UPDATE_TITLE', newTitle);
},
randomizeSubtitle({ commit }) {
const newSubtitle = randomWords();
commit('UPDATE_SUBTITLE', newSubtitle);
},
randomizeTitleAndSubtitle({ dispatch }) {
dispatch('randomizeTitle');
dispatch('randomizeSubtitle');
},
updateTitleAndSubtitle({ commit }, inputUser) {
/* I changed the structure of this action to work correctly */
console.log(inputUser);
commit('UPDATE_TITLE', inputUser.title);
commit('UPDATE_SUBTITLE', inputUser.subtitle);
},
},
modules: {
},
});
Also here is the new Parent component code:
Parent component:
<template>
<main class="home-page page">
<div v-if="!editMode" class="display-information">
<div class="title">
<span class="bold">Title: </span>{{title}}
</div>
<div class="subtitle">
<span class="bold">Subtitle: </span>{{subtitle}}
</div>
<div class="controls">
<button id="randomize-button" class="control-button" #click="randomizeTitleAndSubtitle">
Randomize
</button>
<button id="edit-button" class="control-button" #click="onEdit">Edit</button>
</div>
</div>
<div v-else class="edit-controls">
<CustomInput
:title="title"
:subtitle="subtitle"
#titleEvent = "myFuncTitle"
#subTitleEvent = "myFuncSubTitle"
/>
<!--
I removed this part from your component.
#update="v => onSave(v)"
and also added custom events (titleEvent and subTitleEvent) to the component
-->
<div class="controls">
<button id="cancel-button" class="control-button" #click="onCancel">Cancel</button>
<button id="save-button" class="control-button" #click="onSave">Save</button>
</div>
</div>
</main>
</template>
<script>
// # is an alias to /src
import CustomInput from '../components/CustomInput.vue';
import { mapActions } from 'vuex';
export default {
name: 'Parent',
components: {
CustomInput,
},
data() {
return {
editMode: false,
/* defining new data for handling "cancel" button functionality */
temporaryTitle: "",
temporarySubTitle: ""
};
},
computed: {
/* defining setter and getter for each computed value separately */
title: {
// getter
get: function () {
return this.$store.state.title;
},
// setter
set: function (newValue) {
this.$store.commit('UPDATE_TITLE', newValue);
}
},
subtitle: {
// getter
get: function () {
return this.$store.state.subtitle;
},
// setter
set: function (newValue) {
this.$store.commit('UPDATE_SUBTITLE', newValue);
}
},
},
methods: {
/* changing the name of actions according to the names defined in "store" */
...mapActions(['randomizeTitleAndSubtitle', 'updateTitleAndSubtitle']),
onEdit() {
this.editMode = true;
this.temporaryTitle = this.$store.state.title;
this.temporarySubTitle = this.$store.state.subtitle;
},
onCancel() {
this.editMode = false;
this.$store.commit('UPDATE_TITLE', this.temporaryTitle);
this.$store.commit('UPDATE_SUBTITLE', this.temporarySubTitle);
},
myFuncTitle(event) {
console.log(event);
/* we could not set values to "computed" properties, if we had not defined "set: function ..." for them above. */
this.title = event;
},
myFuncSubTitle(event) {
this.subtitle = event;
},
onSave(v) {
this.editMode = false;
console.log(v); /* "v" is not related to your data. notice the console */
// this.title = v.title;
// this.subtitle = v.subtitle;
const payload = {
title: this.title,
subtitle: this.subtitle,
};
this.updateTitleAndSubtitle(payload);
},
},
created() {
this.randomizeTitleAndSubtitle();
},
};
</script>
And finally here is the code of new Custom Input component:
Custom Input:
<template>
<div>
<label for="title">Edit Title: </label>
<input
type="text"
id="title"
v-model="inputTitle"
#input="$emit('titleEvent', $event.target.value)"
/>
<!-- emitting event like above code for each input -->
<label for="title">Edit Subtitle: </label>
<input
type="text"
id="subtitle"
v-model="inputSubtitle"
#input="$emit('subTitleEvent', $event.target.value)"
/>
</div>
</template>
<script>
export default {
name: 'CustomInput',
props: {
title: String,
subtitle: String,
},
computed: {
inputTitle: {
get() {
console.log('set title: ', this.title);
return this.title;
},
set(title) {
console.log('set title: ', title);
},
},
inputSubtitle: {
get() {
return this.subtitle;
},
set(subtitle) {
console.log('set subtitle: ', subtitle);
},
},
},
};
</script>
<style scoped>
</style>
I tried to comment some changes to the codes, but the main changes are related to changing the name of mapActions actions according to the names defined in "store" and also provide a setter for computed properties.
I suggest that you read more in vue and vuex documentations, especially the page that is related to custom events and computed setters and vuex actions, if you have problems with my codes.
i try to build a little clothing web shop with nuxtjs. You can choose the color on the details page. The details page represents a pice of clothing. The ColorMenu is a component. If you choose something a color, it will emit it back to the details page and will send a new details request to my backend.
However, changing the color only works if you don't choose another piece of clothing. If you choose another piece of clothing (so the route parameters will change) and choose another color in the menu, there is a always an error that it cannot load anything. it seems that it sends repeated requests until the request is blocked.
The details routes are built according to this scheme: localhost/details/{sellableId}/{ideaId}/{appearanceId}
Details Page:
<template>
<section class="section">
<div v-if="details">
<div class="columns">
<div class="column">
<ImageCaroussel :images="details.images"></ImageCaroussel>
</div>
<div class="column">
<h3>Farben</h3>
<ColorMenu
:appearances="productType.appearances"
:appearanceIds="details.appearanceIds"
></ColorMenu>
</div>
</div>
</div>
</section>
</template>
<script>
import { mapState } from 'vuex'
import Dropdown from '~/components/details/Dropdown.vue'
import ColorMenu from '~/components/details/ColorMenu.vue'
import ImageCaroussel from '~/components/details/ImageCaroussel.vue'
export default {
created() {
this.$nuxt.$on('selected', ($event) => (this.selected = $event))
this.$nuxt.$on('selectedColor', ($event) => this.setSelectedColor($event))
},
data() {
return {
modal: false,
selected: '',
selectedColor: '',
}
},
async asyncData({ store, params }) {
console.log('asyncfirst')
if (params.sellableId && params.appearanceId && params.ideaId) {
await store.dispatch('details/get_details', {
sellableId: params.sellableId,
appearanceId: params.appearanceId,
ideaId: params.ideaId,
})
let sellableId = params.sellableId
let appearanceId = params.appearanceId
let ideaId = params.ideaId
console.log('asyncsecond!')
return { sellableId, appearanceId, ideaId }
}
},
mounted() {
this.sellableId = this.$route.params.sellableId
this.appearanceId = this.$route.params.appearanceId
this.ideaId = this.$route.params.ideaId
console.log('Mounted!')
},
components: {
Dropdown,
ColorMenu,
ImageCaroussel,
},
computed: {
...mapState({
details: (state) => {
return state.details.details
},
currency: (state) => {
return state.sellable.currency
},
productType: (state) => {
return state.details.productType
},
}),
},
methods: {
checkout: async function (sellableId, size, appearanceId) {
let link = await this.$backendrepositories.basket.checkout(
sellableId,
size,
appearanceId
)
if (link.status === 200 && link.data) {
this.modal = true
setTimeout(() => {
window.location.href = link.data.link
}, 3000)
}
},
setSelectedColor: async function (event) {
this.selectedColor = event
await this.$store.dispatch('details/get_details', {
sellableId: this.sellableId,
appearanceId: this.selectedColor,
ideaId: this.ideaId,
})
},
},
}
</script>
ColorMenu Component:
<template>
<div>
<div
v-for="(cell, index) in appearances"
:key="index"
style="display: inline-block"
>
<label v-if="appearanceIds.includes(cell.id)" class="self-container">
<input type="radio" checked="checked" name="color" />
<span
class="checkmark"
:style="`background-color: ${cell.colors[0].value}`"
#click="select(cell.id)"
></span>
</label>
</div>
</div>
</template>
<script>
export default {
data: function () {
return {
selected: '',
}
},
props: ['appearances', 'appearanceIds'],
methods: {
select(select) {
this.selected = select
this.$nuxt.$emit('selectedColor', this.selected)
},
},
}
</script>
There is a live demo at https://akano-frontend.vercel.app/
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)
},
},
I'm implementing Vue paper dashboard sidebar. So I have something like this:
Into Index I have
<template>
<div>
AdminIndex
<side-bar>
</side-bar>
</div>
</template>
<script>
import { faBox, faImages } from '#fortawesome/fontawesome-free-solid';
import Sidebar from '#/components/sidebar/SideBar';
export default {
name: 'admin-index-view',
components: {
SideBar,
},
data() {
return {
showSidebar: false,
sidebarLinks: [
{
name: 'admin.menu.products',
icon: faBoxes,
route: { name: 'adminProducts' },
},
{
name: 'admin.menu.sliders',
icon: faImages,
route: { name: '/admin/stats' },
},
],
};
},
methods: {
displaySidebar(value) {
this.showSidebar = value;
},
},
};
</script>
SideBar component:
<template>
<div :class="sidebarClasses"
:data-background-color="backgroundColor"
:data-active-color="activeColor">
<!--
Tip 1: you can change the color of the sidebar's background using: data-background-color="white | black | darkblue"
Tip 2: you can change the color of the active button using the data-active-color="primary | info | success | warning | danger"
-->
<!-- -->
<div class="sidebar-wrapper"
id="style-3">
<div class="logo">
<a href="#"
class="simple-text">
<div class="logo-img">
<img src="static/img/vue-logo.png"
alt="">
</div>
Paper Dashboard
</a>
</div>
<slot>
</slot>
<ul :class="navClasses">
<!--By default vue-router adds an active class to each route link. This way the links are colored when clicked-->
<router-link v-for="(link,index) in sidebarLinks"
:key="index"
:to="link.route"
tag="li"
:ref="link.name">
<a>
<font-awesome-icon :icon="link.icon" />
<p v-t="link.name" />
</a>
</router-link>
</ul>
<moving-arrow :move-y="arrowMovePx">
</moving-arrow>
</div>
</div>
</template>
<script>
import FontAwesomeIcon from '#fortawesome/vue-fontawesome';
import MovingArrow from './MovingArrow';
export default {
name: 'side-bar',
components: {
MovingArrow,
FontAwesomeIcon,
},
props: {
type: {
type: String,
default: 'sidebar',
validator: value => {
const acceptedValues = ['sidebar', 'navbar'];
return acceptedValues.indexOf(value) !== -1;
},
},
backgroundColor: {
type: String,
default: 'black',
validator: value => {
const acceptedValues = ['white', 'black', 'darkblue'];
return acceptedValues.indexOf(value) !== -1;
},
},
activeColor: {
type: String,
default: 'success',
validator: value => {
const acceptedValues = [
'primary',
'info',
'success',
'warning',
'danger',
];
return acceptedValues.indexOf(value) !== -1;
},
},
sidebarLinks: {
type: Array,
default: () => [],
},
},
data() {
return {
linkHeight: 60,
activeLinkIndex: 0,
windowWidth: 0,
isWindows: false,
hasAutoHeight: false,
};
},
computed: {
sidebarClasses() {
if (this.type === 'sidebar') {
return 'sidebar';
}
return 'collapse navbar-collapse off-canvas-sidebar';
},
navClasses() {
if (this.type === 'sidebar') {
return 'nav';
}
return 'nav navbar-nav';
},
/**
* Styles to animate the arrow near the current active sidebar link
* #returns {{transform: string}}
*/
arrowMovePx() {
return this.linkHeight * this.activeLinkIndex;
},
},
watch: {
$route() {
this.findActiveLink();
},
},
methods: {
findActiveLink() {
this.sidebarLinks.find((element, index) => {
const found = element.path === this.$route.path;
if (found) {
this.activeLinkIndex = index;
}
return found;
});
},
},
mounted() {
this.findActiveLink();
},
};
</script>
I dont receive any issues or vue errors, sidebar just don't display. In Chrome console just return empty: <side-bar data-v-66018f3c=""></side-bar> Someone knows why sidebar is not binded? What I need to do to get correctly implementation of it? Regards
Chrome console error:
[Vue warn]: Unknown custom element: - did you register the
component correctly? For recursive components, make sure to provide
the "name" option.
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
});