Event listeners (and maybe other attributes, not sure) don't get removed when re-using an element.
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>
})
a. Notice how it properly alerts 馃悰b. Notice how it doesn't do anythingtoggleb. WTF?馃悰 馃悶 馃悵 馃
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.