htmlx-org / HTMLx

One Template to rule them all
586 stars 8 forks source link

RFC: Attribute Prefixes #11

Open lukeed opened 6 years ago

lukeed commented 6 years ago

Yo~!

I wonder if it'd be easier to parse & filter down to dynamic attributes via a common prefix match. In this case, "easier" is meant to cover the dev declarations and parser's workload. This is purely because of the consistent syntax & ability (on the parser) side to lazily infer if work will need to be done on X attribute later on.

// Values
// ---

// current
<button disabled={disabled}>...</button>
<button {disabled}>...</button>

// ideal?
<div :disabled={ disabled }>...</button>
<div :disabled>...</button>

// untouched
<Widget {...things}/>

// Events
// ---

// current
<button on:click=handleClick()>click me!</button>
<button on:click="handleClick({ foo: bar })">click me!</button>

// ideal?
<button :onclick=handleClick>click me!</button>
<button :onclick="handleClick">click me!</button>
<button :onclick={ handleClick }>click me!</button>
<button :onclick="handleClick({ foo: bar })">click me!</button>

Like stated in readme, any combination (or lack thereof) of " and {} is valid, as that doesn't really matter and essentially is like a semicolons-in-js debate. They're stylistic & will be required for compiler clarification in few cases.

Another aside, I think passing functions should work like JSX. It passes a reference when uninitialized, or you may pass along initial values that set up a new functional return value. I can be swayed on this 😇

// <button :onclick=onFoo>click me!</button>
button.onclick = onFoo;

// <button :onclick={ onBar(123) }>click me!</button>
const onBar = id => e => alert('hello', id, e.target.className);
button.onclick = onBar(123);

Custom Directives

Lastly, that leaves custom directives. This will borrow a lot from Vue & from Svelte's on:*, of course, but the difference here is that the syntax change illustrates a completely custom handler rather than attempting to write into a native attribute or event handler.

<form :onsubmit={ handler } @error={ onError } @success={ onLogin }>...</form>

function fire(el, name, data, opts) {
  opts = opts || {};
  if (data) opts.detail = detail;
  el.dispatchEvent(new CustomEvent(name, opts));
}

function handler(e) {
  e.preventDefault();

  let el = e.target;
  let foo = validate(el, my.rules);

  if (foo.errors.length > 0) {
    return fire(el, 'error', foo.errors); //~> onError called
  }

  return fire(el, 'success', foo.data); //~> onLogin called
}

// because ":onclick"
form.onclick = handler;

// because "@" is used
form.addEventListener('error', onError);
form.addEventListener('success', onLogin);

The parser then has a really easy time figure out if something is meant to be static or dynamic. Essentially it's just this:

function isDynamic(str) {
  let c = str.charCodeAt(0);
  return c === 58 || c === 64; // : or @
}

let dyna = SUSPECTS.filter(isDynamic);
lukeed commented 6 years ago

Of course, @ could still be used for any native handler assignment. It just would not compile down to a direct x.on___ assignment.

// <div :onclick="foo" />
div.onclick = foo;

// <div @click="foo" />
div.addEventListener('click', foo);
Rich-Harris commented 6 years ago

Don't the brackets signify that an attribute is dynamic? The parser can see here that class is fixed but disabled isn't:

<button class="large" disabled={inactive}>click me</button>

Another aside, I think passing functions should work like JSX. It passes a reference when uninitialized, or you may pass along initial values that set up a new functional return value

I think this is something that HTMLx itself shouldn't have an opinion on — the parser just expects a valid JavaScript expression, and it's up to the framework itself to interpret the expression in a particular way. I'm firmly convinced that the ergonomics of the Svelte approach are better 😀 (if counterintuitive at first, especially for people coming from a React background), but it's definitely something that reasonable people can disagree on.

Not sure I totally understood the part about :onfoo/on:foo vs @foo? It's probably worth noting though that custom elements will often invent their own custom events, which blurs the line a bit between native DOM events and component-specific ones.

lukeed commented 6 years ago

Don't the brackets signify that an attribute is dynamic?

Yes, but you have to parse the value to determine if it is, rather than the attribute name directly. It's an extra two steps.

Not sure I totally understood the part about :onfoo/on:foo vs @foo?

This is basically just @ being an alias for x.addEventListener('foo') which covers both custom and native elements, while :onfoo maps to x.onfoo, which is limited to native events only. IMO there should be an ability to choose either approach.

Rich-Harris commented 6 years ago

It's an extra two steps.

Hmm — I haven't found it burdensome in the context of Svelte's parser. In any case I reckon it's ok to add complexity to the parser if it makes the developer's experience nicer.

This is basically just @ being an alias for x.addEventListener('foo')

Ah, interesting. I have to admit I never ever use div.onclick and friends, I always use addEventListener, so I didn't see that distinction at first. Is there an advantage to using things like onclick?

lukeed commented 6 years ago

No worries!

It's just another way to add & remove event listeners. (It's also more compact, ignoring any on() helpers after n-repeats.) The idea here is that since onclick and friends are native attributes, I should be able to assign a listener that way when I want to, since it's no different than something like :disabled (or disabled={}).

arxpoetica commented 6 years ago

I'm on the fence about losing on: since that is descriptive, minimal character count, and helps n00bs know immediately what they're seeing.

lukeed commented 6 years ago

Understandable! 👍 I'm still on the other side, and found that it was really easy to get used to in Vue.

maxmilton commented 6 years ago

@lukeed thank you for putting this RFC together, it's great to see this kind of discussion :)

I'm a long time Vue user but I actually found Svelte's syntax refreshing as it more closely resembles regular HTML. The simplicity is a big win in my opinion.

Would multiple ways to handle events keep that simplicity? I'm not against having both but since assignment to on* with native events (e.g. onclick) is rare, should we have syntax support? Developers can still set up such a handler during the component/app life cycle, albeit with extra effort. On the flip side, having these event handlers in the template itself is better for clarity of event driven logic.

Back to the original topic of prefixes, I'm in favour of on:foo over :onfoo because, at least in Svelte, it aligns with the other directives — e.g bind:*, ref:*, use:*, transition:*, in:*, out:*. Isn't :transitionfoo harder to read?

lukeed commented 6 years ago

@MaxMilton No problem, thanks!

I'm a user & fan of both 😄 And, working a lot with Preact, I do actually write onclick={} 99% of the time. Immediate/direct access to the DOM in Preact is a feature IMO, not a bug as some will call it.

I've thought about this some more — it's been a while. So long as I can write to any HTMLElement attribute, then I'm satisfied. Eg, if I can write onclick= in the same way I can write to disabled=, great!

The point I was trying to make is that something like on:* or transition:* is awesome, but it should 100% signify that whatever you're doing is tied to a specialized, framework-specific thing & not the DOM equivalent.

For example:

<div onclick={ onNative } />
<div onclick={ this.onNative } />

<div on:click={ this.onSpecial } />

///

function onNative(ev) {
  console.log('DOM Element:', this);
  console.log('~> same as:', ev.target);
  // ...
}

// borrowing Vue/Svelte definition here
export default {
  methods: {
    // mount the same handler
    onNative,

    onSpecial(ev) {
      // whatever preprocessing XYZ does for you
      // eg, this can be React's custom event system
      console.log('Component instance:', this);
      console.log('~> target:', ev.target);
    }
  }
}

The native handler(s) would be untouched, nothing special added to them. Using the library/framework's eventing system/modifiers/etc would only kick in behind a on:* namespaced assignment.

arxpoetica commented 6 years ago

Yes, the special appearance of : should indicate something framework-y.