Skip to content

GXT is `glimmer-vm` runtime alternative, only 7kb gzipped

License

Notifications You must be signed in to change notification settings

lifeart/glimmer-next

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

513 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GXT Netlify Status

Philosopher’s stone, logo of PostCSS

GXT is a cutting-edge, compilable runtime environment designed as glimmer-vm alternative, showcasing the power and flexibility of modern web component development. This repo includes a live example of how GXT can be used in real-world applications, providing developers with a practical and interactive experience. Explore our sample at netlify.

Benefits

  • 🔥 Hot Module Replacement (Reloading)
  • 🌑 Native shadow-dom support
  • ⌛ Async element destructors support
  • 🖥️ Server Side Rendering
  • 💧 Rehydration
  • 🔧 Ember Developer Tools support
  • 🍃 Runtime code tree-shaking
  • 📦 Small Bundle Size
  • ✍️ Typed Templates with Glint
  • 🤝 Ember syntax compatibility
  • 🚀 40% performance improvement compared to GlimmerVM
  • 💾 2x less memory usage compared to GlimmerVM
  • 🧹 Template linting support via Ember Template Lint
  • ⚛️ Built-in reactivity system

Development tools for VS Code

Quick Links

Documentation

  • Runtime Compiler - Compile templates at runtime for dynamic content, CMS integration, or development tools

Component sample

Based on template imports RFC

import { RemoveIcon } from "./RemoveIcon.gts";
import type { Item } from "@/utils/data";
import { type Cell, cellFor, Component } from "@lifeart/gxt";

type RowArgs = {
  Args: {
    item: Item;
    selectedCell: Cell<number>;
    onRemove: (item: Item) => void;
  };
};

export class Row extends Component<RowArgs> {
  get labelCell() {
    return cellFor(this.args.item, "label");
  }
  get id() {
    return this.args.item.id;
  }
  get selected() {
    return this.args.selectedCell.value;
  }
  set selected(value: number) {
    this.args.selectedCell.value = value;
  }
  get isSelected() {
    return this.selected === this.id;
  }
  get className() {
    return this.isSelected ? "danger" : "";
  }
  onClick = () => {
    this.selected = this.isSelected ? 0 : this.id;
  };
  onClickRemove = (e: Event) => {
    this.args.onRemove(this.args.item);
  };
  <template>
    <tr class={{this.className}}>
      <td class="col-md-1">{{this.id}}</td>
      <td class="col-md-4">
        <a {{on "click" this.onClick}} data-test-select>{{this.labelCell}}</a>
      </td>
      <td class="col-md-1">
        <a {{on "click" this.onClickRemove}} data-test-remove>
          <RemoveIcon />
        </a>
      </td>
      <td class="col-md-6"></td>
    </tr>
  </template>
}

Key Features

Simple and Expressive Component Model

  • Component as Functions: Every component in gNext is a function, executed only once for efficiency and better performance.
  • Class based components: Class based components are supported as well.
  • Basic Glint Support: Integration with Glint for improved TypeScript support and developer experience.
  • Comprehensive Slot Support: Full support for different kinds of slots, including {{yield}}, enhancing the flexibility in component composition.
  • Modifiers and Helpers APIs: Modifiers for element-specific logic. Helpers for reusable logic across components.
  • Template Imports: Import templates from other files, enabling better code organization and reusability.
  • Template Compilation: Compile templates to JavaScript functions for improved performance and efficiency.
  • Opcodes tree-shaking: Opcodes tree-shaking for smaller bundle size. We don't include unused DOM and component, flow-control opcodes in the bundle.

Reactive Primitives

  • Mutable State with cell<T>: Use cell for creating reactive, mutable states. Updating and accessing cell values is straightforward and efficient.
  • Derived State with formula: Create derived states that automatically update when dependencies change, ensuring reactive and responsive UIs.
  • Support for destructors: Enables clean-up and resource management, preventing memory leaks.

Benefits and Use Cases

gNext serves as a powerful tool for web developers looking to harness the capabilities of Glimmer-VM in a real-world setting. Its benefits and use cases include:

  • Efficient DOM Rendering: Experience fast and efficient DOM updates and rendering, crucial for high-performance web applications.
  • Reactive State Management: Manage component states reactively, ensuring UIs are always up-to-date with the underlying data.
  • Enhanced Developer Experience: Enjoy a seamless development experience with features like TypeScript support, comprehensive API documentation, and easy-to-understand examples.
  • Flexible Component Composition: Leverage advanced component composition techniques to build complex UIs with ease.
  • Resource Management: Efficiently manage resources with destructors, preventing common issues like memory leaks.

gNext is not just a library; it's a gateway to building modern, efficient, and reactive web applications using Glimmer-VM. Whether you are building dynamic user interfaces, complex single-page applications, or just experimenting with new front-end technologies, gNext provides the tools and capabilities to bring your ideas to life.

