Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Declarative DOM event binding #27

Closed
Rich-Harris opened this issue May 3, 2013 · 2 comments
Closed

Declarative DOM event binding #27

Rich-Harris opened this issue May 3, 2013 · 2 comments

Comments

@Rich-Harris
Copy link
Member

This is a common pattern:

<div class='dialog'>
  <p>{{dialogText}}</p>
  <a id='okay' class='button'>OK</a>
</div>
view = new Ractive({
  el: '#container',
  template: dialogTemplate,
  data: { dialogText: 'Press OK to close this dialog' }
};

document.getElementById( 'okay' ).addEventListener( 'click', function () {
  view.teardown();
});

This is fine, as far as it goes, and using jQuery or similar makes it slightly less verbose. But the fact that we're giving an element an ID just so we can easily find it a moment later and bind event handlers to it is a bit... yucky. It goes against the spirit of declarative programming to have to throw in these hooks. Sure, the ID could be more descriptive - 'closeDialog' or something - but that's not really what IDs are for. Our abstractions are leaking.

It would be nicer to be able to do something like this:

<div class='dialog'>
  <p>{{dialogText}}</p>
  <a on-click='closeDialog' class='button'>OK</a>
</div>
view = new Ractive({
  el: '#container',
  template: dialogTemplate,
  data: { dialogText: 'Press OK to close this dialog' }
};

view.on( 'closeDialog', function () {
  this.teardown();
});

That way things are better separated. It also allows us to define multiple behaviours in a nicer way:

view.on({
  collapse: function () {
    // code goes here
  },
  expand: function () {
    // code goes here
  }
});

Some additional thoughts right off the bat: it would be useful to pass along the event data, and also the element that was the subject of the DOM event (i.e. the event's target or one of its ancestors). Normally with event handlers, this is the element, but within a view.on() handler this === view, and it probably isn't a good idea to change that. A good compromise would be to pass along the element as an argument:

view.on( 'open', function ( event, el ) {
  var target;

  if ( event.shiftKey ) {
    el = el.getAttribute( 'data-target' ); // or whatever
    // do something
  }
});

So. The real problem to figure out here is what syntax to use in templates to make this happen. It's probably best to use an attribute, as many editors barf if you start throwing illegal characters around inside a document. The colour coding gets messed up. That matters, because the moment that starts happening everything begins to feel like a hack.

For that reason I'm not all that keen on something like Ember's {{action}} helper.

Angular does it using an ng-click attribute (though rather than an event label, you're using pseudo-JavaScript which corresponds to a method on the relevant controller's $scope object). This isn't a bad solution - it means the template doesn't validate as HTML, but who really cares? We could do something similar with rv-click, or something. I'm not in love with the aesthetics though.

One thing we definitely shouldn't do is use an on-click attribute or similar, example above notwithstanding. That's too similar to onclick where the value is eval'ed as JavaScript in the global context. Ugly, ugly, ugly stuff.

Two final considerations:

  • Do we need a way to pass arguments along? If not, we can still grab data from the DOM, but that's not totally ideal.
  • Do we support custom events? E.g. I often define a 'tap' event which normalises behaviour between mouse and touch interfaces, and prevents mousedown-waggle-hold-mouseup sequences from being interpreted as clicks. If so, how?
@Rich-Harris
Copy link
Member Author

I quite like this syntax:

<div class='dialog'>
  <p>{{dialogText}}</p>
  <a click='closeDialog' class='button'>OK</a>
</div>

It sort of implies that click is a variable and closeDialog is the value, which I suppose it is. It gets right to the point and is aesthetically pleasing (to me at least). AFAIK there are no event names that are also valid element attributes, so no collisions. And it is easy to test whether we're dealing with an event directive or an attribute:

if ( node[ 'on' + attrName ] !== undefined ) {
  // event directive
} else {
  // attribute
}

For the initial implementation I will ignore situations where you need to pass along additional data, or have values that include mustaches. If they turn out to be necessary we can update the implementation.

@Rich-Harris
Copy link
Member Author

Hmm. On second thoughts this doesn't work so well. It really needs to be something that can be identified at compile-time, rather than at render time. A good way to do so (and also to disambiguate between these and regular attributes) would be to prefix every attribute. A short but accurately descriptive prefix is proxy:

<div class='dialog'>
  <p>{{dialogText}}</p>
  <a proxy-click='closeDialog' class='button'>OK</a>
</div>

I'm going to go with that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant