Hyperapp: Events Don't Get Removed When Re-Using An Element

Created on 13 Mar 2017  路  5Comments  路  Source: jorgebucaran/hyperapp

The Issue

Event listeners (and maybe other attributes, not sure) don't get removed when re-using an element.

Broken Code <>

const { h, app } = hyperapp
/** @jsx h */

app({
  model: true,
  actions: {
    toggle: model => !model
  },
  view: (model, actions) =>
    <div>
      {model && <div onClick={() => alert('馃悰')}>a</div>}
      <div>b</div>
      <button onClick={actions.toggle}>toggle</button>
    </div>
})

How to reproduce with the codepen?

  1. Click a. Notice how it properly alerts 馃悰
  2. Click b. Notice how it doesn't do anything
  3. Click toggle
  4. Click b. WTF?
Bug

All 5 comments

馃悰 馃悶 馃悵 馃

Bug was in removeElementData, where only attributes were being cleared. This fixes it:

  function removeElementData(element, name, value, oldValue) {
    if (name[0] === "o" && name[1] === "n") {
      element.removeEventListener(name.substr(2).toLowerCase(), oldValue)
    } else {
      element[name] = value
      element.removeAttribute(name)
    }
  }

This is very similar to what we do in setElementData to _not_ re-attached event listeners when updating and element's data, so we need to factor that into a little function.

This is why I don't like inline events. Event delegation solves this:

app({
  model: true,
  actions: {
    toggle: model => !model
  },
  view: (model, actions) =>
    <div id='parent'>
      {model && <div class='clickable'>a</div>}
      <div>b</div>
      <button onClick={actions.toggle}>toggle</button>
    </div>,

  subscriptions: [
    function() {
      // Define event on clickable parent.
      document.querySelector('#parent').addEventListener('click', function(e) {
        // Fire event when event target is "clickable".
        if (e.target.className === 'clickable') {
          alert('馃悰')
        }
      })
    }
  ]
})

With event delegation you can add and delete event targets as you please without worrying about event management.

Of course, writing event delegation from scratch every time gets annoy, so best to use a delegation helper:

function delegate(options) {
  var element = options.element
  var root = options.root || document.body
  var type = options.type
  var callback = options.callback
  if (typeof root === 'string') root = document.querySelector(root)
  var eventListener = function(e) {
    var target = e.target
    var elements
    if (element.nodeType) elements = [element]
    else elements = Array.prototype.slice.apply(root.querySelectorAll(element))
    do {
      var len = elements.length
      for (var i = 0; i < len; i++) {
        if (target === elements[i]) {
          callback.call(elements[i], e)
          break
        }
      }
    } while (target = target.parentNode)
  }
  root.addEventListener(type, eventListener)
}
app({
  model: true,
  actions: {
    toggle: model => !model
  },
  view: (model, actions) =>
    <div id='parent'>
      {model && <div class='clickable'>a</div>}
      <div>b</div>
      <button id='btn'>toggle</button>
    </div>,

  subscriptions: [
    (_, actions) => {
      // Use delegated events:
      delegate({
        root: '#parent',
        element: '.clickable',
        type: 'click',
        callback: function() {
          alert('馃悰')
        }
      })
      delegate({
        root: '#parent',
        element: '#btn',
        type: 'click',
        callback: function() {
          actions.toggle()
        }
      })
    }
  ]
})

I think is fixed in master.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dmitrykurmanov picture dmitrykurmanov  路  4Comments

jbrodriguez picture jbrodriguez  路  4Comments

jacobtipp picture jacobtipp  路  3Comments

dmitrykurmanov picture dmitrykurmanov  路  3Comments

jorgebucaran picture jorgebucaran  路  3Comments