Ember - No Data in hasMany Relationship On Initial Load - javascript

ember-cli - 3.20, ember-data - 3.30
I am trying to modify the data in a hasMany relationship in the controller setup but the relationship has no data. However, all the data is there after the page is fully loaded (i.e. in my template/actions, all relationship data is there)
I have a Quiz application with Many-Many relationship with Questions.
models/Quiz.js
import { computed } from '#ember/object';
import DS from 'ember-data';
const { attr, hasMany, Model } = DS;
export default Model.extend({
description: attr('string'),
questions: hasMany('question', {async: true}) //also tried with async false
});
models/Question.js
export default Model.extend({
question: attr('string'),
quizzes: hasMany('quiz', {async: true}) //also tried with async false
});
Go to url '/quiz/1' and Route calls findRecord on quiz
routes/quizzes/quiz.js
import Route from '#ember/routing/route';
export default Route.extend({
model(params) { return this.store.findRecord('quiz', params.quiz_id); }
});
controllers/quizzes/quiz.js
import { computed } from '#ember/object';
import Controller from '#ember/controller';
export default Controller.extend({
quiz: computed.alias('model'),
//also attempted in setupController/afterModel in router
modelChanged: function() {
let quiz = this.get('quiz');
let questions = quiz.get('questions'); //questions has no data
questions.then(questions => {
Promise.all(questions.map(question => {
//modify questions/answers here
}));
});
}.observes('model')
actions: {
getQuestions() {
let questions = this.get('quiz.questions'); //questions now has data
}
})};
I have tried to get the question data in both setupController() and afterModel() with no luck.
Note:
The quizzes are nested routes able to select between each quiz to display. So if you navigate from '/quiz/1' to '/quiz/2' and then back to 'quiz/1', the question data is available in the observer, setupController, afterModel, etc. So, the second time you access a specific quiz, the data is available in setup. (data is always available in template/actions).
Any ideas?

