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

Proposal: Add event modifiers to stache #3865

Open
justinbmeyer opened this Issue Jan 26, 2018 · 10 comments

Comments

Projects
None yet
5 participants
@justinbmeyer
Copy link
Contributor

justinbmeyer commented Jan 26, 2018

tldr; Support declarative event modifiers for preventDefault and stopPropagation like on:click:stop="doSomething()" or on:submit.prevent="doSomething()".

This was discussed on a recent live stream (33:40) and a previous live stream (11:09).

One of the most common places where DOM and view concerns infect view-models (and cause unnecessary boilerplate) is making sure that preventDefault is called on an event handler. For example:

Component.extend({
  view: `
    <form on:submit="sendData(scope.event)"/>
       ...
    </form>
  `,
  ViewModel: {
    sendData(ev) {
      ev.preventDefault();
      // DO STUFF
    }
  }
})

This makes sendData() more difficult to test without the DOM.

Implement through event registry

Ideally, I'd like can-dom-events type registries to be able to support event modifiers (similar to how vuejs supports them). For example, any event binding to a registry can look like:

domEvents.addEventListener(div, "event.modifier1.modifier2", function(){

});

Where modifier1 and modifier2 are names of registered event modifiers. We should be able to create prevent and stop modifiers such that if a user wanted to call preventDefault() and .stopPropagation() on the submit event, they could write:

<form on:submit.stop.prevent="sendData()"/>

or

<form on:submit.prevent.stop="sendData()"/>

There are a lot of API choices to enable this.

Express middleware

If all we are wanting to do is support simple things, I think it could work similar to express middleware:

domEvents.addModifier("prevent", function(event, next){
    event.preventDefault();
    return next.call(this, event)
});

When a modifier is being used, the actual handler bound would be wrapped with modifiers so that the modifier chain is called in order from left to right. <form on:submit.stop.prevent="sendData()"/> would call the stop modifier, then the prevent modifier, then the callback registered by can-stache-bindings (which will call sendData).

Full lifecycle control

If we want these things about to do CRAZY stuff, they would be able to take over the addEventListener and removeEventListener of the primary event binding. This would be powerful enough to setup the sort of express middleware behavior above:

var overWrittenHandlers = new WeakMap();
domEvents.addModifier("prevent", {
  addEventListener(next) {
    var overWrittenHandler = function(event){
      event.preventDefault();
      next.handler.call(this, event)
    };
    overWrittenHandlers.set(next.handler, overWrittenHandler);
    next.addEventListener(next.target, next.eventType, overWrittenHandler);
  },
  removeEventListener(next) {
    next.removeEventListener(next.target, next.eventType, overWrittenHandlers.get(next.handler));
  }
});

Getting this to work right would be a bit tricky. So I'd rather not solve it, but leave room for it should the need arise.

Implement through can-stache-bindings

We could make this part of can-stache-bindings like: on:click:stop="doSomething()"

But I'd prefer not to as there's other places like Component.events and Control that would benefit if it's at a lower level.

@andrejewski

This comment has been minimized.

Copy link
Contributor

andrejewski commented Jan 26, 2018

I vote for "Implement through can-stache-bindings". No need to overcomplicate a more broad API for problems with event bindings.

I think we should wait for significant interest from the community, and have more concrete examples before going any further than that. From what I have seen Vue doesn't have many of these modifier things. As far as I'm concerned these typing affordances can be resolved through proper component composition.

@justinbmeyer

This comment has been minimized.

Copy link
Contributor Author

justinbmeyer commented Jan 26, 2018

@justinbmeyer

This comment has been minimized.

Copy link
Contributor Author

justinbmeyer commented Jan 26, 2018

@chasenlehara chasenlehara changed the title Event modifiers Proposal: Add event modifiers to stache Jan 26, 2018

@phillipskevin

This comment has been minimized.

Copy link
Collaborator

phillipskevin commented Jan 26, 2018

I really think this is the way to go:

supporting multiple expressions in stache

I don't think people should have to learn new syntax to do this.

@justinbmeyer

This comment has been minimized.

Copy link
Contributor Author

justinbmeyer commented Jan 26, 2018

@phillipskevin .. that wouldn't fix the capture and likely once problems

@justinbmeyer

This comment has been minimized.

Copy link
Contributor Author

justinbmeyer commented Jan 26, 2018

Vuejs's modifiers:

  • .stop
  • .prevent
  • .capture
  • .self
  • .once
  • .enter
  • .tab
  • .delete (captures both “Delete” and “Backspace” keys)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right
  • .left
  • .right
  • .middle
  • .ctrl
  • .alt
  • .shift
  • .meta

This allows stuff like:

focus.capture.self.once.

@rjgotten

This comment has been minimized.

Copy link

rjgotten commented Apr 26, 2018

My $ 0,02 as a framework consumer having some experience also with Vue: this sounds like an awesome feature to have.

