[Proposal] CHTML

I envision a time when we can natively do what a UI component framework does now. Time has proven that this is beyond the scope of Web Components; we’ve got to develop new language primitives to support bindings and “reactivity”; we’ve got to develop more compositional powers in HTML! This is what I seek to push with CHTML.

CHTML is a (proposed) suite of new DOM features that brings language support for modern UI development paradigms: a component-based architecture, data binding, and reactivity, more composition. This will be helping us bank more on the platform and less on abstractions.

Here is the explainer: https://github.com/web-native/chtml/blob/master/explainer.md

Here is the current polyfill implementation: https://github.com/web-native/chtml

3 Likes

I whole-heartedly agree that we should bank more on the platform. Back in the 80s and 90s there was never this problem with developing in proprietary languages. They were stable. You knew where you stood. People got better and more in demand the more they learned the language. Web dev should be, ideally, a place where the more you can do, the more in demand you are. Ideally, the platform should be the main platform for web development. One’s skills should be built on native technology. It should be possible to become a master native programmer, revered by all, spoken about in whispers.

But clearly JavaScript is too low-level for a lot of companies - hence frameworks to aid productivity. There’s no point now, everyone writing their own libraries from native JavaScript and notably it makes no sense from a recruitment agency viewpoint. Companies want to employ coders in specific skills, like React, Vue, Angular, etc. - it’s not like the pioneer days where you can write your own library or learn someone else’s library from scratch. Try getting a job as a native developer and then listen to the reasons companies require that “real” programmers know frameworks. I’m in that situation right now. 49 years of age. Awesome developer. Wrote my own frameworks and libraries. Don’t know any popular frameworks. Having difficulty finding a new job.

Unless you are living under a rock, it is obvious that there needs to be a higher-level common ground for reactive and component functionality, and the browser platform itself should be the place where the magic happens at a high-enough level for people to be able to understand it without needing a CS degree. Ease-of-use is one of the purposes for the internet itself. So it needs to be easy to write UI for the internet. Else we’ll be stuck with static brochure websites forevermore.

The more this elephant in the room can be worked over, the better.

You hit the nail on the head with the platform enhancement. You should get a docs website up with examples, to show how it works in practice. As long as your suggestions work in a 100% backward-compatible way, are as flexible as a framework, and are easier to use than using a framework, then there is a chance. Anything that can be introduced natively that get rid of a framework need is a good thing. Having options for doing things on a web page natively are a good thing. But I would suggest that you get more case studies and examples up there.

I took the other approach to solving this issue of JavaScript being too low-level - extending CSS to be a fully event-driven, modern reactive framework. Sounds a bit weird, I concur. But equally weirdly, it does actually work in practice. It’s like a major extension based on the :hover command. It adds :click, :mouseover, and hundreds of other things. It can be added as-is to CSS as it stands right now, with no changes to any existing CSS specs (AFAIK).

Active CSS has the same scoped variableness of which you speak (great minds, clearly), essentially all the things you mention. is dramatically higher-level than JavaScript, which, let’s face it, is getting a bit beyond your novice web developer when it comes to components and that sort of thing (it takes at least a year to even understand what a shadow DOM is, and another year to stumble upon a valid need for isolated CSS, and then another 100 cups of coffee to grasp the API).

https://activecss.org. It could be incorporated into the browser in 100% backward-compatible fashion by design. I’m just right now sorting out sequential commands and nested loops to make it look more like a language, and hopefully then can start writing up proper tutorials. I keep getting distracted by missing bits in it and so feel the need to code more to make it complete before telling more people about it. But this post got my attention…

2 Likes

I’m very delighted to see you share my values - banking more on the platform.

You made a valid point in the second paragraph about companies requiring being skilled in a framework. A little note here is that the status quo is based on current limitations and possibilities - which CHTML seeks to change. We’re having to get by with frameworks because we lack native features that help us keep the UI in sync with the state of an application.

With CHTML being a foundational technology, companies can always create higher-level tooling that is specifically adapted to their needs - this time, with less engineering and zero interoperability issues. With or without tooling, the question of building the UI from smaller-sized components and keeping these in sync with an application would have forever been based on a common ground.

I am certainly eager to work on more examples, and overall, a clearer documentation than what we have at https://docs.web-native.dev/chtml. The current proof of concept has been my priority the past many months. I should be back to work on seeing a good amount of interest in CHTML.

