developit / htm

Hyperscript Tagged Markup: JSX alternative using standard tagged templates, with compiler support.
Apache License 2.0
8.71k stars 170 forks source link

`is` property, or make html clean again #81

Closed dy closed 5 years ago

dy commented 5 years ago

htm looks brilliant so far. The only thing that does not seem elegant is components. Both having tag name as variable and having closing tag seems external to html syntax. Besides, trying to couple components with semantic HTML oftentimes brings unnecessary wrapping and long nesting, also creates shallow tags like Contexs, Providers, HOCs etc.

A possible elegant way to bring clean html to htm would be making components just modifiers for actual tags, like

<nav is=${NavBar()}/>
<main is=${App(props)}>
  <header is=${Header(props)} />
  <section ${Content()}>...</section>
  <footer is=${Footer(props)} />
</main>

Compared to current code:

<${NavBar}/>
<${App} ...${props}>
  <${Header} ...${props}/>
  <${Content}>...<//>
  <${Footer} ...${props} />
<//>

That idea would allow collapsing HOCs/Providers as well:

<main is=${App()} provider=${[Lang, Store, Theme]}>
<aside context=${Lang} compose=${[withPagination, reduxForm()]}/>
</main>

Just wonder - if that's worth of separate package and what's possible way to introduce that to htm.

developit commented 5 years ago

Hi @dy. The issue with this approach is that invoking a function is fundamentally different than passing a component reference as the tag name. Component references can be classes or functions, and their calling signature is actually much more nuanced than simply fn(props) and varies from library to library.

Also, the tag name used when is= is provided is simply ignored, which seems really confusing.

FWIW, in prior versions of HTM, there was an optional syntax that's sortof similar to what you showed:

html`
  <@c ${NavBar} />
  <@c ${App}>
    .. etc
  </@c>
`

However, it didn't make sense to preserve this in the 2.0 parser since it was just a workaround intermediary to allow complex tag names in HTML.

dy commented 5 years ago

Actually is is a custom elements attribute, so that if we create custom element class that extends some tag, is is used natively (ref):

class ExpandableList extends HTMLUListElement { ... }
customElements.define('expandable-list', ExpandableList, {extends: 'ul'})

<ul is="expandable-list">
</ul>

That would be genuinely strong improvement for some framework to abandon jsx components in favor of native custom elements. Just using hooks to create native custom elements.

function CustomComponent(props) {
// hooks-based code (fx is shorthand for useEffect)
fx(() => {
  // attachedCallback
  return () => {
    // detachedCallback
  }
}, [])
}

htm`
<ul is=${CustomComponent}></ul>
`

Melding that approach with htm would allow to register custom elements on the fly, because in DOM creating custom element takes redundant customElements.define(..., {extends: tagName}) option as well as not elegant class CustomElement extends HTMLTargetElement. htm has access to both tagName and component, so it could just internally register components as native custom elements when the code is up to render.

And most importantly - this would be strong step off the bundling.

<link href="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css" rel="stylesheet">
<script src="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.js"></script>

<script>
import htm from 'https://unpkg.com/htm'

// Register MaterialUI custom elements
function Button(element, props) {
  element.mdcButton = new mdc.button.MDCButton
  element.mdcRipple = new mdc.ripple.MDCRipple
}

function App () {
  return htm`<button is=${Button} class="mdc-button">Button</button>`
}
</script>
developit commented 5 years ago

Just so I'm following along properly, what's the difference between your example and this one?

<link href="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css" rel="stylesheet">
<script src="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.js"></script>

<script>
import htm from 'https://unpkg.com/htm'

// Register MaterialUI custom elements
class Button extends HTMLButtonElement {
  static toString(){ return 'mcw-button'; }
  constructor() {
    super();
    this.mdcButton = new mdc.button.MDCButton
    this.mdcRipple = new mdc.ripple.MDCRipple
  }
}
customElements.define(Button+'', Button);

function App () {
  return htm`<${Button} class="mdc-button">Button<//>`
}
</script>
dy commented 5 years ago

@developit good point, I overlooked that, there's no difference for custom elements to be passed directly as tags. Personally I prefer is attribute though - that keeps html clean/standard.

Just found an Easter egg of htm. Some special h function can accept non-strings as tags:

html`
<${document.body}>
  <div>Our app code to render into body</div>
<//>

<${document.querySelector('.dialogs-container')}>
  <section is=${Dialog}>Portal code</section>
<//>
`

that's not possible with JSX. That enables quite elegantly portals, side-effects, hydration and rids of render method in API.

developit commented 5 years ago

woah. that is bloody clever...........

developit commented 5 years ago

For the curious, here's the implementation:

import { createElement } from 'preact';
import { createPortal } from 'preact/compat';

export function h(tag, props, child) {
  if (typeof tag === 'object' && tag && 'ownerDocument' in tag) {
    return createPortal(child, tag);
  }
  return createElement.apply(this, arguments);
}
dy commented 5 years ago

More I play around with possible syntax of hypothetical framework, more I discover how brilliant htm is. A couple more Easter eggs:

// react fragments are well-supported
html`<></>`

// seems that we don't need second slash for closing tag
html`<a></>`

//direct properties are possible
html`<mount=${target}></mount>`
// ↑ meaning literally a `mount` property of a fragment
// calls h({mount: target})
dy commented 5 years ago

Ok, the only questionable potential aspect of htm is anonymous props:

htm`<a ${disabled}></a>`

That could be useful in some situations, but the implementation isn't clear.

The rest addressed by this issue can be solved with external framework.

Pyrolistical commented 2 years ago

Just found an Easter egg of htm. Some special h function can accept non-strings as tags:

html`
<${document.body}>
  <div>Our app code to render into body</div>
<//>
...
`

that is cursed...so i had to implement it https://stackblitz.com/edit/node-wup3qi?file=index.html