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.
Related
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 ...
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.[]')
I am trying to add some middleware to Marionette's extended version of Backbone's router. Here's my code.
AppName.Router = Backbone.Marionette.AppRouter.extend({
appRoutes:{
// routes
},
route: function(route, name, callback) {
var router = this;
if (!callback) {
callback = this[name];
}
var middleware = function() {
console.log('in middleware');
callback.apply(router, arguments);
};
return Backbone.Router.prototype.route.call(this, route, name, middleware);
}
});
What I think should be happening is whenever I load a route, the console prints 'in middleware'.
What is happening is whenever I load the first route and only the first route, the console prints 'in middleware'.
I researched by using the top solution on this question and these are the results that I get.
Edit: I have also tried 'execute' as specified in the documentation and have had the same results.
I'm giving Vue.js a try and so far I'm loving it because it's much simpler than angular. I'm currently using vue-router and vue-resource in my single page app, which connects to an API on the back end. I think I've got things mostly working with a the primary app.js, which loads vue-router and vue-resource, and several separate components for each route.
Here's my question: How do I use props to pass global data to the child components when the data is fetched using an asynchronous AJAX call? For example, the list of users can be used in just about any child component, so I would like the primary app.js to fetch the list of users and then allow each child component to have access to that list of users. The reason I would like to have the app.js fetch the list of users is so I only have to make one AJAX call for the entire app. Is there something else I should be considering?
When I use the props in the child components right now, I only get the empty array that the users variable was initialized as, not the data that gets fetched after the AJAX call. Here is some sample code:
Simplified App.js
var Vue = require('vue');
var VueRouter = require('vue-router')
Vue.use(VueRouter);
var router = new VueRouter({
// Options
});
router.map({
'*': {
component: {
template: '<p>Not found!</p>'
}
},
'/' : require('./components/dashboard.js'),
});
Vue.use(require('vue-resource'));
var App = Vue.extend({
ready: function() {
this.fetchUsers();
},
data: function() {
return {
users: [],
};
},
methods: {
fetchUsers: function() {
this.$http.get('/api/v1/users/list', function(data, status, response) {
this.users = data;
}).error(function (data, status, request) {
// handle error
});
}
}
});
router.start(App, '#app')
Simplified app.html
<div id="app" v-cloak>
<router-view users = "{{ users }}">
</router-view>
</div>
Simplified dashboard.js
module.exports = {
component: {
ready: function() {
console.log(this.users);
},
props: ['users'],
},
};
When dashboard.js gets run, it prints an empty array to the console because that's what app.js initializes the users variable as. How can I allow dashboard.js to have access to the users variable from app.js? Thanks in advance for your help!
p.s. I don't want to use the inherit: true option because I don't want ALL the app.js variables to be made available in the child components.
I believe this is actually working and you are being misled by the asynchronous behavior of $http. Because your $http call does not complete immediately, your console.log is executing before the $http call is complete.
Try putting a watch on the component against users and put a console.log in that handler.
Like this:
module.exports = {
component: {
ready: function() {
console.log(this.users);
},
props: ['users'],
watch: {
users: {
handler: function (newValue, oldValue) {
console.log("users is now", this.users);
},
deep: true
}
}
}
};
In the new version of Vue 1.0.0+ you can simply do the following, users inside your component is automatically updated:
<div id="app" v-cloak>
<router-view :users="users"></router-view>
</div>
i am using Ember CLI + Ember Data + Simple Auth. The authenticator is working fine. But when im am doing a Rest Call with Ember Data Rest Adapter this.store.findAll("user"); the authorize function in my custom authorizer don't gets called.
The Rest API Endpoint is on an other domain, so i added the url to the crossOriginWhitelist in my environment.js.
environment.js:
module.exports = function(environment) {
var ENV = {
// some configuration
};
ENV['simple-auth'] = {
crossOriginWhitelist: ['http://api.xxxx.com'],
authorizer: 'authorizer:xxxx',
routeAfterAuthentication: 'dashboard',
};
return ENV;
};
authorizer
import Ember from 'ember';
import Base from 'simple-auth/authorizers/base';
var XXXXAuthorizer = Base.extend({
authorize: function(jqXHR, requestOptions) {
// Some Code, gets not called, damn it :(
}
});
export default {
name: 'authorization',
before: 'simple-auth',
initialize: function(container) {
container.register('authorizer:xxxx', XXXXAuthorizer);
}
};
index.html
....
<script>
window.XXXXWebclientENV = {{ENV}};
window.ENV = window.MyAppENV;
window.EmberENV = window.XXXXWebclientENV.EmberENV;
</script>
<script>
window.XXXXWebclient = require('xxxx-webclient/app')['default'].create(XXXXWebclientENV.APP);
</script>
....
Thanks for help :)
I had a similar problem. For me it was the crossOriginWhitelist config.
I set it like this:
// config/environment.js
ENV['simple-auth'] = {
crossOriginWhitelist: ['*'] // <-- Make sure it's an array, not a string
};
to see if I could get it working (I could), then I could narrow it down to figure out exactly what URL I should use to enforce the restriction (port number and hostname etc).
Don't leave it like that though!
You should actually figure out what URL works for the whitelist, and use that.
I am facing the same issue. I have same setup but the authorize function is not being called. May be you can try by adding the port number in your crossOriginWhiteList url.
I am adding window.ENV = window.MyAppENV line in new initializer which runs before simple-auth. You have added that in index file and may be that is the reason why simple-auth is not able to read your configuration.
Does the other configuration routeAfterAuthentication: 'dashboard', works properly? If not then this might be the reason. Try adding new initializer like
export default {
name: 'simple-auth-config',
before: 'simple-auth',
initialize: function() {
window.ENV = window.MyAppNameENV;
}
};