Attaching jquery events to bound items


#1

Has anyone had problems attaching event handlers to bound data items?

For instance, simple example below:

# hbs
{{#each}}
  <li>{{name}}</li>
{{/each}}
<li>this one works</li>

# View
didInsertElement: function ()  {
  this.$('li').click(function (e) { alert('hi') })
}

Does not trigger an alert when the selected item is a bound item (inside an #each block) but an unbound item works fine.


#2

There might be something else going on, because I got it working here. Maybe try the newer preferred method of using didInsertElement-

#View 
setupListItems: function(){
  this.$('li').click(function(e){alert('stuff');}); 
}.on('didInsertElement')

#3

Probably you are loading data via ajax, so initially the {{each}} helper is empty, because the data isn’t fetched yet. After the view is inserted, doesn’t have any item, this.$('li').length == 0, because the ajax is pending. So no events are added. The ajax finish, the {{each}} update because of data binding, but the didInsertElement doesn’t trigger again.

You can use the afterRender queue and schedule a function to be triggered when the rendering is finished:

# hbs
{{#each}}
  <li>{{name}}</li>
{{/each}}
<li>this one works</li>
# View
didInsertElement: function ()  {
  Ember.run.scheduleOnce('afterRender', this, function() {
    this.$('li').click(function (e) { alert('hi') })
  });  
}

Problems using 3rd party plugins alongside ember
#4

Thanks - good to know of this preferred syntax.


#5

Interesting. can you explain the need for Ember.run here…


#6

I believe the didInsertElement function runs synchronously, so if your data is async, it’s possible for the code in the didInsertElement function to run before the li elements within the each block even exist. Scheduling the code to run after the render ensures it runs after the data has been loaded and rendered.


#7

@matthooks explained above :smile:

I found a more simple approach (no run loops), instead of attach the event in each li, just create a custom class for each element using the itemViewClass option. And in that custom view, listen the click event overriding the click method. Something like this:

{{each tagName="ul" itemViewClass="App.ItemView"}} 

App.ItemView = Ember.View.extend({
    tagName: 'li',
    template: Ember.Handlebars.compile('{{firstName}} {{lastName}}'),
    click: function() {
        alert('Hi ' + this.$().text());
    }
})

I updated here


#8

I’m betting this was a simple case presented to help figure out something with instantiating a jquery plugin.


#9

Thanks for this. I’ll try it.


#11

@trabus That’s right - I was having trouble instantiating the Sly.js (carousel) jquery plugin : $(’#element’).sly()

@matthooks Interesting - I would expect didinsertelement to run after the route’s model promise has fulfilled. i’ll keep this in mind.


#12

On a related note, does anyone see any problems with this approach? http://madhatted.com/2013/6/8/lifecycle-hooks-in-ember-js-views

Notice the use of ‘willClearRender’ to clean up custom event handlers… agree this is a good practice?

I’m looking for best practices to create event handlers and then clean up.


How to do DOM cleanup?
#13

Yes this is a good pratice, because ember doesn’t know about events added by this.$().on(...), so it need to be removed manually, when the view is destroyed.

The typical setup to integrate jquery events in ember, is the following :

App.MyCustomView = Ember.View.extend({
  myEventHandler: function() {
    // events trigereds out of the ember context, like jquery plugins,
    // will run out of the runloop, so the Ember.run is needed
    Ember.run(this, function() {
      // do someting
    })
  },
  // the view is ready, and the template is rendered
  didInsertElement: function() {
    // atach the custom event
    this.$().on('someEvent', this.myEventHandler);
  },
  // view will be removed
  willClearRender: function() {
    // remove the custom event 
    this.$().off('someEvent');
  }
})

#14

(Code review request) … Related to the above pattern for incorporating jquery plugins, here’s what I’m doing to attach a plugin on domready. Is it a best practice to attach domready handlers in didInsertElement like this? Also, I’m running the event handler code in Ember.run, although in this case, I’m not sure it’s needed. Any thoughts on better ways to do this?

App.ApplicationView = Ember.View.extend({
idleTarget: Ember.$(document),
addIdleTimer: function() {
  // events triggered out of the ember context, like jquery plugins,
  // will run out of the runloop, so the Ember.run is needed
  Ember.run(this, function() {
    var doc = this.idleTarget,
      self = this;

    doc.idleTimer(App.SETTINGS.idleTimeout);
    doc.on("idle.idleTimer", function logOff(){
      // function you want to fire when the user goes idle
      App.Util.logger.debug("timed out");
      self.get("controller.target").transitionTo("logout");
      doc.idleTimer("destroy");
    });
  });
},
// the view is ready, and the template is rendered
didInsertElement: function() {
  // attach the custom event
  var self = this;
  Ember.$(function attachIdleTimer() {
    self.addIdleTimer();
  });
},
// view will be removed
willClearRender: function() {
  // remove the custom event
  // Prob not needed here b/c the setup also destroys
  // but just good housekeeping 
  this.idleTarget.idleTimer("destroy");
}
});

#15

There is no need to use jquery ready on didInsertElement because ApplicationView and all others views will just start to render and call didInsertElement after the document load event.

The place where you need to manually create a run loop (put an Ember.run) is doc.on("idle.idleTimer" like the following:

App.ApplicationView = Ember.View.extend({
  idleTarget: Ember.$(document),
  addIdleTimer: function() {  
    var doc = this.idleTarget, self = this;
    doc.idleTimer(App.SETTINGS.idleTimeout);
    doc.on("idle.idleTimer", function logOff() {
        // the Ember.run is needed here because ember doesn't 
        // know about events manually attached with elem.on(eventName, func)
        Ember.run(function() {
          App.Util.logger.debug("timed out");
          self.get("controller.target").transitionTo("logout");
          doc.idleTimer("destroy");          
        })
      });
  },  
  didInsertElement: function() {    
    this.addIdleTimer();
  },  
  willClearRender: function() {    
    this.idleTarget.idleTimer("destroy");
  }
});

Because the on method attaches an event handler in the document object, and ember doesn’t know about it.


#16

@marcioj Thanks for the review. So do we need the outer Ember.run or just the one you added?

The Ember runloop still eludes me, except for the general notion that it keeps the bound data in sync with the views. Can you recommend a good resource online to explain the inner workings?


#17

Hi @marcioj - just checking if you’ve had time to review (again). I think I almost got it but am wondering if Ember.run should be outside or within the doc.on event binding. Here, we have it in both places - any benefit or need for that?

Thanks again!


#18

You’re right, it isn’t needed the outer runloop, sorry by the confusion. I updated the code.

About resources, this is some of my favorites:

Just remember that the ember runloop was extracted to a project called backburner (link above), so maybe you could find resources about it too.

There is some situations where you need to deal with the runloop:

  1. When you need to interact with the generated dom of a view, in these cases you use Ember.scheduleOnce('afterRender', ...). this post gives further info.

  2. Integrating with events from custom plugins (your case). Because ember doesn’t know about events attached via view.$(selector).on(evtName, func).

  3. ?

About the first one, I think we could hide this from the user, creating a new view callback like didRenderTemplate. So ember views would handle in that way:

if (typeof view.didRenderTemplate == "function") {
  Ember.run.scheduleOnce('afterRender', view, view.didRenderTemplate);
}

Thoughts …?

In the second, I don’t see a way to abstract from the user, but since this commit, now we can do it in a better way:

App.SomeView = Ember.View.extend({
    didInsertElement: function() {
        this.$().on('click', Ember.run.bind(function() {
            alert('hello')
        }));
    }
}); 

Feel free to reply if you have some question :smile:


#19

How do we then use $().off ? It doesn’t seem to be working for me. I’m supplying the same event, and same Ember.run.bind… etc.


#20

Nevermind.

Here’s the solution: