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.
Related
In my Ember App, I have a large number of modal dialog components that I render in my Application route like so:
{{component modalComponent options=modalOptions}}
All dialog components extend from a single base class, where, for convenience, I have overridden sendAction. The point of the override is to always trigger some action on the target, as opposed to sendAction's default behavior of "if the property is undefined, do nothing". Here is what that looks like:
sendAction: function (actionName) {
if (Em.isEmpty(this.get(actionName))) {
this.set(actionName, actionName);
}
this._super(...arguments);
},
This seems to work as I would expect: always triggering an action on the target that will then bubble up the stack. What I'm wondering is...
Are there any implications/side-effects of overriding sendAction that I am not aware of?
Currently, one of the more accepted ways to handle actions in components is through closure actions:
In template:
{{do-button id="save" clickHandler=(action "storeEvent") contextMenuHandler=(action "logEvent") buttonText="Store It"}}
In component:
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
clickHandler(event) {
this.get('clickHandler')(event);
},
contextMenuHandler(event) {
event.preventDefault();
this.get('contextMenuHandler')(event);
}
}
});
And finally, an excerpt from the controller:
actions: {
doStuff(event) {
alert(event);
},
logEvent(event) {
console.log(event);
},
So basically, you are taking the action passed into the component and calling it, passing in whatever arguments you want to from the component. Closure actions are pretty sweet, and they make working with actions a lot easier. Hope this gets your wheels turning :)
I am having troubles with closing my responsive menu in Ember. Currently I have a menu icon, if someone presses this, the following action fires:
export default Ember.Controller.extend({
actions: {
openMenu() {
$('#menu').toggle();
}
}
});
Which naturally opens my menu. However, when someone presses a link (regardless of where on the page this is) I obviously want the menu to close again. I am finding it difficult how to perform this.
Basically what I want is this:
$('a').click(function() {
$('#menu').hide();
});
But I simply do not know where to place this code.
I have thought about adding a new helper which mimics the Ember linkTo helper, but I am sure that this would not be the correct solution to the problem. Or is it?
Disclaimer: I am really new to Ember. I use version 2.2 and I do know that they're phasing out controllers, I am working on that too ;).
Given that Ember is phasing out controllers, you should put your openMenu action in the route instead. You can add the hide event to the didTransition hook in the same route.
//route-name.js
import Ember from 'ember';
export default Ember.Route.extend({
actions: {
didTransition: function() {
Ember.$('a').click(function() {
Ember.$('#menu').hide();
});
},
openMenu: function() {
Ember.$('#menu').toggle();
}
}
})
A more graceful solution would be to listen to click event on the component itself.
export default Ember.Component.extend({
actions: {
openMenu() {
this.$('#menu').toggle();
}
},
// use Ember's built in click handler
// https://guides.emberjs.com/v2.1.0/components/handling-events/
click(event) {
// check if the link is clicked
if (event.target.tagName.toLowerCase() === 'a') {
this.$('#menu').hide();
}
}
});
Hey I'm facing a problem with removing a view.
The view is used as navbar
{{view "inner-form-navbar" navbarParams=innerNavObject}}
Where params look like this
innerNavObject: {
...
routeToReturn: 'someroute.index',
...
},
On the navbar there's a small "back" button when it's clicked the parent index route is opened.
It currently works like this:
this.get('controller').transitionToRoute(routeToReturn);
But this won't work in a component and is sketchy anyways. Do i need to somehow inject router to component? Or has anyone gotten a solution for this? The navbar is used in so many places so adding a property to navbarObject to have certain action defined is not a really good solution imo.
Went for this solution :
export default {
name: 'inject-store-into-components',
after: 'store',
initialize: function(container, application) {
application.inject('component', 'store', 'service:store');
application.inject('component', 'router', 'router:main');
}
};
Now i can do
this.get('router').transitionTo('blah')
Well you can try to use a service that provides the routing capabilities and then inject into the component.
There's an addon that seems to do just that - ember-cli-routing-service
Example taken from the link, adapted for you scenario:
export default Ember.Component.extend({
routing: Ember.inject.service(),
someFunc () {
this.get('routing').transitionTo(this.get('innerNavObject'). routeToReturn);
}
});
Having a component control your route/controller is typically bad practice. Instead, you would want to have an action that lives on your route or controller. Your component can then send that action up and your route or controller will catch it (data down, actions up).
In your controller or route, you would have your transition action:
actions: {
transitionFunction(route) {
this.transitionTo(route);
}
}
You would also define the the current route name in your route or controller and pass that to your nav bar component. Controller could then look like:
export default Controller.extend({
application: inject.controller(),
currentRoute: computed('application.currentRouteName', function(){
return get(this, 'application.currentRouteName');
}),
actions: {
transitionFunction(route) {
this.transitionTo(route);
}
}
});
Then call your component and pass the currentRoute CP to it:
{{nav-bar-component currentRoute=currentRoute action='transitionFunction'}}
Then, in your component, you can have a function that finds the parent route from the currentRoute:
export default Component.extend({
click() { // or however you are handling this action
// current route gives us a string that we split by the . and append index
const indexRoute = get(this, currentRoute).split('.')[0] + '.index';
this.sendAction('action', indexRoute);
}
});
Extending a route
Per your comment, you may want to have this across multiple routes or controllers. In that case, create one route and have your others extend from it. Create your route (just as I created the Controller above) with the action. Then import it for routes you need:
import OurCustomRoute from '../routes/yourRouteName';
export default OurCustomRoute.extend({
... // additional code here
});
Then your routes will have access to any actions or properties set on your first route.
I have a dashboard and inside it some buttons for filter to posts.
This dashboard appear in all pages, so I a create a view dashboard with a template of same name.
to trigger filter, I have created an view filter-button:
export default Ember.View.extend(Ember.TargetActionSupport, {
tagName: 'button',
click(event) {
this._toggleComponentState();
this._dispatchAction();
},
_dispatchAction() {
this.triggerAction({
action: "filter",
target: this.get('controller'),
actionContext: this.get('context')
});
},
_toggleComponentState() {
this.$().toggleClass('active');
}
});
this action filter this sent to application controller, but I need send to an specific controller posts.index, hierarchically posts and dashboard have no connection. how can I create a communication correctly between my components?
To trigger an action from controller A to controller B, you need to have B in A's needs. Then you can do this.get('controllers.b').send('someAction').
Example code:
App.IndexController = Ember.Controller.extend({
needs: ['foo'],
actions: {
lolAction: function() {
var fooCtrl = this.get('controllers.foo');
fooCtrl.send('anotherLolAction');
}
}
});
Demo: http://emberjs.jsbin.com/tevagu/1/edit?html,js,output
But as #sushant said, stop using controllers and views.
Use controllers and views for only top level on each route. E. g. any route will have one controller, one view and one template and (and you don't have to explicitly define them all).
For content nested in routes' templates, don't use controllers and views. Use components instead.
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