How to keep track of an array change in Vue.js when the index is a dynamic value? - vue.js

I am building an app using Node.js and Vue.
My DATA for the component is the following:
data() {
return {
campaign: {
buses: [],
weeks: [
{
oneWayBuses: [],
returnBuses: []
}
]
},
busesMap: {
// id is the bus ID. Value is the index in the campaign.buses array.
},
};
},
I fill the buses and weeks array in MOUNTED section in two separate methods after getting the data from the server:
responseForWeeks => {
responseForWeeks.forEach(
week => this.campaign.weeks.push(week);
)
}
responseForBuses => {
responseForBuses.forEach(
bus => this.campaign.buses.push(bus);
// Here I also fill the busesMap to link each week to its bus index in the array of buses
this.busesMap[bus.id] = this.campaign.buses.length - 1;
)
}
So the idea is that my busesMap looks like busesId keys and index values:
busesMap = {
'k3jbjdlkk': 0,
'xjkxh834b': 1,
'hkf37sndd': 2
}
However, when I try to iterate over weeks, v-if does not update so no bus info is shown:
<ul>
<li
v-for="(busId, index) in week.oneWayBuses"
:key="index"
:item="busId"
>
<span v-if="campaign.buses[busesMap.busId]">
<strong>{{ campaign.buses[busesMap.busId].busLabel }}</strong>
leaves on the
<span>{{ campaign.buses[busesMap.busId].oneWayDepartureDate.toDate() | formatDate }}</span>
</span>
</li>
</ul>
On the other side, if I shorten the v-if condition to campaign.buses, then I get into the condition but campaign.buses[busesMap.busId] is still undefined, so I get an ERROR trying to display busLabel and oneWayDepartureDate
I've read vue in depth documentation, but couldn't come up with a resolution.
Any gotchas you can find out?

Try this:
async mounted(){
await responseForWeeks
await responseForBuses
responseForWeeks => {
responseForWeeks.forEach(
week => this.campaign.weeks.push(week);
)
}
// this is partial since it is what you provided
responseForBuses => {
responseForBuses.forEach(
bus => this.campaign.buses.push(bus);
// Here I also fill the busesMap to link each week to its bus index in the array of buses
this.busesMap[bus.id] = this.campaign.buses.length - 1;
)
}
}
Basically you want to make sure that before your component loads your data is in place. You can also create computed properties which will force re rendering if dependencies are changed and they are in the dom.

Actually, the problem was indeed in the HTML.
When trying to access the object keys, better use [] intead of a dot .
Final HTML result would be as follows:
<ul>
<li
v-for="(busId, index) in week.oneWayBuses"
:key="index"
:item="busId"
>
<span v-if="campaign.buses[[busesMap[busId]]]">
<strong>{{ campaign.buses[busesMap[busId]].busLabel }}</strong>
leaves on the
<span>{{ campaign.buses[busesMap[busId]].oneWayDepartureDate.toDate() | formatDate }}</span>
</span>
</li>
</ul>
What was happening is that previously campaign.buses[busesMap.busId] did not exist, thus not rendering anything. Solved to campaign.buses[busesMap[busId]]. Also used claudators for the displayed moustache sintach.
Hope it helps someone else messing with Objects!

Related

Invert boolean on click with v-for?

