Ember.js: questions concerning controllers, 'this', 'content' and model structure - javascript

I am getting a little deeper into my first functional app and need to better understand what it going on in my controller.
Here I have a controller that handles the action when a user clicks on an 'Option'. Looking at the this object raises a few questions:
What exactly is this? I would expect it to be an instance of my Option model, but it is missing some properties (like "identity: 'model: Option'").
If this is an instance of my Option model, why is the 'model' property undefined? Why doesn't it just know that?
What is this.content? It looks like some stuff is inside content (id and isSuppressed) and some is not (this.isSelected) - why is that?
Disclaimer: Though there aren't any presenting problems so far, there certainly could be errors in my ember app architecture.
Screenshot debugging controller:
Option Model & Controller
App.Option = Ember.Object.extend({
identity: 'model: Option',
id: '',
cost: '',
isSelected: false,
isSuppressed: false
});
App.OptionController = Ember.Controller.extend({
actions: {
toggleOption: function() {
this.set('isSelected', !this.get('isSelected'));
var id = this.get('content.id');
this.send('deselect', this.get('content.id'));
}
}
});
App.OptionsController = Ember.ArrayController.extend({
actions: {
deselect: function(exception) {
var opts = this.rejectBy('id', exception)
opts.setEach('isSuppressed', true);
}
}
});

It depends where this is, if your in the controller it's the controller. If your controller is an ObjectController/ArrayController it will proxy get/set calls down to the underlying model. content/model are the same thing in the context of the controller.
The properties rarely live directly on the instance, usually they are hidden to discourage accessing the properties without using the getters/setters.
In your code above there is a good chance your OptionController should be extending ObjectController. Unless the controller isn't backed by a model. If you use Ember.Controller.extend then it won't proxy getters/setters down to the model, it will store, retrieve properties from the controller itself.

Related

Ember route or controller expose data model as json

