Hyperhtml: Possibility to extend define to work on custom attributes

Created on 27 Jul 2018  路  6Comments  路  Source: WebReflection/hyperHTML

Hi Andrea,

Really love how hyperHTML has been coming along and the tools that you've built around it. Is there any possibility of extending the functionality of define so that it can work with custom attributes to modify the element in some manner?

Specifically what I'm tying to do is use hyperHTML with Stylable, which so far has been working great, with the exception that Stylable has custom state handling in CSS, which it achieves by using data attributes on the element. The part that causes a problem is that Stylable namespaces everything to prevent naming collisions, including these data attributes, so we don't know what the attribute is called until after the stylesheet has been processed.

eg. it would be nice to be able to define a function to extend hyperHTML so that I could take a template like this:

render`<button class=${style.selectButton} onclick=${e => select(thing)} state=${stateAttrs}  title=${button.text}></button>`;

and have that render the following html, assuming stateAttrs is an object with properties loading = true and selected = true.

<button class=".myComponent4223602468--selectButton" data-myComponent4223602468--selected="true" data-myComponent4223602468--loading="true" title="select thing"></button>

Stylable provides the mechanism to dynamically get what the attributes are, I just need a way to add them to my hyperHTML template. The rest of this already works as outlined above. How I've been working around it so far is by hanging onto the element after it's returned by hyperHTML and then adding/removing attributes after render if they've changed, which feels far from ideal. It would be nice to keep the declarative syntax that hyperHTML allows with everything else.

Here's a relevant issue I filed under stylable a while back: https://github.com/wix/stylable/issues/316

lit-html makes this possible using directives, but I've no intention of changing frameworks. If you have alternative thoughts I'd love to hear them as well.

Thanks,
Scott

Most helpful comment

Hey @Scott-MacD, after few tests, more thoughts, and some excitement, I think I'll go for this improvement since it's the best I could think of, and it covers every single use case.

It's also probably the best usage of intents so far, and I now regret I didn't pass the target node to generic intent interface too, but I might flip to version 3 just for that, so both attributes and node content intent signatures will be aligned.

I hope it works enough for you, happy to hear possible suggestions for improvements anyway.

Regards

All 6 comments

It would be nice to keep the declarative syntax that hyperHTML allows with everything else.

If I understand correctly, you'd like to be able to do <button ${'data-attribute-name="value"'}`/> but I'm afraid if you understand how hyperHTML works there's no way to offer that without creating a DOM parser.

How I've been working around it so far is by hanging onto the element after it's returned by hyperHTML and then adding/removing attributes after render if they've changed, which feels far from ideal.

What doesn't work with this approach? Why couldn't you make a nice API around this?

// why wouldn't this look elegant too ?
styled(dataAttributes, render)`<button
  class=${style.selectButton}
  onclick=${e => select(thing)}
  state=${stateAttrs}
  title=${button.text}></button>`;

All hyperHTML offer is a callback you can wrap as you like and return something else instead, able to perform before, or after, operations for you.

I don't have enough info about your code, and also seeing render like that I don't know if you are trashing layout each time or if that render is a bind or a wire but I could help defining above utility, if that works for you.

It would be nice to keep the declarative syntax that hyperHTML allows with everything else.

I need to think about this ... maybe I could provide something like:

// define custom attributes via `:` and a callback
hyperHTML.define('hyper:data', (target, any) => {
  // do whatever you want with target and optionally return a value
  return any(target);
});

render`<button hyper:data=${styledFunctionality} />`;

The hyper:data is just an example, let's say that since define is used for content and objects are used in there, as long as the name has a : in it, it'll be set as attribute, instead of inner content.

At the end of the day, the define was never usable with attributes, maybe it's time to provide such functionality.

Edit I have already implemented this, might push a branch soon so you can test if the behavior is cool and good enough.

Hi @Scott-MacD please have a look at the build from the hyper-attributes branch.

This is the raw minified file:
https://raw.githubusercontent.com/WebReflection/hyperHTML/hyper-attributes/min.js

That version would allow you to:

// define a behavior to intercept all stylable:data attributes
hyperHTML.define('stylable-data', (target, any) => {
  // eventually modify or do whatever with target
  // optionally return a value for that attribute (empty string by default)
  // example
  const data = any.$cssStates({ a:true, b:true });
  Object.keys(data).forEach(key => {
    node.setAttribute(key, data[key]);
  });
});

// you can now have ...
render`<button stylable-data=${style} />`;

Above is only an example, since I've no idea about Styled, but I hope the improvement I've made, which are simple enough but also powerful, are enough to satisfy your use case.

Update

After some test I've realized it makes no sense to use : as attribute identifier because that's somehow reserved for XML namespaces and it cannot be styled via [what-ever] as CSS selector.

The current amend uses hyper-attribute instead with the exact same convention and behavior.

Hey @Scott-MacD, after few tests, more thoughts, and some excitement, I think I'll go for this improvement since it's the best I could think of, and it covers every single use case.

It's also probably the best usage of intents so far, and I now regret I didn't pass the target node to generic intent interface too, but I might flip to version 3 just for that, so both attributes and node content intent signatures will be aligned.

I hope it works enough for you, happy to hear possible suggestions for improvements anyway.

Regards

Thanks for the update @WebReflection! I pulled in the new changes and this works quite well for what I had in mind.

Here's what I ended up putting together to seamlessly meld with stylable states:

hyper.define('style-attrs', (() => {

    const attrCache = new WeakMap();

    return (el, dataAttrs) => {
        const newAttrs = new Set();
        const prevAttrs = attrCache.get(el);

        Object.entries(dataAttrs).forEach(([attr, value]) => {
            if (value === undefined) return;
            newAttrs.add(attr);

            if (prevAttrs) prevAttrs.delete(attr);

            el.setAttribute(attr, value);
        });

        if (prevAttrs) {
            prevAttrs.forEach(attr => el.removeAttribute(attr));
        }

        attrCache.set(el, newAttrs);
    }

})());

Then inside my component code I have a function that converts my relevant state to the namespace data attributes used by stylable, and can ultimately do something like this:

render`<button class=${style.selectButton} onclick=${e => select(thing)} style-attrs=${st({selected: isSelected})}  title=${button.text}></button>`;

render in the example I posted above just as an FYI is either a wire bound to an object, or directly bound to an element. I typically build my components in closures that return a render function, with the outer function defining what the component is actually being rendered to.

Andrea, you're my hero! This is an extremely valuable enhancement also for us.

Now we can use hyperHTMLElement as the view layer for "Final Form" (https://github.com/final-form/final-form) with a single attribute both for reading/setting field values from/to the form state:

HyperHTMLElement.intent(
    'field-name',
    (node, value) => {
        node.value = this.form.getFieldState(fieldName).value;
        return value;
    }
);
<input class="input" type="text" placeholder="Myndighet/Organisation"
 field-name="${'myndighet_org'}" onchange=${this}>

So who needs React?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

shanghaikid picture shanghaikid  路  8Comments

BentleyDavis picture BentleyDavis  路  8Comments

jaschaio picture jaschaio  路  3Comments

pmowrer picture pmowrer  路  5Comments

davidmerrique picture davidmerrique  路  7Comments