Beginner to JS and VueCLI so I'll try to explain as best as I can. I'm using Express as my back-end.
I'm trying to change the boolean in my array of objects on click. I'm able to accomplish that but when I click on a different list item in my v-for loop it's flipping the boolean in all other indexes of my array. Here's my code:
Express: /routes:
// fake data store
const tasks = [
{ id: 1, task: 't1', completed: false},
{ id: 2, task: 't2', completed: false},
{ id: 3, task: 't3', completed: false}
];
/**
* GET handler for /tasks route
* #returns {Array.<{id: Number, task: String, completed: Boolean}>} array of task objects
*/
router.get('/', (req, res) => {
res.send(tasks);
});
Webapp:
/**
* GET /tasks
* #returns Promise => {Array.<{id: Number, task: String, completed: Boolean}>} array of task objects
*/
export function getTasks() {
return request('tasks');
}
and now my Vue component:
<template>
<div id="tasks">
<h2>Movies to Add</h2>
<ul class="todo-list">
<li v-for='task in tasks' :id="task.id" v-on:click="completeMovie($event)" :key='task.id' class="todo-list__li">
<input class="todo-list__input" type="checkbox" :name='task.task' :id="task.task">
<div class="todo-list__checkbox">
<span class="todo-list__checkbox-inner"><i></i></span>
</div>
<label :for='task.task'>{{ task.task }}</label>
</li>
</ul>
</div>
</template>
<script>
import {getTasks} from './../../services/tasks';
export default {
name: 'TaskList',
data: function() {
return {
tasks: []
};
},
created: function() {
getTasks()
.then(res => this.tasks = res);
},
methods: {
completeMovie: function (event) {
var taskId = event.currentTarget.id -1;
getTasks()
.then((res) => {
this.tasks = res;
res[taskId].completed = !res[taskId].completed;
});
}
}
}
</script>
So when I click on my first list item it changes the Task: t1 to True but if I click on the second list item it changes t1 back to False and t2 to True. I'm not sure exactly what I'm doing wrong. I'm not even sure this is the best way to do this. My main issue is I'm not sure why it's happening.
Any help is much appreciated!
You're probably over-complicating this.
All you need is
<li v-for="task in tasks" :key="task.id"
#click="task.completed = !task.completed"
class="todo-list__li">
Demo ~ https://jsfiddle.net/xy1q0auL/2/
There's no (obvious) need to re-fetch the tasks every time you click on one. This is why your previous changes are reset; it's because you overwrite all the data with the unmodified values from getTasks().
When completeMovie() is called, you send an HTTP request to your server that is gonna send you back unaltered task list. I don't understand clearly what's you're trying to achieve but your callback in the promise has no sens. In the callback you reaffect the "tasks" list in your vue :
In this case, here is the event timeline you have to do :
When client page is loaded, Vue Initialized, the vue component calls your webservice to get the task list then print it.
Then when I click on a task, you have to make a another HTTP call to your webservice (on another route), that will update the task list and will return it.
Then in the call of your vue component you reaffect the new task list.
You can make its simply:
<li v-for='task in tasks' :id="task.id" v-on:click="completeMovie(task.id)" :key='task.id' class="todo-list__li">
<input class="todo-list__input" type="checkbox" :name='task.task' :id="task.task">
<div class="todo-list__checkbox">
<span class="todo-list__checkbox-inner"><i></i></span>
</div>
<label :for='task.task'>{{ task.task }}</label>
</li>
Ans for the method
completeMovie: function ($taskId) {
this.tasks[$taskId].completed = !this.tasks[$taskId].completed;
}
}

Angular: Getting the last 4 posts from Contentful

