Ember JS: templates, views, and components - ember.js

Does anyone have a code snippet (jsfiddle, example perhaps) that puts into context the usage of templates, views and components in a single example? Looking for a practical demonstration of when and how to use to use one vs the other. Especially views and components which seem conceptually very close.
The guides suggest views when more complex event handling is required.
In particular I am interested in learning more about how you use these idiomatic approaches for better code reuse and more DRY view layer code. Especially wondering about the creation of nested view hierarchies and how to manage the event bubbling.

I have found that for 99% of the time templates are all you need. Views are when you need to interact with a template or have a UI thing that you want to re-use. As an example I created a view component for a tree view which had some complex user interaction that I needed to use in several different places in an app.
I have also used views to handle 'infinite' scrolling with the data in a template which binds the browser scroll action to a method in the view. This then triggers a method in the controller to fetch more results when the web page is scrolled to the bottom:
App.CompoundPathwaysIndexView = Ember.View.extend({
didInsertElement: function() {
var view = this;
$(window).bind("scroll", function() {
view.didScroll();
});
},
willDestroyElement: function() {
$(window).unbind("scroll");
},
didScroll: function() {
if(this.isScrolledToBottom() && !this.get('controller').get('fetching')) {
this.get('controller').set('fetching', true);
this.get('controller').send('fetchMore');
}
},
isScrolledToBottom: function() {
var documentHeight = $(document).height();
var windowHeight = $(window).height();
var top = $(document).scrollTop();
var scrollPercent = (top/(documentHeight-windowHeight)) * 100;
return scrollPercent > 99;
}
});
Other examples of views are to inject script tags in to a template after it is rendered using the didInsertElement method (since it is apparently bad practice to add these in a handlebars template).
For example, activating the bootstrap typeahead functionality on a text box:
The template:
{{input type="text" placeholder="search" value=search action="query" id="search_box" class="search-query span4"}}
The view:
App.ApplicationView = Ember.View.extend({
didInsertElement: function() {
$('#search_box').typeahead({
source: function (query, process) {
$.getJSON(typeaheadUrl, { query: query }, function (data) {
return process(data);
})
}
});
}
});

Related

Ember computed.alias in nested views

I'm trying to create a reusable generated element that can react to changing outside data. I'm doing this in an included view and using computed.alias, but this may be the wrong approach, because I can't seem to access the generic controller object at all.
http://emberjs.jsbin.com/nibuwevu/1/edit
App = Ember.Application.create();
App.AwesomeChartController = Ember.Object.extend({
data: [],
init: function() {
this.setData();
},
setData: function() {
var self = this;
// Get data from the server
self.set('data', [
{
id: 1,
complete: 50,
totoal: 100
},
{
id: 2,
complete: 70,
total: 200
}
]);
}
});
App.IndexController = Ember.Controller.extend({
needs: ['awesome_chart']
});
App.ChartView = Ember.View.extend({
tagName: 'svg',
attributeBindings: 'width height'.w(),
content: Ember.computed.alias('awesome_chart.data'),
render: function() {
var width = this.get('width'),
height = this.get('height');
var svg = d3.select('#'+this.get('elementId'));
svg.append('text')
.text('Got content, and it is ' + typeof(content))
.attr('width', width)
.attr('height', height)
.attr('x', 20)
.attr('y', 20);
}.on('didInsertElement')
});
And the HTML
<script type="text/x-handlebars">
<h2> Welcome to Ember.js</h2>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="index">
<h2>Awesome chart</h2>
{{view App.ChartView width=400 height=100}}
</script>
For what it's worth, this didn't seem to work as a component, either. Is the ApplicationController the only place for code that will be used on multiple pages? The 'needs' seems to work, but the nested view can't access it. If I make a proper Ember.Controller instance to decorate the view, that doesn't seem to work either.
Any help is much appreciated.
Update:
I can't edit my comment below, but I found a good answer on how to use related, and unrelated, models in a single route.
How to use multiple models with a single route in EmberJS / Ember Data?
Firstly, your controllers should extend ObjectController/ArrayController/Controller
App.AwesomeChartController = Ember.Controller.extend({...});
Secondly when you create a view the view takes the controller of the parent, unless explicitly defined.
{{view App.ChartView width=400 height=100 controller=controllers.awesomeChart}}
Thirdly you already had set up the needs (needed a minor tweak), but just as a reminder for those reading this, in order to access a different controller from a controller you need to specify the controller name in the needs property of that controller.
App.IndexController = Ember.Controller.extend({
needs: ['awesomeChart']
});
Fourthly from inside the view your computed alias changes to controller.data. Inside the view it no longer knows it as AwesomeChart, just as controller
content: Ember.computed.alias('controller.data')
Fifthly inside your on('init') method you need to actually get('content') before you attempt to display what it is. content doesn't live in the scope of that method.
var content = this.get('content');
http://emberjs.jsbin.com/nibuwevu/2/edit
First, AwesomeChart does sound like it's gonna be a reusable self-contained component. In which case you should better user Ember.Component instead of Ember.View (as a bonus, you get a nice helper: {{awesome-chart}}).
App.AwesomeChartComponent = Ember.Component.extend({ /* ... */ });
// instead of App.ChartView...
Second, for AwesomeChart to be truly reusable, it shouldn't be concerned with getting data or anything. Instead, it should assume that it gets its data explicitly.
To do this, you basically need to remove the "content:" line from the awesome chart component and then pass the data in the template:
{{awesome-chart content=controllers.awesomeChart.data}}
Already, it's more reusable than it was before. http://emberjs.jsbin.com/minucuqa/2/edit
But why stop there? Having a separate controller for pulling chart data is odd. This belongs to model:
App.ChartData = Ember.Object.extend();
App.ChartData.reopenClass({
fetch: function() {
return new Ember.RSVP.Promise(function(resolve) {
resolve([
{
id: 1,
complete: 50,
total: 100
},
{
id: 2,
complete: 70,
total: 200
}
]);
// or, in case of http request:
$.ajax({
url: 'someURL',
success: function(data) { resolve(data); }
});
});
}
});
And wiring up the model with the controller belongs to route:
App.IndexController = Ember.ObjectController.extend();
App.IndexRoute = Ember.Route.extend({
model: function() {
return App.ChartData.fetch();
}
});
Finally, render it this way:
{{awesome-chart content=model}}
http://emberjs.jsbin.com/minucuqa/3/edit

