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

a standard hyperscript API #66

Open
ahdinosaur opened this issue Mar 30, 2017 · 17 comments
Open

a standard hyperscript API #66

ahdinosaur opened this issue Mar 30, 2017 · 17 comments

Comments

@ahdinosaur
Copy link

ahdinosaur commented Mar 30, 2017

hey,

i think hyperscript is great, it's a simple functional way to compose html trees, which has led to a vibrant ecosystem. but in this vibrant ecosystem, each module has a slightly different API.

i'm interested in creating a standard hyperscript API. with this we could have standard tests that any hyperscript-compatible module must pass. (similar to: abstract-leveldown, abstract-blob-store, abstract-chunk-store, test-flumelog, etc)

so how do we come up with a standard?

i'll try some history:

  • hyperscript: the OG module: h (tag, attrs, [text?, Elements?,...])
    • tags could be name.class1.class2#id
    • attrs could be any positional arg (!)
    • attrs can be either html properties, attributes, or event listeners. is automatically inferred by the name
    • styles can be either a css string or object with css attributes and values (in original css format, not camelCase)
    • children can be either null, string, html element, or deeply nested array, and can be as any positional argument
    • supports observables as values (but not documented?)
  • virtual-hyperscript: h(selector, properties, children)
    • properties is optional, otherwise arguments are fixed
    • special properties: key and namespace
    • event properties are prefixed with ev-
    • can also pass in "hooks" as properties: hooks.md
    • separate module for svg
  • hyperx: <div a=${b}>...</div>
  • bel: h(tag, props, children)
    • meant to be used with hyperx, so has all fixed arguments
    • also infers what props do what
    • uses onevent props as event listeners
    • uses special prop namespace
    • infers when something is svg
  • mutant/html-element: h(tag, props, children)
    • events are in props.events or in props with key starting with ev-
    • attributes are in props.attributes
    • "hooks" ars in props.hooks
    • props.style is same as hyperscript
    • also supports observables as values, anywhere
    • separate module for svg
  • @skatejs/val: h(tag, props, ...children)
    • events are in props.events
    • attributes are in props.attrs

i could go on, given all the many many virtual dom modules, but that's a start.

so here's my proposal (based on my own hyps):

a hyperscript-compatible module is of shape:

element = h(tagName, [properties], [children])

tagName is a string that specifies the type of element to be created, which is passed into document.createElement.

properties is an object with the following:

  • attributes: an object mapping attribute names to values, which is passed into element.setAttribute(name, value).
  • events: an object mapping event names to listeners. which is passed into element.addEventListener(name, listener) or element.on${name} = listener
  • anything that's not attributes or events is set with element[name] = value

children is either an Array, Node, Element, or String. an Array may contained nested Arrays.

element returned is an instanceof Node or Element.

open questions:

  • do we want to require supporting special tag names (with classes and ids) out of the box? or should it be a re-usable module that can be added later: parse-class mutant/lib/parse-tag
  • hooks? i like how mutant does this (based on virtual-hyperscript): an array of functions ("hooks") called when element is added to the dom. if hook returns function, that will be called when the element is removed from the dom.
  • style? the obvious one is the style property as a string, but do we support objects and if so, in what format? (camelCase like react or css names or)
  • how to handle svg namespacing?
  • data properties?

anyways, there's my brain dump. what do other people think?

@dominictarr
Copy link
Collaborator

This is probably a good idea. I think this wants a test suite that all those listed modules can pass.
Can you link to documentation about how hooks work/are used? (this feels like a special feature, I wouldn't expect all the above to handle it, and I'm also not sure how it would be implemented reliably)

@reqshark
Copy link
Member

wow great idea!

i'm interested in creating a standard hyperscript API

+1 to that! :)

I would have a use-case to plug virtual-hyperscript into the standard like a levelup or hyperup

@balupton
Copy link
Member

Perhaps we should cc the creators of the other libraries into this as well?

@dmitriz
Copy link

dmitriz commented Apr 22, 2017

