I'm facing a problem where I need to link state manager, controller and view together and at the same time avoid to get an ugly spaghetti code. And the question is : which of these object should be created first and has the responsibility to create the others ?
To be specific, here is my example. First the view is a subclass of a container view that has a collection view as child :
App.MyView = Ember.ContainerView.extend {
childViews: ['streamView']
streamView: Ember.CollectionView.extend {
}
}
The controller is just as subclass of Ember.ArrayController with a load method :
App.MyController = Ember.ArrayController.extend {
load: ->
console.log "loading data"
}
The state manager has a view state which will instantiate App.MyView :
App.MyStateManager = Ember.StateManager.extend {
initialeState: 'ready'
ready: Ember.ViewState.extend {
view: App.MyView
}
}
Now I need to run the following test :
controller = App.MyController.create {}
manager = App.MyStateManager.create {}
expect(manager.getPath('currentState.name').toEqual('ready')
expect(controller.load).toHaveBeenCalled()
streamView = manager.getPath('currentState.view.streamView.content')
expect(streamView.content).toEqual(controller.content)
In order to make the last expectation to work, I need to bind the content of my streamView that is a child of App.MyView with the controller content. How can I do this cleanly ?
Furthermore, how to pass a reference to the state manager to the view and the controller in order to notify the manager when some event occur so it need to transit to another state. For example a click on an item or controller did finish a job ?
Take a look at this gist by Yehuda Katz discussing the new router implementation.
https://gist.github.com/2728699
The suggestion seems to be to make the router and by extension the state manager the point at which the 3 layers are tied together.
Related
I have a component which has, inside it, a list of child components (being drawn with a yield inside the parent component):
parent-component
for item in items
child-component item=item childProperty=parentProperty
Inside child-component, I have an observer on "childProperty", which correctly fires any time parentProperty changes. The problem is that I'd like to trigger that observer in a time when the property hasn't actually changed.
to do this, in my parent-component, I have:
this.notifyPropertyChange('parentProperty')
For some reason, this isn't making it to the child component. Here's a JS bin showing:
http://emberjs.jsbin.com/caxedatogazo/1/edit
While I'm happy to talk through my use-case more, I'm more interested in whether the JS bin should work, and if not, why..
Thanks so much for any help!
When you call notifyPropertyChange on the controller, only observers registered within the controller are notified of the property change.
In your case, the observer is within the component controller and not the parent controller from where the notifyPropertyChange is called.
There is a hacky way to ensure that the component controller is notified of the property change. This can be done by adding the following method to the Component.
didInsertElement: function() {
var controller = this.get('targetObject');
controller.addObserver('foo', this, this.onDataChange);
},
What we are doing is, getting the parent controller, registering an observer for foo with the parent controller.
Here is the emberjs fiddle for the same: http://emberjs.jsbin.com/rajojufibesa/1/edit
Hope this helps!
I expanded on ViRa's answer.
This code below will allow your components to be passed data with different property keys on the controller. For instance, if the controller has a property data or wants to use the model from the router, the property key does not matter. The component does not need to have a fixed property key that is always used on the controller, such as "foo", instead it will dynamically find it.
didInsertElement: function() {
var controller = this.get('targetObject');
// Find the key on the controller for the data passed to this component
// See http://stackoverflow.com/a/9907509/2578205
var propertyKey;
var data = this.get('data');
for ( var prop in controller ) {
if ( controller.hasOwnProperty( prop ) ) {
if ( controller[ prop ] === data ) {
propertyKey = prop;
break;
}
}
}
if (Ember.isEmpty(propertyKey)) {
console.log('Could not find propertyKey', data);
} else {
console.log('Found key!', propertyKey, data);
controller.addObserver(propertyKey, this, this.onDataChange);
}
}
Update: Here is a JSBin: http://emberjs.jsbin.com/nafapo/edit?console,output
I'm trying to use the "Buffered Proxy" pattern on a collection of items in a form from a hasMany model. When complete, I'm trying to have a "Save" button, which triggers a save action, allow me to save all the as-yet unsaved changes I've made. More info on the BP in Ember:
http://coryforsyth.com/2013/06/27/ember-buffered-proxy-and-method-missing/
I can get all this to work fine with a top level model attribute, but I'm confused as to how to tell all my non-singleton itemControllers that I want them to save their buffers, then be able to call the grandparent to save the whole enchilada. I was hoping I'd be able to do something like this from the parent array controller:
actions: {
saveStuff: function() {
// Something like this possible?
this.get('allTheNonSingletonItemControllerChildren').send('saveThoseBuffers');
}
}
Child controller:
saveThoseBuffers: function() {
var grandParent = this.get('parentController').get('parentController');
this.applyBufferedChanges();
grandParent.saveEntireRecord(); // Not sure how this would work yet - can't use 'needs' because none of these controllers are singletons.
}
Grandparent:
saveEntireRecord: function() {
this.get('model').save().then(function() {
//other stuff;
}
}
View is something like:
{{#each stuff in childitems itemController="childController"}}
{{input type="text" value=stuff.name}}
{{/each}}
<button {{action 'saveStuff'}}>Save</button>
Nothing in the docs or SO has revealed the incantations for this.
UPDATE:
Based on a suggestion, I also tried:
children = this.get('content');
children.forEach(function(child) {
child.send('saveThoseBuffers');
});
but received:
"Uncaught Error: Attempted to handle event saveThoseBuffers on while in state root.loaded.saved."
UPDATE 2:
Versions:
DEBUG: Ember : 1.5.0-beta.2 ember.js:3496
DEBUG: Ember Data : 1.0.0-beta.7+canary.b45e23ba ember.js:3496
DEBUG: Handlebars : 1.3.0 ember.js:3496
DEBUG: jQuery : 1.9.1 ember.js:3496
UPDATE 3:
Tried getting access to subcontrollers using:
var children = this.get('_subControllers');
That always returns an empty array, regardless of where itemController is set (in the ArrayController or in each loops using itemController=)
UPDATE 4:
I've created a JSFiddle that shows what I'm attempting is possible using _subControllers:
http://jsfiddle.net/spA9Q/5/
However, it only works by doing some setup in the route using setupController, which I don't see how I can use in my application (the controller in question cannot be named the same as the model, as it's for one 'mode' of viewing/editing that model using {{render}} and it uses an async
hasMany relationship.)
None of the above methods worked (hopefully Buffered Proxy will be fleshed out and officially support/integrated into Ember someday soon, as not saving nested models until buttons are pushed is a common use case) so I wound up with the following in the parent controller, which does the job:
childModels = this.get('child.content.content');
childModels.forEach(function(child) {
child.rollback(); // or .save()
});
The ember way:
According to ember's documentation about views' eventManagers, they must be created in the parent classes definition like so:
AView = Ember.View.extend({
eventManager: Ember.Object.create({
which encapsulates and isolates them from their parent view (AView).
The only way of accessing the context of events is through the view parameter that gets passed in along with each event
dragEnter: function(event, view) {
My situation:
I'm doing a lot of work with the various drag events inside a large view with many subviews, inputs, checkboxes, etc.
Following this form, my code is beginning to go to great lengths to determine which sub-view each event originated from, and then taking different paths to access the common parent controller:
drop: function(event, view) {
var myController;
if(view.$().hasClass('is-selected') ||
view.$().hasClass('list-map-container')) {
myController = view.get('controller.controllers.myController');
} else if(view.$().hasClass('ember-text-field')) {
myController = view.get('parentView.parentView.controller');
} else {
myController = view.get('controller');
}
// do work with myController
}
My hack:
In order to simplify I used the didInsertElement hook in the parent view to assign the desired controller as a property on the eventManager:
App.MyView = Ember.View.extend({
didInsertElement: function() {
this.set('eventManager.controller', this.get('controller'));
},
eventManager: Ember.Object.create({
controller: null,
// ...
This works to significantly simplify my event handlers:
drop: function(event, view) {
var myController = this.get('controller');
// do work with myController
My question:
My intuition tells me this hack-around isn't the best solution.
Perhaps I shouldn't be doing all the work in the eventManager? Rather move all this work to a controller and just forward the events from the view?
But if the eventManager is an acceptable workspace, then what is the best way to access the parent view's controller?
I know this is a late answer but this SO question appears as a result of google. Here is how I did this when searching through emberjs examples.
To access the view within the eventManager, you have to specify two argument in the event function handler :
eventManager: Ember.Object.create({
keyUp: function(event, view){
view = view.get('parentView'); // The view parameter might not be the current view but the emberjs internal input view.
view.get('controller'); // <-- controller
}
}),
Correct me if I'm wrong, but it looks like all the controller logic is encapsulated to a text-field--if so, I think a component might better suited for this use case. It's essentially a controller and view as one, and the eventManager's callbacks' view parameter gives you control over the component/controller itself.
If you need access to the component's parent controller, you might want to bind to events on the component from the parent controller, because the component really shouldn't know about anything outside its scope.
I have a component that represent a map and after an action in my controller I want to call a method on the component to center the map. The code looks like this
App.PlacesController = Ember.Controller.extend({
actions : {
centerMap : function () {
// how to call from here to GoogleMapComponent.centerMap ??
}
}
});
App.GoogleMapComponent = Ember.Component.extend({
centerMap : function () {
}
});
template
{{google-map}}
<button {{action "centerMap"}}>Center Map</button>
I have found a workaround but I don't think this is the Ember way of doing this.
{{google-map viewName="mapView"}}
<button class="center-map">Center Map</button>
App.PlacesView = Ember.View.extend({
didInsertElement : function () {
this.$(".center-map").click(this.clickCenterMap.bind(this));
},
clickCenterMap : function () {
this.get("mapView").centerMap();
}
});
In Ember, views (Components are glorified views) know about their controller, but controllers do NOT know about views. This is by design (MVC) to keep things decoupled, and so you can have many views that are being "powered" by a single controller, and the controller is none the wiser. So when thinking about the relationship, changes can happen to a controller and a view will react to those changes. So, just to reiterate, you should never try to access a view/component from within a controller.
There are a few options I can think of when dealing with your example.
Make the button part of your component! Components are meant to handle user input, like button clicks, so you may want to consider making the button a part of the map component and handle clicks in the actions hash of your component. If this buttons is always going to accompany the map component, then I certainly recommend this approach.
You could have a boolean property on your controller like isCentered, and when the button is clicked it's set to true. In your component you can bind to that controller's property, and react whenever that property changes. It's a two-way binding so you can also change your locally bound property to false if the user moves the map, for example.
Controller:
...
isCentered: false,
actions: {
centerMap: {
this.set('isCentered', true);
}
}
...
Component:
...
isCenteredBinding: 'controller.isCentered',
onIsCenteredChange: function () {
//do your thing
}.observes('isCentered'),
...
Jeremy Green's solution can work if you mix in the Ember.Evented mixin into the controller (which adds the pub/sub trigger and on methods)
You can use on to have your component listen for an event from the controller, then you can use trigger in the controller to emit an event.
So in your component you might have something like this:
didInsertElement : function(){
this.get('controller').on('recenter', $.proxy(this.recenter, this));
},
recenter : function(){
this.get("mapView").centerMap()
}
And in your controller you could have :
actions : {
centerMap : function () {
this.trigger('recenter');
}
}
Bind a component property to the controller property in the template:
{{google-map componentProperty=controllerProperty}}
Then observe the component property in the component:
onChange: function () {
// Do your thing
}.observes('componentProperty')
Now every time controllerProperty is changed in the controller, onChange in the component will be called.
From this answer, second paragraph.
I think it's OK to have a reference in your controller to your component. It's true that your component encapsulates it's own behaviour, but public methods like reload etc. are perfectly fine.
My solution for this is to pass the current controller to the component and set a property on the controller within the component.
Example
template.hbs:
{{#component delegate=controller property="refComponent"}}
component.js:
init: function() {
this._super.apply(this, arguments);
if (this.get("delegate")) {
this.get('delegate').set(this.get("property") || "default", this);
}
}
Now in your controller you can simply get a reference to your component with this.get("refComponent").
Steffen
Inside of your component call:
var parentController = this.get('targetObject');
See: http://emberjs.com/api/classes/Ember.Component.html#property_targetObject
I am testing my application, so I am doing the following:
I show an index view (#/locators/index), of Locator objects, which I initially load with App.Locator.find();
I modify the backend manually
Manually (with a button/action) I trigger a refresh of the data in the ember frontend, without changing the route. I do this with App.Locator.find().then(function(recordArray) {recordArray.update();});. I see via console logging that a list request is sent to the backend, and that the up-to-date data is received. I assume this is used to update the store.
BUT: The view does not update itself to show this new data
Why does the view not get automatically updated when the store receives new data? Isn't that the whole point of the data binding in Ember?
If I now do the following:
Open any other route
Go back to the locators index route (#/locators/index)
Ember sends a new request to list the locators
The index view is shown, with the correct data (since it was already in the store?)
New data is received
(I am not 100% sure that 4 and 5 happen in that order, but I am quite certain)
So, my impression is that the data is properly updated in the store, but that somehow a full re-rendering of the view is needed to display this new data, for example by leaving and re-entering the route. Is this true? Can I force this re-rendering programmatically?
Ember changes view data when the underlying model is changed by the controller(Which is binded to the view)
(Only when the state of the application changes(url changes) router hooks are called)
Your problem could be solved when you do this.refesh() inside your route by capturing the action triggered by your view.
App.IndexRoute = Ember.Route.extend({
actions: {
dataChanged: function() {
this.refresh();
}
},
//rest of your code goes here
});
for this to work your handlebar template which modifies the data shoud have an action called dataChanged
example :
Assume this action is responsible for changing/modifying/deleting the underlying data
<button {{action 'dataChanged'}}> Change Data </button>
Refresh method actually does a model refresh and passes it to the corresponding controller which indeed changes the view.
There a couple of things that come to mind you could try:
If you are inside of an ArrayController force the content to be replaced with the new data:
this.replaceContent(0, recordArray.get('length'), recordArray);
Or try to call reload on every single record trough looping the recordArray:
App.Locator.find().then(function(recordArray) {
recordArray.forEach(function(index, record) {
record.reload();
}
}
And if the second approach works, you could also override the didLoad hook in your model class without having to loop over them one by one:
App.Locator = DS.Model.extend({
...
didLoad: function(){
this.reload();
}
});
If this works and you need this behaviour in more model classes consider creating a general mixin to use in more model classes:
App.AutoReloadMixin = Ember.Mixin.create({
didLoad: function() {
this._super();
this.reload();
}
});
App.Locator = DS.Model.extend(App.AutoReloadMixin, {
...
});
App.Phone = DS.Model.extend(App.AutoReloadMixin, {
...
});
Update in response to your answer
Handlebars.registerHelper is not binding aware, I'm sure this was causing your binding not to fire. You should have used Handlebars.registerBoundHelper or simply Handlebars.helper which is equivalent:
Handlebars.helper('grayOutIfUndef', function(property, txt_if_not_def) {
...
});
Hope this helps.
Somehow this seems to be due to the fact that I am using custom handlebar helpers, like the following:
Handlebars.registerHelper('grayOutIfUndef', function(property, txt_if_not_def) {
// HANDLEBARS passes a context object in txt_if_not_def if we do not give a default value
if (typeof txt_if_not_def !== 'string') { txt_if_not_def = DEFAULT_UNDEFINED_STR; }
// If property is not defined, we return the grayed out txt_if_not_def
var value = Ember.Handlebars.get(this, property);
if (!value) { value = App.grayOut(txt_if_not_def); }
return new Handlebars.SafeString(value);
});
Which I have been using like this:
{{grayOutIfUndef formattedStartnode}
Now I have moved to a view:
{{view App.NodeIconView nodeIdBinding="outputs.startnode"}}
Which is implemented like this:
App.NodeIconView = Ember.View.extend({
render: function(buffer) {
var nodeId = this.get('nodeId'), node, html;
if (nodeId) {
node = App.getNode(nodeId);
}
if (node) {
html = App.formattedLabel.call(node, true);
} else {
html = App.grayOut(UNDEFINED_NODE_NAME);
}
return buffer.push(html);
}
});
I am not sure why, but it seems the use of the custom handlebars helper breaks the property binding mechanism (maybe my implementation was wrong)