I’ll now be heading over to https://activecss.org to see what you have up there.

So I thought to revise the EXPLAINER to put the discussion points in focus. So, here are the components of the CHTML suite:

  • Scoped HTML - Introduces namespaces into HTML to help us structure an HTML document as a hierarchy of scopes and subscopes. (Or components and sub components, to use the “component” terminology.)
  • Scoped CSS - Reproposes scoped stylesheets as a language feature to help us define styling as part of any element.
  • Scoped JS - Introduces scoped scripts into HTML - plain JavaScript that works as a “reactive” data-binding language - to enable us define behaviour as part of any element.
  • HTML Partials - Introduces Slots-Based composition into the “open” HTML and a means to define, access, and import reusable HTML snippets.

There is also the examples section that shows how the different parts of the suite fit together and how everything could work with any tool of choice.

Also it would be great to see examples of how this plays with Custom Elements. How would custom element authors use each of these features? I now CE authors don’t have to use these, and the features can be useful by themselves, but just curious to see what benefit the features may give to CE authors.

This is a good question. I do apologise for a delayed response - it happend to be a time of vacation for me.

So, I would say that, while not drastically changing how we author web components, CHTML does offer “shortcuts”, consistency, and a good amount of “neatness” to how we code. These add up to improve the overall developer experience and maintainability of our work.

For example, the namespace API in the Scoped HTML specs can improve how we name the structural parts of our custom elements and how we access these parts in the JavaScript code. Here is an example of a “collapsible” component:

The markup (based on scoped IDs):

<my-collapsible namespace>
  <div>
    <div id="control"></div>
    <div id="content"></div>
  </div>
</my-collapsible>

The JavaScript:

customElements.define('my-collapsible', class extends HTMLElement {
  constructor() {
    super();
    this.namespace.control.addEventListener('click', () => {
      // Do something to this.namespace.content
    });
  }
});

Here, we are now coding against a structural API instead of CSS selectors.

  • This gives us loose coupling between the JavaScript and the HTML.

  • Using this.namespace instead of this.querySelector() does not only make everything neat, but also self-documenting - giving us the component’s semantics at a glance.

  • Also, with this.namespace being observable, it becomes easy to work with a component whose parts are dynamically added.

  • But the most significant win, arguably, is that we also avoid the specificity wars that using CSS selectors would bring at scale. For example, where this component is nested, we’d hit a terrible problem with a selector like .control at the parent component level:

    <my-collapsible>
      <div>
        <div class="control"></div>
        <div class="content">
    
          <my-collapsible>
            <div>
              <div class="control"></div>
              <div class="content"></div>
            </div>
          </my-collapsible>
    
        </div>
      </div>
    </my-collapsible>
    

Another way CHTML could improve how we author and consume custom elements is in how we define and access <template> elements for use in the shadow DOM. Currently, we query the document by CSS selectors to find a <template> element. But with the document.templates API, that wouldn’t be necessary.

Using the “collapsible” component above:

The HTML (defining the <template> for the component’s Shadow DOM):

<head>

  <template name="collapsible">
    <div>
      <div id="control"></div>
      <div id="content"></div>
    </div>
  </template>

</head>

The JavaScript (accessing the <template> for the component’s Shadow DOM):

customElements.define('my-collapsible', class extends HTMLElement {
  constructor() {
    super();
    let shadowRoot = this.attachShadow({mode: 'open'});
 
   shadowRoot.append(document.templates.collapsible.innerHTML);
  }
});

We could even “invert the control” of <template> selection by allowing the <template> element to be decided at component consumption time.

Here, the <template> to use for the Shadow DOM is defined at consumption time:

<my-collapsible template="collapsible"></my-collapsible>

For this, the JavaScript would use the Element.template API:

shadowRoot.append(this.template.innerHTML);

Lastly, CHTML has provided an elegant answer to the all-important subject of data binding and reactivity. Before now, you couldn’t approach this without a “tool” and a template language.

Using Scoped JS, here is how the collapsible component above could receive and remder data:

The HTML:

<my-collapsible namespace>

  <div>
    <div id="control"></div>
    <div id="content"></div>
  </div>

  <script scoped>
    this.namespace.content.innerHTML = content;
  </script>

</my-collapsible>

The JavaScript:

let data = {content: 'Collapsible content'};
document.querySelector('my-collapsible').bind(data);

Update data.content anytime and the UI is automatically updated.