Temporary Workaround:
Use an observer on 'quiz.questions' along with a flag to check if first time hitting observer.
import { computed } from '#ember/object';
import Controller from '#ember/controller';
export default Controller.extend({
quiz: computed.alias('model'),
areAnswersSet: false,
observeQuestions: function() {
let questions = this.get('quiz.questions');
if (!this.areAnswersSet && questions.length !== 0) {
this.toggleProperty('areAnswersSet');
questions.forEach(question => { //modify question });
}
}.observes('quiz.questions.[]')
Drawback: Observer will still get called on every questions change. Only needed on initial load.

There were a few bugs in Ember Data 3.3.0 that were related to relationships. It’s worth upgrading to Ember Data 3.3.1 to see if your issue goes away ...

Related

ember normalizeResponse when navigated to page from link-to

When I navigate to a specific page, the overridden function normalizeResponse in my serializer used in conjunction with code in my router model function, to add meta data to my model, works correctly. Basically, normalizeResponse runs first, then my model function in my router.
serializers/application.js
import App from '../app';
import JSONAPISerializer from 'ember-data/serializers/json-api';
App.storeMeta = {};
export default JSONAPISerializer.extend({
normalizeResponse(store, primaryModelClass, payload){
App.storeMeta[primaryModelClass.modelName] = payload.meta;
return this._super(...arguments);
}
});
And in my model.
import App from '../app'
...
model(params){
const data = {};
return this.store.findRecord('myModelType', params.id).then((myModelType)=>{
myModelType.meta = App.storeMeta['myModelType'];
return myModelType;
},()=>{ //error
this.get('session').invalidate();
});
}
When I navigate to that specific page through a link-to from another page, the model code gets called first, so there is no meta data being attached to the model.
How do I get the normalizeResponse function to run before the model function when navigated to from link-to?
Any help would greatly be appreciated.
The answer for anyone who sees this is to add {reload: true} as a param to the findRecord function.
So the second code snippet from my original post would know look like the following:
import App from '../app'
...
model(params){
const data = {};
return this.store.findRecord('myModelType', params.id, {reload: true}).then((myModelType)=>{
myModelType.meta = App.storeMeta['myModelType'];
return myModelType;
},()=>{ //error
this.get('session').invalidate();
});
}
More info here. Thanks to that site for the answer.

How to asynchronously load and append data to model on checkbox change of component?

I am currently developing an ember application which has two components.
One component represents a map the other one represents a friendslist.
Both components are placed in the same handlebar template.
What I try to achieve is that a user can check a checkbox in the friendslist component and in the next step his or her posts are loaded asynchronously from facebook (the friend itself was already loaded in the beforeModel hook). Those asynchronously loaded posts should be append to the already existing friend object in the model. Afterwards the map component should be informed about the changes and refresh itself or call a function which will draw points on the map.
At the moment I am trying to set the checked property of a single friend (which would be the same approach as appending the posts but will be easier for now):
// index.hbs
{{map-widget posts=model.posts friends=model.friends}}
{{friends-list checkedFriend='checkedFriend' friends=model.friends}}
// friends-list.hbs (component)
<ul>
{{#each friends as |friend|}}
<li>
{{input type="checkbox" id=friend.facebookID checked=friend.checked change=(action checkedFriend)}} <p>{{friend.name}}</p>
</li>
{{/each}}
</ul>
// friends-list.js (component)
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
checkedFriend: function () {
this.sendAction('checkedFriend');
}
}
});
// index.js (route)
export default Ember.Route.extend(AuthenticatedRouteMixin, {
...
model: function() {
return Ember.RSVP.hash({
posts: this.get('currentUserPosts'),
friends: this.get('friends')
});
},
actions: {
checkedFriend: function () {
// Update just the first friend here to see if the approach works
// Get the friends array from the model
const model = this.controller.get('model');
const friends = model.friends;
// Update the friend
Ember.set(friends[0], 'checked', true);
// Map component receives an update here,
// but "DEPRECATION: You modified (mut model.friends) twice in a single render." warning occurs
this.set('friends', friends);
}
}
})
The current approach works more or less. However, I get a depreciation warning that I modified the model twice in a single render which in my opinion is a sign for a bad design from myside.
What I would like know is how a good approach for my task described above would look like. If I am already on the right way I would be glad if anyone could tell me why this double rendering error appears.
The core problem is how to correctly update the model and how to inform the components especially the component which did not set the action about the changes so that those are refreshed.
Thank you in advance.
You could make a Class - FriendEntry. By calling its constructor you will recieve an instance of FriendEntry. Now you will be modifying instance instead of original record (which indeed is not right).
var FriendEntry = Ember.Object.extend({
init: function() {
this._super(...arguments);
this.set('somethingFriendly', true);
}
});
export default Ember.Controller.extend({
friendsEntries: Ember.computed.map('model.friends', function(friend) {
// Call the constructor
return FriendEntry.create({
friend: friend,
checked: false,
posts: []
})
})
});
Ok so your component would look something like this.
{{friends-list checkedFriend='changeFriendCheckedStatus' entries=friendEntries}}
// friends-list.hbs (component)
<ul>
{{#each entries as |entry|}}
{{input type="checkbox" checked=entry.friend.checked change=(action checkedFriend entry)}} <p>{{entry.friend.name}}</p>
{{/each}}
</ul>
// friends-list.js (component)
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
checkedFriend: function (entry) {
this.sendAction('checkedFriend', entry);
}
}
});
Back to controller
actions: {
changeFriendCheckedStatus(friendEntry) {
ic.ajax.request(API.HOST + '/someUrlForPosts/' + friendEntry.get('id)).then(givenFriendPosts => {
entry.get('posts').pushObjects(givenFriendPosts);
})
}
}
If i understood correctly you have 2 models I friends and posts (DS.belongsTo('friend')). You would need to encapsulate both into friendEntry (friend, posts).
So your map-widget would also look like this {{map-widget friendEntries=friendEntries}}
Instead of querying posts in model you could encapsulate them like this.
friendsEntries: function() {
return DS.PromiseArray.create({
promise: Ember.RSVP.all(this.get('model.friends')).then(friends => {
return friends.map(friend => {
return FriendEntry.create({
friend: friend,
checked: false,
posts: store.query('posts', { friend: friend.get('id') }
});
});
})
});
}.property('model.friends.[]')

Promise in Ember route model not resolving/updating

I'm using Ember Simple Auth and a service that gets injected into application controller to keep track of currently logged in user. I can use {{accountName}} for the currently logged in user in my application template by doing the following:
//controllers/applications.js
import Ember from 'ember';
export default Ember.Controller.extend({
session: Ember.inject.service(),
userFromSession: Ember.inject.service('session-user'),
accountName: Ember.computed('session.data.authenticated.userId', function(){
this.get('userFromSession.user').then((user)=>{
if (Ember.isEmpty(user.get('company'))) {
this.set('accountName', user.get('firstName') + ' ' + user.get('firstName'));
} else {
this.set('accountName', user.get('company.name'));
}
});
})
});
My session-user service looks like the following:
//services/session-user.js
import Ember from 'ember';
import DS from 'ember-data';
const { service } = Ember.inject;
export default Ember.Service.extend({
session: service('session'),
store: service(),
user: Ember.computed('session.data.authenticated.userId', function() {
const userId = this.get('session.data.authenticated.userId');
if (!Ember.isEmpty(userId)) {
return DS.PromiseObject.create({
promise: this.get('store').find('user', userId)
});
}
})
});
Now, a user has a company, and a company has opportunities. I would like to retrieve the company opportunities, based on the currently logged in user. How do I do this? In my opportunities route I have tried the following:
//routes/opportunities/index.js
import Ember from 'ember';
export default Ember.Route.extend({
sessionUser: Ember.inject.service('session-user'),
model: function(){
this.get('sessionUser.user').then((user)=>{
let companySlug = user.get('company.slug');
console.log(companySlug);
return this.store.findRecord('company', companySlug);
});
}
});
When using {{model.opportunities}} in my template, it just stays blank and looks like the promise never resolves. However, I can see the data populating in the Ember Inspector, as well as the expected output of my console logs. Further, when I do the following, it works fine:
//routes/opportunities/index.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function(){
let companySlug = 'known-company-slug';
return this.store.findRecord('company', companySlug);
}
});
Which means that the problem lies with model not resolving/updating for some reason. What am I doing wrong here?
Okay so I was trying to get the company model when I already had access to it through the sessionUser service.
//routes/opportunities/index.js
import Ember from 'ember';
export default Ember.Route.extend({
sessionUser: Ember.inject.service('session-user'),
model: function(){
return this.get('sessionUser.user').then(user => user.get('company'));
}
});

