Scenario: An autocomplete widget with its own controller and view so that it is a modular component which can be rendered into any other view with {{render "autocomplete"}} or possibly the new {{control}} helper once it is moved out of experimental stage. In order to make the autocomplete independent of everything else, it should not be aware of the parent controller or what action occurs when an item is selected.
I have been trying to figure out how to use the Ember.Evented mixin so that I can trigger an event from the autocomplete such as itemSelected which can then bubble up and be caught by the parent view or controller.
So as always, the Ember documentation is lacking. Especially when it comes to Ember.Evented. (To be fair the documentation has come a long way, but I still want more. They say Convention over Configuration without providing conventions!) Anyways, enough complaining, the documentation shows ember evented working on generic objects and I found this post which goes into a little more depth: Does EmberJs support publish/subscriber eventing pattern? but I think a more real-world example with controllers, views, routes, and components like autocompletes would be great.
I thought these types of events were suppose to be the responsibility of the views but that didn't work so I put the mixin on the controllers as well. This will make sense when you see the jsFiddle
In it you will see there is a fake autocomplete controller which just gets a static set of content from a fixture and shows two buttons for each item which you can click to trigger an event from the view or the controller. In a real example the input in the text box would be observed and the content would be updated to present different suggestions based on the input but that's not important for this example.
jsFiddle: http://jsfiddle.net/wCfb9/
I'm most suspicious of these lines: this.on("myEvent", this.addItem);
since the function that triggers the event is clearly being called, but it's as if the event is not propagating or the index controller and view are not actually setup properly to respond to the event.
Here are the main questions:
What do I need to change so that the index controller can respond to an event triggered by the autocomplete?
What is the most MVC appropriate way to acheive this? Should triggering / receiving be on the view / controller?
Is there a better way to do this?
Side note:
My current solution for this is to add a needs: ["index"]
in the autocomplete controller and instead of triggering events, I am just explicitly calling the method in the index controller from the autocomplete controller. This has many problems, first it's a tight coupling between the controllers, and doesn't scale well if the autocomplete is suppose to affect multiple controllers or be re-used elsewhere and has to be reconfigured etc.
Hope I explained that clearly enough. All help is appreciated.
Wow, great question.
In order to make the autocomplete independent of everything else, it should not be aware of the parent controller or what action occurs when an item is selected.
Yes I think you are on the right track in trying to keep things as isolated as possible.
Is there a better way to do this?
I think yes the way to go is using the new 'components' feature that was introduced in RC6. See Ember Component API Docs for details.
With the component approach, you can meet the goal of having autocomplete independent of parent controller and what happens when an item gets selected. It's pretty close to how the built-in Ember.Select view operates. So for example:
{{app-autocomplete items=model selectedItem=selectedPerson}}
Here i've set the autocomplete's items and selectedItem properties to bind to values on the current controller. app-autocomplete doesn't know what is on the other side of those bindings or how the caller will react when the values change. Your index controller can then observe it's own selectedPerson property and react when it changes. Something like this:
App.IndexController = Ember.ArrayController.extend({
selectedPerson: null,
selectedPersonDidChange: function() {
Ember.Logger.log("Index.contoller addItem: ", this.get('selectedPerson').toString());
}.observes('selectedPerson'),
});
The component is really simple as well. It expects it's items
property to contain an array of objects with a name
and when one is clicked it sets it's own selectedItem
property.
<script type="text/x-handlebars" id="components/app-autocomplete">
{{input}}
{{#each item in items}}
<li><button {{action itemSelected item}}>{{item.name}}</button></li>
{{/each}}
</script>
App.AppAutocompleteComponent = Ember.Component.extend({
itemSelected: function (item) {
this.set("selectedItem", item);
Ember.Logger.log("component.itemSelected: myEvent triggered wit h: ", item.toString());
}
});
Full example here: http://jsfiddle.net/dKUf4/
Based on Mike Grassotti's suggestion we're thinking Ember.Evented is not actually the right solution and began investigating {{control}} and Ember.Component.
I'm still unsure of the jsFiddle link he posted, but I had enough information from his post and created a jsBin to simulate both the attempt at using {{control}} and at using Ember.Component. There are pros and cons to each but in the end, Ember.Component won because the {{control}} did not bind properly.
See: http://jsbin.com/ehokak/3/ ( I couldn't figure out how to set the ENV.EXPERIMENTAL_CONTROL_HELPER in jsFiddle which is why I used jsBin and I ended up liking jsBin more... )
In it you will see two autocompletes. One implemented with the {{control}} helper which uses an AutocompleteController and AutocompleteView and one with the Ember.Component as Mike Grassotti suggested.
#1 Using the {{control}} helper:
Pros: I like the idea of having a controller for the autocomplete becuase I think it's a better separation of concerns / single responsibility . The Autocomplete controller should know how process the text in the input box, how to send a request, and parse the data if needed.
Cons: The first issue I had was that the control helper seems to require a model. However, the index controller should not care what type of model the autocomplete uses, all it should know is that it will receive the selected item on the property we choose to bind. See: controlItemSelectedBinding="itemSelected1"
Second issue which was a show stopper is that it seems the control helper's bindings do not work as expected. Because of this, the Index Controller never observed the change on itemSelected1. I was confused about how to write bindings on the control helper since they are different on components. Some use *Binding suffix, quotes on values, etc and I tried all the combinations and none of them worked. I'm not sure if it's a bug or a feature, but it's almost like the bindings are one-way.
#2 Using Ember.Component:
Pros: It actually worked! As you can see, there is some dummy code to update the content when you type into the input box, and when you click a button next to the item, it sets the local componentItemSelected
variable and because this is bound to the IndexControllers.itemSelected2, the change is observed and the index controller can take appropriate action.
Cons: I don't like the idea of the component being responsible for working with models etc since it's my understanding components are supposed to be extensions of views, but considering there is no practical alternative this is the winner by default.
Again, big thanks to Mike for the tip about using Ember.Component.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With