Hi I'm trying to get the last four blog posts from contentful api in angular 5. I've created a service and I'm capable of retrieving 1 with the following code.
getContent(contentId) {
const promise = this.client.getEntry(contentId);
return Observable.fromPromise(promise).map(entry => entry.fields);
};
I would like to return it as an observable, however if returned as a promise, I would like to know how to work with a promise in the component.ts and -.html.
getLastByCount(number) {
var promise = this.client.getEntries({
limit: number
})
...
}
If i do the same for getting multiple entries I get a 'PromiseObservable', which contains a 'promise: ZoneAwarePromise'. In which it has 'items: Array', where object are as when i log the single entry. How do I work which such objects?
Edited:
I've done as suggested by: Stephan
getLastByCount(number) {
var promise = this.client.getEntries({
limit: number
})
return Observable.fromPromise(promise).mergeMap((collection) => (
Observable.from(collection.items)
))
}
And in my component.ts in OnInit()
posts$: Observable<any>;
this.posts$ = this.contentfulService.getLastByCount(4).map(entry => entry.fields);
In my component.html, i do this when displaying the one entry
<div *ngIf="post$ | async as post">
<h1>{{ post.headline }}</h1>
<time>Published on {{ post.published | date: 'fullDate' }}</time>
<hr>-->
</div>
I try this when using the collection:
<div *ngIf="posts$ | async as posts">
<ul>
<li class="post" *ngFor="let post of posts$ | async">
<h1>{{ post.headline }}</h1>
<time>Published on {{ post.published | date: 'fullDate' }}</time>
<hr>
</li>
</ul>
</div>
I get this error: Error: Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays.
What you probably want is an Observable that emits every single item of the collection that the Contentful client returns.
To do so, you can use the mergeMap functionality:
Observable
.fromPromise(promise)
.mergeMap((collection) => (
Rx.Observable.from(collection.items)
))
Now you can work with every item in the observable sequence as with the single entry above, e.g. you can
.map(entry => entry.fields)
EDIT:
Turns out Angular expects you to have an Observable<Entry[]>, instead of Observable<Entry>, so the proper way is:
Observable
.fromPromise(promise)
.map((collection) => collection.items)

how to do pagination in vuejs

