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

Provide examples of using web components with bindings #1004

Merged
merged 3 commits into from
Feb 5, 2025

Conversation

alexeyraspopov
Copy link
Contributor

@alexeyraspopov alexeyraspopov commented Jan 31, 2025

This PR implements a bunch of integration specs that can serve as an example of using custom elements with our bindings library. This suppose to be the path of least resistance to get into component-based UI development wherever the complexity requires, still without switching to full fledged UI framework. Keeping things lightweight and using the platform as far as we can feasibly do.

Framework component vs web component

A framework's component (React, Vue) is not the same as Web component. A framework component mainly about separating knowledge/structure based on application domain. Web component is a set of different technologies that allow building reusable custom elements. Custom elements by themselves is also another particular API in the web platform that allows defining HTML elements with custom behavior. Dynamic behavior can be based on user events, changing attributes, etc. It can be anything inside of it, but the application of custom elements remains the same, anywhere. This is the important part of web components, comparing to a framework components.

We are going to use custom elements, Shadow DOM, and inertial to define what a custom component is. It is going to be a structure, a pattern we can follow to build isolated components based on our domain (e.g. smaller reusable parts of a large complex form).

Custom component and its lifecycle

Here's briefly the structure I've used in this PR:

import { ObservableScope } from "inertial";
import { applyBindings, html } from "./bindings";

// Using custom elements, we define custom component as a class that extends HTMLElement
// On the inside, it behaves just like another class
// Custom elements provide certain methods that can be defined to react on host events: element attached/detached, attributes changed
class CustomComponent extends HTMLElement {
  // Each instance of a custom component going to have its own scope for reactive values so they don't interfere
  os = ObservableScope();
  
  // Using the scope, we define internal state in the same way we define a View Model
  internalState = this.os.signal(/* initial state */);

  // The parent scope needs a way to pass data down to a custom component
  // Here we use property setters so any kind of data can be passed, unlike attributes that only handle strings
  // Following setter triggers when you do `element.someValue = ...` 
  //   which is what happens when you bind `data-prop-some-value="..."`
  set someValue(value) {
    // Triggering changes in internal state is how we update the component based on what the parent scope needs
    this.internalState(value);
  }

  // Since the class context is being used as View Model for its own template bindings,
  // we can define custom methods to describe certain behaviors outside of the template
  someMethod() {
    // We can use custom events to trigger reaction in the parent scope
    //   that can bind to the event e.g. `data-on-something="this.handle(event.detail)"`
    this.dispatchEvent(new CustomEvent("something", { detail: info }));
  }

  // To make it easier to navigate, I defined the component's template as a separate property
  // It uses JS template strings for convenient multiline editing 
  // `html` tag (imported from bindings module) does nothing in runtime, but can enable HTML syntax highlight
  template = html`
    <p data-text="this.internalState()"></p>
    <button data-on-click="this.someMethod()"></button>
  `

  // This part is what makes everything work
  // This method is triggered by the browser when the custom element is attached to the page
  connectedCallback() {
    // Here we hide the internal template from the parent scope, so parent bindings don't see internal template
    const shadow = this.attachShadow({ mode: "open" });
    // The shadow DOM of an element receives the template defined earlier
    shadow.innerHTML = this.template;
    // And gets bindings applied, using the instance of the class as view model
    applyBindings(shadow, this.os, this);
  }
}

// Here we register the class we implement as a custom HTML element so it can be used in a parent template
customElements.define("custom-component", CustomComponent);

HTML in JS syntax highlight

Now that we put HTML template directly in JS/TS file and it's not JSX, it becomes harder to work with the code, since it is just a template string in terms of JS. In this PR I tagged the templates with html so that it can trigger certain setups and IDEs to treat the contents of following string as HTML. It also works on GitHub! Not sure if vscode suppose to support this by default, but if not, you can use es6-string-html vscode extension to enable this feature.

const { fake } = await import("sinon");
const root = document.createElement("main");
root.innerHTML = /* html */ `
<custom-form data-on-bubble="this.handleBubble(event.detail)"></custom-form>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫧

@alexeyraspopov alexeyraspopov marked this pull request as ready for review February 4, 2025 17:14
@alexeyraspopov alexeyraspopov requested a review from a team as a code owner February 4, 2025 17:14
@alexeyraspopov alexeyraspopov merged commit a96effe into main Feb 5, 2025
2 checks passed
@alexeyraspopov alexeyraspopov deleted the oleksii/CustomElementsExamples branch February 5, 2025 14:27
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

Successfully merging this pull request may close these issues.

2 participants