I am trying to write an Ember.Component to handle Bootstrap-style tabs. I implemented it as a reusable TabHolderComponent which takes an array as parameter and outputs one tab per item and then one content container per item (à la Bootstrap). So far it works well, but I also need to be able to refresh the content in the tabs with a button in the tab. Is there an elegant way of sending a refresh event/action to a child component?
I have several different solutions to this (moving the refresh button into the component, which would be less user-friendly, or binding a parent component refresh property that contains the name of the tabs that should be refreshed and that the child component observes), but none of them are elegant.
I also saw this suggestion, but don't know how to make that work for dynamically created tabs where I can't hardcode the tab name as a parent component property.
You can use the observable pattern (It's similar to what this post is saying).
{{my-thing parent=controller}}
App.MyThingComponent = Em.Component.extend({
setup: function(){
this.get('parent').register(this); // you should check to see if parent exists, I'm lazy
}.on('init'),
sayHello: function(){
console.log('hello');
}
});
And in the parent controller (Array, Object, or plain controller)
App.FooController = Em.Controller.extend({
observers: null,
setup: function(){
this.observers = [];
}.on('init'),
register: function(observer){
this.observers.pushObject(observer);
},
talkToChildren: function(){
this.observers.forEach(function(child){
child.sayHello();
});
}
});
Example: http://emberjs.jsbin.com/yoyod/1/edit
Related
I am just starting with ember and trying to do a simple test.
Which, also very simple, got me stuck for some reason and I cant find the answer anywhere.
So I need load data from the server without transition to another route and do it from within a submit action (or any other action for that matter).
I have a simple input form where I type in manually an object ID and
I want it to be loaded say right underneath. Simple enough. Seams to be a three minutes job in angular. Here, I just cant get the hang of communication between route and controller.
So given this little emblem
form submit="submit"
= input type="text" value=oid
button type="submit" Submit
#display
= person
And this route
import Ember from 'ember';
export default Ember.Route.extend({
model: {
person: null
},
actions: {
submit: function() {
var oid = this.controllerFor('application').get('oid');
var person = this.store.find('person', oid);
this.modelFor('application').set('person', person);
}
}
});
This is as far as I could think. I want to click submit with ID of an object and I want that object loaded and displayed in the div#display.
So what am I doing wrong? What is the right way to do it?
First, I don't even know where to put such an action? Controller or route?
If I put it in controller, I don't know how to refresh the model. If I put it in route, I am stuck with the above. Would be also nice to see how to do it if action was placed in the controller.
For simplicity I just do it all in application route, template, controller ...
Thank you
The best place to put your code is on Controller given it responds to UI, so doing that on your controller the code is much more simple.
On this jsfiddle I have put some dummy code which tries to do something what you want to achieve.
//Index Route
App.IndexRoute = Ember.Route.extend({
model: function () {
return ['red', 'yellow', 'blue'];
}
});
//Here my dummy controller.
App.IndexController = Ember.Controller.extend({
oid: 1,
actions: {
submitAction() {
//Here your logic to find record given the input and attach
//the response to the model object as UI is binding to model
//if you add/remove new records they will show up.
//On this example I have added a new object.
this.get('model').addObject('green');
}
}
})
Enjoy!
I've made a component that produces a drop down list used to select an item from a possible list of items based on the passed in type:
{{item-selector type="type1"}}
I dont want to load the items on page load cause theres potentially a lot of them and many times they won't need to be loaded. I would prefer loading them on user interaction with the component. That nixes out loading them in the route.
Since components don't have access to the store, I've been experimenting with passing in a controller for the items:
{{item-selector type="type1" widget=controllers.type1}}
I'm using widget cause I can't think of a better property name and controller is already taken. Suggestions welcome.
But now I'm unsure about how to load the models in the controller. Should I do it in init? Or maybe a computed property? I'm unsure.
Suggestions on how to solve this would be appreciated.
Thanks to two way databinding you can just pass an empty array, that will populate only when needed.
{{item-selector type="type1" items=items action="loadItems"}}
Here we are passing items as the elements of your list, which at the beginning would be empty.
we are also passing an action, that would ask the route to load the items on demand.
// controller.js
export default Ember.ArrayController.extend({
items: null
});
// route.js
export default Ember.Route.extend({
actions: {
loadItems: function() {
var controller = this.get('controller');
this.store.find('item').then(items => {
controller.set('items', items);
});
}
}
});
Some where in your component, you will be firing the action to load the items as part of the user interaction you mentioned.
I have a controller in Ember like so:
App.TasksController = Ember.ArrayController.extend({
search: function(term){ ... }
})
And I have the relative view, with a custom text field, as such:
App.TasksView = Ember.View.extend({
searchField: Ember.TextField.extend({
keyUp: function(){ this.get('controller').search() }
})
})
I however get an error saying that there is no such method.
I was wondering:
How can I correctly call the method defined in the controller from the view?
How can I debug which is the currently active controller? If I console.log(this.get('controller')) I get an object, but it is very confusing to navigate and to understand exactly which controller is that.
the scope of this on the text field isn't the same scope as the tasksview, so it doesn't have access to the controller.
Honestly a better way to handle this is to bind the textfield value to something and add an observer to it. When it changes fire off a search (or probably better would be to debounce it so you aren't firing off requests every single character they type)
http://emberjs.jsbin.com/IRAXinoP/3/edit
I can't figure out the correct way to handle modal states/views with the new Ember router. More generally, how do you handle states that you can enter and exit without affecting the "main" state (the URL)?
For example, a "New Message" button that is always available regardless of the current leaf state. Clicking "New Message" should open the new message modal over the current view, without affecting the URL.
Currently, I'm using an approach like this:
Routes:
App.Router.map(function() {
this.route('inbox');
this.route('archive');
});
App.IndexRoute = Em.Route.extend({
...
events: {
newMessage: function() {
this.render('new_message', { into: 'application', outlet: 'modalView' });
},
// Clicking 'Save' or 'Cancel' in the new message modal triggers this event to remove the view:
hideModal: function() {
// BAD - using private API
this.router._lookupActiveView('application').disconnectOutlet('modalView');
}
}
});
App.InboxRoute = Em.Route.extend({
...
renderTemplate: function(controller, model) {
// BAD - need to specify the application template, instead of using default implementation
this.render('inbox', { into: 'application' });
}
});
App.ArchiveRoute = ... // basically the same as InboxRoute
application.handlebars:
<button {{action newMessage}}>New Message</button>
{{outlet}}
{{outlet modalView}}
I've obviously left out some code for brevity.
This approach 'works' but has the two problems identified above:
I'm using a private API to remove the modal view in the hideModal event handler.
I need to specify the application template in all of my subroutes, because if I don't, the default implementation of renderTemplate will attempt to render into the modal's template instead of into application if you open the modal, close it, and then navigate between the inbox and archive states (because the modal's template has become the lastRenderedTemplate for the IndexRoute).
Obviously, neither of these problems are dealbreakers but it would be nice to know if there is a better approach that I'm missing or if this is just a gap in the current router API.
We do kind of the same thing but without accessing the private API.
I don't know if our solution is a best practice, but it works.
In the events of our RootRoute I have an event (same as your newMessage), where we create the view we need to render, and then append it.
events: {
showNewSomething: function(){
var newSomethingView = app.NewSomethingView.create({
controller: this.controllerFor('newSomething')
});
newSomethingView.append();
}
}
This appends the modal view into our app.
On cancel or save in the newSomethingView we call this.remove() to destroy the view and removing it from the app again.
Again, this doesn't feel like a best practice, but it works. Feel free to comment on this if someone have a better solution.
Don't know if you are using the Bootstrap Modal script or which one, but if you are, this question has a proposed solution. Haven't figured out all the pieces myself yet, but is looking for a similar type of solution myself to be able to use Colorbox in an "Ember best practices"-compliant way.
I'm in the middle of updating my current application to 1.0pre from 0.9.8 and I'm experimenting something I can't understand very well.
One of my templates has a containerview bound to a property:
...
{{view Ember.ContainerView currentViewBinding="oTabPanelFrame"}}
...
And then I changed the container's view by code doing something like this (code executed by an observer):
...
if (sender[key])
this.set('oTabPanelFrame', sender.get('oFrameView'));
...
Since 1.0pre this works only one time, the second time it tries to set the view, its state changed to 'destroyed' (in 0.9.8 was always 'preRender') and then it's not shown anymore.
So, this is what happens:
User clicks on an UI and it shows view A properly.
User clicks on other UI item and it shows view B properly as well.
User clicks on first UI item and it tries to show view A, but it doesn't. After diving into the view properties, I realized its state changed to 'destroyed'.
Is this a normal behavior?I've checked the 1.0pre changelog and different posts related to view states, but I can't see anything similar.
Thanks in advance!
P.S.: I think code is not relevant here, if you want me to publish more code let me know.
Well, I don't know what exactly appends between 0.9.8 and 1.0-pre, but I think some work has been done in order to avoid memory leaks. Then as you said, it seems that the views are destroyed when removed from the containerView.
The solution I've found is to create the view each time you need it. I think this is not the optimal solution. I will try do do it better, especially by using the childViews property of the Ember.ContainerView.
Here is the implementation very close to yours:
Test = Ember.Application.create();
Test.TabPanel = Ember.View.extend({
templateName: 'tabpanel',
tagName: 'span',
init: function() {
this._super();
this.set('oTabPanelFrame', Ember.View.create());
},
click1: function(event) {
event.context.set('oTabPanelFrame', event.context.get('view1').create());
},
click2: function(event) {
event.context.set('oTabPanelFrame', event.context.get('view2').create());
},
view1: Ember.View.extend({
defaultTemplate: Ember.Handlebars.compile("View1's contents")
}),
view2: Ember.View.extend({
defaultTemplate: Ember.Handlebars.compile("View2's contents")
}),
});
var oMyPanel = Test.TabPanel.create();
oMyPanel.appendTo('#content');
UPDATE
Since this does not work for you, with the 1.0 version, I think the preferred way to implement this is to use the outlets.
Here, there is a great example of how it's implemented: emberjs - how to mark active menu item using router infrastructure
I've modified the fiddle to show you how to keep the state when switching views. In fact, when the view are 'connected', they are also instanciated each time. But the state should be persisted in the controllers or the models.
Here is the fiddle: http://jsfiddle.net/Sly7/z8ssG/11/