The benefit of achieving this at the language level is that the modern UI can go without a tool. We shouldn’t really need specialised tools to do this.

Do let me know what you think.

:clap: :clap: :clap: :clap: Very impressive!!!

1 Like

There’s no required timeline for free work. :slight_smile:

Those are some really great points that make it clear how custom element benefit. I see how they can be useful. Thanks!

If I understand correctly, Scoped JS could be used inside a custom element implementation like this:

class NameCard extends HTMLElement {
  _root = null
  _div = null

  connectedCallback() {
    if (!this._root) this._root = this.attachShadow({mode: 'open'})

    this._root.innerHTML = html`
      <div namespace>
        <div>Hello, my name is:</div>
        <div id="name"></div>
        <script scoped>
          this.namespace.name.innerHTML = name;
        </script>
      </div>
    `

    this._div = this._root.firstChild
  }

  _values = {}

  attributeChangedCallback(attr, oldVal, newVal) {
    if (this_div && attr === 'name') {
      this._values.name = newVal ?? 'John Doe'
      this._div.bind(this._values) 
    }
  }
}

customElements.define('name-card', NameCard)

And the end user consumption:

<name-card name="Omiron Centis"></name-card>

Are you implying that the code in <script scoped> is a “reactive computation” that gets re-run any time the values of variables (that are dependencies in the script’s code) bound to the containing namespace change?

Is there a declarative way to place the values into the DOM? (f.e. <div>The content: {content}</div>)?

For that particular use case, here’s how we can do it without ScopedJS, which is a little shorter:

class NameCard extends HTMLElement {
  _root = null
  _div = null

  connectedCallback() {
    if (!this._root) this._root = this.attachShadow({mode: 'open'})

    this._root.innerHTML = html`
      <div namespace>
        <div>Hello, my name is:</div>
        <div id="name"></div>
      </div>
    `

    this._div = this._root.firstChild
  }

  attributeChangedCallback(attr, oldVal, newVal) {
    if (this_div && attr === 'name') {
      this._div.innerHTML = newVal ?? 'John Doe'
    }
  }
}

customElements.define('name-card', NameCard)

Thank you for this, and for the good thinking that went into it!

So, looking at the use case you gave, I see that another approach would be necessary! Problem with the first code is redundancy. Here, the <name-card> element is observing input data both via attribute-change detection and via Scoped JS’s reactivity. Actually, only one approach would do. Either of the two will be just fine. So, here are different ways I would do data-binding with the <name-card> element using Scoped JS alone, depending on the specific use-case:

  • If the shadow DOM isn’t really a requirement:

    <name-card namespace>
    
      <div>Hello, my name is:</div>
      <div id="name"></div>
    
      <script scoped>
        this.namespace.name.innerHTML = name;
        // If reflecting this value on an attribute is a requirement... say for SEO
        this.setAttribute('name', name);
        // If the component exposed some method... say toggle
        this.toggle(state);
      </script>
    
    </name-card>
    

    Now in the application:

    var data = {name: 'John Doe', state: 'active'};
    nameCard.bind(data);
    
  • If the shadow DOM is a requirement and much of the work happens inside, you want to send the data in by exposing some method that accepts and uses the data - say a setName() method (more about this method shortly):

    <name-card namespace>
    
      <script scoped>
        this.setName(name);
        // If the component exposed some other method... say toggle
        this.toggle(state);
      </script>
    
    </name-card>
    

    So, we are practically feeding the component - via its methods - from the reactive script, not the other way around

    Now, again, in the application:

    var data = {name: 'John Doe', state: 'active'};
    nameCard.bind(data);
    

    But, lest it seems that it would as well be fine for the application to call the component’s methods directly - nameCard.setName(data.name); nameCard.toggle(data.state) - instead of via the scoped script, we would be missing out on the reactivity of a scoped script and would have to be manually tracking changes.

    // This is a case where the data is coming from somewhere else
    var data = user.data;
    
    // Initial call
    nameCard.setName(data.name);
    nameCard.toggle(data.state);
    
    // On updates
    Observer.observe(data, 'name', update => {
        nameCard.setName(update.value);
    });
    Observer.observe(data, 'state', update => {
        nameCard.toggle(update.value);
    });
    

    This is essentially what a reactive script would do for us when we call nameCard.bind(data).

    In addition, we also get to avoid littering the application layer with much of the implementation details of the presentation layer (having to know the methods, attributes, and the intended behaviour of DOM elements). More on this shortly. For now, at least, we can see that the application code doesn’t have to worry about what happens with the data in the UI.

    Now, for the setName() method, here’s how we could implement it:

    class NameCard extends HTMLElement {
    
        connectedCallback() {
            if (!this._root) this._root = this.attachShadow({mode: 'open'})
    
            this._root.innerHTML = `
            <div namespace>
              <div>Hello, my name is:</div>
              <div id="name"></div>
            </div>`;
    
            this._tree = this._root.firstChild;
        }
    
        setName(name) {
            this._tree.namespace.name.innerHTML = name;
            // If reflecting this value on an attribute is a requirement... say for SEO
            this.setAttribute('name', name);
        }
    }
    