return first element from array as computed property from Ember controller subclass

I am trying to return the first element of my array as a computed property to my template from my controller. My code is below. I am 100% about my array and template. Problem is my syntax about in the controller.
Basically my array is works made of Work objects. And I would ideally return the first element Work. Is that even possible in javascript?
Best.
//controller works.js
import Ember from "ember";
export default Ember.Controller.extend({
firstElement: function () {
var arr = this.get('model');
return arr[0];
console.log(arr[0]);
}.property('Work')
});
//template works.js
<div class="right">
{{#liquid-with model as |currentModel|}}
{{firstElement}}
{{/liquid-with}}
</div>
//route works.js
import Ember from 'ember';
var Work = Ember.Object.extend({
name: '',
year: '',
description:'',
image:'',
logo:'',
work_id: function() {
return this.get('name').dasherize();
}.property('name'),
});
var minibook = Work.create({
id: 1,
name: 'MINIBOOK',
year: '2014',
description:'MiniBook is an iphone app that explores storytelling in its own format. The format',
image:'assets/images/minibook_iphone.png',
logo:'assets/images/minibook_logo.png'
});
var poetics = Work.create({
id: 2,
name: 'POETICS',
year: '2013',
description:'Lorem Ipsum Poetics',
image:'assets/images/poetics_iphone.png',
logo:'assets/images/poetics_logo.png'
});
var WorksCollection = Ember.ArrayProxy.extend(Ember.SortableMixin, {
sortProperties: ['id'],
sortAscending: true,
content: []
});
var works = WorksCollection.create();
works.pushObjects([poetics, minibook]);
export default Ember.Route.extend({
model: function() {
return works;
}
});
This would work.
Long way (just to improve your computed property code):
// controller work.js
import Ember from "ember";
export default Ember.Controller.extend({
firstElement: function () {
return this.get('model').get('firstObject'); // or this.get('model.firstObject');
}.property('model.[]')
});
1) you set works as model in route, so you could get it as model in controller
2) .property(model.[]) means computed property on array, so adding and deleting array element will fire update. You could also choose some specific property i.e. .property(model.#each.modelProperty)
3) fistObject is proper method (not [0]), since you are working with Ember.ArrayProxy, see http://emberjs.com/api/classes/Ember.ArrayProxy.html
4) you could use {{firstElement}} in template
Lazy way:
1) set model in route as array or as promise resolved in array
// works = ... as is
export default Ember.Route.extend({
model: function() {
return works;
}
});
2) get model.firstObject in directly in template
//template works
{{model.firstObject}} {{!-- first object of model array --}}
{{model.firstObject.name}} {{!-- name of first object --}}
UPDATE:
use proper iteration syntax http://ef4.github.io/liquid-fire/#/helpers/liquid-with/10
{{#liquid-with model as currentModel}}
{{currentModel.firstObject.name}}
{{/liquid-with}}
A bit more updated (or different) example
import Controller from '#ember/controller';
import { readOnly } from '#ember/object/computed';
export default Controller.extend({
firstElement: readOnly('model.firstObject')
});
Or if you want bidirectional data flow (can set firstElement)
import Controller from '#ember/controller';
import { alias } from '#ember/object/computed';
export default Controller.extend({
firstElement: alias('model.firstObject')
});

Liquid Fire Modal Odd Effect or Open/Close/Open?

I'm using liquid-fire to open a modal window in Ember.js. I have almost two identical sections – route, controller and template all extremely similar, but for some reason that I have not been able to debug, one section is producing a peculiar visual effect, almost as though the modal is opening, closing and then re-opening.
http://staging.ckdu.ca/shows
http://staging.ckdu.ca/schedule
Wondering if anyone has seen anything similar to this and might have any advice.
This is what the code looks like:
router.js:
Router.map(function() {
this.modal('show-modal', {withParams: ['show_id'], otherParams: 'show'});
this.resource('schedule', function () {});
});
application controller.js:
import Ember from "ember";
export default Ember.Controller.extend({
queryParams: ['show_id'],
show_id: null
});
schedule/index/controller.js:
import Ember from "ember";
export default Ember.Controller.extend({
needs: ['application'],
filteringByCategory: null,
init: function() {
var show_id = this.get('controllers.application').get('show_id');
if (show_id !== null) { this.setShow(show_id); }
this._super();
},
setShow: function (show_id) {
if (show_id !== null) {
var app = this.get('controllers.application');
var show = this.store.find('show', show_id);
app.set('show_id', show_id);
app.set('show', show);
}
return false;
},
actions: {
openShowModal: function (show_id) {
this.setShow(show_id);
return false;
}
}
});
schedule/index/route.js:
import Ember from 'ember';
export default Ember.Route.extend({
model: function () { return this.store.all('time_slot'); },
setupController: function (controller, model) {
controller.set('model', model);
}
});
schedule/index/template.hbs:
<div {{action 'openShowModal' slot.show_id}}>
transitions.js:
export default function() {
var duration = 100;
this.transition(
this.use('fade', {duration: duration}),
this.reverse('fade', {duration: duration})
);
this.transition(
this.fromRoute('shows.index'),
this.toRoute('shows.show'),
this.use('scrollThen', 'toLeft', {duration: duration}),
this.reverse('scrollThen', 'toRight')
);
}
Figured this out with help from this Ember Observe Returns Callback Twice When Used With Query Params.
The salient point is that Ember query params, according to that Stack Overflow answer above, are converted to strings.
My shows section was accessing the Show model, and this model was using the default id attribute, where my schedule section was using a TimeSlot model, which had an attribute for show ids that I had set manually as DS.attr('number').
So for my schedule section, I was setting the query param, which changed it once – and liquid-fire observed that change and started its process – and then that queryParam was converted to a string, which changed it a second time – and liquid-fire observed that change as well, and interrupted itself, etc.
At any rate, thanks #runspired for your help with this.

Categories

Resources