microsoft / fast

The adaptive interface system for modern web experiences.
https://www.fast.design
Other
9.23k stars 590 forks source link

rfc: add Declartive Bindings to fast-element #6914

Open mohamedmansour opened 6 months ago

mohamedmansour commented 6 months ago

đź’¬ RFC

Update FAST-Element Templating to understand declarative bindings

🔦 Context

I am proposing a new Web Architecture called "Build Time Rendering" BTR for short that combines the insights gained from integrating Islands, SSR, and SSG. BTR determines the rendering process in advance, guided by cues provided by the web application itself. A key feature of BTR is the introduction of a simplified streaming protocol. This protocol is language-agnostic, enabling web servers written in any programming language to deliver experiences akin to SSR, eliminating the dependency on Node.js.

BDR requires hints within your web application relating to conditionals, lists, and variables. During the post-build process, these hints facilitate the division of the initially rendered page into streamable segments. The server no longer requires Node.js, as its primary function becomes reading the protocol of these streamable chunks and writing to the stream.

Only three declarative bindings are needed to facilitate the BTR process in generating streamable chunks post build process. The client side JavaScript will use these bindings when it hydrates the DOM into each Web Component’s @observable decorator. These bindings adhere to the dot notation, thereby supporting any JSON/Object notations. Finally, the client will resume as it was.

At the end, zero JavaScript is needed for FCP since everything will be streamed in from server. JS will kick in after paint, to add resumability with Web Components to make them interactive.

Minimum Declarative Bindings needed

To make that BDR concept work for FAST Element, we need to introduce three attributes that FAST understands. Intentionally named it after fast nomenclature.

f-signal

The f-signal binding serves as a wrapper around a value, capable of alerting relevant consumers when the value undergoes a change.

<div f-signal=”name”>Default Name</div>

In this example, the name class variable within the Web Component is assigned to the value "Default Name". Any mutation to this.name triggers an update in the corresponding DOM element.

f-when

The f-when binding is a conditional element that can toggle between true and false states.

<span f-when=”active”>Active</span>  

In this instance, the active class variable within the Web Component is set to true. If the user alters this to false, the associated element ceases to be displayed. It supports these operators '&&', '||', '!', '==', '>', '>=', '<', '<=' and supports object dot notations.

f-repeat

The f-repeat binding represents an array of templates.

<div f-repeat=”users” w-component=”name-item”>
  <name-item>Mohamed</name-item>
  <name-item>Alex</name-item>
  <name-item>Andrew</name-item>
  <name-item>Lisa</name-item>
</div>

In the above example, users class variable in the Web Component is set to a list of names that it is set to what its current children are. Same as the other bindings, once mutated, the DOM mutates. I am still not strict on convention, the w-component here just tells it what the web component for the children is, but it would be better if the f-repeat was a list of objects, then we could reserve two keys (key, and component) so we could create dynamic components.

đź’» Examples

The following is a todo app, fully streamable, fully SSRable, requires 0 JS at start, and resumability happens after paint fast:

  <div class="header">
    Todo (<span f-signal="items.length">0</span>)
  </div>

  <p class="toast" f-when="toast" f-signal="toast"></p>

  <div>
    <app-button f-on-click="onClick">Add New</app-button>
    <app-button f-on-click="onClear">Clear All</app-button>
  </div>

  <div f-when="items.length > 5">You have more than 5 items!</div>

  <div class="items" f-repeat="items" w-component="app-item">
  </div>

  <div f-when="!items.length">No items to show</div>

@customElement({ name: 'app-shell', cssModule: './app-shell.css' })
export class AppElement extends FASTElement {
  @observer
  toast: string | null = null

  @observer
  items: string[] = []

  onClick(_e: Event) {
    this.setItems([...this.items, `Item ${++this.itemId}`])
    this.setToast(`Item ${this.itemId} added`)
  }

  onClear(_e: Event) {
    this.setItems([])
    this.setToast('Items cleared')
  }

  setItems(items: string[]) {
    this.items = items
    fetch('/api/items', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(items),
    })
  }

  setToast(msg: string | null) {
    this.toast = msg
    if (msg) {
      clearTimeout(this.toastId)
      this.toastId = window.setTimeout(() => {
        this.toast = null
      }, 2000)
    }
  }
}

Prototype

I have created a micro (1.75KB) FAST Element framework which does this end to end. Please take a look at the repo https://github.com/mohamedmansour/build-time-rendering-js where end to end prototype showcases how this works.