I am trying to work with Ember.js
Can I expose my data model as JSON through a route or controller?
I have an object like this saved in the store:
this.store.createRecord('Person', {
id: 1,
name: this.get('name'),
email: this.get('email')
});
I want to expose this data from a route or controller as JSON object. I don't want to use any view.
Is it possible to do this?
Thanks for help!
EDIT
My route is:
App.ResultRoute = Ember.Route.extend({
model: function() {
return this.store.find('person', 1);
}
});
There is '1' because I want only this record.
In this way It works and I see in the view the {{name}} and the {{email} of the Person object.
I want to see only the JSON, I tried to do how you suggest me :
App.ResultRoute = Ember.Route.extend({
afterModel: function (model) {
model.get('content').forEach(function (item) {
console.log(item.get('content'));
});
}
});
But I receive this error:
Uncaught Error: Assertion Failed: Error: More context objects were passed than there are dynamic segments for the route: error
What is my error?
The way I would do this would be, I would have an api in my model which would return a plain json object to whoever asked it. So the Person model would have a getPersonDetails method which will hide all the internal details, including the attributes and associations and whatever else, and return the state of the person object it is invoked upon.
So, for example, if you wanted to display a table of persons or something, you would do a createRecord, and just ask the newly created person object for it's details.
Start from the beginning of this guide. http://emberjs.com/guides/routing/specifying-a-routes-model/ It will show you how to specify a model for a route.
Then, read this entire guide on controllers: http://emberjs.com/guides/controllers/
In general, you would access that data from the route's model hook with:
this.store.find('person') // All records
If you wanted to access that first object as JSON, you could do:
var person_JSON = this.store.find('person').then(function (persons) {
//The persons records are now available so you can do whatever you want with them
console.log(persons.objectAt(0).get('content'));
});
You could also iterate over all records and strip out the content to produce raw json without the Ember wrapping... Just depends on what you need to really do.
Really the best place to put this would be the route's afterModel hook, though. You wouldn't be working with a promise, as Ember would have dealt with that for you:
afterModel: function (model) {
model.get('content').forEach(function (item) {
console.log(item.get('content'));
});
}
Hope that helps.
Edit: Since you have one record try this:
afterModel: function (model) {
console.log(model.get('content'));
}

Ember.js: dependencies between two controllers failing

I am trying to access one of two models in a controller that uses needs on a sibling controller. My router looks like the following:
App.Router.map(function() {
this.route('login');
this.route('mlb.lineups', {path: 'tools/mlb/lineups'})
this.resource('mlb.lineups.site', { path: 'tools/mlb/lineups/site/:site_id' });
});
The mlb.lineups route definition looks like the following:
App.MlbLineupsRoute = Ember.Route.extend({
model: function() {
var self = this;
return Ember.RSVP.hash({
sites: self.store.find('site')
})
},
setupController: function(controller, models) {
controller.set('model', models.get('sites'));
},
afterModel: function(models) {
var site = models.sites.get('firstObject');
this.transitionTo('mlb.lineups.site', site);
}
});
The reason I am using Ember.RSVP.hash({}) here is I plan on adding another model to be retrieved after I retrieve the site model.
Now in my MlbLineupsSiteController I am trying to access the sites model with the following:
App.MlbLineupsSiteController = Ember.ArrayController.extend({
needs: "mlb.lineups",
sites: Ember.computed.alias("controllers.models.sites")
});
This is the error I'm getting in my Ember console: needs must not specify dependencies with periods in their names (mlb.lineups)
What's the best way to make the sites model from the MlbLineups controller available in my MlbLineupsSiteController?
Note:
#NicholasJohn16's answer isn't valid anymore. It always gives an error that controller couldn't be found. Generally you should also never use needs property and always use Ember.inject.controller if you have to make your controllers dependent on each other. I'd also recommend using services instead of dependencies between controllers. It's easier to maintain code which contains communication between controllers through services, than controller directly accessing other controller's properties. You might not always be aware of such access, and using services gives you another layer of security.
Solution:
Tested in Ember.js 1.10.0-beta.4. Use following code in Controller to reference nested controller in needs:
needs: ['classic/about']
Then you can access it later using:
const aboutController = this.get('controllers.classic/about');
const aboutProperty = aboutController.get('customProperty');
Works as expected. Basically you need to replace dots with slashes.
It should be:
needs:" MlbLineupsSite "
Basically, the name of the controller you want to include, minus the word controller.
Everything else you posted should work.

Ember Model and Component, getting components to detect changes in models

I am confused and in a fix with regards to controllers and components detecting changes in models. According to the Ember Official Documentation --
"By default, the value returned from your model hook will be assigned to the model property of the associated controller. For example, if your App.PostsRoute returns an object from its model hook, that object will be set as the model property of the App.PostsController."
Therefore, shouldn't the controller update when the model changes in the route or, asynchronously by way of an external function?
App.IndexRoute = Ember.Route.extend({
model: function(){
App.set('localStore', this.get('store'));
App.localStore.createRecord('stats', {'name': 'cde'});
return this.store.find("stats");
}
});
App.IndexController = Ember.Controller.extend({
modelObs: function() {
// Never triggered!
console.log("CONTROLLER: model updated!");
}.property('model')
});
// Component with the controller's model property passed to it as localModel
// in the template
App.NewCompComponent = Ember.Component.extend({
localModel: null,
modelObs: function() {
console.log("COMPONENT: Model updated!");
}.property('localModel')
Here's a Jsbin that illustrates the problem - http://jsbin.com/tavis/3/edit
Is there anything I might be doing wrong? How to get a component detect a change in a passed model from the controller, and similarly how to get the controller detect a change in the route's model? Perhaps I'm missing a thing or two -- pointers are appreciated! Thanks
Computed properties aren't evaluated unless they are used. They are lazily evaluated.
Using either of those properties will cause them to be evaluated and your logs will occur.
http://jsbin.com/suheroya/1/edit
Additionally it's important to know that the model itself isn't changing, so that computed property won't be called over and over. Properties on the model are changing, and if you want the computed property to be called over and over you'll need to watch those properties.

Allow user to specify a Backbone model's ID at creation time

When saving a model, Backbone determines whether to send an HTTP POST or PUT request by whether or not the model's ID attribute is set. If there is an ID, the model is considered to already exist.
For my application, this logic is incorrect because I must allow the user to specify an ID (as I interact with a poorly designed legacy system).
How should I handle this problem? I still would like to use PUT if the model is changed.
I am considering the following options:
Override isNew, which is the Backbone method that simply checks if an ID is present.
Override sync.
Determine if the concept of cid would somehow solve the problem.
One solution is to address the symptoms rather than the cause. Consider adding to your model a new create method:
var FooModel = Backbone.Model.extend({
urlRoot: '/api/foo',
create: function () {
return this.save(null, {
type: 'post', // make it a POST rather than PUT
url: this.urlRoot // send the request to /api/foo rather than /api/foo/:id
});
}
});
This is the solution I use, but I don't consider it ideal because the view logic/caller now needs to call create rather than save when creating (which is rather easy to do). This extended API bothers me for my use-case (despite working and being rather small), but perhaps it'll work for yours.
I'd love to see some additional answers to this question.
So I went down the path of trying to change up isNew.
I came up with new criteria that would answer whether a model is new:
Was the model created via a fetch from a collection? Then it's definitely not new.
Was the model created with an ID attribute? This is a choice I made for my case, see disadvantages below for the effect of doing this, but I wanted to make new Model({ id: 1, name: 'bob' }) not be considered new, while setting the ID later on (new Model({ name:
bob'}).set('id', 1)) would be.
Was the model ever synced? If the model was successfully synced at any point, it's definitely not new because the server knows about it.
Here's what this looks like:
var UserDefinedIDModel = Backbone.Model.extend({
// Properties
_wasCreatedWithID: false,
_wasConstructedByFetch: false,
_wasSynced: false,
// Backbone Overrides
idAttribute: 'some_id',
urlRoot: '/api/foo',
constructor: function (obj, options) {
this._wasCreatedWithID = !!obj[this.idAttribute];
this._wasConstructedByFetch = options && options.xhr && options.xhr.status === 200;
// Preserve default constructor
return Backbone.Model.prototype.constructor.apply(this, arguments);
},
initialize: function () {
this.on('sync', this.onSync.bind(this));
},
isNew: function () {
// We definitely know it's not new
if (this._wasSynced || this._wasConstructedByFetch) return false;
// It might be new based on this. Take your pick as to whether its new or not.
return !this._wasCreatedWithID;
},
// Backbone Events
onSync: function () {
this._wasSynced = true;
}
});
Advantages over the other answers
No logic outside of the backbone model for handling this odd usecase.
No server-side changes to support this
No new pseudo properties
Disadvantages
This is a lot of code when you could just create a new create method as per my other answer.
Currently myCollection.create({ some_id: 'something' }); issues a PUT. I think if you need support for this you'll have to do myCollection.create({ some_id: 'something' }, { url: '/api/foo', type: 'post' }); You can remove the _wasCreatedWithoutID check to fix this, but then any construction of a new model that derives its data from an existing one will be treated as new (in my case, this is undesirable).
Here's another solution :
In your model define an idAttribute that don't exists in your server model/table/... and that wouldn't be displayed to the DOM.
So let's suppose that the JSON that you send to the server is as follows :
{
'id': 1,
'name': 'My name',
'description': 'a description'
}
Your model should look like :
var MyModel = Backbone.Model.extend({
idAttribute: 'fakeId'
});
Now, when you create a new model and try to save it to the server, no one would initialize the fakeId and it would be considered a new object (POST).
When you fetch your model from the server you have to set the fakeId in your model, and your server must duplicate the id in the fakeId like this your model will be considered as an existing (PUT)

Using Bindings with the EmberJS Router?

I'm working with Ember.Router, and one thing I can't figure out is how to bind objects to controllers that the Router is instantiating.
For instance, here is a controller class (extended) that the Router will instantiate for a specific route ('page'), as well as a controller object (created), say to handle user administration tasks on a part of the application outside of the Router:
// controller used by Router to render the "page" route
App.PageController = Em.ObjectController.extend({
content: Em.Object.extend({
foo: 'bar'
})
});
// global controller for users
App.usersController = Em.ObjectController.extend({
content: Em.Object.extend({
fooBinding: App.PageController.foo
// the above will not work since Em.Router
// instantiates the page controller dynamically
})
});
So when the router loads it will instantiate App.PageController into App.router.pageController, but that's after App.usersController is already created. So how can App.usersController access data in a controller that the Router is managing?
Any ideas?
There are a few mistakes in your sample.
First, you should never directly setup a property with an Object value at declaration time: this value would be shared across all instances of the class. Here, it does not really matter, but it's a bad practice. In this case, the good way of setting up the PageController content is to bind it in router, at connectOutlet call, like that:
connectOutlets: function (router) {
var theContainerController = router.get('theContainerController'),
objectWithFooBar = Ember.Object.create({
foo: 'bar'
});
theContainerController.connectOutlet('page', objectWithFooBar);
}
Second mistake is the naming of usersController: it should be UsersController, as it is a class, which will be injected in the router as usersController during initialize call. It seems also quite strange to have users pluralized & ObjectController. Certainly should be singularized...
Last, and certainly what will be the most interesting regarding the question, once you will have preceding remarks applied, you will be able to setup the binding using:
App.UserController = Em.ObjectController.extend({
fooBinding: 'App.router.pageController.foo'
});
App.router can be setup before your call to App.initialize. It is definitively a bad coupling to have UserController using a global symbol to directly access to PageController, but it does the job in your case.
A definitely yet better solution would also be to bind UserController's content in a connectOutlet call.

Categories