hi i want to do pagination in my view page.can anyone tell me how to do that in vuejs..
Here is my view page:
<div class="container">
<div class="row">
<el-row :gutter="12">
<el-col>
<p>View Candidates</p>
</el-col>
</el-row>
<el-row :gutter="12">
<template v-for="c in candidates">
<el-col :span="6">
<Candidate :c="c" :key="c.id"></Candidate>
</el-col>
</template>
</el-row>
</div>
here is my js page:
const app = new Vue({
el: '#app',
data() {
return {
candidates: window.data.candidates,
}
},
components: { Candidate }
});
i am working on laravel5.4 and vuejs 2
Please can anyone help me..how to do this..
For real pagination you will need to ensure that your endpoints (from your post I'd say something like /candidates) will return json AND that it will return a pagniated object ofcourse.
In Laravel you'd do it like
public function index() {
return Candidates::paginate(10);
}
EDIT: for more information regarding laravel pagination you can take a look at their examples and docs: https://laravel.com/docs/5.4/pagination
A full example is rather hard to give but here a really short one
routes/web.php
Route::get('candidates', 'CandidateController#index');
app/http/controller/CandidateController
public function index() {
$candidates = App\Candidate::paginate(10);
return $candidates;
}
For a more detailed version of the laravel part you should provide your Controller, Migration, Routing setup.
In Vue I'd suggest you load all your data from within Vue and not with blade. Even though you could keep it as it is - it would be more "unified".
data: function() {
return { paginator: null }
},
created: function() {
// load initial first 10 entries
axios.get('/candidates').then(function(response) {
this.paginator = response.data;
}.bind(this));
}
Ok so now you have the initial load as you had it before. You can loop through pagniator.data which is your actual list now. Small example:
<ul v-if="paginator"><!-- important !not loaded on initial render-->
<li v-for="paginator.data as candidate">{{ candidate.name }}</li>
</ul>
Now to the load more. Let's say you want a button for that. The paginator has a pro called next_page_url to give you the next http endpoint. If it's null - now data is left to load.
<button v-if="paginator && paginator.next_page_url" #click.prevent="loadMore">Load more</button>
Button is setup - now the load more
methods: {
loadMore: function() {
// load next 10 elements
axios.get(this.paginator.next_page_url).then(function(response) {
// you have two options now. Either replace the paginator fully - then you will actually "page" your results.
// only 10 will be visible at any time
this.paginator = response.data;
}.bind(this));
}
}
There you go this is an actual pagination. If you want to loadMore to add 10 elements to your current list it is a little bit more tricky because you don't want to replace the paginator.data with the new loaded stuff. You want to concat it.
...
axios.get(this.paginator.next_page_url).then(function(response) {
response.data.data = this.paginator.data.concat(response.data.data);
this.paginator = response.data;
}.bind(this));

Aurelia iterate over map where keys are strings

I'm having trouble getting Aurelia to iterate over a map where the keys are strings (UUIDs).
Here is an example of the data I'm getting from an API running somewhere else:
my_data = {
"5EI22GER7NE2XLDCPXPT5I2ABE": {
"my_property": "a value"
},
"XWBFODLN6FHGXN3TWF22RBDA7A": {
"my_property": "another value"
}
}
And I'm trying to use something like this:
<template>
<div class="my_class">
<ul class="list-group">
<li repeat.for="[key, value] of my_data" class="list-group-item">
<span>${key} - ${value.my_property}</span>
</li>
</ul>
</div>
</template>
But Aurelia is telling me that Value for 'my_data' is non-repeatable.
I've found various answer by googling, but they have not been clearly explained or incomplete. Either I'm googling wrong or a good SO question and answer is needed.
As another resource to the one supplied by ry8806, I also use a Value Converter:
export class KeysValueConverter {
toView(obj) {
if (obj !== null && typeof obj === 'object') {
return Reflect.ownKeys(obj).filter(x => x !== '__observers__');
} else {
return null;
}
}
}
It can easily be used to do what you're attempting, like this:
<template>
<div class="my_class">
<ul class="list-group">
<li repeat.for="key of my_data | keys" class="list-group-item">
<span>${key} - ${my_data[key]}</span>
</li>
</ul>
</div>
</template>
The easiest method would be to convert this into an array yourself (in the ViewModel code)
Or you could use a ValueConverter inside repeat.for as described in this article Iterating Objects
The code...
// A ValueConverter for iterating an Object's properties inside of a repeat.for in Aurelia
export class ObjectKeysValueConverter {
toView(obj) {
// Create a temporary array to populate with object keys
let temp = [];
// A basic for..in loop to get object properties
// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/for...in
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
temp.push(obj[prop]);
}
}
return temp;
}
}
/**
* Usage
* Shows how to use the custom ValueConverter to iterate an objects properties
* aka its keys.
*
* <require from="ObjectKeys"></require>
* <li repeat.for="prop of myVmObject | objectKeys">${prop}</li>
*/
OR, you could use the Aurelia Repeat Strategies provided by an Aurelia Core Team member
You'd have to import the plugin into your app.
Then you'd use it using the pipe syntax in your repeat.for....like so....
<div repeat.for="[key, value] of data | iterable">
${key} ${value.my_property}
</div>

vue interpolated value not updating