Ember JS Router not filtering when I change the controller from default

My router in it's entirety:
Books.Router.map(function () {
this.resource('books', { path: '/' }, function () {
this.route('search', { path: 'search/:keyword' });
});
});
Books.BooksRoute = Ember.Route.extend({
model: function(){
return this.store.find('book');
},
actions: {
postAlert: function (msg, classes) {
var postAlert = $('#alert');
postAlert.html(msg).toggleClass(classes).toggle(1000);
setTimeout(function () {
postAlert.toggle(1000, function () {
postAlert.toggleClass(classes)
});
}, 3000);
}
}
});
Books.BooksIndexRoute = Ember.Route.extend({
model: function () {
return this.modelFor('books');
},
renderTemplate: function () {
this.render({ controller: 'books' });
}
});
Books.BooksSearchRoute = Ember.Route.extend({
model: function (params) {
return this.store.filter('book', function (book) {
return book.get('titleSlug').indexOf(params.keyword) > -1;
})
},
renderTemplate: function (controller) {
this.render('books/index', { controller: controller });
}
});
Now let's focus on the last bit of the router, the BooksSearchRoute. When I leave my router as it is right now and go to the route localhost/#/search/the_adventures_of_huckleberry_finn then I will see the books/index template populated with the model where the titleSlug contained the dynamic segment which is great, exactly what I expect.
Now when I try to use an action defined in my books controller from that URL I get an error that nothing handled the action. In response to that I switched the renderTemplate line so that it uses 'books' as the controller instead of the default controller.
renderTemplate: function () {
this.render('books/index', { controller: 'books' });
}
That change allows me to access the actions in the books controller that I need. However after making the change the filter does not appear to work anymore as all of the books in the libray are displayed rather than just thouse matching the search term. Can someone please explain to me what is happening here?
That is actually the expected behaviour.
Explanation
When a Route is hit, it obtains the model and passes that to the Controller associated with that route. It determines which one using the Ember naming conventions.
Override template
What you have done here is override renderTemplate here to specify that a different template should be used, than the one that the naming conventions tell it to use.
That works fine in this case because the model for the BooksIndexRoute and the model for the BooksSearchRoute are compatible - they are both arrays of Books.
Override template AND controller
The next thing that you did was to override renderTemplate here to specify that a different template should be used, and it should use a different controller too, BooksController, according to the naming convention.
Of course, BooksController doesn't know that you have done this, and will use the model that it is aware of, the model returned by its own Route, which is this case was this.store.find('book').
... and since that model is not filtered, the template renders the full set of Book models.
Suggested solution
You can probably continue along this path, where you override the template and controller, and refactor the required actions such that they are available on both controllers. However, I would not suggest this, as it goes against the grain of how Ember was designed, plus it will involve quite a lot of spaghetti code.
The canonical solution would involve using the routes and controllers that you already have, but do not override renderTemplate in BooksIndexRoute.
Instead, extract the code that renders your list of books into a separate template and put it into a folder called partials, then invoke that partial from both the templates:
from books (or books\index as the case may be), as well as
from books\search
The syntax looks like this
I do not know what your templates look like, but if you post them, I can show you how to do so.

How to access parent view and controller from ember eventManager

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.

Loading a 3rd party embed widget within an Ember.View

I'm loading the RebelMouse embed widget into an Ember.View, like so:
App.RebelMouseView = Em.View.extend({
didInsertElement: function() {
var widgetEmbedCode = '<script type="text/javascript" class="rebelmouse-embed-script" src="https://www.rebelmouse.com/static/js-build/embed/embed.js?site=W3portals&height=900&flexible=1"></script>';
this.$().append(widgetEmbedCode);
}
});
But I'm hoping there's a better way to do so.
One undesired side-effect is that everytime I initialize that view it reloads the entire widget with a 1 second delay. Thanks.
Ok, good idea on showing/hiding, see http://jsfiddle.net/9EC8F/ for how to do it. Basically, the trick is to keep the view outside any outlet that will be torn up when the route changes. Then, put this in your route:
activate: function() {
$(".rebel-mouse-view").show();
},
deactivate: function () {
$(".rebel-mouse-view").hide();
}
and this in your view:
classNames: ['rebel-mouse-view'],

Get references to EmberJS views created for a given model object?

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.$());
}
});