I would like to add: this syntax also seems like an extremely elegant syntax for some of the simpler binding converters that don't depend on outside parameters, like not. E.g.

<input el:disabled:from.not="enabled" />
@justinbmeyer

This comment has been minimized.

Copy link
Contributor Author

justinbmeyer commented Apr 26, 2018

Thanks. Keep voting for it!

For not, there's already a converter: https://canjs.com/doc/can-stache-converters.not.html

<input el:disabled:from="not(enabled)" />

One other thing I've been thinking about as we are preparing for the next DoneJS Meetup where we are building a swiping carousel is that this syntax really doesn't go far enough to support complex interactions.

A swipe event might have a lot of options that need to be configured like:

  • the duration of the swipe,
  • the distance it needs to cover,
  • if it should start moving the element under it, etc

VueJS-style event modifiers would not allow this. For this, you really need to pass arguments.

A solution that works now

Currently, the solution would be to create "inline helpers" that allow something like:

<div {{swipe(time=200 distance=100 move=true,onSwipeCallThis) }}>

This can currently be done like:

stache.addHelper("swipe", function(options, callback){
  return function(el){
    el.addEventListener("mousemove", function(){
      // test if a swipe happened 
      callback()
    })
  }
})

This same "inline" the element syntax could probably be used to do a lot:

<!-- bindings example -->
<my-component {{ bind(scope.viewModel,"name", userName)) }}
                              {{ from(scope.viewModel,"age", userAge )) }} >

<!-- another bindings example -->
<my-component {{ bind({
  name: this.bindValue("userName")
  age: this.fromValue("useAge") 
}) }} >

Better syntax options?

There's likely better syntax to support this. We could make something like:

<div on:swipe(time=200 distance=100 move=true)="onSwipeCallThis">

or with canjs/can-stache#505, make this work:

<div on:swipe({time:200, distance:100, move:true})="onSwipeCallThis">

React

React folks wouldn't have a special event here. They would reach for a special <Swipe> element, something like:

<Swipe time={200} distance={100} move={true} callback={onSwipeCallThis}>
  <div>...</div>
</Swipe>

@christopherjbaker / @BigAB maybe you can fill me in on how this would be done in react. How does <swipe>, which doesn't exist, get the <div>, setup listeners, and handle tearing things down when <swipe> is removed?

My gut is that this is sorta annoying to build compared to your standard "swipe" event handler.

Things to think about

Anyhoo, all of this is to wonder aloud:

  • Should we be solving this in a way that is less leaky?
  • As <div {{ swipe() }}> effectively solves this and a whole bunch of other problems, should we focus on this instead of creating new syntax? Or should we focus on creating new APIs for this?
  • What would a better on:swipe(...)="bar" syntax look like?
@BigAB

This comment has been minimized.

Copy link
Contributor

BigAB commented Apr 26, 2018

@christopherjbaker / @BigAB maybe you can fill me in on how this would be done in react. How does , which doesn't exist, get the

, setup listeners, and handle tearing things down when is removed?

A quick googling of "react swipe" showed me at least 3 libs that did it this way:
The <Swipe /> component renders something like:

<div
     onTouchStart={ this.handleOnTouchStart }
     onTouchEnd={ this.handleOnTouchEnd }
     onTouchMove={ this.handleOnTouchMove}
    onTouchCancel={ this.handleOnTouchCancel}
  >{ children }</div>

...so the <Swipe /> component component takes props to configure the touch behaviour and the contents of swipe are wrapped in a div that has event handlers.

There might be other ways to do it too, but nothing jumps to mind.

@rjgotten

This comment has been minimized.

Copy link

rjgotten commented Apr 28, 2018

VueJS-style event modifiers would not allow this.

They sort-of do, with pre-established possibilities. Vue is already using modifiers to capture only specific keypresses, e.g.

<input v-on:keydown.esc="..." />

-- but indeed: that's not fully configurable. That's the challenge for CanJS to make it better: making it fully flexible yet continue to be generic and generically applicable.

Something that comes to mind is taking the Vue dot notation one step further by allowing CanJS to parametrize the individual modifiers. E.g.

<input on:keydown.key('esc').prevent="..." />

You could even make some of the execution order-dependent to compose more complicated logic, in effect forming a pipeline that ends with the bound handler. E.g. in the above sample, the prevent modifier could run only when key('esc') already matches.

The hard problem is probably to come up with an exact syntax that works within the constraints of the DOM.


I believe swipe-capability is not a good fit to be expressed with event modifiers, because swiping is a gesture that is composed of multiple events interacting on top of a piece of state, over time. It is neither isolated to a single event, nor represents a simple pipeline filter flow.

I would implement swiping as an element-level helper, e.g.

<div {{ swipeable({ time : 200, distance : 100, move : true }) }}></div>

or as a series of custom attribute handlers working together, e.g.

<div swipe-time="200" swipe-distance="100" swipe-move="true"></div>

(Personal preference for using a helper; less clutter in the DOM and less moving parts.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.