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
Related
In my Ember app I have a model, which is an array loaded from backend. Each item describes a widget. Each widget type is represented by Ember component. User puts input in each widget and now all the widgets needs to be evalueated at once (after pressing a button, which is located outside of all components).
How to achieve that? I thought I could use ember-component-inbound-actions and send an action to each component, however I don't know, how to bind arbitrary number of widgets to arbitrary number of controller properties (strings don't work).
You could create Ember.Service which emits event, inject it into route or controller (place from where you send action when user clicks button) and all components. Then, you should subscribe in your components to event emitted from Ember.Service, or, if it is shared logic, you could create Mixin with specific method and use it in all components, and react to that action emitted from controller.
Example service:
export default Ember.Service.extend(Ember.Evented, {
emitButtonClicked() {
this.trigger('buttonClicked');
}
});
Example component:
export default Ember.Component.extend({
theService: Ember.inject.service('theservice'),
doSomethingWithInput() {
this.set('randomProperty', true);
console.log('Do something with input value: ' + this.get('inputVal'));
},
subscribeToService: Ember.on('init', function() {
this.get('theService').on('buttonClicked', this, this.doSomethingWithInput);
}),
unsubscribeToService: Ember.on('willDestroyElement', function () {
this.get('theService').off('buttonClicked', this, this.doSomethingWithInput);
})
});
Example controller:
export default Ember.Controller.extend({
theService: Ember.inject.service('theservice'),
actions: {
buttonClicked() {
this.get('theService').emitButtonClicked();
}
}
});
And example template:
<button {{action 'buttonClicked'}}>Global Button</button>
First component:
{{my-component}}
Second component:
{{my-component}}
Working demo.
Full code behind demo.
I'm trying to upgrade my project to Ember 1.13, and I'm a bit confused about the behavior of new attrs of component, especially when I have to observe them.
For example, my test component observes bar which is a parameter passed from outside. I know in Ember's new Glimmer engine, the component rerenders once any of its attribute changes. What I can't figure out is that the observer will also be fired at that time if I observe attrs.bar (I didn't modify bar!). But if I observe bar itself then it'll be fine.
The sample code:
HTMLBar:
{{test-cpnt foo=foo bar=bar}}
<input type="button" {{action 'tap'}} value="tap"/>
Controller:
foo: 1,
bar: 100,
actions: {
tap: function(){
this.set('foo', this.get('foo')+1);
}
}
Component:
App.TestCpntComponent = Ember.Component.extend({
barObv1: Ember.observer('bar', function(){
console.log('bar observer is fired!');
}),
barObv2: Ember.observer('attrs.bar', function(){
console.log('attrs.bar observer is fired!');
}),
});
By tapping the button to change foo's value, we will trigger barObv2 as well.
I've created a jsbin for demo:
https://jsbin.com/qiwivu/2/edit?js,console,output
Does anyone know why the observer got triggered?
Well, you don't have to use observers since you are in Ember 1.13, you can make use of didUpdateAttrs which is triggered whenever an attribute is updated.
Ember.Component.extend({
....
didUpdateAttrs({oldAttrs, newAttrs}) {
let oldBar = get(oldAttrs, 'bar.value');
let newBar = get(newAttrs, 'bar.value');
if (oldBar !== new Bar) {
// your logic here
}
}
....
});
If you intend to use observer, you can observe for the changes like below
barDidChange: Ember.observer('bar', function(){
console.log('bar observer is fired!');
});
Listening for attrs.bar will not work properly, whenever an attribute is updated (including init), attrs are mutated every time which causes the observers to trigger, for more refer here. attrs are supposed to be used with angle-bracket components. Here is a nice article why we should not use attrs for curly components.
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
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.
Say I have a list of DefinedWord objects, which are each rendered in an {{#each}} block as a list of DefinedWordView divs at the bottom of the page.
When a user clicks a word, I lookup the associated DefinedWord. Now I want a reference to the DefinedWordView rendered for this DefinedWord, so I can ScrollTo() the DefinedWordView's div.
I can always have the views stamp each model object with a back-reference when they load, but it seems a little ugly. Not a big deal, but I think I'll need to do this for lots of other operations, and I'd rather not litter my model objects with back-references to views.
Anyone have suggestions for an ember-y idiom to handle this? Maybe EmberJS needs a standard "singleton view registry" or something?
Make your model use the Em.Evented mixin:
App.Word = Em.Object.extend(Em.Evented, {
// ...
});
When your model is clicked, trigger an event on it, let's call it selected.
App.WordView = Em.View.extend({
click: function () {
// content == the model
this.get('content').trigger('selected');
}
})
The model's view can bind to that event and when it's fired, scroll to itself:
// just pseudo code:
App.DefinedWordView = Em.View.extend({
init: function () {
this._super();
//listen for the 'selected' event and call 'scrollToDefinition' on 'this'
this.get('content').on('selected', this, 'scrollToDefinition');
},
scrollToDefinition: function () {
$(document).scrollTo( this.$() );
}
})
https://stackoverflow.com/a/13638139/294247 was great, but it didn't seem right to use a property for signalling. I realized I should be using Events dispatched from the object, and letting views react as appropriate.
Using the Ember.Evented mixin:
App.DefinedWord = Ember.Object.extend(Ember.Evented, {
// ...
scrollToDefinition: function () {
this.trigger('scrollToDefinition');
}
});
App.DefinedWordView = Ember.View.extend({
init: function () {
this._super();
this.get('content').on('scrollToDefinition', this, 'scrollToDefinition');
},
scrollToDefinition: function () {
$(document).scrollTo(this.$());
}
});