In all, notice that we are reflecting the value of name on the element’s attribute, not obtaining the value via the attribute!

The Sweet Spot of Scoped JS

Here are a few things about Scoped JS and its .bind() method:

  1. Separation of Concern: Two layers of code don’t have to know each other to be kept in sync. So, above, our application layer can just be concerned with data manipulation, while the DOM layer with data consumption. They are two separate layers that would be kept separate of each other. This principle becomes broken if the application layer were to be directly manipulating DOM attributes and methods as it would be having to know the implementation details of the DOM.

    So there is a down side to the pattern where the communication between the application layer and the DOM layer happens via attributes - element.setAttribute(name) in the application, attributeChangedCallback(attr, oldVal, newVal) {} in the element. But the issue is nothing in a little codebase where the design pattern doesn’t require a strict separation of concern.

  2. True Reactivity: True reactivity lets us work declaratively even with complex data types like nested objects and arrays - an obvious impossibility for attribute-based change-detection mechanisms.

    With Scoped JS, we can bind a nested data tree like this:

    var data = {
        user: {name: 'John Doe'},
        state: 'active',
    };
    nameCard.bind(data);
    

    then consume it like this:

    <script scoped>
      // We can either consume the "user" branch here...
      this.setName(user.name);
      // Or pass it down to a child component...
      this.namespace.user.bind(user);
      // If the component exposed some other method... say toggle
      this.toggle(state);
    </script>
    

    and everything will work reactively! For example, the application can update just the value of data.user.name, and this will trigger just the this.setName(user.name) statement in the entire script above. (Or if we actually did pass the user branch to a child component, only the child component responds to the update.)

    Granular DOM updates!

  3. Simplicity: Notice how much code is required to create a small behaviour using a custom element. We have to be working with a JavaScript class! We also have to ship the JavaScript code to the browser (often, in real life, involving some tooling - think minifiers). So… there is this bunch of extra work that goes into provisioning that small attributeChangedCallback(attr, oldVal, newVal) {} block!

    With scoped scripts, there is practically nothing to do upfront, nor afterward! No JavaScript classes, no loading of an external JavaScript file, just behaviours on the fly! Everything starts and ends right within the element markup!

One Last Thing - the Irony of Everything

Although Scoped JS can drastically improve how we write custom elements, it should actually help us reduce the amount of custom elements we have to write in the first place, as much of the custom elements we write is usually to emulate data-binding or some form of reactive data rending! There is a perfectly reactive data-binding language for that now!

Okay, your thoughts again…

I don’t know if CHTML will solve it, but I generally agree we need a decent state-ful GUI markup standard. Perhaps HTML browsers are too burdened already such that we should split web UI into three sub-groups: 1) Media/games/videos, 2) Documents (html may be good enough already), and 3) Productivity: CRUD and data-oriented GUI.

Perhaps one browser could render all three, but the point is to be able to focus on each so that a single standard doesn’t become Swiss Army Knife pasta. It would be 3 different “engines” but a single browser could potentially mix all three, but in different panels.

Perhaps the “GUI browser” could be based off the Tk or Qt tool kits, since those are the most mature open-source kits. The markup language would roughly resemble Microsoft’s XAML language, but be state-ful in that there is interaction.

We might not need gajillion web UI frameworks if browsers natively supported common GUI idioms and behaviors. I agree that if fashionable “eye candy” is your site’s goal, then having choice allows you to stay current on the latest UI fads. But a lot software is for getting work done and doesn’t need the UI widgets to change every 6 months. Intranet and niche business applications have a different need than public facing sites. I’m tired of social media polluting our standards for work software.