Using component area to show various content - vue.js

I have a relatively simple task although I am just a beginner so it's difficult to proceed.
I have a list of users on the left and a right panel to show that users info. The information about the user has an edit button that I want to take over that right panel and then save will return back to the user details.
What is the best approach to go about this?
Should the 2 pages be different components or should I just use javascript to show and hide content? Is there a better approach then either of those?
Sorry I'm new and just trying to get my had around the concept.
Thanks

I wrote a simple example for you:
const data = [{
id: 1,
name: 'user1',
age: 21
},{
id: 2,
name: 'user2',
age: 33
}]
const mixin = {
props: {
userId: {
required: true
}
},
data () {
return {
user: {}
}
},
methods: {
loadUser () {
/*ajax to get user detail data here*/
setTimeout(_=>{
this.user = data.filter(o=>o.id==this.userId)[0]
},10)
}
},
created () {
this.loadUser()
},
watch: {
userId (newVal) {
if(newVal){
this.loadUser()
}
}
}
}
Vue.component('user-viewer',{
template: `<div>
name:{{user.name}}<br>
age: {{user.age}}<br>
<button #click="edit">edit</button>
</div>`,
mixins: [mixin],
methods: {
edit () {
this.$emit('switch-edit-mode',true)
}
}
});
Vue.component('user-editor',{
template: `<div>
name:<input type="text" v-model="user.name"><br>
age: <input type="text" v-model="user.age"><br>
<button #click="sendData">save</button>
</div>`,
mixins: [mixin],
methods: {
sendData () {
/*ajax send user data here*/
setTimeout(_=>{
/*false means edit complete,so that user list must be reloaded*/
this.$emit('switch-edit-mode',false);
},10)
}
}
});
var app = new Vue({
el: '#app',
data () {
return {
users: [],
isModify: false,
userId: null
}
},
methods: {
toggleModify (modify) {
this.isModify = modify
if(!modify){
this.fetchUsers();
}
},
fetchUsers () {
/*load your user list data here*/
this.users = data.map(o=>({
id: o.id,
name: o.name
}))
}
},
created () {
this.fetchUsers()
}
})
*{
padding:0;
margin:0;
}
ul,li{
list-style:none;
}
.main{
display: flex;
}
.user-list{
width: 250px;
}
.user-list>li{
border:1px solid skyblue;
border-bottom: none;
}
.user-list>li:last-child{
border-bottom:1px solid skyblue;
}
.content-wrapper{
flex:1;
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.js"></script>
<style>
*{
padding:0;
margin:0;
}
ul,li{
list-style:none;
}
.main{
display: flex;
}
.user-list{
width: 250px;
}
.user-list>li{
border:1px solid skyblue;
border-bottom: none;
}
.user-list>li:last-child{
border-bottom:1px solid skyblue;
}
.content-wrapper{
flex:1;
}
</style>
<div id="app">
<div class="main">
<ul class="user-list">
<li v-for="user in users" #click="userId=user.id">{{user.name}}</li>
</ul>
<div class="content-wrapper">
<component v-if="userId" :is="isModify?'user-editor':'user-viewer'" #switch-edit-mode="toggleModify" :user-id="userId"></component>
<div v-else>please choose a user to view or edit</div>
</div>
</div>
</div>

your mixin file:(mixin.js)
export default{
props: {
userId: {
required: true
}
},
data () {
return {
user: {}
}
},
methods: {
loadUser () {
/*ajax to get user detail data here*/
setTimeout(_=>{
this.user = data.filter(o=>o.id==this.userId)[0]
},10)
}
},
created () {
this.loadUser()
},
watch: {
userId (newVal) {
if(newVal){
this.loadUser()
}
}
}
}
usage:
import mixin from 'mixin.js'
export default{
...
mixins: [mixin]
}

Related

Sorting multiple columns based on headers in Vue.js

I want to apply sorting on firstname, lastname and email but i did not undestand How We sort table in ascending and descnding order by clicking on column name in vuejs. I am tried lot of concepts on stackoverflow and other resources but they didn't seem to work . This is very silly question but I am stopped on this from many days.I'm new to Vue.js and any help would be appreciated. Image
<template>
<div class="h-full flex flex-col">
<ListFilter
ref="filter"
:class="{ 'mb-8': hasFilters || search }"
:filter="filter"
:has-filters="hasFilters"
:has-quick-filters="hasQuickFilters"
:query="query"
:search="search"
:search-placeholder="searchPlaceholder"
:filter-classes="filterClasses"
:initial-values="initialFilter"
:apply-filter="values => applyFilter(filterMapper(values))"
:apply-search="applySearch"
:clear-search="clearSearch"
>
<template #filters="props">
<slot
name="filters"
:reset="props.reset"
:filter="filter"
/>
</template>
<template #quickfilter>
<slot
name="quickfilter"
:apply-filter="applyFilter"
/>
</template>
<template #loader>
<loader
:loading="loading"
:backdrop="true"
/>
</template>
</ListFilter>
<!-- <div>
</div> -->
<vuetable
ref="vuetable"
:row-class="rowClass"
:api-mode="false"
:fields="columns"
:data-manager="dataManager"
:css="css.table"
:track-by="trackBy"
:sort-order="innerSortOrder"
:detail-row-component="detailRow"
:detail-row-options="detailOptions"
:no-data-template="noDataTemplate"
pagination-path="pagination"
#vuetable:row-clicked="handleRowClicked"
#vuetable:cell-clicked="props => $emit('cell-clicked', props)"
#vuetable:pagination-data="onPaginationData"
>
<template #empty-result>
<slot name="empty-result" />
</template>
<template #actions="props">
<div v-if="hasActions">
<ListActions :record="props.rowData">
<slot
name="actions"
:record="props.rowData"
/>
</ListActions>
</div>
</template>
<template #inline-actions="props">
<div v-if="hasInlineActions">
<InlineActions :record="props.rowData">
<slot
name="inline-actions"
:record="props.rowData"
/>
</InlineActions>
</div>
</template>
<template #detail-toggler="props">
<div v-if="hasDetailRow">
<DetailToggler
:row-data="props.rowData"
:row-index="props.rowIndex"
>
<template #default="detailProps">
<slot
name="detail-toggler"
:record="props.rowData"
:is-open="detailProps.isOpen"
:toggle-row="detailProps.toggleRow"
/>
</template>
</DetailToggler>
</div>
</template>
</vuetable>
<div
v-if="!infinityScroll"
class="pt-2"
>
<vuetable-pagination
ref="pagination"
:css="css.pagination"
#vuetable-pagination:change-page="onChangePage"
/>
</div>
</div>
</template>
<script>
import { Vuetable, VuetablePagination } from 'vue3-vuetable';
import AuthMixin from '#/components/auth/AuthMixin';
import ModalNavigation from '#/mixins/ModalNavigation';
// import Loader from '#/components/ui/Loader';
import { throttle } from 'lodash-es';
import ListFilter from '#/components/auth/list/ListFilter';
import NotifyMixin from '#/mixins/NotifyMixin';
const css = {
table: {
tableClass: 'table-auto w-full table',
tableBodyClass: '',
tableHeaderClass: 'px-4 py-2',
tableWrapper: 'overflow-x-auto flex-1',
loadingClass: 'loading',
ascendingIcon: 'blue chevron up icon',
descendingIcon: 'blue chevron down icon',
ascendingClass: 'sorted-asc',
descendingClass: 'sorted-desc',
sortableIcon: 'grey sort icon',
handleIcon: 'grey sidebar icon',
detailRowClass: 'bg-blue-100',
},
pagination: {
wrapperClass: 'flex justify-center py-4',
activeClass: 'active',
disabledClass: '',
pageClass: 'btn-paging',
linkClass: 'btn-paging',
paginationClass: '',
paginationInfoClass: 'pagination-info',
dropdownClass: '',
icons: {
first: '',
prev: '',
next: '',
last: '',
},
},
};
export default {
components: {
// Loader,
Vuetable,
VuetablePagination,
ListFilter,
},
mixins: [NotifyMixin, AuthMixin, ModalNavigation],
props: {
css: {
type: Object,
default() {
return css;
},
},
},
data() {
return {
// loading: false,
query: '',
// filter: this.filterMapper(this.initialFilter),
sort: undefined,
showFilters: false,
loading: false,
scrollContainer: null,
innerSortOrder: this.sortOrder || [],
data: [],
columns: [
{
name: 'first_name',
title: 'First Name',
class: 'w-1/4',
},
{
name: 'last_name',
title: 'Last Name',
class: 'w-1/4',
},
{
name: 'email',
title: 'Email',
class: 'w-1/4',
},
{
name: 'phone_number',
title: 'Phone Number',
class: 'w-1/4',
},
{
name: 'communities',
title: 'Communities',
class: 'w-1/4',
},
{
label: 'communities',
field: 'Communities',
class: 'w-1/4',
formatter(value) {
// console.log('ROW', value);
let communities = [];
value.community_has_leasing_agents.forEach(function (row) {
communities.push('<span class="tag">' + row.community_id + '</span>');
});
// console.log('communities=>', communities);
return communities.join('');
},
},
],
};
},
computed: {
search() {
return true;
},
searchPlaceholder() {
return 'Search by name, community, email';
},
hasFilters() {
// return !!this.$slots.filters;
return true;
},
hasQuickFilters() {
// return !!this.$slots['quickfilter'];
return true;
},
hasActions() {
// return !!this.$slots.actions;
return true;
},
hasInlineActions() {
// return !!this.$slots['inline-actions'];
return true;
},
hasDetailRow() {
// return this.detailRow !== "";
return true;
},
hasClickRowListener() {
// return this.$attrs['row-clicked'];
return true;
},
nonClickableRow() {
return (
this.onRowClick === 'none' &&
!this.hasClickRowListener &&
!(this.$route.params?.passFlowTo && this.$route.params?.passFlowTo !== this.$route.name)
);
},
detailOptions() {
return {
...this.detailRowOptions,
vuetable: this.$refs.vuetable,
};
},
},
async mounted() {
console.log('== Leasing Agents Index ==');
console.log(this.profile);
await this.getLeasingAgentsFromAPI();
},
created() {
if (!this.community) {
this.notifyError('please select a community to continue, then refresh the browser');
}
},
methods: {
async getLeasingAgentsFromAPI() {
console.log('LEASING::getLeasingAgentsFromAPI()');
this.loading = true;
return await this.$calendarDataProvider
.get('calendarGetLeasingAgents', {
customer_id: this.profile.customerId,
})
.then(res => {
console.log('LEASING::getLeasingAgentsFromAPI() result:');
console.log(res);
this.data = res;
return res;
// return this.getEvents(res);
})
.catch(err => console.log(err.message))
.finally(() => {
this.loading = false;
});
},
rowClass(dataItem) {
const classes = ['table-row', 'row'];
if (this.nonClickableRow) {
classes.push('table-row-nonclickable');
}
if (this.hasDetailRow && this.$refs.vuetable?.isVisibleDetailRow(dataItem[this.trackBy])) {
classes.push('table-row-detail-open');
}
return classes.join(' ');
},
toggleFilters() {
this.showFilters = !this.showFilters;
},
applySearch(value) {
this.query = value;
},
clearSearch() {
this.query = '';
},
applyFilter(values) {
this.filter = values;
this.$refs.vuetable?.changePage(1);
this.$refs.vuetable?.resetData();
},
doSearch: throttle(
function () {
if (this.query.length > 2 || this.query.length === 0) {
this.reload();
}
},
500,
{ trailing: true }
),
onPaginationData(paginationData) {
if (!this.infinityScroll) {
this.$refs.pagination.setPaginationData(paginationData);
}
},
onChangePage(page) {
this.$refs.vuetable.changePage(page);
},
reload() {
this.$refs.vuetable.resetData();
this.$refs.vuetable.refresh();
},
getSort({ sortField, direction }) {
// sortField: 'type&bundle.name', - sort by two fields
const fields = sortField.split('&');
if (fields.length > 1) {
return fields.map(item => {
const [fieldName, dir = direction] = item.split('|');
return `${fieldName},${dir}`;
});
}
return `${sortField},${direction}`;
},
async dataManager(/*sortOrder = [], pagination = { current_page: 1 }*/) {
// allow static data
// if (this.data) {
// return {
// data: this.data,
// };
// }
const data = await this.getLeasingAgentsFromAPI();
return {
data: data,
};
// const prevData = this.$refs.vuetable?.tableData ?? [];
// const { pageSize: size, query, filter, sort: oldSort } = this;
// const sort = sortOrder[0] ? this.getSort(sortOrder[0]) : undefined;
// this.loading = true;
// return this[this.dataProvider]
// .getList(this.resource, {
// page: pagination.current_page - 1,
// size,
// sort,
// query,
// ...this.requestParams,
// ...filter,
// })
// .then(this.responseMapper)
// .then(({ content: nextData, totalElements }) => {
// const newPagination = this.$refs.vuetable?.makePagination(totalElements, this.pageSize, pagination.current_page);
// this.sort = sort;
// this.innerSortOrder = sortOrder;
// return {
// pagination: newPagination,
// data: this.infinityScroll && oldSort === sort ? [...prevData, ...nextData] : nextData,
// };
// })
// .catch(error => {
// this.notifyError(error.message);
// return {
// pagination: this.$refs.vuetable?.tablePagination,
// data: this.$refs.vuetable?.tableData,
// };
// })
// .finally(() => (this.loading = false));
},
handleRowClicked({ data, ...rest }) {
if (this.$route.params?.passFlowTo && this.$route.params?.passFlowTo !== this.$route.name) {
this.$router.push({
name: this.$route.params?.passFlowTo,
params: {
...this.$route.params,
[this.routeIdParam]: data[this.trackBy],
},
});
} else {
switch (this.onRowClick) {
case 'edit':
this.$router.replace({ path: `${this.basePath}/${data[this.trackBy]}` });
break;
case 'details':
this.$router.replace({ path: `${this.basePath}/${data[this.trackBy]}/details` });
break;
default:
this.$emit('row-clicked', { data, ...rest });
}
}
},
handleScrollContainer(e) {
if (this.$refs.vuetable.tablePagination && e.target.scrollHeight - e.target.scrollTop - e.target.clientHeight <= 2) {
this.onChangePage('next');
}
},
},
};
</script>
<style scoped>
.tag {
display: inline-flex;
align-items: center;
overflow: hidden;
margin-right: 0.25rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
border-radius: 0.0625rem;
--bg-opacity: 1;
background-color: #eaf6ff;
background-color: rgba(234, 246, 255, var(--bg-opacity));
--text-opacity: 1;
color: #105d91;
color: rgba(16, 93, 145, var(--text-opacity));
font-family: 'FrankNew', sans-serif;
font-weight: 500;
}
</style>
I'm attaching my index.vue file and image.

Vuejs auto suggest input box leading to extra space in the html

Auto suggest uses extra spaces in the html, because of the reason the next elements like H1, H2 and paragraph are going down when I start typing in the suggestion box
App.vue
<template>
<div id="app">
<Autocomplete
:items="[
'Apple',
'Banana',
'Orange',
'Mango',
'Pear',
'Peach',
'Grape',
'Tangerine',
'Pineapple',
]"
/>
<p>p1</p>
<h1>h1</h1>
<h1>h2</h1>
</div>
</template>
<script>
import Autocomplete from "./components/AutoComplete";
import AutoCompleteSearch from "./directives/auto-complete-search.js";
import axios from "axios";
export default {
name: "App",
directives: {
AutoCompleteSearch,
},
components: {
Autocomplete,
},
data() {
return {
data: [],
};
},
methods: {
async getUser() {
await axios
.get("https://jsonplaceholder.typicode.com/users")
.then((res) => (this.data = res.data))
.catch((err) => console.log(err));
},
},
mounted() {
this.data = [{ name: "Elie" }, { name: "John" }];
},
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
AutocompletSearch.js -- directive
import Vue from "vue";
/**
* Add the list attribute to the bound input field.
*
* #param element
* #param binding
* #returns {HTMLElement}
*/
const addAttribute = (element, binding) => {
const { id } = binding.value;
element.setAttribute("list", id);
return element;
};
/**
* The search data for auto-completions
*
* #param binding
* #returns {Object|Array}
*/
const searchData = (binding) => {
const { data } = binding.value;
return data;
};
/**
* Construct the datalist HTMLElement
*
* #param element
* #param binding
* #returns {HTMLDataListElement}
*/
const constructDataList = (element, binding) => {
const datalist = document.createElement("datalist");
datalist.id = binding.value.id;
const data = searchData(binding);
data.forEach((item) => {
const { autoCompleteValue } = binding.value;
const option = document.createElement("option");
option.value = item[autoCompleteValue];
datalist.appendChild(option);
});
return datalist;
};
/**
* #param el the element where to bind the directive
* #param binding {id & autoCompleteValue & data}
*/
const init = (el, binding) => {
const element = addAttribute(el, binding);
const wrapper = element.parentNode;
const datalist = constructDataList(element, binding);
wrapper.appendChild(datalist);
};
export default Vue.directive("auto-complete-search", {
update(el, binding, vnode) {
const vm = vnode.context;
const bindingData = binding.value;
const componentName = vm.$options.name;
const customCallbackName = bindingData.callbackName;
console.log("component updated");
// call the function to fetch auto complete dat
vm.$options.methods.getUser();
if (!vm.$options.methods.getUser && customCallbackName === undefined)
return console.error(`the getUser() is neither defined or overriden in ${componentName} component, on
the auto-complete-search directive`);
if (binding.value.apply) {
init(el, binding);
}
if (!binding.value.apply) {
el.removeAttribute("list");
}
}
});
AutoComplete.vue
<template>
<div class="autocomplete">
<input
type="text"
#input="onChange"
v-model="search"
#keydown.down="onArrowDown"
#keydown.up="onArrowUp"
#keydown.enter="onEnter"
/>
<ul id="autocomplete-results" v-show="isOpen" class="autocomplete-results">
<li class="loading" v-if="isLoading">Loading results...</li>
<li
v-else
v-for="(result, i) in results"
:key="i"
#click="setResult(result)"
class="autocomplete-result"
:class="{ 'is-active': i === arrowCounter }"
>
{{ result }}
</li>
</ul>
</div>
</template>
<script>
export default {
name: "AutoComplete",
props: {
items: {
type: Array,
required: false,
default: () => [],
},
isAsync: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
isOpen: false,
results: [],
search: "",
isLoading: false,
arrowCounter: -1,
};
},
watch: {
items: function (value, oldValue) {
if (value.length !== oldValue.length) {
this.results = value;
this.isLoading = false;
}
},
},
mounted() {
document.addEventListener("click", this.handleClickOutside);
},
destroyed() {
document.removeEventListener("click", this.handleClickOutside);
},
methods: {
setResult(result) {
this.search = result;
this.isOpen = false;
},
filterResults() {
this.results = this.items.filter((item) => {
return item.toLowerCase().indexOf(this.search.toLowerCase()) > -1;
});
},
onChange() {
this.$emit("input", this.search);
if (this.isAsync) {
this.isLoading = true;
} else {
this.filterResults();
this.isOpen = true;
}
},
handleClickOutside(event) {
if (!this.$el.contains(event.target)) {
this.isOpen = false;
this.arrowCounter = -1;
}
},
onArrowDown() {
if (this.arrowCounter < this.results.length) {
this.arrowCounter = this.arrowCounter + 1;
}
},
onArrowUp() {
if (this.arrowCounter > 0) {
this.arrowCounter = this.arrowCounter - 1;
}
},
onEnter() {
this.search = this.results[this.arrowCounter];
this.isOpen = false;
this.arrowCounter = -1;
},
},
};
</script>
<style>
.autocomplete {
position: relative;
}
.autocomplete-results {
padding: 0;
margin: 0;
border: 1px solid #eeeeee;
height: 120px;
overflow: auto;
}
.autocomplete-result {
list-style: none;
text-align: left;
padding: 4px 2px;
cursor: pointer;
}
.autocomplete-result.is-active,
.autocomplete-result:hover {
background-color: #4AAE9B;
color: white;
}
</style>
Please help me out this, auto suggestion can show like popup-modal instead of using extra spaces in the html. like
Here is my code link - https://codesandbox.io/s/vuejs-autocomplete-input-forked-fh31tr
After checking out your codesandbox just add these 3 css properties to your autocomplete class:
position: absolute;
width: 100%;
background: white;

