UNPKG

@lifeart/gxt

Version:

<img align="right" width="95" height="95" alt="Philosopher’s stone, logo of PostCSS" src="./public/logo.png">

523 lines (395 loc) 16.1 kB
# GXT [![Netlify Status](https://api.netlify.com/api/v1/badges/43af359b-56a7-4607-9e01-04ca3a545470/deploy-status)](https://app.netlify.com/sites/g-next/deploys) <img align="right" width="95" height="95" alt="Philosopher’s stone, logo of PostCSS" src="./public/logo.png"> `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](https://g-next.netlify.app/) 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 - [Language Server](https://marketplace.visualstudio.com/items?itemName=lifeart.vscode-ember-unstable) - [Template Syntax](https://marketplace.visualstudio.com/items?itemName=lifeart.vscode-glimmer-syntax) - [Templates Type checking](https://marketplace.visualstudio.com/items?itemName=typed-ember.glint-vscode) ## Quick Links - Related issue: [glimmer-vm/issues/1540](https://github.com/glimmerjs/glimmer-vm/issues/1540) - Related PR: [glimmer-vm/pull/1541](https://github.com/glimmerjs/glimmer-vm/pull/1541) - Sample App: [js-framework-benchmark](https://github.com/krausest/js-framework-benchmark/tree/master/frameworks/keyed/gxt) ## Documentation - [Runtime Compiler](./docs/runtime-compiler.md) - Compile templates at runtime for dynamic content, CMS integration, or development tools ## Component sample Based on [template imports RFC](https://rfcs.emberjs.com/id/0779-first-class-component-templates/) ```gjs 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 - <b>Component as Functions:</b> Every component in gNext is a function, executed only once for efficiency and better performance. - <b>Class based components:</b> Class based components are supported as well. - <b>Basic Glint Support:</b> Integration with Glint for improved TypeScript support and developer experience. - <b>Comprehensive Slot Support:</b> Full support for different kinds of slots, including {{yield}}, enhancing the flexibility in component composition. - <b>Modifiers and Helpers APIs:</b> Modifiers for element-specific logic. Helpers for reusable logic across components. - <b>Template Imports:</b> Import templates from other files, enabling better code organization and reusability. - <b>Template Compilation:</b> Compile templates to JavaScript functions for improved performance and efficiency. - <b>Opcodes tree-shaking:</b> Opcodes tree-shaking for smaller bundle size. We don't include unused DOM and component, flow-control opcodes in the bundle. ### Reactive Primitives - <b>Mutable State with `cell<T>`:</b> Use cell<T> for creating reactive, mutable states. Updating and accessing cell values is straightforward and efficient. - <b>Derived State with `formula`:</b> Create derived states that automatically update when dependencies change, ensuring reactive and responsive UIs. - <b>Support for destructors:</b> Enables clean-up and resource management, preventing memory leaks. ## Benefits and Use Cases <b>gNext</b> 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: - <b>Efficient DOM Rendering:</b> Experience fast and efficient DOM updates and rendering, crucial for high-performance web applications. - <b>Reactive State Management:</b> Manage component states reactively, ensuring UIs are always up-to-date with the underlying data. - <b>Enhanced Developer Experience:</b> Enjoy a seamless development experience with features like TypeScript support, comprehensive API documentation, and easy-to-understand examples. - <b>Flexible Component Composition:</b> Leverage advanced component composition techniques to build complex UIs with ease. - <b>Resource Management:</b> Efficiently manage resources with destructors, preventing common issues like memory leaks. <b>gNext</b> 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 <b>gNext</b> 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](https://react-pdf.org/): ```ts 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:** ```ts 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](https://g-next.netlify.app/renderers) for interactive examples of all renderers. ### Notes - modifiers API: ```js function modifier(element: Element, ...args: Args) { return () => { // destructor } } ``` - helpers API: ```js 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. ```ts 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}}` ```gts <template> {{#if this.isVisible}} <div>Content is visible</div> {{else}} <div>Content is hidden</div> {{/if}} </template> ``` #### List rendering with `{{#each}}` ```gts <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): ```gts {{#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: ```ts 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: ```gts 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: ```gts <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: ```ts 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: ```js import { defineConfig } from "vite"; import { compiler } from "@lifeart/gxt/compiler"; export default defineConfig(({ mode }) => ({ plugins: [compiler(mode)], })); ``` To render root component, use `renderComponent` function. ```js 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. ```js import { destroyElement } from "@lifeart/gxt"; destroyElement(Instance); ``` ### Testing GXT provides test utilities for writing component tests with QUnit: ```ts 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`: ```json { "compilerOptions": { // ... your options }, "glint": { "environment": "glint-environment-gxt" } } ``` This enables type-safe templates with autocompletion and error checking in your IDE.