Perhaps it would be good to have the opinions of @paldepind, creator of Snabbdom and @staltz, creator of CycleJS that uses Snabbdom, who is also a top contributor to Snabbdom? And also of @lhorie, creator of MithrilJS with its own hyperscript-like library?

@dmitriz
Copy link

dmitriz commented Apr 23, 2017

To make things tangible, I would like to propose to have (1) a standard "strict API" that libraries have to conform to, and (2) "lax APIs" that allows for more flexibility to the user, which can differ from library to library.

The more flexible lax APi can be more convenient for the user to code the view manually, but can be a pain to fully implement and properly test and can easily become a source of bugs. Also likely various libraries will have their own opinions on different flexibility options.

For the strict API, I would propose the closest one to the HTML structure:

h( tag : String, props : Object, children : Array ),

with all 3 parameters mandatory or, equivalently, a single object parameter, called here the "Abstract Node Tree (ANT)", of the signature:

h( {tag: tag, props: props, children : children} )

with values of the same corresponding types - String, Object, Array, again with all the 3 properties mandatory. Any other properties are allowed on the Abstract Nodes but must be ignored by the h. The elements of the children array can be either String or another ANT. The h should be applied recursively to every Abstract Node until only Strings are left over.

That will serve uniformity and consistency and will require the minimum number of manual checks needed from the libraries implementing the spec. It would not be meant to be used manually by the user due to its verbosity.

Further, again for simplicity and consistency, all values of the corresponding type should be allowed. No validation checks need to be performed at the stage of the virtual node creation.
That is e.g. in line with how Snabbdom's Virtual Node is implemented, with all the checks and adjustments delegated to the individual modules. It is then up the library to enforce of not more specific rules.

Example

As simple example,

<a href="https://github.com/hyperhype/hyperscript">
    I love
    <strong>Hyperscript</strong>
</a>

in the strict API would look like

h('a', {href: 'https://github.com/hyperhype/hyperscript'}, [
    'I love',
    h('strong', {}, ['Hyperscript'])
])

and the corresponding ANT would be

{
    tag: 'a',
    props: {href: 'https://github.com/hyperhype/hyperscript'},
    children: [
        'I love',
        {
            tag: 'strong',
            props: {},
            children: ['Hyperscript']
        }
    ]
}

which would be a pure library-agnostic JSON that is incredibly convenient to pass around, serialise and transform via abstract pure functions.

Let me know what you think.

@staltz
Copy link

staltz commented Apr 23, 2017

Good idea. Just a note, as a snabbdom (a virtual DOM) library user, this assumption does not apply:

children is either an Array, Node, Element, or String

because h() returns a VNode in snabbdom, so a child can also be a VNode.

@dmitriz
Copy link

dmitriz commented Apr 23, 2017

Here is the proof of concept with very simple implementation of the extension of h to the Abstract Node Tree arguments: https://github.com/dmitriz/hyperscript-strict

@ahdinosaur
Copy link
Author

ahdinosaur commented Apr 24, 2017

hey @dmitriz, awesome!

for context, my motivation for this issue is more than just standardizing h(tag, properties, children), i also want to standardize some common behaviors in the properties, namely:

  • attributes: an object mapping attribute names to values, which is passed into element.setAttribute(name, value) or similar.
  • events: an object mapping event names to listeners, which is passed into element.addEventListener(name, listener) or element.on${name} = listener

and maybe also class, values (element[name] = value), style, data, hook, as described above or from snabbdom. but opinions here vary widely, so wonder what is most common.

what do you think about this?

@balupton
Copy link
Member

why would events need to be seperate? they'd just be anything with an on prefix... right?

@dmitriz
Copy link

dmitriz commented Apr 24, 2017

Thanks @ahdinosaur, I see what you mean.
I am trying to follow the path of maximal simplicity and be as close as possible to HTML,
where the attributes and events are treated the same way:

<input type="text" onkeypress="doSomething();">

So the closest API in the HyperScript would be

h('input', {type: 'text', onkeypress: doSomething}, [])

or in the single object (Abstract Node) notation

h({tag: 'input', props: {type: 'text', onkeypress: doSomething}, children: []})