Check if the value change in loop Vuejs

I'm making chat app by Vuejs and want to handle if messages in loop belong to new user for styling color/background-color of user's message.
<template v-for="msg in allMsgs">
<li :key=msg.id> //I want to add some class to handle if next message belong to new user.
<span class="chatname">{{msg.user.name}}</span>
{{msg.content}}
</li>
</template>
https://prnt.sc/114ynuq
Thank you so much
You can use a computed property to determine the position of each message according to the sequence, and then, use class-binding as follows:
new Vue({
el:"#app",
data: () => ({
allMsgs: [
{ id:1, user: { name:'B' }, content:'contentB' },
{ id:2, user: { name:'A' }, content:'contentA' },
{ id:3, user: { name:'A' }, content:'contentA' },
{ id:4, user: { name:'B' }, content:'contentB' },
{ id:5, user: { name:'B' }, content:'contentB' },
{ id:6, user: { name:'A' }, content:'contentA' },
{ id:7, user: { name:'A' }, content:'contentA' }
]
}),
computed: {
messages: function() {
let pos = 1, prev = null;
return this.allMsgs.map((msg, index) => {
// if msg is not the first, and it belongs to a new user, opposite pos
if(index !== 0 && msg.user.name !== prev.user.name) pos *= -1;
msg.position = pos;
prev = msg;
return msg;
});
}
}
});
.chatname { font-weight:bold; }
.left { text-align:left; }
.right { text-align:right; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<template v-for="(msg, index) in messages">
<li
:key=msg.id
:class="{ 'right': msg.position === 1, 'left': msg.position === -1 }"
>
<span class="chatname">
{{msg.user.name}}
</span>
{{msg.content}}
</li>
</template>
</div>

vue component data watch outside

In my application i have a component and i want wath his properties outside of the component.
I've created this example:
Vue.component('vue-table', {
template: '<div><template v-for="row in apiData.content"><span>{{row.name}}</span><button #click="remove(row)">remove</button><br></template></div>',
data: function() {
return {
//this data will be loaded from api
apiData: {
total: 20,
content: [
{id: 10, name: 'Test'},
{id: 12, name: 'John'},
{id: 13, name: 'David'},
],
},
};
},
methods: {
remove(row) {
this.apiData.content.splice(this.apiData.content.indexOf(row), 1);
},
},
})
new Vue({
el: '#app',
methods: {
isActive(){
//how can i check if in vue-table apiData.content > 0?
//return this.$refs.table.apiData.data.length > 0;
},
},
})
http://jsfiddle.net/z11fe07p/2806/
So i want to change class of span to 'active' when the length of vue-table apiData.content.length > 0
How can i do this?
The standard practice would be to emit an event in the child and have the parent receive and act on it. You might wonder whether you can watch the length of an array -- one that doesn't even exist when the component is instantiated -- and the answer is yes.
Look at the watch section. IMO, this is so cool that it's probably frowned upon.
Vue.component('vue-table', {
template: '<div><template v-for="row in apiData.content"><span>{{row.name}}</span><button #click="remove(row)">remove</button><br></template></div>',
data: function() {
return {
//this data will be loaded from api
apiData: {},
};
},
methods: {
remove(row) {
this.apiData.content.splice(this.apiData.content.indexOf(row), 1);
},
},
watch: {
'apiData.content.length': function(is, was) {
this.$emit('content-length', is);
}
},
created() {
this.apiData = {
total: 20,
content: [{
id: 10,
name: 'Test'
}, {
id: 12,
name: 'John'
}, {
id: 13,
name: 'David'
}, ],
};
}
})
new Vue({
el: '#app',
data: {
isActive: false
},
methods: {
setActive(contentLength) {
this.isActive = contentLength > 0;
}
},
})
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.active {
font-weight: bold;
}
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<p>
<span :class="{active: isActive}">Users:</span>
</p>
<vue-table refs="table" #content-length="setActive"></vue-table>
</div>

How to update the props for a child component in Vue.js?

I have a parent component that will do an API call for some data. When the response gets back I update the data. I sent the data to a child component. This child component only renders the initial value, which is an empty array, but never the updated data. I know that in React updating a property will result in a re-render of the child component. How can I achieve this in Vue.js?
This is the parent component that will pass the data:
<template>
<div id="app">
<Table v-bind:users='users' />
</div>
</template>
<script>
import Table from './components/Table'
export default {
name: 'app',
components: {
Table
},
data () {
return {
users: []
}
},
created: () => {
fetch('http://jsonplaceholder.typicode.com/users').then((response) => {
response.json().then((data) => {
console.log(data)
this.users = data
})
})
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
This is the child component that will receive the data and update the view:
<template>
<ul>
<li v-for='user in users'>
{{ user.name }}
</li>
</ul>
</template>
<script>
export default {
name: 'Table',
data () {
return {
}
},
props: ['users'],
}
</script>
<style scoped>
</style>
The fetch response looks like this:
[
{
"id":1,
"name":"Leanne Graham",
"username":"Bret",
"email":"Sincere#april.biz",
"address":{
"street":"Kulas Light",
"suite":"Apt. 556",
"city":"Gwenborough",
"zipcode":"92998-3874",
"geo":{
"lat":"-37.3159",
"lng":"81.1496"
}
},
"phone":"1-770-736-8031 x56442",
"website":"hildegard.org",
"company":{
"name":"Romaguera-Crona",
"catchPhrase":"Multi-layered client-server neural-net",
"bs":"harness real-time e-markets"
}
},
{
"id":2,
"name":"Ervin Howell",
"username":"Antonette",
"email":"Shanna#melissa.tv",
"address":{
"street":"Victor Plains",
"suite":"Suite 879",
"city":"Wisokyburgh",
"zipcode":"90566-7771",
"geo":{
"lat":"-43.9509",
"lng":"-34.4618"
}
},
"phone":"010-692-6593 x09125",
"website":"anastasia.net",
"company":{
"name":"Deckow-Crist",
"catchPhrase":"Proactive didactic contingency",
"bs":"synergize scalable supply-chains"
}
},
{
"id":3,
"name":"Clementine Bauch",
"username":"Samantha",
"email":"Nathan#yesenia.net",
"address":{
"street":"Douglas Extension",
"suite":"Suite 847",
"city":"McKenziehaven",
"zipcode":"59590-4157",
"geo":{
"lat":"-68.6102",
"lng":"-47.0653"
}
},
"phone":"1-463-123-4447",
"website":"ramiro.info",
"company":{
"name":"Romaguera-Jacobson",
"catchPhrase":"Face to face bifurcated interface",
"bs":"e-enable strategic applications"
}
},
{
"id":4,
"name":"Patricia Lebsack",
"username":"Karianne",
"email":"Julianne.OConner#kory.org",
"address":{
"street":"Hoeger Mall",
"suite":"Apt. 692",
"city":"South Elvis",
"zipcode":"53919-4257",
"geo":{
"lat":"29.4572",
"lng":"-164.2990"
}
},
"phone":"493-170-9623 x156",
"website":"kale.biz",
"company":{
"name":"Robel-Corkery",
"catchPhrase":"Multi-tiered zero tolerance productivity",
"bs":"transition cutting-edge web services"
}
},
{
"id":5,
"name":"Chelsey Dietrich",
"username":"Kamren",
"email":"Lucio_Hettinger#annie.ca",
"address":{
"street":"Skiles Walks",
"suite":"Suite 351",
"city":"Roscoeview",
"zipcode":"33263",
"geo":{
"lat":"-31.8129",
"lng":"62.5342"
}
},
"phone":"(254)954-1289",
"website":"demarco.info",
"company":{
"name":"Keebler LLC",
"catchPhrase":"User-centric fault-tolerant solution",
"bs":"revolutionize end-to-end systems"
}
},
{
"id":6,
"name":"Mrs. Dennis Schulist",
"username":"Leopoldo_Corkery",
"email":"Karley_Dach#jasper.info",
"address":{
"street":"Norberto Crossing",
"suite":"Apt. 950",
"city":"South Christy",
"zipcode":"23505-1337",
"geo":{
"lat":"-71.4197",
"lng":"71.7478"
}
},
"phone":"1-477-935-8478 x6430",
"website":"ola.org",
"company":{
"name":"Considine-Lockman",
"catchPhrase":"Synchronised bottom-line interface",
"bs":"e-enable innovative applications"
}
},
{
"id":7,
"name":"Kurtis Weissnat",
"username":"Elwyn.Skiles",
"email":"Telly.Hoeger#billy.biz",
"address":{
"street":"Rex Trail",
"suite":"Suite 280",
"city":"Howemouth",
"zipcode":"58804-1099",
"geo":{
"lat":"24.8918",
"lng":"21.8984"
}
},
"phone":"210.067.6132",
"website":"elvis.io",
"company":{
"name":"Johns Group",
"catchPhrase":"Configurable multimedia task-force",
"bs":"generate enterprise e-tailers"
}
},
{
"id":8,
"name":"Nicholas Runolfsdottir V",
"username":"Maxime_Nienow",
"email":"Sherwood#rosamond.me",
"address":{
"street":"Ellsworth Summit",
"suite":"Suite 729",
"city":"Aliyaview",
"zipcode":"45169",
"geo":{
"lat":"-14.3990",
"lng":"-120.7677"
}
},
"phone":"586.493.6943 x140",
"website":"jacynthe.com",
"company":{
"name":"Abernathy Group",
"catchPhrase":"Implemented secondary concept",
"bs":"e-enable extensible e-tailers"
}
},
{
"id":9,
"name":"Glenna Reichert",
"username":"Delphine",
"email":"Chaim_McDermott#dana.io",
"address":{
"street":"Dayna Park",
"suite":"Suite 449",
"city":"Bartholomebury",
"zipcode":"76495-3109",
"geo":{
"lat":"24.6463",
"lng":"-168.8889"
}
},
"phone":"(775)976-6794 x41206",
"website":"conrad.com",
"company":{
"name":"Yost and Sons",
"catchPhrase":"Switchable contextually-based project",
"bs":"aggregate real-time technologies"
}
},
{
"id":10,
"name":"Clementina DuBuque",
"username":"Moriah.Stanton",
"email":"Rey.Padberg#karina.biz",
"address":{
"street":"Kattie Turnpike",
"suite":"Suite 198",
"city":"Lebsackbury",
"zipcode":"31428-2261",
"geo":{
"lat":"-38.2386",
"lng":"57.2232"
}
},
"phone":"024-648-3804",
"website":"ambrose.net",
"company":{
"name":"Hoeger LLC",
"catchPhrase":"Centralized empowering task-force",
"bs":"target end-to-end models"
}
}
]
I've figured it out myself!
The problem was the syntax I was using.
I had:
created: () => {
fetch('http://jsonplaceholder.typicode.com/users').then((response) => {
response.json().then((data) => {
console.log(data)
this.users = data
})
})
}
I changed it to:
created() {
fetch('http://jsonplaceholder.typicode.com/users').then((response) => {
response.json().then((data) => {
console.log(data)
this.users = data
})
})
}
I guess it's just a binding issue with the es6 fat arrow.
You can check it in this example, its ractive when you set props it will pass to child automatically and trigger re-render
Vue.component('child', {
template: '#child',
props: ['users']
});
new Vue({
el: '#app',
data: {
users: []
},
created: function() {
var vm = this;
setTimeout(function() {
vm.users = [{ name: 'hardik'}, { name: 'someone'}];
}, 2000)
},
methods: {
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.3/vue.js"></script>
<div id="app">
<child :users="users"></child>
</div>
<template id="child">
<ul>
<li v-for='user in users'>
{{ user.name }}
</li>
</ul>
</template>