Skip to content

Instantly share code, notes, and snippets.

@developit
Last active October 20, 2016 21:39
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save developit/ea5feeb00ca6315e19fc81e656acd729 to your computer and use it in GitHub Desktop.
Save developit/ea5feeb00ca6315e19fc81e656acd729 to your computer and use it in GitHub Desktop.
DelegatedComponent

Experiment: Backbone-style events in Preact

What if we wanted to use backbone-style event delegation within the component paradigm afforded by react/preact/etc? Would that be useful?

Example

Basic usage:

class Foo extends DelegatedComponent {
  events = {
    'click ul': 'selectItem'
  };
  selectItem(e) {
    console.log(e.currentTarget.textContent); // 3
  }
  render() {
    return <ul>{ [1,2,3,4,5].map( n => <li>{n}</li> ) }</ul>
  }
}

Example 2: Re-usable Component

Maybe we want to have a component that provides this?

Option 1: Wrapper Component
const Demo = () => (
  <Delegate {{
    'click li': e => console.log(e.target.textContent)
  }}>
    <ul>
      <li>a</li>
      <li>b</li>
    </ul>
  </Delegate>
);

render(<Demo />, document.body);
Option 2: HoC

Via a higher order function:

const Demo2 = delegate({
  'click li': e => console.log(e.target.textContent)
})( () => (
  <ul>
    <li>a</li>
    <li>b</li>
  </ul>
));

render(<Demo />, document.body);

... or via a decorator:

@delegate({
  'click li': e => console.log(e.target.textContent)
})
class Demo {
  render() {
    return (
      <ul>
        <li>a</li>
        <li>b</li>
      </ul>
    );
  }
}

render(<Demo />, document.body);
import { h, cloneElement, Component } from 'preact';
export class Delegate {
render({ children, ...props }) {
return cloneElement(children[0], delegates(props));
}
}
export const delegate = events => Child => props => <Delegate {...events}><Child {...props} /></Delegate>
/** @example
* class Foo extends DelegatedComponent {
* events = {
* 'click ul': 'selectItem'
* };
* selectItem(e) {
* console.log(e.currentTarget.textContent); // 3
* }
* render() {
* return <ul>{ [1,2,3,4,5].map( n => <li>{n}</li> ) }</ul>
* }
* }
*/
export default class DelegatedComponent extends Component {
constructor(props, context) {
super(props, context);
let { render } = this;
this.render = (...args) => {
let vdom = render.apply(this, args);
if (vdom) {
vdom.attributes = {
...(vdom.attributes || {}),
...delegates(this)
};
}
return vdom;
};
}
}
/** Create delegate event handlers for a component that can then be passed as props to its base. */
export function delegates(component) {
let delegateHandler = component._delegateHandler || createDelegateHandler(component),
handlers = {};
for (let i in component.events) {
let [, type, selector] = i.match(/^([^\s]+)\s+(.+)$/);
handlers[type] = delegateHandler;
}
return handlers;
}
function createDelegateHandler(component) {
return e => {
let { target } = e,
handlers = [];
for (let i in component.events) {
let [, type, selector] = i.match(/^([^\s]+)\s+(.+)$/);
if (type.toLowerCase()===e.type) {
handlers.push([selector, component.events[i]]);
}
}
do {
for (let i=handlers.length; i--; ) {
let [sel, h] = handlers[i][1];
if (target.matches(sel)) {
e.currentTarget = e.delegationTarget = target;
if ((typeof h==='string' ? component[h] : h)(e)===false) return false;
}
}
} while((target=target.parentNode) && target!==component.base);
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment