Click event handler only working once in Backbone.js

I'm new to Backbone and am using it with jQuery in Rails. In my view's render method, I use delegateEvents to bind an event handler to the "click" event of a button with id btn-go. This button is itself rendered by the view in question.

Clicking the button stores the form's values in an array and causes the view to re-render. This button is itself rendered by the view in question. This works the first time I click the button, but nothing happens the second time, even though the view does correctly re-render its template.

class MyApp.Views.ChainsNew extends Backbone.View

    # @template is defined by a conditional inside render()

    render: (step_number) ->
        window.model = @model
        @step_number = step_number

        @template = if @step_number is 1 then JST['chains/new'] else JST['chains/step']
        $(@el).html(@template())

        @delegateEvents({
            'click #btn-go': 'add_step'
        })
        @

    add_step: ->
        #divide array into arrays of steps before and after step being edited
        steps = @model.get('steps')
        array1 = steps.slice(0, @step_number - 1)
        array2 = steps.slice(@step_number)
        array1.push(@$el.find('textarea').val())
        newArray = array2.concat(array1)

        @model.set({
            steps: newArray
        })

The view's render method is called by the router. As you can see in the code below, the router is listening to the change event on the model. This causes it to update the URL, which in turn triggers the router's step method to be called, and it's within that method that the view's render method is finally called.

class MyApp.Routers.Chains extends Backbone.Router
    routes:
        'chains/new(/)': 'new'
        'chains/new/step/:step_number(/)': 'step'

    initialize: ->
        # Model
        @model = new MyApp.Models.Chain()
        @listenTo(@model, "change", ->
            @goto_step_number @model.get('steps').length + 1
        )

        # Views
        @view_new = new MyApp.Views.ChainsNew({
            model: @model
        })


    step: (url_step_number) ->
        # Before rendering the view, make sure URL & number of steps in model are correctly related
        url_step_number = parseInt url_step_number
        steps_entered = @model.get('steps').length

        if url_step_number > steps_entered + 1
            @goto_step_number steps_entered + 1
        else
            $('#main-container').html(@view_new.render(url_step_number).el)

    new: ->
        @goto_step_number 1

    goto_step_number: (step_number) ->
        @.navigate('chains/new/step/' + step_number, trigger: true)

Why doesn't anything happen the second time I click the button? I'm guessing that the event handler hasn't been correctly bound to the button, but I have no idea why.

Answers:

Answer

Your problem is right here:

$('#main-container').html(@view_new.render(url_step_number).el)

From the fine manual:

.html( htmlString )
[...]
When .html() is used to set an element's content, any content that was in that element is completely replaced by the new content. Additionally, jQuery removes other constructs such as data and event handlers from child elements before replacing those elements with the new content.

Note the removes other constructs such as data and event handlers part. The sequence of events goes like this:

  1. You call render.
  2. render calls delegateEvents to attached a jQuery event delegator to the view's el.
  3. You call $x.html(view.el) but view.el is already there so jQuery detaches all the event bindings (including the one you just added in 2), clears out $x, and then puts view.el back into $x.

But when view.el is put back on the page, the events are already gone. This is roughly equivalent to what you're doing:

# In the view...
add_step: ->
    re_render(@step_number + 1)

and

v = new YourView
$('#main-container').append(v.render(1).el)
re_render = (step_number) ->
    $('#main-container').html(v.render(step_number).el)

Demo: http://jsfiddle.net/ambiguous/4rJyB/

You need to stop calling .html all the time. Once the view is on the page, you can simply tell it to re-render itself and that's all you need to do. So, if the view has been rendered once to get its el into #main-container, you just need to:

@view_new.render(url_step_number)

and that's it. Then you can remove the @delegateEvents call from render and use the usual events map on the view:

class MyApp.Views.ChainsNew extends Backbone.View
    events:
        'click #btn-go': 'add_step'
    render: (step_number) ->
        window.model = @model
        @step_number = step_number

        @template = if @step_number is 1 then JST['chains/new'] else JST['chains/step']
        @$el.html(@template())
        @
    #...

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.