I'm using Vue JS 2.0 and this is a table I'm trying to display. Everything displays properly initially but when I update(in a method called editPlayerStat) editedPlayers[any-Player-Id], it doesn't update the interpolated value in the respective small tag.
At the editPlayerStat() I check to see if editedPlayers[any-Player-Id] is changing properly and it is. But for some reason it isn't getting updated in the small tag. Also, in the span tag's v-if doesn't update and has the same problem.
While in the same method editPlayerStat(), I manually update player.stats.month.goals you see interpolated, to see if also has the same problem and IT DOESN'T. It works just fine.
What am I doing wrong?
<tr v-if="showMonth" v-for="player in players">
<td><input type="checkbox"></td>
<td>{{ player.name }}</td>
<td><a #click.prevent="editPlayerStat(player._id,'goals',false)" href=""><i class="fa fa-arrow-down"></i></a>
{{ player.stats.month.goals }}
<a #click.prevent="editPlayerStat(player._id,'goals',true)" href=""><i class="fa fa-arrow-up"></i></a>
<span id="changed"> <small>({{ editedPlayers[player._id].stats.goals }})</small></span>
</td>
<td><a #click.prevent="editPlayerStat(player._id,'assists',false)" href=""><i class="fa fa-arrow-down"></i></a>
{{ player.stats.month.assists }}
<a #click.prevent="editPlayerStat(player._id,'assists',true)" href=""><i class="fa fa-arrow-up"></i></a>
<span id="changed" v-if="editedPlayers[player._id].stats.assists > 0"> <small>({{ editedPlayers[player._id].stats.assists }})</small></span>
</td>
</tr>
This is my editPlayerStat() method:
editPlayerStat(id, type, isIncrease) {
console.log('editPlayerStat...');
console.log('editedPlayers[player._id].stats.goals...' + this.editedPlayers[id].stats.goals );
if(isIncrease) {
console.log('increase');
this.editedPlayers[id].stats[type]++;
this.editedPlayers[id].isEdited = true;
} else if(!isIncrease && this.editedPlayers[id].stats[type] > 0) {
console.log('decrease');
this.editedPlayers[id].stats[type]--;
this.editedPlayers[id].isEdited = true;
}
}
ANOTHER instance:
I use v-model on an input and display the interpolated value beside and it updates just fine when I update the input tag value. But when I 'watch' for changes in the object player, the watch function doesn't run except for the first time.
<input class="form-control" id="name" placeholder="Name" type="text" v-model="player.name">{{ player.name }}
watch: {
player: function(player) {
console.log('from watch... '+JSON.stringify(player));
}
},
methods: {
getPlayerInfo() {
var vm = this;
axios.get('http://localhost:3000/admin/players/info/'+'5847a093a5e13203a4d0455f')
.then(function (response) {
vm.player = response.data;
console.log(response);
console.log(JSON.stringify(vm.player));
console.log('showForm: ' + vm.showForm);
})
.catch(function (error) {
console.log(error);
});
},
created: {
this.getPlayerInfo();
}
Output:
Object { data: Object, status: 200, statusText: "OK", headers: Object, config: Object, request: XMLHttpRequest }
{"_id":"5847a093a5e13203a4d0455f","name":"Michael","age":25,"dateOfBirth":"03-04-1998","phone":7987997799,"image":"imgur::djnwkndkjnkn","preferredFoot":"Right","isActive":true,"positions":["CB","RB","RW"]}
showForm: true
from watch... {"_id":"5847a093a5e13203a4d0455f","name":"Michael","age":25,"dateOfBirth":"03-04-1998","phone":7987997799,"image":"imgur::djnwkndkjnkn","preferredFoot":"Right","isActive":true,"positions":["CB","RB","RW"]}
As you can see, even though the interpolated player.name is updated when I edit the value in the input tag, the watch function for player property doesn't run except for that first time when player was initialized with an Object.
Thanks to #vbranden and participants in an issue I raised here. So here's what was going wrong.
I was running into this behavior in Vue 2.0, which according to the documentation, "cannot detect property addition or deletion". You will run into this sooner or later.
The thing was that, I was adding properties to an Object directly (or as I thought 'string indexed array' values to an array. Note: also see issue; javascript doesn't allow associative arrays) and Vue couldn't watch for changes in it due to this caveat.
Solution: Instead of directly assigning properties to an Object, you need to use Vue.set() or this.$set() to assign them. This allows Vue to track for changes in them.
Original Problem: fiddle
editedPlayers: []
this.editedPlayers['someID'] = { name: 'John', stats: { goals: 10, assists: 10 }};
this.editedPlayers['someOtherID'] = { name: 'Flint', stats: { goals: 10, assists: 10 }};
Solution: fiddle
editedPlayers: {}
Vue.set(this.editedPlayers, 'someID', { name: 'John', stats: { goals: 10, assists: 10 }});
Vue.set(this.editedPlayers, 'someOtherID', { name: 'Flint', stats: { goals: 10, assists: 10 }});