That way both attributes and events are treated equally in a simple flat object, and their names will never clash by the mere HTML design, so there will never be any confusion.

That would also apply to class, style, data-*, as these are just attributes, so again, would never clash. I would also treat props equally, like ReactJS does if I understand correctly (but I am no React expert, so open for corrections if I am wrong).

React's vDom goes a bit further by introducing some non-native conventions that sometimes feel a bit like solutions to their problems 😈. For instance, their famous className instead of class is unnecessary in HyperScript, where the class object property is perfectly ok.

Snabbdom treats events slightly differently by grouping them inside the on prop (for which I would like to know the reason) but still the standard strict API can be remapped into their via a transformer similar to this.

Just my few cents.

@ahdinosaur
Copy link
Author

I am trying to follow the path of maximal simplicity and be as close as possible to HTML,
where the attributes and events are treated the same way:

i'm not sure how these are treated the same way... the problem is that hypescript is the DOM, not HTML, and there are many differences between the DOM and HTML.

why would events need to be seperate? they'd just be anything with an on prefix... right?

that's how a many hyperscript implementations do events, so yes this is a legitimate approach. however i think it adds complexity because then you have many special cases to handle.

so maybe a good way to describe my proposal is that it acknowledges the difference in behavior between keys in properties and then reduces special cases (since each behavior is in a dedicated sub-object). maybe this isn't what others want, but it's definitely what i want, in my mind special cases are the source of unnecessary complexity. 😸

@dmitriz
Copy link

dmitriz commented Apr 24, 2017

i'm not sure how these are treated the same way... the problem is that hypescript is the DOM, not HTML, and there are many differences between the DOM and HTML.

True, the props are slightly more painful but I understand that e.g. React gets away treating them the same way? If the attributes are merely the initial values of the same-named props, do we really need to store them separately on every node? Are there use cases?

@ghost
Copy link

ghost commented Apr 24, 2017

Having events separated from the attributes is definitely a boon when it comes to maintenance/creating a new implementation as it favours explicit behaviour over an implicit one (Especially in terms of performance, as you do not need to special-case the events themselves when iterating over the object).

However, having to define attributeson every single call seems overly verbose for most use-cases. Perhaps a bit of terseness (as in, attrs), similarly to props can be a happy middle ground.

That ship may have sailed though, considering all existing implementations.

@dmitriz
Copy link

dmitriz commented Apr 25, 2017

@Michael-Zapata
Mithril is one popular framework that keeps attributes, props and events together.

Would be interesting to hear the opinion from @lhorie, Mithril's creator.

It seems most libraries keep their virtual node events directly on the props, notably React (and hence Preact, Inferno etc.). Snabbdom is one of the few doing it, where you have to write

	h('button'
	, {on: {click: () => pipeToActions(increment)}}
	, `Modify score by ${increment}`
	)

instead of

	h('button'
	, {onclick: () => pipeToActions(increment)}
	, `Modify score by ${increment}`
	)

where the latter feels less cluttered and without the annoying double braces. It might be slightly more work for the library writer (but then also can be less work without having to manage the nested structure), but at the end the library is only written once.

On the other side, the shorter way will benefit the user and result in considerable code reduction, given how many times it is used.

@lhorie
Copy link

lhorie commented Jul 8, 2017

Hi, Mithril author here. Sorry I'm a bit late to the party.

IMHO, the base API should support at a minimum what JSX compiles to, as it makes it super easy for anyone to plug it into babel via a pragma, and it's how a lot of people use Mithril and Preact. Personally, I'm not a JSX person, but I've witnessed a number of people express in the past that they would not use a hyperscript without JSX.

In terms of API variations, the landscape is a bit all over the place. As was already pointed out, Snabbdom has a nested map of events instead of inline in the props/attrs object. If you consider React.createElement a hyperscript, it doesn't support CSS selectors, and for that matter, even optional props/attrs. The level of CSS selector support also varies across other implementations (Mithril, for example, supports[href=javascript:void(0)] and every permutation of id, class, attrs thereof)

