This is an Ember component that will need this at some point:
export default Component.extend({
filteredSubs: computed.filter('model.subs', function() {
// this will always return true in development http://localhost:4200/dummy
// but will always return false in test because 'this' becomes undefined
return this;
})
});
Dummy has a one-to-many relationship to Sub:
export default Model.extend({
subs: hasMany('sub')
});
export default Model.extend({
dummy: belongsTo('dummy')
});
This test fails but shouldn't:
test('it renders', function(assert) {
let dummy = server.create('dummy');
server.create('sub', { dummy });
this.set('dummy', dummy);
this.render(hbs`{{show-dummy model=dummy}}`);
assert.equal(this.$().text().trim(), 'Hi! There are 1 sub-dummies');
});
not ok 13 Chrome 63.0 - Integration | Component | show dummy: it renders
actual: Hi! There are 0 sub-dummies
expected: Hi! There are 1 sub-dummies
Not sure if this could be an Ember bug or ember-cli-mirage bug,
I have isolated the issue in a fresh repo https://github.com/stephanebruckert/ember-bug-16052
Also posted on https://github.com/emberjs/ember.js/issues/16052
Your problem comes from an unfortune sequense of falsy assumptions.
The first assumption of you is that this inside a Ember.computed.filter should be the corresponding object. I'm not 100% sure this is documented behaviour, and personally wouldnt rely on it. If you need full access to this I would go with a simple Ember.computed.
However your primary mistake is in your test. And this also explains why you only have this problem in testing. Your directly using a mirage model as model for your component:
let dummy = server.create('dummy');
server.create('sub', {
dummy
});
this.set('dummy', dummy);
this.render(hbs`{{show-dummy model=dummy}}`);
Here you assume that the result of server.create, a mirage model, is in some ways similar to a ember-data model. It is not! In fact, a mirage model is not even an ember object! So you can't use .get or .set on it, or anything you defined on your model, and definitly should not use it ever as an model für component testing. Instead you should use mirage as data-source for your ember-data models.
The question why this is undefined if your model is a mirage model leads to this line in ember-cli-mirage:
filter(f) {
let filteredModels = this.models.filter(f);
return new Collection(this.modelName, filteredModels);
}
where the this-context gets lost. Basically mirage is overriding the .filter function on their custom array-like structure, and doesnt ensure to keep the this-context.
Related
I am trying to integrate a Javascript library I'm building with EmberJS.
Example almost working integration:
https://github.com/pubnub/open-chat-framework/blob/ember/examples/ember
The library returns a single object with many nested objects. The library is based on network events, so the child objects are updated periodically without user input. The updated fire events that can be listened to.
This causes problems for EmberJS, because Ember requires every property update to be done via Ember.set() which my library does not use.
The library is a general purpose JS library so I am refusing to add Ember specific code to it. I am wondering how to solve the above error without rewriting my library.
How can I wrap an event based library in a way Ember would like? I have previously tried globals and an Ember service.
In other examples I have seen people wrap every method of the library in Ember specific code. This seems repetitive.
Is it possible to manually tell Ember of changes to the root object and have Ember ignore all other changes? Meaning, can I have ember NOT observe changes and manually tell ember when things change?
The library includes a root event emitter that is notified of all changes to any object within the tree.
ember-cli: 2.11.1
node: 6.7.0
os: darwin x64
You don't need to change the awesome lib you created.
Everybody wants to use it, should use it in the way of ember.
Just need to change controller of example like this :
import Ember from 'ember';
export default Ember.Controller.extend({
OCF: null,
me: null,
messages: [],
messageInput: '',
init: function() {
this._super(...arguments);
// test
let OCF = window.OpenChatFramework.create({
rltm: {
service: 'pubnub',
config: {
publishKey: 'pub-c-07824b7a-6637-4e6d-91b4-7f0505d3de3f',
subscribeKey: 'sub-c-43b48ad6-d453-11e6-bd29-0619f8945a4f',
restore: false
}
},
globalChannel: 'ocf-demo-ember-2'
});
this.set('OCF', OCF);
// create a user for myself and store as ```me```
let me = this.get('OCF').connect(new Date().getTime());
this.set('me', me);
this.get('me').plugin(window.OpenChatFramework.plugin.randomUsername(this.get('OCF').globalChat));
this.get('OCF').globalChat.on('message', (payload) => {
console.log(payload)
this.get('messages').pushObject(payload);
});
},
actions: {
sendChat: function() {
let messageInput = this.get('messageInput');
if(messageInput) {
this.get('OCF').globalChat.send('message', {
text: messageInput
});
Ember.set(this, 'messageInput', '');
}
return false;
}
}
});
I have a trouble with asynchronously loaded models in Ember. I thought I have already understood the whole "background Ember magic", but I haven't.
I have two models, let's say foo and boo with these properties:
foo: category: DS.belongsTo("boo", { async: true })
boo color: DS.attr("string")
In my route, I load all foos:
model: function(params) {
return this.store.findAll("task", "");
},
than in my template I render a component: {{my-component model=model}}. In the component's code I need to transform the model into another form, so I have:
final_data: function() {
this.get("model").forEach(function(node) {
console.log(node.get("category"));
});
return {};
}.property("model"),
When I try to access the "category" in the model, my code crashes:
EmberError#http://localhost:4200/assets/vendor.js:25705:15
ember$data$lib$adapters$errors$$AdapterError#http://localhost:4200/assets/vendor.js:69218:7
ember$data$lib$adapters$rest$adapter$$RestAdapter<.handleResponse#http://localhost:4200/assets/vendor.js:70383:16
ember$data$lib$adapters$rest$adapter$$RestAdapter<.ajax/</hash.error#http://localhost:4200/assets/vendor.js:70473:25
jQuery.Callbacks/fire#http://localhost:4200/assets/vendor.js:3350:10
jQuery.Callbacks/self.fireWith#http://localhost:4200/assets/vendor.js:3462:7
done#http://localhost:4200/assets/vendor.js:9518:1
.send/callback#http://localhost:4200/assets/vendor.js:9920:8
It seems to me, like the Ember didn't load the boos. How should I access them right to make Ember load them?
It's trying to load category, but the adapter is encountering some error. Can't tell what from your example.
Check your network tab.
When you access an async association from a template, Ember knows what to do. From code, such as your component's logic, Ember has no idea it needs to retrieve the association until you try to get it. The get will trigger the load, but will return a promise. You can do this:
get_final_data: function() {
Ember.RSVP.Promise.all(this.get("model") . map(node => node.get('category'))
.then(vals => this.set('final_data', vals));
}
As I'm sure you can see, this creates an array of promises for each node's category, calls Promise.all to wait for them all to complete, then stores the result into the final_data property.
Note, this is not a computed property; it's a function/method which must be called at some point, perhaps in afterModel.
I am trying to define a computed property that consists of a filtered hasMany relationship. When I loop over the items of the PromiseManyArray, I get undefined when trying to access the attribute I want to filter on. On later calls to this computed property, everything works fine.
This is a simplified version of my controller code:
export default Ember.Controller.extend({
availableModules: function () {
let thisModule = this.get('model')
console.log(thisModule.get('library.modules')) // This logs <DS.PromiseManyArray:ember604>
// loop over siblings
return thisModule.get('library.modules').filter(mod => {
// mod.classification is undefined
return mod.get('classification') !== 'basis'
})
}.property('model')
})
For the Module model we can assume that it has a classification attribute, and it belongs to a Library object, and the Library model hasMany modules.
I have tried something like this, and it logs properly the attribute classification, but I don't know how to return anything so that the template can render it.
availableModules: function () {
let thisModule = this.get('model')
thisModule.get('library.modules').then(mods => {
mods.forEach(mod => {
console.log(mod.get('classification'))
})
})
}.property('model')
So the problem seems to be that inside of the PromiseManyArray.filter method, the attributes of the found objects are not yet resolved... How can I create a promise that will return all filtered objects once those have been resolved? I don't know how to get my head around this. Thanks.
Inspired by Bloomfield's comment, and with help of this thread in the ember forum, I have found an acceptable solution. Basically it consists of resolving all the relationships in the route, so that when the controller is called, you don't have to deal with promises.
Solution:
In the model hook of the route, return a hash of promises of all the needed information
Define a custom setupController, and inside of it, store the model and the extra data in the controller
The route code looks like this:
export default Ember.Route.extend({
model(params) {
let module = this.store.findRecord('module', params.mod_id)
return Ember.RSVP.hash({
module: module,
siblingModules: module.then(mod => mod.get('library.modules')), // promise based on previous promise
})
},
setupController(controller, hash) {
controller.set('model', hash.module)
controller.set('siblingModules', hash.siblingModules)
},
})
Note: for the route to still work properly, the {{#link-to 'route' model}} have to explicitly use an attribute, like the id: {{#link-to 'route' model.id}}
Extra info
Bloomfield's approach consisted of using the afterModel hook to load the extra data in an attribute of the Route object, and then in the setupController, set the extra data in the Controller. Something like this:
export default Ember.Route.extend({
model(params) {
return this.store.findRecord('module', params.mod_id)
},
afterModel(model) {
return model.get('library.modules').then(modules => {
this.set('siblingModules', modules)
})
},
siblingModules: null, // provisional store
setupController(controller, model) {
controller.set('model', model)
controller.set('siblingModules', this.get('siblingModules'))
},
})
But this feels like a hack. You have to return a promise in afterModel, but you can't access the result. Instead the result has to be accessed via .thenand then stored in theRoute` object... which is not a nice flow of information. This has however the advantage that you don't have to specify any attribute for the links in the template.
There are more options like using PromiseProxyArray, but that's too complicated for a newcomer like me.
For anyone running into PromiseManyArray issues in modern times, make sure you have async: false explicitly set on any hasMany relationships directly serialized by the API. Modern versions of Ember will behave unexpectedly if you don't, such as computed properties not working when you use pushObject.
I have a signup process that consists of a few steps and would like to store the state within a service that can be accessed by each of the controllers for each of the steps.
I was able to get this working, but in a way that doesn't seem to jive with Ember's way of doing things. Instead of setting the controller's needs: value I had to add an initializer, which contains the following:
export default {
name: 'signup-state',
initialize: function(container, app) {
app.inject('controller:signup/index', 'signup-state', 'service:signup-state');
app.inject('controller:signup/method', 'signup-state', 'service:signup-state');
app.inject('route:signup/method', 'signup-state', 'service:signup-state');
}
};
The above was based on a comment by wycats on the discuss board [1].
Doing this just seems wrong. I would think that the needs controller would take care of this. So if this is just plain wrong stop me here since doing this a better way may fix the problem.
The above works, except for when it comes time to test the controller. When I call a method on the controller, that calls a method on the service, I get an error.
Here is the controller code
export default Ember.Controller.extend({
/**
* Reference to the signup-state service => initializers/signup-state.js
*/
setState: function(key, val) {
var state = this.get('signup-state');
state.set(key, val); <== state is undefined in tests
},
actions: {
signupAsAdmin: function() {
this.setState('userType', 'admin');
this.transitionToRoute('signup.method');
}
}
});
And here is the controller TEST code
import { test, moduleFor } from 'ember-qunit';
moduleFor('controller:signup/index', 'SignupController', {
needs: ['service:signup-state']
});
test('signing up as an admin set the userType state to admin', function() {
var controller = this.subject();
// blows up here => Cannot read property 'set' of undefined
controller.send('signupAsAdmin');
});
Calling the signupAsAdmin function within the controller, results in making a set call on the service object, which results in an “undefined” error.
The initializer code is run as noted by adding console.log statements, but doesn't seem to result in making the service available to the controller during the tests.
Any help is appreciated.
Note: I am using ember-cli, so I don't have a global App variable available.
Update Manually registering (something I thought that ember-cli was doing) does work.
export default {
name: 'signup-state',
initialize: function(container, app) {
app.register('service:signup-state', 'signup-state');
// Remove Injects
// app.inject('controller:signup/index', 'signup-state', 'service:signup-state');
// app.inject('controller:signup/method', 'signup-state', 'service:signup-state');
}
};
The above results in a null value returned when calling the get('signup-state') in the controller.
http://discuss.emberjs.com/t/services-a-rumination-on-introducing-a-new-role-into-the-ember-programming-model/4947/10?u=olsen_chris
I'm new to the idea of using the dependency injection for a service so I might be missing something, but looking at this example test in the ember code base made me wonder, are you just missing a app.register('service:signup-state',App.ModelForSignupState) to give it bones?
I'm having some trouble testing an Ember.Component... I'm pretty sure I must be missing something, but it doesn't seem to be live binding the values in the template?
Perhaps someone could shed some light on why the first test on this jsbin pastie passes, but the second fails?
http://jsbin.com/UNivugu/22/edit
This is the broken one, (but see the jsbin for explicits):
test('Outputs a different attribute value on the component, when set and present in the template', function() {
Ember.run(function() {
component.set('someAttr', 'a non-default value');
var result = component.$().text().trim();
equal(result, "a non-default value", "it should render the default value");
});
});
and here's the Component:
var SomeComponent = Ember.Component.extend({
template: Ember.Handlebars.compile('{{someAttr}}'),
someAttr: 'default value'
});
Thanks heaps!
Actually the problem is you're attempting to access the view before Ember's had a chance to re-render the view. This is all part of how the run loop works, (for a fun example by Alex Matchneer look here: https://machty.s3.amazonaws.com/ember-run-loop-visual/index.html). Essentially Ember is going to wait until the very end of all of your changes to actually render in the page (this is due to rendering being the most expensive process). So what's happening is you're setting the value, Ember has a new task to update everyone who's watching that property, and after everyone is updated, Ember will finally re-render the property on the page. This is super useful if you happen to change the property multiple times in a row, Ember figures it will just update based on the last property.
All that being said, you can just schedule a task after the page has rendered to then see if the property has updated.
test('Outputs a different attribute value on the component, when set and present in the template', function() {
Ember.run(function() {
component.set('someAttr', 'a non-default value');
stop();
Em.run.schedule('afterRender',function(){
start();
var result = component.$().text().trim();
equal(result, "a non-default value", "it should render the default value");
});
});
});
http://jsbin.com/UNivugu/27/edit