How to create dynamic links based on an API request - vue.js

I am completely new to both vue.js and Javascript. How do I dynamically create nav links from an Axios request?
I am wanting to follow what's being done in the item section which is currently static information, but i want to dynamically return links based on whats returned in the json request.
import * as types from '../../mutation-types'
import lazyLoading from './lazyLoading'
import charts from './charts'
// gathering items from API
const url = 'http://localhost:8080/items/'
data: {
items: []
},
mounted() {
axios.get(url).then(response => {
this.results = items.data
})
}
// Sidebar links are statically created here
const state = {
items: [
{
name: 'Dashboard',
path: '/dashboard',
meta: {
icon: 'fa-tachometer',
link: 'dashboard/index.vue'
},
component: lazyLoading('dashboard', true)
},
{
name: 'Axios',
path: '/axiosDemo',
meta: {
auth: true,
icon: 'fa-rocket',
link: 'axios/index.vue'
},
component: lazyLoading('axios', true)
},
charts,
]
}
const mutations = {
[types.EXPAND_MENU] (state, menuItem) {
if (menuItem.index > -1) {
if (state.items[menuItem.index] && state.items[menuItem.index].meta) {
state.items[menuItem.index].meta.expanded = menuItem.expanded
}
} else if (menuItem.item && 'expanded' in menuItem.item.meta) {
menuItem.item.meta.expanded = menuItem.expanded
}
}
}
export default {
state,
mutations
}
I think what I am wanting to do is something like this (python example):
items:
for i in items_payload:
{
name: i.name,
path: i.url,
meta: {
icon: 'fa-tachometer',
link: i.name+'/index.vue'
},
},
How do I best accomplish this in vue.js? Any help would be appreciated. Thanks.

If you are making the api request from a component, you can create a list of links like follows:
// for demo purposes, let's say links are returned as an array of objects
[
{
href: '/path/to/page1',
linkText: 'Page 1'
},
{
href: '/path/to/page2',
linkText: 'Page 2'
}
]
// MyComponent.vue
<template>
<div class="sidebar">
<ul>
<li v-for="(item, index) in items" :key="index">
<a :href="item.href">{{ item.linkText }}</a>
</li>
</ul>
</div>
</template>
export default {
data () {
return {
links: []
}
},
mounted() {
axios.get(url).then(response => {
this.links = items.data
})
}
}

Related

How to make routing differently than is now vuejs routing

