Closed rbiggs closed 7 years ago
@rbiggs Can you provide a concrete example?
It would be nice if there was some way to delegate the events off of the view root.
Like attaching an onclick to the window and reuse the function for doing some computation? Similar to how choo and HyperApp handle routing/anchors.
In all your examples you have events created directly on elements in your hyperx code.
That's just for conciseness.
Does this make sense?
const clickHandler = msg => e => msg.text(e.target.textContent)
app({
model: 0,
update: {
text: (_, data) => data
},
view: (model, msg) => html`
<div>
<h1>${model}</h1>
<button onclick=${clickHandler(msg)}>1</button>
<button onclick=${clickHandler(msg)}>2</button>
<button onclick=${clickHandler(msg)}>3</button>
<button onclick=${clickHandler(msg)}>4</button>
</div>
`
})
Certainly. Say you have a list, and each list item hold several actionable buttons for different purposes, whatever that might be (add, delete, selector, etc.). Adding events for each button in each list item on a long list results in sluggish behavior, especially on mobile. Delegating the event means attaching it to the parent element, in our case the UL and capturing the event target and seeing if it is the intended button or or not. If it is, fire the callback.
Below is how you delegate an event in vanilla JavaScript:
function delegate(element, event, selector, callback) {
const delegateEl = document.querySelector(element)
const eventListener = function(e) {
let el = e.target
const els = Array.prototype.slice.apply(delegateEl.querySelectorAll(selector))
do {
els.forEach((elem) => {
if (el === elem) {
callback.call(elem, e)
return;
}
})
} while (el = el.parentNode)
}
delegateEl.addEventListener(event, eventListener)
return eventListener
}
Then you would delegate your list item button like this:
const buttonHandler = function(e) {
const button = e.delegateTarget;
if(!button.classList.contains("selected"))
button.classList.add("selected")
else
button.classList.remove("selected")
};
const list = document.querySelector('ul')
/**
* Delegate button events onto list:
*/
delegate('ul', 'click', buttonHandler)
This lets you add or remove list items without worrying about removing the events from the buttons or any other child element events you might have registered. All events get registered on the view's root. Then if you want to destroy the view, you have only one element to remove events from.
See, that's the problem I'm talking about. You have onclick=${clickHandler(msg)}
on each button. Where event delegation would have the click event on the parent div and check the event target to see if the user clicked on a button. If the event target was the button, it would handle the event. If the user just clicked between the buttons directly on the div, nothing would happen.
You example is simple. Imagine you have a list with 300 items. You want your items to be interactive, the user clicks to select, or whatever. Registering an event on each list item is inefficient. Delegation solves this by registering the event on the list itself and listens for the lick on the list items. One event to rule them all.
@rbiggs Your delegate function third argument is selector
, but you are passing the buttonHandler.
Did you mean
delegate('ul', 'click', list, buttonHandler)
?
@rbiggs What's delegateTarget inside buttonHandler?
@rbiggs Figured it out. Here's a working CodePen.
function delegate(element, eventName, selector, callback) {
const delegateEl = document.querySelector(element)
const eventListener = function(e) {
let el = e.target
let els = delegateEl.querySelectorAll(selector)
do {
els.forEach((elem) => {
if (el === elem) {
e.delegateTarget = el
callback.call(elem, e)
return;
}
})
} while (el = el.parentNode)
}
delegateEl.addEventListener(eventName, eventListener)
return eventListener
}
const buttonHandler = function(e) {
const button = e.delegateTarget;
if(!button.classList.contains("selected")) {
button.classList.add("selected")
} else {
button.classList.remove("selected")
}
};
delegate('ul', 'click', "li", buttonHandler)
And here's some HTML to go with it.
<ul>
<li class="selected">One</li>
<li class="selected">Two</li>
<li class="selected">Three</li>
<li class="selected">Four</li>
<li class="selected">Five</li>
</ul>
And some CSS.
.selected {
background-color: lightblue;
width: 5%;
}
Now that I understand the issue. Let me ask you, what do you think HyperApp should do in this case? and how would the API look like?
How can we accomplish this in a functional manner?
app({
model: [
{ text: "One", selected: false},
{ text: "Two", selected: false},
{ text: "Three", selected: false}
],
view: model => {
const clickHandler = e => {
// detect list item and check if the class is selected
// dispatch a "changeText" action to update model
}
return html`
<ul onclick=${clickHandler}>
${model.map(i => html`
<li class=${model.selected ? "selected" : ""}>${i.text}</li>
`)}
</ul>
`
}
})
Are you sure that Codepen example is working?
For implementing, when the user puts an event such as onClick=${whatever}
on a view's element, you would delegate that automatically to the view's root. Vue, React, Angular and most other frameworks already do this. But since you're using Hyperx underneath to create the DOM nodes, diff, patch, etc. I'm not sure how feasible this is. I already opened an issue on the Hyperx repository because I could see how they would delegate events. If Hyperx can and doesn't want to add it, I what I would do is parse the template literal first before passing it to Hyperx, check for events, if events, remove from string and create delegate event on view root, then pass cleaned script to Hyperx. That unfortunately would mean you would have to create your own html
function as the intermediary to the Hyperx one. :-/.
You should probably wait and see how Hyperx people respond to my question about event delegation. It is Friday night, so it will probably take a few days.
Actually in my own code I have a separate function for wiring up events on views. It's a method the takes a array so you can register multiple events:
const myView = View({
element: 'ul',
template: `<li>${person.name.first} ${person.name.last}</li>`,
events: [
// Register event on list:
{
element: 'ul',
event: 'click',
callback: function(e) {
alert('You clicked the list!')
}
},
// Delegate event for list items:
{
element: 'li',
event: 'mousedown'
callback: function(e) {
alert(this.textContent)
}
}
]
});
I'm not saying that's how you SHOULD do, this is the pattern I've been using for my stuff just to give you some idea. Elm does this under the covers for you. Not quite sure how Choo does it. Using my pattern, I can bind events for any child elements of the view. I can add and delete child elements without worrying about their events. I only handle removing events if I need to destroy the view itself.
@rbiggs How would the API look like? I mean, could you sketch some pseudo code?
I'm willing to sacrifice readability to some extent for performance, but I'd never trade user friendliness for anything.
Btw choo uses hyperx too, so I don't think they do anything.
Yeah, I'm getting a bit groggy. So tomorrow. I need to think a bit about your current API and how that would fit best. I like the pattern, pretty much Elm Architecture in JavaScript. And you kept it simple. Choo is kind of a lot of things going on underneath do to too many dependencies. You don't want to introduce anything too soon so that you're stuck with it. I'm really interested to see what the Hyperx guys say. If they have a way to delegate events, then you can just use that.
Thanks @rbiggs! I really hope they get back at us though!
as a counter point, Vue doesn't use delegation: https://forum.vuejs.org/t/is-event-delegation-necessary/3701
Well, delegation has two main advantages: one is practical - it saves you from having to add (and remove!!) those listeners individually. But Vue already does that for you.
The other one is performance / memory. But since every click listener in a v-vor loop would use the same callback, this is minimal unless you have hundreds or thousands of rows.
OK, so I set up some tests adding and deleting hundreds of events with list items and watch the memory usage in Chrome Dev Tools and Safari Web Inspector. I didn't see the disaster I was expecting. Memory usage stayed pretty flat over time. As such I'm gonna say this isn't a big problem for the current implementation.
Going to close here since there's nothing actionable. Thanks @rbiggs.
V1 and V2 share a common event proxy now.
In all your examples you have events created directly on elements in your hyperx code. This is fine for simple examples. But if you have a situation where you are creating a complex UI with many interactive elements, attaching events to every single one is inefficient, and eats up memory. It would be nice if there was some way to delegate the events off of the view root. Maybe this is a problem due to using hyperx, since I don't see event delegation as possible in that framework either. Luckily Elm itself does have a solution for event delegation.