3

I am creating an Ember Component, and I need to bind the scroll event to a div that is dynamically created.

My handlebars code looks like this:

<div class="searchbarContainer" tabindex="0" bubbles=false>
    {{input type="search" name="searchFor" class="searchTextField" placeholder="Search..." value=searchKey}}

    {{#if searchKeyNotNull}}
    <div class="searchResultsContainer box-shadow" {{bind-attr id="searchBarID"}}>
        {{!-- BINDS ID TO searchBarID for searchResultsContainer--}}
            {{#if noResults}} {{!-- Then say : "No Results. " --}}
                      <div class="applicantsName noResults">
                        No Results found.
                      </div>      
            {{else}}
                {{!-- For each Row, include visual elements. --}} 
                {{#each toSearch }}
                    <div> ... </div>
                {{/each}}
            <div class='endOfResults'>
                End of Results
            </div> 
            {{/if}}
    </div>
    {{/if}}
</div>

The logic is set so that according to what is entered in the input part of this, searchKeyNotNull is updated to 'true' or 'false', as is noResults.

As it functions now, the div searchResultsContainer becomes populated with divs that contain the results, and there is a max height. I have also enabled overflow, so the user can scroll through the results.

I need to bind a scroll event to this div so that when it reaches the end, an action is fired.

Now, here's the event code I started with:

        $('#searchBarID').bind('scroll',function(){
        if($(this).scrollTop() + $(this).innerHeight()>=$(this)[0].scrollHeight)
        {
          alert('end reached');
        }
      });

Which works. Unfortunately, when this snippet is run, the div has not been created yet, so this event is never bound. I also tried to use the jQuery.on function, but it seems that scroll doesn't bubble, so

        $('body').on('scroll','#searchBarID', function(){
        if($(this).scrollTop() + $(this).innerHeight()>=$(this)[0].scrollHeight)
        {
          alert('end reached');
        }
      });

does not work.

I am at my wit's end trying to find a workaround for this; I need to either: a) find a way to bind scroll to a dynamically added element, or b) find out when Ember creates elements so I can insert this bind code right then.

A solution that does either of these two would help me greatly!

Darshan
  • 937
  • 3
  • 15
  • 27
  • Check out the didInsertElement hook: http://stackoverflow.com/questions/8881040/using-ember-js-how-do-i-run-some-js-after-a-view-is-rendered – Oliver Jul 25 '14 at 18:40
  • What causes the element with the id searchBarID to be added to the DOM? I'm not familiar with Ember, but given a similar problem, I would try to attach a callback to whatever creates searchBarID so that the event binding is sure to follow the creation of the DOM node. – J E Carter II Jul 25 '14 at 18:45
  • I definitely thought of going that route: unfortunately I couldn't find good docs on when and how DOM elements are created. Thanks! – Darshan Jul 25 '14 at 20:25
  • Oliver, I did check out the hook and it definitely helps clear up other questions I hadn't asked yet, but for this particular case, the didInsertElement only fires once, and DOM elements are re-created multiple times after it. So unfortunately it doesn't work for this case. – Darshan Jul 25 '14 at 20:26
  • @Darshan the `didInsertElement` hook is definitely what you are looking for. You just need to modify your template a bit. – Matthew Blancarte Jul 25 '14 at 23:54
  • @MatthewBlancarte, after stubbornly trying to make it such that .on and scroll applied to dynamic elements - and running into unnecessary bugs - I agree with you: the best thing to do is undoubtedly what you and jordy suggested. I will look into it and update as soon as I see progress. – Darshan Jul 27 '14 at 14:23

3 Answers3

1

I countered this issue a couple of times. I find it easiest hiding the element instead of placing it an a if block.

You could do so by using expanding your bind-attr

<div class="searchResultsContainer box-shadow" {{bind-attr id="searchBarID" class="searchKeyNotNull::hidden"}}>

Where is hidden is a class that will set the display to none. This will cause the element to renderd only once, will allows you to use the didInsertElement hook like so:

onDidInsert: function () {
    var element = $('#searchBarID', this.$());
    element.on('scroll', function () {
        // your impl.
    })
}.on('didInsertElement')

in your component.

The reason I use the bind-attr to hide the element (instead of not rendering it!) is because ember will create a new element every time the searchKeyNotNull changes. The didInsertElement hook will only trigger once however.

Jordy Langen
  • 3,591
  • 4
  • 23
  • 32
  • Hm. Definitely a solid suggestion: something seems to be breaking when I have it created first, then tampered with. I'll update this comment as soon as I can confirm it's working. – Darshan Jul 25 '14 at 20:24
0

scroll, load, error events not bubble up(see: https://developer.mozilla.org/en-US/docs/Web/Events/scroll), therefore you must update handlers on adding scrollable element.

maximkou
  • 5,252
  • 1
  • 20
  • 41
  • I am aware that it doesn't bubble; that's why I'm asking if, potentially, there is a way to bind scroll when the element is created and for that I would need to know when and how Ember creates DOM elements. I wonder if someone could provide me with an answer to this. – Darshan Jul 25 '14 at 20:23
0

Most of the time there is a way to do something you normally do in jQuery with Ember view hooks, actions, and events.

An alternate approach would be to wrap that DOM node in a {{#view}} and then make use of the didInsertElement hook. For example, let's wrap it in {{#view App.SearchResultsView}}:

<div class="searchbarContainer" tabindex="0" bubbles=false>
    {{input type="search" name="searchFor" class="searchTextField" placeholder="Search..." value=searchKey}}

    {{#if searchKeyNotNull}}
      {{#view App.SearchResultsView}}
        <div class="searchResultsContainer box-shadow">
          <!-- MORE HBS HERE -->
        </div>
      {{/view}}
    {{/if}}
</div>

Now, let's define App.SearchResultsView:

App.SearchResultsView = Ember.View.extend({
  didInsertElement: function () {
    this.$('.searchResultsContainer').on('scroll', function (e) {
      // scrolling...
    });
  }
});

Hope that helps! Let me know if I can further explain anything.

Matthew Blancarte
  • 8,251
  • 2
  • 25
  • 34
  • whoa whoa whoa wait: I'm relatively new to Ember but are you telling me that I can actually associate a component with a view? I mean if so - whoa - every time the dom element changes, the view is recreated, and every time the view is recreated, the didInsertElement is fired, and therefore every time I need the scroll bound, it gets bound right up there. I have got to try this route asap. Will update soon after. – Darshan Jul 27 '14 at 14:25
  • @Darshan That is precisely what I am telling you. :D Pretty easy pattern to follow, right? Any time a view is rendered, it receives a unique id. Thus, no worries about the bindings. – Matthew Blancarte Jul 27 '14 at 18:29
  • ahhh- i've run into an issue. It seems that once it is inside the view, the view can no longer access methods defined in SearchBarComponent. So although the scroll binding works, since I cannot access the logic part of component, I cannot call the function I need. Unless I'm missing something here... – Darshan Jul 28 '14 at 17:15
  • Yeah, you are missing some things. :) You can access the current controller from both the component and view... `this.get('controller')` and you can use the `Ember.Evented` as a mixin with that controller to communicate as needed. You can have the view scroll event call a controller method that fires something in the view. http://emberjs.com/api/classes/Ember.Evented.html Let your controller do the "thinking" and just have your views send it messages. Hope that makes sense. – Matthew Blancarte Jul 28 '14 at 18:26
  • @Darshan So in your component, you could do something like `this.get('controller').on('searchTriggerFired', this.doSomethingThatViewToldMeToDoThroughTheController);` – Matthew Blancarte Jul 28 '14 at 18:28
  • I did try that route - however in my unfortunate case, it is a component that has access to the store (that access is passed in) and so the action is in App.SearchBarComponent as opposed to an array/object controller. I tried passing in controller: 'searchBarComponent', but I suspect that it looks for controllers using naming conventions. Hmm... I've implemented Jordy's way for now, but I want to explore the view method. Any ideas? My apologies if this is dragging on. – Darshan Jul 28 '14 at 18:30
  • @Darshan Huh? Components are custom-tag views... I think you may be misunderstanding me. Your view and component can both access `this.get('controller')`. Try it. `console.log(this.get('controller').toString())`. – Matthew Blancarte Jul 28 '14 at 18:33