I have a problem. I made routing what according how many addresses have user it will display them in manner 'Addresses(addrescount)'. global variable for getting address count is '$auth.user.address_count'
Task was if user have addresses show their count in sidebar if user do not have addresses show nothing .I did that but is any chance to have another solution to this problem
Is any chance to make it different then it now is?
how it looks like in programm
see line before {{route.display}}
<template>
<div class="col-lg-3">
<div class="profile-sidebar">
<ul class="list-unstyled">
<li v-for="route in nodes" :key="route.name">
<router-link :to="route.fullPath">
<template v-if="route.display === route.meta.title && $auth.user.address_count>0">
{{ route.display }}({{$auth.user.address_count}})
</template>
<template v-else>
{{ route.display }}
</template>
</router-link>
</li>
</ul>
</div>
</div>
</template>
<script>
import { routes } from '#/routes/routes';
export default {
data() {
return {
nodes: [],
}
},
async created() {
if (!this.$auth.user)
await this.$auth.updateUserInfo();
this.loadProfileNodes();
},
methods: {
loadProfileNodes() {
let node = routes.filter(route => route.path === '/profile').pop();
let hasLocalAccount = this.$auth.hasLocalAccount;
this.nodes = [];
node.children.forEach((route) => {
route['fullPath'] = node.path + '/' + route.path;
if ((!hasLocalAccount && route.path !== 'change-password') || (hasLocalAccount && route.path !== 'set-password')) {
this.nodes.push(route);
}
});
}
},
}
</script>
my routes.js see /profile children address-list route.
const routeOptions = [
{ path: '/', name: 'default', view: 'home', display: 'Home', meta: { showInMenu: true } },
{ path: '/401-forbidden', name: 'forbidden', view: 'errors/401-forbidden', display: '401 Forbidden' },
{ path: '/404-not-found', name: 'not-found', view: 'errors/404-not-found', display: '404 Page Not Found' },
{ path: '/login', name: 'login', view: 'auth/login' },
{ path: '/register', name: 'register', view: 'auth/register' },
{ path: '/auth/forgot-password', view: 'auth/forgot-password' },
{ path: '/auth/reset-password', view: 'auth/reset-password', props: (route) => ({ code: route.query.code }) },
{ path: '/auth/confirm-email', view: 'auth/confirm-email', props: (route) => ({ userId: route.query.userId, token: route.query.token }) },
{ path: '/admin/user-list', view: 'admin/users/user-list', display: 'Users', meta: { showInMenu: true, auth: { roles: 'Admin' } } },
{ path: '/admin/company-list', view: 'admin/companies/company-list', display: 'Companies', meta: { showInMenu: true, auth: { roles: 'Admin' } } },
{
path: '/profile',
view: 'profile/profile',
display: 'Edit profile',
meta: { auth: true },
children: [
{
path: '',
display: 'My Profile',
view: 'profile/edit-profile',
meta: { auth: true }
},
{
path: 'manage-logins',
display: 'External Logins',
view: 'profile/manage-logins',
meta: { auth: true }
},
{
path: 'address-list',
display: 'Addresses',
view: 'profile/addresses/address-list',
meta: { auth: true, title: 'Addresses' }
},
{
path: 'change-password',
display: 'Change Password',
view: 'profile/change-password',
meta: { auth: true }
},
{
path: 'set-password',
display: 'Set Password',
view: 'profile/set-password',
meta: { auth: true }
}
]
},
{ path: '*', redirect: { name: 'not-found' } }
];
function addDynamicImport(route) {
if (!route.view)
return route;
if (route.children && route.children.length) {
route.children = route.children.map(child => {
return addDynamicImport(child);
});
}
return {
...route,
component: () => import(/* webpackChunkName: "[request]" */ `#/components/views/${route.view}`)
}
}
const routes = routeOptions.map(route => {
return addDynamicImport(route);
})
export { routes }
router.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import { routes } from './routes';
Vue.use(VueRouter);
let router = new VueRouter({
mode: 'history',
routes
});
export default router;
Answer is simple. You can add flag into '/profile/addres-list' route meta tag and check it if it is true/ True means we found the needed page
{
path: 'address-list',
display: 'Addresses',
view: 'profile/addresses/address-list',
meta: { auth: true, title: 'Addresses' }
},
into this (difference in the last line)
{
path: 'address-list',
display: 'Addresses',
view: 'profile/addresses/address-list',
meta: { auth: true, isAddressList: true }
}
and change
<template v-if="route.display === route.meta.title &&$auth.user.address_count>0">
to this
<template v-if="route.meta.isAddressList && $auth.user.address_count>0">

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.

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

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

vuejs reactivity of complex objects

I am using Vue.js 2.5.17 I have two components, App (parent) and TreeNode (child), which display a tree structure of items from a deeply nested object. Each node in the tree is presented with the TreeNode component which is a recursive component.
TreeNode component
const TreeNode = Vue.component('TreeNode', {
name: 'TreeNode',
template: '#treenode',
props: {
model: Object,
},
data() {
return {
open: false,
};
},
computed: {
isExpandable() {
return this.model.children &&
this.model.children.length;
},
},
methods: {
toggle() {
if (this.isExpandable) {
this.open = !this.open;
}
},
},
});
TreeNode template
<script type="text/x-template" id="treenode">
<li>
<input type="checkbox" :id="model.name" style="display:none;"/>
<label :for="model.name" style="color:gray;" #click="toggle">
{{ model.name }}
{{ model.data.example }}
</label>
<ul v-show="open" v-if="isExpandable">
<TreeNode
class="item"
v-for="(model, index) in model.children"
:key="index"
:model="model">
</TreeNode>
</ul>
</li>
</script>
App component template
<script type="text/x-template" id="oulist">
<div>
<div id="unitsTable" class="row filterlist treelist b_widget2" style="width:85%;">
<div class="css-treeview">
<TreeNode
class="item"
:model="items">
</TreeNode>
</div>
</div>
</script>
App component
const App = Vue.component('App', {
template: '#oulist',
components: {
TreeNode,
},
data() {
return {
items: {
name: 'item1',
data: { example: '1' },
children: [
{
name: 'item11',
children: [],
data: { example: '1' },
},
{
name: 'item12',
children: [
{ name: 'item121', children: [], data: { example: '1' } },
{ name: 'item122', children: [], data: { example: '1' } },
],
data: { example: '1' },
},
],
},
};
},
methods: {
updateItem(currNode, name, data) {
if (currNode.name === name) {
Object.assign(currNode.data, data);
this.items = Object.assign({}, this.items); // tried to create a new object here and overwrite it, but it didn't help
return;
}
if (currNode.children) {
currNode.children.forEach(c => this.updateItem(c, name, data));
}
},
},
});
The object posted above is just an example, my actual object has a lot more nested levels and items per level.
The problem am I facing is that whenever a property deep within my items object is changed (more specifically, the example property of the data object inside a node), the DOM is not updated. I read through the reactivity caveats and saw that adding new properties is not reactive by default, but I am not adding new properties here, just changing the existing ones.
When data from a tree node is updated, I traverse the object to find the correct node and update its properties as follows:
updateItem(currNode, name, data) {
if (currNode.name === name) {
Object.assign(currNode.data, data);
this.items = Object.assign({}, this.items); // tried to create a new object here and overwrite it, but it didn't help
return;
}
if (currNode.children) {
currNode.children.forEach(c => this.updateItem(c, name, data));
}
},
this.updateItem(this.items, 'item121', { example: 'newVal' });
Any tips ? Thanks!
EDIT: The data is always changed only in the parent (App) component.

Reusing Vue Component, removing data

I have setup a component system using vue-router for a simple event system. I'd like to be able to be able to use the same component for both editing existing events and creating new events.
I can't figure out how to remove the data from the component when I want to navigating from editing one event to creating another.
I have tried the following things, which don't work:
setting eventId: null in v-link
setting eventId to null through v-on:click
setting eventId with: this.$route.params.eventId
Router Map: the create and the eventDashboard route point to the same component.
router.map({
'/': {
name: 'calendar',
component: Vue.component('calendar'),
subRoutes: {
'/rightView': {
name: 'rightView',
component: Vue.component('rightView'),
},
},
},
'create': {
name: 'create',
component: Vue.component('create'),
subRoutes: {
'/rightView': {
name: 'rightView',
component: Vue.component('rightView'),
},
},
},
'eventdashboard/:eventId': {
name: 'event',
component: Vue.component('create'),
subRoutes: {
'/rightView': {
name: 'rightView',
component: Vue.component('rightView'),
},
},
},
})
Here is the button used to create a new event:
<a v-link="{name: 'create', params: { eventId: null }, replace: true}" class="btn btn-success"><i class="fa fa-plus"></i> Create New Event</a>
And component:
Vue.component('create',
{
template: '#create',
data: function(){
return {
eventId: this.$route.params.eventId,
event: []
}
},
ready: function() {
this.getEvent();
},
methods: {
getEvent: function(eventId){
var getList = this.$http.get('event/'+this.eventId)
.success(function(data){
this.event = data;
}.bind(this));
},
}
});
Please refer vue-routers data hook to understand this. http://router.vuejs.org/en/pipeline/data.html
Data transition hook is called when the route has changed and the current component is reused.
You can pass your logic of getting the data in the data transition hook and based on whether the route has :eventId, you can decide if it is a create page or add page. If its an add page reset the event object to empty array.
Vue.component('create', {
template: '#create',
data: function() {
return {
event: []
}
},
route: {
data: function(transition) {
if (transition.to.params.eventId) { //get events data if eventId is present in the route params
return this.$http.get({
url: 'event/' + transition.to.params.eventId
}).then(function(response) {
return {
event: response.data
}
}, function() {
console.log('request failed')
})
} else { // Its add mode, set event object to empty array
setTimeout(function() {
transition.next({
event: []
})
}, 1000)
}
}
}
});
Also your add button should be like:
<a v-link="{name: 'create'}" class="btn btn-success"><i class="fa fa-plus"></i> Create New Event</a>
And edit should be:
<a v-link="{name: 'event', params: { eventId: 'Your Event Id'}}" class="btn btn-success"><i class="fa fa-plus"></i> Edit Event</a>