I have a Vue method that is called at mounted lifecycle hook. For some reason, it does not work if I refresh but does if the browser is hot reladed in development. I expect an object to be returned. Instead, it's undefined.
data () {
return {
randomAd: '',
startingIndex: 0,
businesses: [...]
}
},
mounted () {
this.getRandomAd()
},
methods: {
getRandomAd () {
console.log(this.businesses.length)
let randomNum = Math.floor(Math.random() * this.businesses.length)
console.log(randomNum)
this.startingIndex = randomNum
let randomAd = this.businesses[randomNum]
this.randomAd = randomAd
console.log(this.businesses[randomNum])
},
The output of the three console logs are: [__ob__: Observer], 0, and undefined. The first object contains all the data but it seems to break at the randomNum.
When the browser is hot reloaded I get what I expect with [__ob__: Observer], 17, 11.
Update
I just realized that the data comes from firebase. If I remove the firebase businesses array and create a dummy one this code works. Do I need to add an async/await somewhere? The thing I find weird is that the first console.log of the array shows the data already there.
Related
I'm developing a helpdesk tool in which I have a kanban view.
I previously used nested serializers in my backend and I managed to have everything working with a single query but it's not scalable (and it was ugly) so I switched to another schema :
I query my helpdesk team ('test' in the screenshot)
I query the stages of that team ('new', 'in progress')
I query tickets for each stage in stages
So when I mount my component, I do the following :
async mounted () {
if (this.helpdeskTeamId) {
await this.getTeam(this.helpdeskTeamId)
if (this.team) {
await this.getTeamStages(this.helpdeskTeamId)
if (this.stages) {
for (let stage of this.stages) {
await this.getStageTickets(stage)
}
}
}
}
},
where getTeam, getTeamStages and getStageTickets are :
async getTeam (teamId) {
this.team = await HelpdeskTeamService.getTeam(teamId)
},
async getTeamStages (teamId) {
this.stages = await HelpdeskTeamService.getTeamStages(teamId)
for (let stage of this.stages) {
this.$set(stage, 'tickets', [])
}
},
async getStageTickets (stage) {
const tickets = await HelpdeskTeamService.getTeamStageTickets(this.helpdeskTeamId, stage.id)
// tried many things here below but nothing worked.
// stage.tickets = stage.tickets.splice(0, 0, tickets)
// Even if I try to only put one :
// this.$set(this.stages[this.stages.indexOf(stage)].tickets, 0, tickets[0])
// I see it in the data but It doesn't appear in the view...
// Even replacing the whole stage with its tickets :
// stage.tickets = tickets
// this.stages.splice(this.stages.indexOf(stage), 1, stage)
},
In getTeamStages I add an attribute 'tickets' to every stage to an empty list. The problem is when I query all the tickets for every stage. I know how to insert a single object in an array with splice or how to delete one object from an array but I don't know how to assign a whole array to an attribute of an object that is in an array while triggering the Vue reactivity. Here I'd like to put all the tickets (which is a list), to stage.tickets.
Is it possible to achieve this ?
If not, what is the correct design to achieve something similar ?
Thanks in advance !
EDIT:
It turns out that there was an error generated by the template part. I didn't think it was the root cause since a part of the view was rendered. I thought that it would have prevent the whole view from being rendered if it was the case. But finally, in my template I had a part doing stage.tickets.length which was working when using a single query to populate my view. When making my API more granular and querying tickets independently from stages, there is a moment when stage has no tickets attribute until I set it manually with this.$set(stage, 'tickets', []). Because of that, the template stops rendering and raises an issue. But the ways of updating my stage.tickets would have worked without that template issue.
I could update the stages reactively. Here is my full code; I used the push method of an array object and it works:
<template>
<div>
<li v-for="item in stages" :key="item.stageId">
{{ item }}
</li>
</div>
</template>
<script>
export default {
data() {
return {
stages: [],
};
},
methods: {
async getTeamStages() {
this.stages = [{ stageId: 1 }, { stageId: 2 }];
for (let stage of this.stages) {
this.$set(stage, "tickets", []);
}
for (let stage of this.stages) {
await this.getStageTickets(stage);
}
},
async getStageTickets(stage) {
const tickets = ["a", "b", "c"];
for (let ticket of tickets) {
this.stages[this.stages.indexOf(stage)].tickets.push(ticket);
}
},
},
mounted() {
this.getTeamStages();
},
};
</script>
It should be noted that I used the concat method of an array object and also works:
this.stages[this.stages.indexOf(stage)].tickets = this.stages[this.stages.indexOf(stage)].tickets.concat(tickets);
I tried your approaches some of them work correctly:
NOT WORKED
this.$set(this.stages[this.stages.indexOf(stage)].tickets, tickets)
WORKED
this.$set(this.stages[this.stages.indexOf(stage)].tickets, 0, tickets[0]);
WORKED
stage.tickets = tickets
this.stages.splice(this.stages.indexOf(stage), 1, stage)
I'm sure it is XY problem..
A possible solution would be to watch the selected team and load the values from there. You seem to be loading everything from the mounted() hook, and I suspect this won't actually load all the content on demand as you'd expect.
I managed to make it work here without needing to resort to $set magic, just the pure old traditional vue magic. Vue will notice the properties of new objects and automatically make then reactive, so if you assign to them later, everything will respond accordingly.
My setup was something like this (showing just the relevant parts) -- typing from memory here, beware of typos:
data(){
teams: [],
teamId: null,
team: null
},
watch:{
teamId(v){
this.refreshTeam(v)
}
},
methods: {
async refreshTeam(id){
let team = await fetchTeam(id)
if(!team) return
//here, vue will auomaticlly make this.team.stages reactive
this.team = {stages:[], ...team}
let stages = await fetchStages(team.id)
if(!stages) return
//since this.team.stages is reactive, vue will update reactivelly
//turning the {tickets} property of each stage reactive also
this.team.stages = stages.map(v => ({tickets:[], ...v}))
for(let stage of this.team.stages){
let tickets = await fetchTickets(stage.id)
if(!tickets) continue
//since tickets is reactive, vue will update it accordingly
stage.tickets = tickets
}
}
},
async mounted(){
this.teams = fetchTeams()
}
Notice that my 'fetchXXX' methods would just return the data retrieved from the server, without trying to actually set the component data
Edit: typos
I've got some form data that I display using a readonly input that is styled to look like plain text. When users click an edit button, they can then edit the inputs and either save or cancel.
My issue is obviously that when a user clicks cancel, the data they entered into the input remains (even though it isn't saved to the DB). I'm trying to figure out a way to reset the input to its initial data. I'm aware of this answer, but it doesn't seem to work because the data is fetched on creation.
This fiddle is similar except for the fact that the data in the real app comes from an axios call. The equivalent call is essentially:
fetch() {
axios.get(this.endpoint)
.then(({data}) => {
this.name = data.data;
});
}
Annoyingly, the fiddle actually works. However in my actual implementation it doesn't. The only difference with the app is that the data is an array.
How can I make this work?
This fiddle represents what my code actually does.
In the code:
data: () => ({
endpoint: 'https://reqres.in/api/users',
users: [],
initialData: []
}),
//...
edit: function(index) {
this.users[index].disabled = false
this.initialData = this.users
},
reset: function(index) {
this.users[index].disabled = true
this.users = this.initialData
}
Since users and initialData are arrays, you must use index when you access them.
So, at first sight, the change would be from:
this.initialData = this.users
To
this.initialData[index] = this.users[index]
But this won't work. Since this.users[index] is an object, whenever you change it, it will change what this.initialData[index] holds, since they are both just pointing to the same object. Another problem is that when you set it like that, the initialData won't be reactive, so you must use Vue.set().
Another thing, since you just want to reset the first_name property (the one you use at <input v-model="user.first_name" >), you should then assign user[].first_name to initialData[index].
Considering those changes to edit(), in the reset() method, the addition of [index] and of the .first_name field are enough. Final code:
edit: function(index) {
this.users[index].disabled = false
Vue.set(this.initialData, index, this.users[index].first_name);
},
reset: function(index) {
this.users[index].disabled = true
this.users[index].first_name = this.initialData[index]
}
JSFiddle: https://jsfiddle.net/acdcjunior/z60etaqf/28/
Note: If you want to back up the whole user (not just first_name) you will have to clone it. An change the order of the disabled property:
edit: function(index) {
Vue.set(this.initialData, index, {...this.users[index]});
this.users[index].disabled = false
},
reset: function(index) {
Vue.set(this.users, index, this.initialData[index]);
}
JSFiddle here. In the example above the clone is created using the spread syntax.
Input is immediately updating the model. If you want to do something like edit and save you have to take a copy and edit that. I use lodash clone to copy objects then update the fields back when save is clicked. (of course sending message to server.)
I have to pull in data from 12 specific departments. When my Vue store gets mounted, I fire off some actions to load in these orders via 12 different ajax calls. (These calls are to different API end points and servers).
Once I get a response I commit them to the state. So in my state I would have the following data set to arrays:
state: {
sportsOrders: [],
photographicOrders: []
//And more..
},
Now I need to filter these specific orders based on if they are late, in production, etc. So I have a getter that is setup like so:
getters: {
getDepartmentLateOrders: (state) => (department) => {
let resultArray = []
state.department.forEach(function(order, index) {
let now = new Date()
let orderShipDate = new Date(order.ExpectedShipDate)
let diff = date.getDateDiff(orderShipDate, now)
if(diff < 0) {
resultArray.push(order)
}
})
return resultArray
},
}
And then when I need to use it in a component...
return this.$store.getters.getDepartmentLateOrders(this.department+'Orders')
//for example it will end up passing 'sportsOrders' to the getter
However, when I do this, Vue doesn't use the state data for sportsOrders so I know I'm not accessing it correctly. It says:
vue.runtime.esm.js?ff9b:574 [Vue warn]: Error in render: "TypeError: Cannot read property 'forEach' of undefined"
If I hardcode my getter like so it works as expected though (but I don't want to do this for 12 departments)..
state.sportsOrders.forEach(function(order, index) {
...
}
How can I set this up so I can use this one getter and have it work with all 12 departments? I only want to pass the name of the department if possible.
I want to stress that this problem only occurs outside of a template, such as when I try to access properties of related objects while in a controller, unit test, etc. Rendering the template seem to get the property well and work as expected.
Here is a simple example in JS Bin with a failing test http://jsbin.com/ihumuk/4/edit which repros my problem. The passing test asserts that the property is accessible and rendered in the template as expected. The failing test shows that I get null when I try to access the property with get. Really nothing fancy here but I don't understand why it's returning null.
Here is the application part of the JS Bin example:
App.ApplicationRoute = Em.Route.extend({
model: function() {
return App.Foo.find();
}
});
App.Store = DS.Store.extend({
adapter: DS.FixtureAdapter.create()
});
App.Foo = DS.Model.extend({
name: DS.attr("string"),
/**
* The subject under test
*/
childName: function() {
return this.get("child.name");
}.property("child.name"),
child: DS.belongsTo("App.Bar")
});
App.Bar = DS.Model.extend({
name: DS.attr("string")
});
App.Foo.FIXTURES = [{
id: 1,
name: "Fred",
child: 3
}, {
id: 2,
name: "Barney",
child: 4
}];
App.Bar.FIXTURES = [{
id: 3,
name: "Pebbles"
}, {
id: 4,
name: "Bam Bam"
}];
This passes.
test("Child name is rendered", function() {
expect(1);
visit("/").then(function() {
ok(find("div:contains(Pebbles)").length);
});
});
This fails.
test("Child name is accessed", function() {
expect(2);
var foo = App.Foo.find(1);
equal(foo.get("childName"), "Pebbles");
equal(foo.get("child.name"), "Pebbles");
});
This has to be something simple/stupid like forgetting a character or something, but I think I've driven myself too far into frustration to think clearly for a while. Thanks in advance for any help.
You need to use the then to know when the data is loaded
asyncTest("Child name is accessed", function() {
expect(2);
// load the data from server
App.Foo.find(1).then(function(foo) {
// the child id is 3, we need to fetch the remaining data
// and this is async, because of the ajax request
foo.get("child").then(function(child) {
equal(child.get("name"), "Pebbles");
// childName call child.name, but since the
// data is loaded, isn't necessary to use a second then
equal(foo.get("childName"), "Pebbles");
start();
});
});
});
In ember data, like major of the orm's, the data is lazy loaded, for relationships. This is because, isn't needed to return all loaded object graph, let's leave the user ask for what it want, and then load.
Because some implementations are async, like: websql, indexeddb, ajax, websockets etc. The interface of ember-data is async, so you need to use the then method to know when the data is loaded or failed.
The things work in your template, because it are binding aware. Even when the change are async, it will be finished later, and the bindings will be notified and updated.
I have updated your demo, and the tests pass http://jsbin.com/eqojaj/1/edit
I have a WinJS application with listviews in which if quickly navigate between pages before the listview is fully loaded, the next page shows the listview with all elements in it bound as "undefined".
So say I have a hub page with a "to do" that is filtered to only show 6 items, and there is a header that navigates to the full "to do" page, when the hub page is displayed but before it is fully loaded I click on the header link to the "to do" page, the app then goes to the "to do" page, but the items show up with all the properties in the tile as "undefined".
I am using IndexedDB as my data store.
My home page code looks like this:
WinJS.UI.Pages.define("/pages/home/home.html", {
ready: function (element, options) {
WinJS.Utilities.query("a").listen("click", function (e) {
e.preventDefault();
WinJS.Navigation.navigate(e.currentTarget.href);
}, false);
viewModel = new HomeViewModel(element);
viewModel.load(); //loads from indexed db
},
//etc...
To Do Page:
WinJS.UI.Pages.define("/pages/ToDo/ToDo.html", {
ready: function (element, options) {
viewModel = new ToDoViewModel(element);
viewModel.load();
},
etc//
I know there isn't much to go off, but any ideas would be appreciated.
Also tips on how to debug something like this would be great.
Update
I narrowed it down to this one line from the Hub Page:
myLib.GetData(todaysDate, function (result) {
that.trendsModel.today = result;
WinJS.Binding.processAll(that.el.querySelector("#dataPanel"), that.trendsModel); //<--Right Here
});
If I remove that, then when I load the second page the data doesn't show as undefined. What is interesting is the data initially shows correctly on the second page and then it changes to "undefined".
Solution
My fix:
myLib.GetData(todaysDate, function (result) {
var element = that.el.querySelector("#dataPanel");
that.trendsModel.today = result;
if(element) {
WinJS.Binding.processAll(element, that.trendsModel);
}
});
At the point when when the callback returns, I am already on the second page. So the selector was not found returning null. If you pass null to processAll it tries to bind the whole page which is why I was able to see the correct data for a second then it changes to undefined...Wow, what a doozy. I guess it makes sense but what a pain to find.
Hope it helps someone in the future :)
Your ToDoViewModel, and HomeViewModel need to be observable. This means they need to mix in from WinJS.Binding.mixin, and for the properties that you pull in asynchronously, they need to call this.notify("propertyName", newVal, oldVal) from the property setter.
Note that you need to have getter/setter properties. e.g.
var bindingBase = WinJS.Class.mix(function() {}, WinJS.Binding.mixin);
WinJS.Namespace.define("YourNamespace", {
ToDoViewModel: WinJS.Class.derive(bindingBase, function constructor() {
}, {
_titleStorage: "",
title: {
get: function() { return this._titleStorage; },
set: function(newValue) {
if(newValue === this._titleStorage) {
return;
}
var old = this._titleStorage;
this._titleStorage = newValue;
this.notify("title", newValue, old);
}
}
}),
});
myLib.GetData(todaysDate, function (result) {
var element = that.el.querySelector("#dataPanel");
that.trendsModel.today = result;
if(element) {
WinJS.Binding.processAll(element, that.trendsModel);
}
});
At the point when when the callback returns, I am already on the second page. So the selector was not found returning null. If you pass null to processAll it tries to bind the whole page which is why I was able to see the correct data for a second then it change to undefined...Wow, what doozy. I guess it makes sense but what a pain to find.