There are variations in how attributes are handled, with some implementations allowing class/readonly/contenteditable instead of className/readOnly/contentEditable and some not. There are also spec deviations (e.g. React's onClick vs Mithril/HTML/JS's onclick). In Mithril, the recommendation is to stay close to HTML spec, but it also supports the JS API (i.e. both readonly and readOnly)

The variations go further in regards to support for HTML namespacing and SVG/MathML, React being one that has unusual naming edge cases. Mithril uses the xlink:href convention, which I feel is the most guessable.

Mithril also includes support for properties such as innerHTML and select.value/select.selectedIndex, as well as attribute normalization (i.e. h("input", {disabled: false}) does attribute removal, instead of treating it as a conventional but confusing boolean-to-string cast). Mithril also does normalization of event declarations (notably, it ensures ontouchstart/ontouchend/friends work)

Another aspect where hyperscript implementations vary is in terms of varargs support, i.e. h(tag, attrs, child1, child2, ...), as well as whether an array is a valid top level tree (both in and out of components).

Many implementations have special vnode types. Mithril has a fragment type (basically, a range of elements can be keyed and have lifecycle hooks) and a trusted HTML type (i.e. m.trust("<b>This is some markdown/bbcode output</b>")). Snabbdom has thunks.

Another significant aspect is component support. Mithril, React, Preact and friends support components as first args (again, because of JSX), but some systems don't have the concept of components at all (e.g. Snabbdom). Component hooks have different names, and their APIs vary very significantly, especially when it comes to APIs related to state and children.

Mithril and inferno support hooks at the vnode level. If you're going to support it, I'd recommend baking it in. The onbeforeremove hook in particular is useful for animations (it defers DOM removal until a callback), but has a very deep impact on diffing algorithms that is hard to pull out of core.

For class, both React and Mithril decided to defer to the classnames project, so I'd follow along with that convention. For style, both allow objects in camel case. React, however, does not accept a string, whereas Mithril does (including in the CSS selector syntax). The string syntax can be used (and was in at least one occasion) deliberately to squeeze performance, so there's that to consider.

For data-* attributes, Mithril supports arbitrary attributes, as that feels in line with what one would expect out of HTML.

With regards to attrs vs props, as far as DOM elements are concerned in Mithril, attrs are typically always attrs and data usually get into hook functions via closures. Components are where things get a bit murky, where props are usually data-oriented but they might also include HTML attributes (typically class or onchange) and where children can be abused to do strange things. Personally I think people should think of a component as a opaque element vnode that quacks like regular HTML (and in Mithril, it's generally expected that nesting anything inside of anything would behave as HTML does), but that's certainly not the only way to go about it. React components can flat out forbid all expando props via PropTypes (and that's considered a good thing in the places where such a policy is in place). React Router is another example where arbitrary child text nodes are not allowed for the sake of keeping their DSL clean. YMMV.

@laduke
Copy link

laduke commented Feb 23, 2018

hey, should the .outerHTML method be part of this standard API?

  • hyperscript :: outerHTML
  • hyperx :: toString()
  • react :: renderToString() ?

Reasoning: if you're using some little framework designed for hyperx -for example-, but want to use hyperscript instead, then the framework calls toString() when it server renders and you get [object Object]

(outerHTML is the right choice)

@alshdavid
Copy link

alshdavid commented Jun 15, 2019

I very much like the idea of a single object to define the element

h(opts: HyperscriptOptions = {}, ...children: HTMLElement[])

// use this overload if the first parameter is an HTMLElement, default options.tag to 'div'
h(...children: HTMLElement[])
const el = h({
   tag: 'a',
   attr: {
      href: 'https://gogogo.com',
      'data-whatever': 'sweet!'
   },
   styles: {
       // this is of type: ElementCSSInlineStyle
   },
   events: {
      onclick: console.log
   }
   innerHTML: '<strong>Ok</strong>'
}, h())

document.body.appendChild(el)

makes

<a href="https://gogogo.com" data-whatever="sweet">
    <div>
        <strong>Ok</strong>
    </div>
</a>

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

No branches or pull requests

9 participants