Get references to EmberJS views created for a given model object? - ember.js

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

Related

Call custom function when view is rendered, from one place and not repeating code for every view

in ember application i want to call my custom function which does some modifications of dom elements, the only solution i found is to repeat below code as many times as many views/routes i have
for example
rendering indexView
indexView = Ember.View.Extend({
didInsertElement:function(){
//my custom function call goes here.. myFunction();
}
});
rendering OtherView
OtherView = Ember.View.Extend({
didInsertElement:function(){
//my custom function call goes here.. myFunction();
}
});
rendering MoreView
MoreView = Ember.View.Extend({
didInsertElement:function(){
//my custom function call goes here.. myFunction();
}
});
Is there a way of calling myfunction globaly whenever any view is rendered? I really do not want to repeat code for every single view i render.
thanks!
You can create a Mixin:
App.SomeMixin = Ember.Mixin.create({
didInsertElement: function() {
this._super();
//do your common stuff here
}
});
And use it in your views:
App.SomeView = Ember.View.Extend(App.SomeMixin, {
didInsertElement: function() {
this._super();
//do your custom stuff here
}
});
I would use a mixin to do this. If, however, you find that you are using this mixin into every single view that you create, it might be better to reopen the Ember.View class and add this functionality there. Also,if you reopened the class, what you could do is, depending upon the use case, create a static function inside Ember.View.reopenClass which you would pass to a
Ember.run.scheduleOnce()
utility that ember provides so that, if the function that you need doesn't need any state (for example, just does something to the page after the content has loaded or something.), it will just run the function once after all the views are rendered in the page.
There are two ways to handle this. Create a base class, or reopen the global View class and insert your code. I prefer the first, since it's easier to maintain and track down issues.
Reopen the View class and tack on an additional method that triggers on didInsertElement
Ember.View.reopen({
doStuff:function(){
//myFunction();
}.on('didInsertElement')
});
In the case of debouncing
Ember.View.reopen({
doStuff:function(){
Ember.run.debounce(this.doRealStuff, 200);
}.on('didInsertElement'),
doRealStuff: function(){
console.log('foo');
}
});
http://emberjs.jsbin.com/gecoziwe/1/edit
Create a base class and extend from it (My preferred method, since it's easier to track down issues, can't say this enough)
App.BaseView = Ember.View.extend({
doStuff:function(){
//myFunction();
}.on('didInsertElement')
});
App.FooView = App.BaseView.extend({
});
App.BarView = App.BaseView.extend({
});

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 call method of a component from a controller

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

Ember JS: templates, views, and components

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);
})
}
});
}
});

Index view not refreshing after receiving updated data from backend

I am testing my application, so I am doing the following:
I show an index view (#/locators/index), of Locator objects, which I initially load with App.Locator.find();
I modify the backend manually
Manually (with a button/action) I trigger a refresh of the data in the ember frontend, without changing the route. I do this with App.Locator.find().then(function(recordArray) {recordArray.update();});. I see via console logging that a list request is sent to the backend, and that the up-to-date data is received. I assume this is used to update the store.
BUT: The view does not update itself to show this new data
Why does the view not get automatically updated when the store receives new data? Isn't that the whole point of the data binding in Ember?
If I now do the following:
Open any other route
Go back to the locators index route (#/locators/index)
Ember sends a new request to list the locators
The index view is shown, with the correct data (since it was already in the store?)
New data is received
(I am not 100% sure that 4 and 5 happen in that order, but I am quite certain)
So, my impression is that the data is properly updated in the store, but that somehow a full re-rendering of the view is needed to display this new data, for example by leaving and re-entering the route. Is this true? Can I force this re-rendering programmatically?
Ember changes view data when the underlying model is changed by the controller(Which is binded to the view)
(Only when the state of the application changes(url changes) router hooks are called)
Your problem could be solved when you do this.refesh() inside your route by capturing the action triggered by your view.
App.IndexRoute = Ember.Route.extend({
actions: {
dataChanged: function() {
this.refresh();
}
},
//rest of your code goes here
});
for this to work your handlebar template which modifies the data shoud have an action called dataChanged
example :
Assume this action is responsible for changing/modifying/deleting the underlying data
<button {{action 'dataChanged'}}> Change Data </button>
Refresh method actually does a model refresh and passes it to the corresponding controller which indeed changes the view.
There a couple of things that come to mind you could try:
If you are inside of an ArrayController force the content to be replaced with the new data:
this.replaceContent(0, recordArray.get('length'), recordArray);
Or try to call reload on every single record trough looping the recordArray:
App.Locator.find().then(function(recordArray) {
recordArray.forEach(function(index, record) {
record.reload();
}
}
And if the second approach works, you could also override the didLoad hook in your model class without having to loop over them one by one:
App.Locator = DS.Model.extend({
...
didLoad: function(){
this.reload();
}
});
If this works and you need this behaviour in more model classes consider creating a general mixin to use in more model classes:
App.AutoReloadMixin = Ember.Mixin.create({
didLoad: function() {
this._super();
this.reload();
}
});
App.Locator = DS.Model.extend(App.AutoReloadMixin, {
...
});
App.Phone = DS.Model.extend(App.AutoReloadMixin, {
...
});
Update in response to your answer
Handlebars.registerHelper is not binding aware, I'm sure this was causing your binding not to fire. You should have used Handlebars.registerBoundHelper or simply Handlebars.helper which is equivalent:
Handlebars.helper('grayOutIfUndef', function(property, txt_if_not_def) {
...
});
Hope this helps.
Somehow this seems to be due to the fact that I am using custom handlebar helpers, like the following:
Handlebars.registerHelper('grayOutIfUndef', function(property, txt_if_not_def) {
// HANDLEBARS passes a context object in txt_if_not_def if we do not give a default value
if (typeof txt_if_not_def !== 'string') { txt_if_not_def = DEFAULT_UNDEFINED_STR; }
// If property is not defined, we return the grayed out txt_if_not_def
var value = Ember.Handlebars.get(this, property);
if (!value) { value = App.grayOut(txt_if_not_def); }
return new Handlebars.SafeString(value);
});
Which I have been using like this:
{{grayOutIfUndef formattedStartnode}
Now I have moved to a view:
{{view App.NodeIconView nodeIdBinding="outputs.startnode"}}
Which is implemented like this:
App.NodeIconView = Ember.View.extend({
render: function(buffer) {
var nodeId = this.get('nodeId'), node, html;
if (nodeId) {
node = App.getNode(nodeId);
}
if (node) {
html = App.formattedLabel.call(node, true);
} else {
html = App.grayOut(UNDEFINED_NODE_NAME);
}
return buffer.push(html);
}
});
I am not sure why, but it seems the use of the custom handlebars helper breaks the property binding mechanism (maybe my implementation was wrong)