Explore gNext and elevate your web development experience!

Custom Renderers

GXT supports multiple rendering targets beyond the standard DOM. Each renderer provides its own API while maintaining full reactivity.

PDF Renderer

Build PDF documents using a declarative component-based API inspired by react-pdf:

import {
  PdfDocument,
  PdfPage,
  PdfView,
  PdfText,
  StyleSheet,
  createPdfApi,
} from "@/utils/renderers/pdf";

// Create styles
const styles = StyleSheet.create({
  page: { padding: 30 },
  title: { fontSize: 24, fontWeight: "bold", marginBottom: 10 },
  text: { fontSize: 12, color: "#333", lineHeight: 1.6 },
});

// Build document programmatically
const api = createPdfApi();

const doc = new PdfDocument();
doc.title = "My Document";
doc.author = "GXT";

const page = new PdfPage();
page.size = "A4";
page.style = styles.page;

const view = new PdfView();

const title = new PdfText();
title.style = styles.title;
title.appendChild(new PdfTextNode("Hello World"));

const paragraph = new PdfText();
paragraph.style = styles.text;
paragraph.appendChild(new PdfTextNode("Generated with GXT PDF Renderer"));

view.appendChild(title);
view.appendChild(paragraph);
page.appendChild(view);
doc.appendChild(page);

api.setDocument(doc);

// Get JSON structure for PDF generation
const structure = api.toJSON();

Available PDF Elements:

  • PdfDocument - Root container with metadata (title, author, subject, etc.)
  • PdfPage - Individual pages with size, orientation, and styling
  • PdfView - Layout container (like a div) with flexbox support
  • PdfText - Text content with typography styling
  • PdfImage - Image embedding (URL, buffer, or base64)
  • PdfLink - Hyperlinks
  • PdfCanvas - Custom drawing with paint function
  • PdfNote - Annotations

StyleSheet Utility:

import { StyleSheet, PageSizes, parseUnit } from "@/utils/renderers/pdf";

// Create named styles
const styles = StyleSheet.create({
  container: { padding: 20, flexDirection: "row" },
  text: { fontSize: 12, color: "#333" },
});

// Flatten/compose styles
const merged = StyleSheet.compose(styles.container, { margin: 10 });

// Get page dimensions
const a4 = PageSizes.A4; // { width: 595, height: 842 }

// Parse CSS units to points
parseUnit("1in"); // 72
parseUnit("2.5cm"); // ~70.87
parseUnit("50%", 200); // 100

Supported Style Properties:

  • Dimensions: width, height, minWidth, maxWidth, minHeight, maxHeight
  • Spacing: margin, padding (with directional variants)
  • Flexbox: flexDirection, justifyContent, alignItems, gap, etc.
  • Positioning: position, top, right, bottom, left, zIndex
  • Typography: fontSize, fontFamily, fontWeight, color, textAlign, lineHeight
  • Borders: borderWidth, borderColor, borderRadius
  • Background: backgroundColor, opacity

Other Renderers

  • Canvas Renderer - Render to HTML Canvas with 2D primitives
  • SVG Renderer - Native SVG with reactive attributes
  • MathML Renderer - Mathematical notation
  • Three.js/Tres Renderer - 3D WebGL graphics

See the live demo for interactive examples of all renderers.

Notes

  • modifiers API:
function modifier(element: Element, ...args: Args) {
    return () => {
        // destructor
    }
}
  • helpers API:
function helper(...args: Args): string | boolean | number | null {
  // helper logic
  return 3 + 2;
}

Reactive primitives

  • @tracked - decorator to mark class property as reactive primitive. It's autotrack dependencies and update when any of them changed. Note, to use it you need to add import 'decorator-transforms/globals'; in top-level file.

  • cell<T>(value) - reactive primitive, for mutable state. We could update cel calling cell.update(value), to get cell value we could use cell.value.

  • cellFor(object, property) - creates a reactive cell for an object property, useful for tracking nested state.

  • formula(fn: () => unknown) - reactive primitive, for derived state.

formula could be used to create derived state from Cell's. It's autotrack dependencies and update when any of them changed.

scope function is used to suspend ts error about unused variables. It's not required for runtime, but required for ts compilation.

destructors supported.

import { registerDestructor, hbs, scope } from "@lifeart/gxt";

export function Icon() {
  registerDestructor(this, () => {
    console.log("destructor");
  });

  return hbs`<i class="glyphicon glyphicon-remove"></i>`;
}

Control Flow

GXT provides built-in control flow components for conditional and list rendering.

