Cami.js Documentation

repository·master·Indexed 19 days ago

https://github.com/kennyfrc/cami.js

A minimalist and flexible toolkit for adding interactive 'islands' and state management to hypermedia-driven, server-rendered, or static HTML web applications. Cami.js provides a reactive component model using ReactiveElement and template tags, a typed state management system via ObservableStore with actions and memos, and browser integration tools like URLStore and versioned localStorage. It supports both lightweight CDN usage and a full TypeScript-enabled workflow.

Tokens
39.8K
Snippets
122
Records
173
Agent score
62%

What's inside cami.js

  1. Add rich interactions to server-rendered HTML with Cami.js

    master

    Cami.js is a toolkit for creating interactive islands within server-rendered multi-page applications (MPAs). It allows you to add reactivity to specific parts of a page (using Rails, FastAPI, Django, Laravel, etc.) without converting the entire application into a Single Page Application (SPA) or requiring a complex build step. You can use the CDN bundle for direct script tag inclusion or install it as an ES module for compiled JavaScript/TypeScript projects.

    <!-- Example of adding a Cami island to an HTML page -->
    <cami-counter></cami-counter>
    <script src="https://unpkg.com/cami@0.4/build/cami.cdn.js"></script>
    <script type="module" src="./island.js"></script>
  2. Core component features in Cami.js

    master

    Cami.js uses an 'islands architecture' to add interactivity to server-rendered or static pages. Components are built using standard custom elements with the following capabilities:

    • Reactive Public Fields: Define properties on standard custom elements that trigger reactivity.
    • Templating: Use lit-html templates via html and svg functions.
    • Lifecycle: Supports native custom-element lifecycle callbacks.
    • Post-render logic: Use the keyed afterRender() method to execute code after the DOM has been committed.
    • Resource Management: Components can own their own resources and manage ephemeral drafts.
  3. Use Cami's core interfaces

    master

    Cami provides several interfaces to manage different types of state and lifecycle responsibilities:

    InterfaceWhat it managesUse it for
    Browser APIsHTML, focus, form behavior, custom-element lifecycleSemantic page structure and progressive enhancement
    ReactiveElementOne island's reactive fields and rendered DOMLocal interaction state
    store() / ObservableStoreNamed shared state and state transitionsCross-island state, queries, mutations, orchestration
    URLStoreURL-derived navigation stateHash routes and route resources
  4. Implement cross-component client state management with Stores

    master

    Cami provides a store utility to manage shared state across multiple components. A store consists of a state object, actions for modifying that state, and memos for computing derived values.

    To use a store:

    1. Create the store: Use store({ name: 'StoreName', state: { ... } }).
    2. Define Actions: Use store.defineAction(name, handler) to mutate state. The handler receives { state, payload }.
    3. Define Memos: Use store.defineMemo(name, selector) to create derived state. The selector receives { state }.
    4. Read State: In a component's template(), call store.getState() to access the current state.
    5. Access Memos: Use store.memo(name) to retrieve a computed value.
    6. Dispatch Actions: Use store.dispatch(name, payload) to trigger state changes.
    import { store, html, ReactiveElement } from 'cami';
    
    // 1. Create the store
    const CartStore = store({
      name: "CartStore",
      state: { cartItems: [] },
    });
    
    // 2. Define actions
    CartStore.defineAction("add", ({ state, payload }) => {
      state.cartItems.push({ ...payload, cartItemId: Date.now().toString() });
    });
    
    // 3. Define memos
    CartStore.defineMemo("cartTotal", ({ state }) => {
      return state.cartItems.reduce((acc, item) => acc + item.price, 0);
    });
    
    // 4. Use in a component
    class CartElement extends ReactiveElement {
      template() {
        const { cartItems } = CartStore.getState(); // Read state
        const total = CartStore.memo("cartTotal"); // Read memo
    
        return html`
          <div>
            <p>Total: $${total}</p>
            <button @click=${() => CartStore.dispatch("remove", { id: 1 })}>Remove</button>
          </div>
        `;
      }
    }
  5. Manage shared state with Cami.js

    master

    Cami.js provides a robust state management system for coordinating data across components:

    • Typed Store Modules: Create named, type-safe modules for shared state.
    • Reactivity: Use synchronous actions and dependency-tracked memos to handle state changes.
    • Data Fetching: Includes support for cached server queries and mutations, optimistic updates, and query invalidation.
    • Advanced Workflows: Orchestrate complex logic using async hooks, specs, and state machines.
  6. How the URL model is parsed

    master

    Cami parses the URL hash into three distinct parts: hashPaths, params (query parameters), and hashParams (parameters following a second #). When a route contains path parameters (e.g., :id), their values are extracted into routeParams.

    #chats/42?tab=documents#preview=page-2
    
    # Results:
    hashPaths  = ["chats", "42"]
    params     = { tab: "documents" }
    hashParams = { preview: "page-2" }
  7. How to create a virtual reference for Anchored Popovers

    master

    Because features like mouse tracking or text selection do not have a persistent DOM element, you must create a 'virtual element'. This object must respond to getBoundingClientRect() to tell the component where to anchor the popover.

    When the user moves the mouse or selects text, you should update the coordinates returned by this virtual element's getBoundingClientRect() method to trigger repositioning.

  8. Use a store for shared state across islands

    master

    Use store() when multiple islands (independent components/elements) need to read from or modify the same state. Stores are also appropriate for named transitions.

    When to use a store:

    • Synchronizing state across different parts of the UI (e.g., cart contents, session UI, editor state).
    • Managing cached server records.
    • Handling state-machine workflows.

    When NOT to use a store:

    • Do not create a store simply to make a value global; only use it when multiple islands actually need to share the state and its operations.
  9. Understand the `Resource<T>` lifecycle and states

    master

    A Resource<T> is a discriminated union that tracks the state of an asynchronous operation. It is keyed by a status field:

    • idle, loading, or refreshing: May contain previous data while the new request is in flight.
    • success: Contains the successfully fetched data.
    • error: Contains an error object and may contain previous data.

    Usage Pattern:

    • Use store queries for server data that needs to be shared across multiple islands.
    • Use a component resource (like useImage) for work that is owned by a single element.
  10. Understand the Cami update loop

    master

    Cami uses a reactive update cycle to ensure predictable rendering. When state changes (via user events, actions, queries, or mutations), Cami marks dependent computations and elements as 'dirty'. The cycle follows these steps:

    1. State Change: A mutation or event triggers a change.
    2. Dirty Marking: Dependent elements are flagged.
    3. Template Execution: Each affected ReactiveElement runs its pure template() method.
    4. DOM Commit: lit-html updates the actual DOM based on template changes.
    5. Post-Render Effects: Keyed afterRender() effects execute.
    6. Settling: afterSettle() reactions run once all renders in the batch have settled.

    Critical Rule: Never perform state writes inside the template() method. State writes should be confined to event handlers, actions, or post-render work to avoid re-entrant render errors.

  11. Use formulas and effects in Cami.js

    master

    Cami.js allows you to define reactive logic using formulas (getters) and effects.

    • Formulas: You can define getter methods that depend on other reactive variables. These act like computed properties in other frameworks. When the underlying variables change, the formula's value is updated.
    • Effects: The effect method allows you to run code whenever observed properties change. This is similar to a watcher or autorun. Cami uses this same mechanism internally to re-render templates when observed properties change.

    In a typical workflow, you might have a variable like count, a formula like doubleCount that returns count * 2, and an effect that logs the new values whenever count is updated.

    // Conceptual example of formulas and effects
    class Counter {
      count = 0;
    
      // A formula (computed property)
      get doubleCount() {
        return this.count * 2;
      }
    
      setup() {
        // An effect (watcher/autorun)
        this.effect(() => {
          console.log('Count changed:', this.count);
          console.log('Double count:', this.doubleCount);
        });
      }
    }