html-delegator

1.0.1 • Public • Published

html-delegator

Decorate elements with delegated events

Motivation

When building a web application you don't want to deal with messing around with the DOM directly if you can avoid it.

What you really want to do is write application logic. Generally how application logic works is that you react to some kind of user input, run some logic and maybe change some state.

Let's take a look at a calendar example. For a calendar we just want to react to a bunch of date change events and update the state of the calendar accordingly. We don't really care what triggers the date change events.

var Calendar = function () {
    var calendarState = { ... }
    var calendarInputs = { ... }
 
    calendarInputs.dateChange(function (newDate) {
        var d = new Date(newDate)
 
        calendarState.theTime.set(d)
    })
}

The above code is basically the application logic you want to express. Now imagine we had some kind of data binding that re-rendered the template / view for the calendar each time we update the calendarState.

function render(calendarState) {
    var WEEK = 1000 * 60 * 60 * 24 * 7
    var prevWeek = +calendarState.theTime - WEEK
    var nextWeek = +calendarState.theTime + WEEK
 
    return h("div", [
        h("table", calendarGrid(calendarState)),
        h("div.controls", [
            h("button.prev", {
                "data-click": "dateChange:" + prevWeek
            }, "previous week"),
            h("button.next", {
                "data-click": "dateChange:" + nextWeek
            }, "next week")
        ])
    ])
}

Something will need to trigger the date change events, since we don't want to write any manual DOM code and since we already have a reactive templating system we should just add hooks to our templates that say when this DOM event occurs I want you to trigger this "meaningful" event in my system

Other motivations

  • solution should be a module, not a framework
  • solution should work recursively without namespace conflicts
  • solution should use event delegation, it shouldnt require binding to each DOM element manually
  • solution should allow rendering logic to be seperate from input handling logic.
  • solution doesn't require passing concrete functions to the rendering logic. The rendering and input handling most be loosely coupled.
  • solution should allow passing data to the event listener.
  • solution should discourage passing a DOM Event object to listener. You shouldn't read the ev or ev.target in your input handling code
  • library should only do input handling. Not input handling AND reactive rendering.

Example DOM

var document = require("global/document")
var Delegator = require("html-delegator")
var h = require("hyperscript")
var addEvent = require("html-delegator/add-event")
var EventSinks = require("event-sinks")
 
var delegator = Delegator(document.body)
var emitter = EventSinks(delegator.id, ["textClicked"])
var sinks = emitter.sinks
var elem = h("div.foo", [
    h("div.bar", "bar"),
    h("span.baz", "baz")
])
var bar = elem.querySelector(".bar")
var baz = elem.querySelector(".baz")
document.body.appendChild(elem)
 
 
// add individual elems.
addEvent(bar, "click", sinks.textClicked, {
  type: "bar"
})
addEvent(baz, "click", sinks.textClicked, {
  type: "baz"
})
 
emitter.on("textClicked", function (tuple) {
    var value = tuple.value
 
    console.log("doSomething", value.type)
})

Example data- attributes

var document = require("global/document")
var Delegator = require("html-delegator")
var h = require("hyperscript")
var event = require("html-delegator/event")
var EventSinks = require("event-sinks")
 
var delegator = Delegator(document.body)
var emitter = EventSinks(delegator.id, ["textClicked"])
var sinks = emitter.sinks
 
var elem = h("div.foo", [
    h("div.bar", { 
        "data-click": event(sinks.textClicked, { type: "bar" })
    }, "bar"),
    h("div.baz", {
        "data-click": event(sinks.textClicked, { type: "baz" })
    }, "baz")
])
document.body.appendChild(elem)
 
emitter.on("textClicked", function (tuple) {
    var value = tuple.value
 
    console.log("doSomething", value.type)
})

Concept.

The concept behind html-delegator is to seperate declaring which UI elements trigger well named user input events from the event handling.

For example you might have an input.js where you handle user input, based on well named, non-DOM specific events.

// input.js
module.exports = Input
 
function Input(state, emitter) {
    // when the input event todo addition occurs
    // create a fresh item and add it
    emitter.on("todoAdded", function (data, ev) {
        var value = ev.currentValue
        var todo = { title: value, completed: false }
        state.todos.push(todo)
    })
 
    // when the input event todo removal occurs
    // find the item and delete it from the list
    emitter.on("todoRemoved", function (data, ev) {
        var id = data
        var index = -1
        state.todos.some(function (todo, itemIndex) {
            if (todo.id() === id) {
                index = itemIndex
                return true
            }
        })
 
        state.todos.splice(index, 1)
    })
}

One thing to notice here is that the input handling is coupled to keypress or click events. Those are implementation details of the declarative UI. The input handling has been described in a loosely coupled way.

The only coupling is that addition is based on the current value of the UI element that triggered the todoAdded event.

Since we have defined our input handling, we now need to add the declarative event hooks to our UI.

We will use HTML and the data- attributes style interface for the delegator

// ui.html
<ul>
    <li data-id="4">
        <span class="todo-title">Some todo</span>
        <button data-click="todoRemoved:4">Remove</button>
    </li>
</ul>
<input class="add-todo" name="title" data-submit="todoAdded" />

Here we have decorated the todo item UI with a click event. whenever it is clicked it will emit the todoRemoved event and data will be set to the value after the : in this case 4.

We also decorated the input with the todoAdded event.

The contract between the input handler and the UI is fairly loosely defined and it should be possible to refactor the UI without breaking the input handler. You can make the todoAdded event more generic like:

// ui.html
 
<div class="todo-addition" data-submit="todoAdded">
    <input class="add-todo" name="title" />
</div>

Then update the input handler

// when the input event todo addition occurs
// create a fresh item and add it
emitter.on("todoAdded", function (tuple) {
    var ev = tuple.ev
    var title = ev.currentValue.title
    var todo = { title: title, completed: false }
    state.todos.push(todo)
})

We have now bound the todoAdded event less tightly to currentValue and as long as there is some kind of input or textarea or custom element with a name called 'title' the input handling code will still work.

Custom events (Not implemented)

The type: "submit" or data-submit event and the type: "change" or data-change event are not the normal DOM events for addEventListener('submit') or addEventListener('change')

data-submit and data-change actually have more complex semantics. They can be bound to a container and well then emit the named event every time any child changes by the submit or change semantics.

If bound to a container it will use FormData(...) to read the currentValue as a hash of name to value of elements.

If bound to a single element it will get the value of that element based on what kind of element it is.

submit semantics

submit triggers on keypress if the key is ENTER or triggers on a click if the click target is a button or input of type "submit"

change semantics

change triggers on change (DOM change event) or on keypress

Installation

npm install html-delegator

Contributors

  • Raynos

MIT Licenced

Readme

Keywords

none

Package Sidebar

Install

npm i html-delegator

Weekly Downloads

2

Version

1.0.1

License

none

Last publish

Collaborators

  • raynos