Conditionals with {{#if}}

<template>
  {{#if this.isVisible}}
    <div>Content is visible</div>
  {{else}}
    <div>Content is hidden</div>
  {{/if}}
</template>

List rendering with {{#each}}

<template>
  <ul>
    {{#each this.items key="id" as |item index|}}
      <li>{{index}}: {{item.name}}</li>
    {{/each}}
  </ul>
</template>

The key attribute is important for efficient list updates - it helps GXT track which items have changed, been added, or removed. You can use key="@identity" for identity-based tracking.

GXT supports multiple root nodes per iteration (fragment-like rendering):

{{#each this.items key="id" as |item|}}
  <dt>{{item.term}}</dt>
  <dd>{{item.definition}}</dd>
{{/each}}

Suspense and Lazy Loading

GXT provides built-in support for async component loading with suspense boundaries.

Lazy Components

Use lazy() to create code-split components that load on demand:

import { lazy } from "@lifeart/gxt/suspense";

const MyAsyncComponent = lazy(() => import("./MyComponent"));

The lazy component will trigger the suspense boundary while loading.

Suspense Boundaries

Wrap lazy components with <Suspense> to show fallback content during loading:

import { Suspense, lazy } from "@lifeart/gxt/suspense";

const AsyncComponent = lazy(() => import("./AsyncComponent"));

function LoadingSpinner() {
  return <template>
    <div>Loading...</div>
  </template>;
}

export function App() {
  return <template>
    <Suspense @fallback={{LoadingSpinner}}>
      <AsyncComponent />
    </Suspense>
  </template>;
}

Suspense boundaries can be nested for fine-grained loading states:

<Suspense @fallback={{PageLoader}}>
  <Header />
  <Suspense @fallback={{ContentLoader}}>
    <MainContent />
  </Suspense>
</Suspense>

Tracking Custom Async Operations

Use followPromise() to track custom async operations within a suspense boundary:

import { Component } from "@lifeart/gxt";
import { followPromise } from "@lifeart/gxt/suspense";

class DataLoader extends Component {
  async loadData() {
    // This promise will be tracked by the nearest suspense boundary
    const data = await followPromise(
      this,
      fetch("/api/data").then((r) => r.json()),
    );
    return data;
  }
}

The followPromise function:

  • Calls start() on the nearest suspense context when the promise begins
  • Calls end() when the promise resolves or rejects
  • Returns a promise that resolves to the same value
  • When you await followPromise(...), end() is guaranteed to have been called

Built-in Helpers

GXT includes several built-in helpers for common template operations:

  • {{eq a b}} - equality comparison
  • {{and a b}} - logical AND
  • {{or a b}} - logical OR
  • {{not a}} - logical NOT
  • {{if condition then else}} - inline conditional
  • {{hash key=value}} - creates an object
  • {{array a b c}} - creates an array
  • {{fn this.method arg}} - partial application
  • {{log value}} - logs to console (for debugging)
  • {{debugger}} - triggers debugger breakpoint

Setup

Start project from this template: https://github.com/lifeart/template-gxt

or

pnpm create vite my-app --template vanilla-ts
pnpm install @lifeart/gxt

Edit vite.config.mts to import compiler:

import { defineConfig } from "vite";
import { compiler } from "@lifeart/gxt/compiler";

export default defineConfig(({ mode }) => ({
  plugins: [compiler(mode)],
}));

To render root component, use renderComponent function.

import { renderComponent } from "@lifeart/gxt";
import App from "./App.gts";

const Instance = renderComponent(App, {
  // application arguments
  args: {
    name: "My App",
  },
  // render target (append to)
  element: document.getElementById("app"),
});

To destroy component, use destroyElement function.

import { destroyElement } from "@lifeart/gxt";

destroyElement(Instance);

Testing

GXT provides test utilities for writing component tests with QUnit:

import { render, rerender, click, find, findAll } from "@lifeart/gxt/test-utils";
import { cell } from "@lifeart/gxt";

test("component renders correctly", async function (assert) {
  const count = cell(0);

  await render(
    <template>
      <button {{on "click" (fn count.update (inc count.value))}}>
        Count: {{count}}
      </button>
    </template>
  );

  assert.dom("button").hasText("Count: 0");

  await click("button");
  await rerender();

  assert.dom("button").hasText("Count: 1");
});

Available test utilities:

  • render(template) - renders a template to the test container
  • rerender() - waits for pending async updates
  • click(selector) - triggers a click event on matching element
  • find(selector) - returns first matching element
  • findAll(selector) - returns all matching elements

Glint Setup (TypeScript Template Type-Checking)

GXT includes a Glint environment for full template type-checking. Add to your tsconfig.json:

{
  "compilerOptions": {
    // ... your options
  },
  "glint": {
    "environment": "glint-environment-gxt"
  }
}

This enables type-safe templates with autocompletion and error checking in your IDE.

Contributors 3

  •  
  •  
  •  

Languages