# Hologram > Full-stack isomorphic Elixir web framework that lets you build interactive web applications entirely in Elixir, without writing JavaScript. - [Website](https://hologram.page) - [GitHub](https://github.com/bartblast/hologram) - [HexDocs](https://hexdocs.pm/hologram) ## Guides ### Introduction Hologram is a full-stack isomorphic Elixir web framework that runs on top of Phoenix. It lets developers create dynamic, interactive web applications entirely in Elixir. Through intelligent code analysis and transformation, Hologram compiles the necessary parts of your Elixir code to JavaScript, delivering modern frontend functionality without requiring any JavaScript frameworks or direct JavaScript coding. #### Key Features - Pure Elixir development: write your entire web application in Elixir without needing to write JavaScript code - Client-side state: state management happens in the browser for snappy user interfaces - Automatic code distribution: Hologram automatically handles the separation and conversion of client/server code - Seamless communication: client-server interaction happens automatically over HTTP/2 request/response and a Server-Sent Events stream - no manual setup needed, with the speed of WebSockets and better scalability - Server-side rendering (SSR): every page is fully rendered to HTML on the server on first load - SEO-friendly and fast to first paint - then becomes interactive on the client #### Framework Philosophy Hologram was created with several key principles in mind: - Developer experience: focus on writing features instead of boilerplate code - Convention over configuration: follow established patterns while maintaining flexibility - Unified language: use Elixir for both client and server-side logic - Component-based architecture: build applications using reusable building blocks #### Inspiration Hologram draws inspiration from several successful web technologies: Elm, Phoenix LiveView, Surface and Ruby on Rails. ### Installation This guide will walk you through the installation process step by step. #### Prerequisites - `Elixir` version 1.19 or higher - `OTP` version 28.1 or higher - A working `Phoenix` application (see Phoenix installation guide: https://hexdocs.pm/phoenix/installation.html#phoenix) - `Node.js` version 20 or higher and `npm`, if not already installed, you can get them via `asdf`: ``` $ asdf plugin add nodejs $ asdf install nodejs latest $ asdf global nodejs latest ``` See asdf installation guide (https://asdf-vm.com/guide/getting-started.html) if you need to install asdf first. #### 1. Add Hologram Package Add the Hologram package to your dependencies list in `mix.exs`: ```elixir {:hologram, "~> 0.11"}, ``` #### 2. Fetch Dependencies Run the following command to fetch the Hologram package: ``` $ mix deps.get ``` #### 3. Configure Compiler Add the Hologram compiler to your project configuration in `mix.exs`: ```elixir def project do [ # ... compilers: Mix.compilers() ++ [:hologram], # ... ] end ``` #### 4. Configure Router Add the Hologram router plug before your Phoenix router plug in your endpoint: ```elixir defmodule MyAppWeb.Endpoint do # ... plug Hologram.Router plug MyAppWeb.Router end ``` #### 5. Configure Static Asset Serving Add the Hologram directory to the list of served static directories in your endpoint: ```elixir plug Plug.Static, # ... only: ["hologram" | MyAppWeb.static_paths()] ``` #### 6. Configure Formatter Import Hologram formatter rules in `.formatter.exs`: ```elixir [ # ... import_deps: [..., :hologram] # ... ] ``` #### 7. Update GitIgnore Add the following line to your `.gitignore` file to exclude Hologram generated JavaScript bundles: ``` # Hologram generated JavaScript bundles. /priv/static/hologram/ ``` #### 8. Optional: Configure App Directory Many developers prefer to organize their Hologram pages and components in an `app` directory outside of `lib` for better project structure. If you want to use this approach, modify your existing `elixirc_paths/1` functions in `mix.exs` to include the `app` directory: ```elixir defp elixirc_paths(:test), do: ["app", "lib", "test/support"] defp elixirc_paths(_env), do: ["app", "lib"] ``` This allows you to organize your code like `app/pages/`, `app/components/`, etc., which many developers find cleaner than keeping everything in `lib/`. #### Running the App Start your app with the `mix holo` task. It runs your Phoenix server with the Hologram compiler and client-side runtime enabled: ``` $ mix holo ``` In the `dev` and `test` environments, Hologram does not start with a plain `mix phx.server` - set the `HOLOGRAM_START=1` environment variable to enable it (which is exactly what `mix holo` does). This keeps the Hologram compiler out of your workflow until you need it, so unrelated tasks and tests stay fast. Production builds always compile Hologram, so this applies only to dev and test. #### Editor Support The official Hologram extension adds syntax highlighting for `.holo` templates and `~HOLO` sigils. Install it from the Visual Studio Marketplace (https://marketplace.visualstudio.com/items?itemName=bartblast.hologram) for VS Code, or the Open VSX Registry (https://open-vsx.org/extension/bartblast/hologram) for Cursor, VSCodium and other compatible editors. #### Umbrella Projects Hologram supports umbrella projects. The steps above apply to the child app that owns the Phoenix endpoint - it takes the Hologram dependency (step 1), the compiler entry (step 3), the router plug (step 4), the static asset configuration (step 5) and the gitignore entry (step 7). Child apps that only define pages and components take the dependency alone, without the compiler entry. Their pages are discovered and bundled when the endpoint app compiles. A few umbrella-specific details: - Endpoint configuration lives in the umbrella root `config` directory, as it does for any umbrella app. - Run `mix holo` from the umbrella root. Started from inside the endpoint app it watches only that app's sources, so changes in sibling apps will not trigger a live reload. - Add `listeners: [Phoenix.CodeReloader]` to the `project` config in the umbrella root `mix.exs`. Mix reads listeners from the project it runs as, so the entry Phoenix generates in the endpoint app is not seen when `mix holo` runs from the root, and every reload logs a missing-listener warning. - npm packages go in an `assets` directory at the umbrella root, and are resolved from there for every child app. - Hologram supports one endpoint app per running BEAM instance. If several apps in the umbrella have a configured Phoenix endpoint, Hologram raises a descriptive error. #### Next Steps After completing the installation, you can start building your isomorphic web application with Hologram. For more information on how to use Hologram's features, please refer to the website's Documentation section. #### Notes - Make sure all dependencies are properly installed and configured - Verify that your Phoenix application is properly set up before installing Hologram ### Quick Start Before starting, make sure you have installed Hologram correctly by following the Installation Guide. Let's build a simple blog application to demonstrate Hologram's key features. Our blog will have: - A home page listing blog posts - Individual post pages - A layout with navigation - The ability to like posts #### Creating the Layout First, let's create a layout that will be shared across all pages: ```elixir defmodule Blog.MainLayout do use Hologram.Component alias Hologram.UI.Link alias Hologram.UI.Runtime def template do ~HOLO""" My Blog
""" end end ``` #### Creating the Home Page Now let's create the home page that lists blog posts: ```elixir defmodule Blog.HomePage do use Hologram.Page alias Blog.Components.PostPreview route "/" layout Blog.MainLayout def init(_params, component, _server) do # In real app, fetch from database posts = [ %{id: 1, title: "First Post", excerpt: "This is my first post"}, %{id: 2, title: "Second Post", excerpt: nil} ] put_state(component, :posts, posts) end def template do ~HOLO"""

Welcome to my Blog

{%for post <- @posts} {/for}
""" end end ``` #### Creating a Post Preview Component Let's create a reusable component for displaying post previews: ```elixir defmodule Blog.Components.PostPreview do use Hologram.Component alias Hologram.UI.Link prop :post, :map def template do ~HOLO"""

{@post.title}

{@post.excerpt}

Read more
""" end end ``` #### Creating the Post Page Now let's create a page for individual posts with a like button: ```elixir defmodule Blog.PostPage do use Hologram.Page route "/posts/:id" param :id, :integer layout Blog.MainLayout def init(params, component, _server) do # In real app, fetch from database post = %{ id: params.id, title: "Example Post", content: "This is the full content...", likes: 0 } put_state(component, :post, post) end def template do ~HOLO"""

{@post.title}

{@post.content}

""" end def action(:like_post, _params, component) do # Update likes locally first for instant feedback component |> put_state([:post, :likes], component.state.post.likes + 1) |> put_command(:save_like, post_id: component.state.post.id) end def command(:save_like, params, server) do # In real app, save to database IO.puts("Liked post #{params.post_id}") server end end ``` #### Key Concepts Demonstrated 1. Pages: both `HomePage` and `PostPage` are Hologram pages with their own routes 2. Components: the `PostPreview` component shows how to create reusable UI elements 3. Layout: the `MainLayout` provides a consistent structure across pages 4. Template Syntax: - Using `{%for}` loops - Interpolating values with `{@var}` - Event binding with `$click` 5. Navigation: using `Link` component to navigate between pages 6. State Management: using `put_state/3` to manage component state 7. Actions & Commands: the like button demonstrates: - Client-side action for immediate UI update - Server-side command for persistence 8. Props: component properties with the `prop/2` macro 9. Parameters: route parameters with the `param/2` macro This example demonstrates the basic building blocks of a Hologram application. The framework handles the client-server communication and state management automatically, letting you focus on building features. Remember that Hologram keeps state on the client side for better performance while using commands for server-side operations when needed. ## Documentation ### Architecture Hologram's architecture is designed around simplicity and developer experience. Here's how it works: #### Pages and Components The framework breaks down web applications into two fundamental building blocks: - Pages: top-level components that represent different routes in your application - Components: reusable UI elements that can be composed together #### Code Distribution Hologram automatically analyzes your code and: 1. Determines which parts should run on the client vs server 2. Compiles necessary Elixir code to JavaScript for browser execution 3. Ships only the code and protocol implementations your app can actually reach, keeping client bundles small 4. Sets up all required client-server communication Protocol implementations are included by type reachability. A struct's implementations (e.g. `String.Chars`) ship to the browser when the compiler finds the struct type in client-reachable code, in server-executed `init` or command code, or in action broadcasting code - not merely because a dependency defines them. If a type is resolved dynamically at runtime (for example from a string or external data), reference its struct module anywhere in client-reachable code to include its implementations. #### Client-Side State State is maintained in the browser, which: - Enables immediate UI updates - Reduces server load - Simplifies the programming model #### Actions and Commands Code execution is organized into two types of operations: - Actions: client-side operations that run in the browser - Commands: server-side operations for tasks like database access Both can be triggered by user interactions, and they can trigger each other. #### Client-Server Communication Communication is wired up automatically - no manual HTTP configuration and no boilerplate code - over two complementary transports: - HTTP/2 request/response handles all action-command round-trips, practically as fast as WebSockets for real-time updates - A Server-Sent Events (SSE) stream carries server-pushed updates - this is how broadcast actions reach every subscribed client without the client polling for changes Both legs are plain HTTP, which keeps the whole system: - Easy to debug with standard HTTP tools and browser DevTools - Seamless through corporate proxies and firewalls #### Runtime Behavior The framework: 1. Loads the initial page from the server 2. Mounts the page in the browser 3. Manages the virtual DOM for efficient updates 4. Handles all client-server communication automatically ### Template Syntax Hologram templates use a custom syntax called "HOLO" that combines HTML with Elixir expressions. #### Regular HTML Markup You can use standard HTML elements and attributes in your templates: ```holo

Hello World

This is a paragraph.

``` #### Accessing Props and State Props and state are accessible in templates using the `@var` syntax. This provides a convenient way to reference component data. For example, if you have a prop or state variable named `count`, you can access it as `@count` in your template. In the following sections, you'll see how to use these variables in Elixir expressions for interpolation and control flow blocks. #### Component Nodes Components can be used as custom elements in your templates. They can receive props (properties) as attributes. String props can be given as regular double-quoted attributes, while other Elixir values (like numbers, booleans, or expressions) are given using curly braces syntax: ```holo ``` For more information about components and their props, see the Components documentation. #### Dynamic Nodes Which node a tag renders doesn't have to be decided literally. A braced Elixir expression in tag-name position is evaluated at render time and its value decides what gets rendered: a component module renders that component, a string renders an HTML element with that tag name, and any other value raises an `ArgumentError`. Components are the usual reason to reach for this - a widget chosen by a content type, a step in a wizard, a row in a heterogeneous list. The attributes become the component's props and the content between the tags becomes its default slot, exactly as with a statically named component: ```holo <{@module} cid="my_component" title="Hello" /> <{@module} cid="my_component">slot content ``` A page module isn't valid here, even though a page is a kind of component itself - it's a root one, owning a route, taking its params from the URL and picking a layout, so it only makes sense at the root of a render, never as a node inside another template. A call site that can't name the module statically usually can't name the props either, so dynamic tags pair naturally with the attribute and prop spread described below - both the module and its props come from the data: ```holo {%for widget <- @widgets} <{widget.module} ...{widget.props} /> {/for} ``` A string value renders an element instead, with the attributes becoming DOM attributes and event bindings - so a heading level or an anchor-versus-button choice can be kept in state. Whatever string arrives is the tag that gets rendered, so tag names must never come from untrusted input: ```holo <{@heading_tag} class="heading">{@text} ``` The expression is ordinary Elixir compiled in the template's module context, so aliases in scope apply and `@var` resolves to a prop or state value, exactly as anywhere else in a template. Most often it's simply a state value holding a module or a tag name: ```elixir put_state(component, module: MyButton, heading_tag: "h2") ``` Nothing beyond the dispatch is new. Once resolved, a dynamic node behaves exactly like the equivalent static one - `<{MyButton} label="x">` is `` and `<{"div"} class="x">` is `
`. All prop, attribute, event, void element, spread and slot semantics carry over unchanged. Since dispatch happens per render, changing the module under a given `cid` replaces the component: the outgoing one is unmounted and the incoming one initializes its own state from scratch. ##### Closing Tags A dynamic tag with children is closed with ``, repeating the opening expression verbatim, as in the examples above. That repetition is a good reason to keep the expression short. Logic written inline - an `if` picking between an anchor and a button, a `case` mapping a content type to a component - gets spelled out twice, once in each tag, and the two copies can drift apart. Keep the value in state or a prop, or compute it with a helper function. ##### Module Reachability To render a dynamic component on the client, the compiler must know its module at build time. It bundles a component whenever the module atom appears as a literal in code it can see - a template, any client-reachable function, a page's or component's `init/3` or `command/3` server callback, or any code that broadcasts an action - `put_broadcast/3,4` inside a handler, `Hologram.Realtime.broadcast_action/2,3` outside one. So a module put into state by `init/3` or sent back in action params by `command/3` works without any extra declaration. The search follows calls forward, so functions those callbacks call count too, but their own callers don't - a module picked elsewhere and passed down into a broadcasting helper won't be found. Modules that exist only at runtime don't work at all: ones built with `Module.concat/1`, resolved with `String.to_existing_atom/1`, or read from the database or config without a literal anywhere in the app's code are not bundled, and rendering them on the client raises. A component referenced in broadcast code goes into the shared runtime bundle rather than a page bundle, so it ships with every page - a broadcast is addressed to a `cid`, and which page has that `cid` mounted is only known at runtime. #### Elixir Expression Interpolation You can embed Elixir expressions in your templates using curly braces: ##### Inside Text ```holo

Hello, {@name}!

``` ##### Inside Attributes and Props You can interpolate Elixir expressions in attributes and props in two ways. First, you can interpolate the entire attribute value: ```holo
Content
``` Or you can interpolate part of the attribute value within double quotes: ```holo
Content
``` Both approaches work for regular HTML attributes and component props: ```holo ``` ###### Conditional Attributes When an attribute expression evaluates to a falsy value (`nil` or `false`), the attribute is not rendered at all. This is useful for conditional attributes: ```holo
Content
``` In this example, when `@loading?` is `false`, the `disabled` attribute won't appear in the HTML. Similarly, when `@active?` is `false`, the `class` attribute will be omitted entirely. ##### Security: Automatic HTML Escaping For security purposes, interpolated expressions in text content and HTML attributes are automatically HTML-escaped to prevent XSS (Cross-Site Scripting) attacks. This means that potentially dangerous characters like `<`, `>`, `&`, and quotes are converted to their HTML entity equivalents (`<`, `>`, `&`, etc.). ```holo

User input: {@user_input}

``` If `@user_input` contains `""`, it will be safely rendered as escaped text rather than executed as JavaScript. This escaping happens automatically for all values interpolated in text content and HTML attributes. ###### Inside Script Elements A ` ``` Whatever the value holds, it cannot end the string literal it sits in, and it cannot end the script element - so the `""` case above is covered here too. An interpolation written without quotes, such as `var x = {@value};`, puts the value in code position rather than text position. It is escaped like any other interpolation in a script, but the escaping only means something inside a string literal. Outside one, a value of `alert(1)` has nothing to escape and simply runs, while a value holding a quote or a `<` becomes `\"` or `\u{3C}`, which are a syntax error in code position and stop the whole script. Being valid JavaScript is not the same as being safe. Write a bare interpolation only for code your own application generates, such as a number or a boolean, and never for anything coming from a user, a request or a database. To pass structured data, interpolate into a string and parse it: `JSON.parse("{@json}")`. ###### Inside Style Elements A ` ``` The braces of a CSS rule are template syntax, so escape them as `\{` and `\}`, or wrap the stylesheet in a `{%raw}...{/raw}` block. No value can end the literal it sits in or the element around it, and `>` and `&` inside a value are left alone, so selectors and `content` strings arrive as written. Without quotes, as in `color: {@value};`, the value lands in code position instead. Most CSS survives that: braces, colons, semicolons, parentheses and whole rules pass through unchanged, so a fragment the application generated can be interpolated in one piece. A newline, a quote and a `<` do not. Each becomes a CSS escape sequence, which is a parse error outside a string literal and drops the declaration around it. A bare interpolation is only as safe as the value. Write one for values the application produced itself, and never for anything coming from a user, a request or a database. #### Attribute and Prop Spread A bare braced expression prefixed with `...` injects a map's or keyword list's entries as the tag's attributes (on elements) or props (on components). The `...` marker sits outside the braces, so their content is ordinary Elixir - any expression is allowed: ```holo
Content
``` The expression must evaluate to a map or a keyword list - anything else, including `nil`, raises an `ArgumentError`. Bare keyword shorthand works inside the braces, the same convention event bindings already use: ```holo
Content
``` Spread adds no rules of its own: every entry behaves exactly as if you had written it as a named attribute or prop at that position. On elements, values are stringified and falsy ones drop the attribute, exactly as described above. On components, values stay raw Elixir terms and undeclared keys are silently ignored, just like named props. ##### Attribute Names On elements, underscores in a key convert to hyphens, whether the key is an atom or a string - `:data_value` and `"data_value"` both render `data-value`. A map or keyword list value nests, composing the name with hyphens at each level, so the example above renders `data-user-id`. Literal markup is never touched, so writing the attribute out by hand remains the exact-name escape hatch. On components, neither rule applies. A key like `:my_prop` sets the prop declared with exactly that name, with no hyphen conversion, since prop names are Elixir atoms. Values don't nest either - a map value is not expanded into multiple props, it's simply what that one prop receives. The `cid` prop may be supplied through a spread. Event bindings cannot be set through a spread. A `$`-prefixed key raises an `ArgumentError`, because silently not binding an intended event would be worse than erroring. Write events as literal attributes. They don't need a spread to be conditional anyway - a binding whose operation resolves to `nil` is switched off, as described in the Events documentation. The entries a single spread expands to render in alphabetical order. Map key order is undefined in Elixir, so sorting is what keeps the output reproducible. Attributes you write out yourself keep their position. ##### Precedence Multiple spreads are allowed, and they interleave freely with named attributes and props. Names resolve by position, and the last one wins - so a literal before a spread is a default, and a literal after it is forced: ```holo
Content
Content
Content
``` ##### Forwarding Wrapper There is nothing to declare on the receiving component - spread resolves at the call site, so props arrive indistinguishable from named ones and every existing component is already a valid target. To let a wrapper pass arbitrary attributes through to an inner element, declare an ordinary map prop: ```elixir prop :html_attrs, :map ``` and spread it in the wrapper's template: ```holo ``` Callers then pass the attributes as a single prop: ```holo Save ``` #### Control Flow Blocks ##### If Block Use `{%if}` blocks for conditional rendering. The condition follows Elixir's truthiness rules - only `nil` and `false` are considered falsy, while any other value is considered truthy. You can optionally include an `{%else}` branch. Simple if block without else: ```holo
{%if @show_message?}

Message is visible

{/if}
``` If block with else branch: ```holo
{%if @show_message?}

Message is visible

{%else}

Message is hidden

{/if}
``` ##### For Block Use `{%for}` blocks to iterate over collections. The syntax follows Elixir's comprehension rules, allowing you to use the same pattern matching and filtering capabilities as regular Elixir comprehensions. ```holo
    {%for item <- @items}
  • {item.name}
  • {/for}
``` #### Event Binding You can bind events to elements using event attributes. For detailed information about event binding syntax and available event types, see the Events documentation. #### Escaping Curly Braces To output literal curly braces in your template, escape them with a backslash: ```holo

\{@literal\} {@variable}

``` #### Raw Block Use `{%raw}` blocks to output content without processing: ```holo
{%raw} This content will be output as-is, including any \{curly braces\} or \{%control\}...\{/flow\} syntax. {/raw}
``` #### HTML Comments You can use standard HTML comments in your templates: ```holo

Content

``` ### Components Components are the building blocks of Hologram applications. They are reusable pieces of UI that can be composed together to create complex interfaces. #### Basic Example Here's a simple stateless component that displays a greeting: ```elixir defmodule Greeting do use Hologram.Component prop :name, :string prop :title, :string, default: "Mr." def template do ~HOLO"""

Hello, {@title} {@name}!

""" end end ``` Usage: ```holo ``` #### Props Props are the primary way to pass data to components. They are defined using the `prop/2` macro receiving `name` and `type` params: ```elixir prop :user_id, :integer ``` or `prop/3` macro that can receive the third `opts` param: ```elixir prop :user_id, :integer, default: 123 ``` ##### Types Props can be typed using the following types: - `:any` - Any type (no type checking) - `:atom` - Atoms - `:boolean` - Booleans - `:bitstring` - Bitstrings - `:float` - Floats - `:function` - Functions - `:integer` - Integers - `:list` - Lists - `:map` - Maps - `:pid` - PIDs - `:port` - Ports - `:reference` - References - `:string` - Strings (UTF-8 encoded binaries) - `:tuple` - Tuples ##### Options ###### Default Value Set a default value for optional props using the `default` option: ```elixir prop :count, :integer, default: 0 ``` ###### Context Source Props can be sourced from the Context using the `from_context` option. This is useful for accessing shared data across components: ```elixir prop :user, :map, from_context: :current_user ``` For more information about Context, see the Context page. #### Stateful vs. Stateless Components Components can be either stateful or stateless: - **Stateless Components**: Don't maintain any internal state. They render based solely on their props. - **Stateful Components**: Maintain internal state and can update it over time. They are identified by a unique `cid` (component ID) and can have actions and commands. Stateful components can be initialized using either `init/3` (server-side) or `init/2` (client-side) functions. ##### Component ID (cid) To make a component stateful, provide a `cid` when using it: ```holo ``` The `cid` is used to target actions and commands at specific components using the `target` field: ```holo ``` ##### Initialization **Important:** Each stateful component instance is initialized **exactly once** during its lifetime. The initialization method depends on where the component's lifecycle starts: - `init/3`: When the component's lifecycle starts as part of server-side page rendering (pages are always first rendered on the server when you navigate to them) - `init/2`: When the component's lifecycle starts by being dynamically added to an already-loaded page A component module can define both functions since you may have multiple instances - some starting on the server and others starting directly on the client. Each instance has its own unique `cid` and separate state. ###### Server-side Initialization (init/3) Called when the component starts its lifecycle on the server. Provides access to cookies and session data through the `Server` struct. It receives three parameters: - `props` - a map containing the component's props - `component` - a `Component` struct representing the client-side state container - `server` - a `Server` struct representing the server-side state container (with access to cookies and session) The function should return either: - A tuple containing `Component` and `Server` structs when modifying both client and server state - Just a `Component` struct when only modifying client state - Just a `Server` struct when only modifying server state Example: ```elixir def init(_props, component, server) do component = put_state(component, :kill_count, 439) server = put_session(server, :name, "John Wick") {component, server} end ``` ###### Client-side Initialization (init/2) Called when the component starts its lifecycle directly on the client. No access to cookies or session data since they are managed through the `Server` struct. It receives two parameters: - `props` - a map containing the component's props - `component` - a `Component` struct representing the client-side state container The function should return a `Component` struct with any initial state. Example: ```elixir def init(_props, component) do put_state(component, kill_count: 439, name: "John Wick") end ``` ###### Chaining Actions in Initialization In both `init/3` and `init/2` functions, you can chain actions that will be executed when the component is mounted on the client. This is useful for triggering setup logic, animations, or data loading after the component becomes available in the browser. Server-side initialization (`init/3`): ```elixir def init(_props, component, _server) do put_action(component, :setup_timer) end ``` Client-side initialization (`init/2`): ```elixir def init(_props, component) do put_action(component, :start_animation) end ``` ###### Optional Initialization Both `init/3` and `init/2` functions are optional. If you don't need to initialize state or perform any preparation, you can omit them entirely - Hologram provides default implementations that simply return the `Component` and `Server` structs unchanged. ###### Props and State Name Collisions Give props and state entries different names. Templates merge the two with state taking precedence, so a state entry sharing a prop's name shadows it entirely. Copying a prop into a same-named state entry during `init` is the usual way to hit this: initialization runs only once, so the copy never refreshes, and the template keeps reading the copy rather than the prop. New values from the parent arrive on every render, none of them reach the screen, and nothing warns you. When a prop supplies a starting value that the component goes on to own, separate names - an `:initial_count` prop feeding a `:count` state entry - keep the prop reachable and make the snapshot obvious. #### Slots Slots allow components to accept and render child content. They are defined using the `` tag in the component's template: ```elixir defmodule Card do use Hologram.Component def template do ~HOLO"""
""" end end ``` Usage: ```holo

Card Title

Card content

``` #### Templates ##### Defining Templates Hologram components can be templated using either a template function within the component module or a separate `.holo` file. These methods offer the same features and syntax - your choice depends on your team's coding style and project structure. ###### Template Function ```elixir def template do ~HOLO"""
Hello, {@name}!
""" end ``` ###### Colocated .holo File Create a file with the same name as your component's module file but with `.holo` extension (e.g., if your component is in `my_component.ex`, create `my_component.holo`). The `.holo` file must be located in the same directory as the module file. Unlike the template function approach, colocated `.holo` files contain only the template markup without the `~HOLO` sigil: ```holo
Hello, {@name}!
``` ##### Template Syntax See the Template Syntax documentation for more details. #### Event Handling Stateful components can handle user interactions and business logic through two mechanisms: - Actions: client-side operations that update local state - Commands: server-side operations that can perform remote tasks Example: ```elixir def action(:increment, params, component) do put_state(component, :count, component.state.count + params.by) end def command(:insert_user, params, server) do {:ok, user} = Repo.insert(%User{first_name: params.first_name}) put_action(server, :user_inserted, user: user) end ``` See the Actions and Commands documentation for more details. ### Pages Pages are specialized components that serve as entry points for different routes in your Hologram application. Together with regular components, they form the fundamental building blocks of Hologram applications. They inherit all the capabilities of components, including templates, state management, actions, commands, and event handling. For detailed information about these shared features, see the Components documentation. #### Key Differences from Regular Components While pages are built on top of components, they have some unique characteristics: - They must define a route using the `route/1` macro - They must specify a layout component using either `layout/1` or `layout/2` macro, which serves as the root of the component tree and wraps the page's content (see Layouts for more information) - They use URL parameters instead of props - They are always stateful - They are always initialized on the server-side using `init/3`, unlike regular components which can be initialized on either client or server - Their `init/3` function receives URL parameters (`params`) instead of component props (`props`) #### Server-Side Rendering (SSR) Every page is fully rendered to HTML on the server on first load, then becomes interactive on the client. Because the browser (and any crawler) receives complete, server-rendered markup, Hologram pages are SEO-friendly and fast to first paint out of the box, with no extra configuration. This is why a page's `init/3` always runs on the server: it produces the state used to render the initial HTML. Once the page is mounted in the browser, client-side state takes over and subsequent interactions happen without full page reloads (see Navigation for how transitions between pages work). #### Basic Example Here's a simple page implementation: ```elixir defmodule MyApp.GreetingPage do use Hologram.Page route "/hello/:username" layout MyApp.MainLayout def init(params, component, _server) do put_state(component, :username, params.username) end def template do ~HOLO"""
Hello, {@username}!
""" end end ``` ##### Chaining Actions in Initialization You can chain actions that will be executed when the page is mounted on the client: ```elixir def init(_params, component, _server) do put_action(component, :load_user_data) end ``` ##### Optional Initialization The `init/3` function is optional. If you don't need to initialize state or perform any preparation, you can omit it entirely - Hologram provides a default implementation that simply returns the `Component` and `Server` structs unchanged. #### Routing and Parameters Pages must define their URL route using the `route/1` macro. Routes can be static: ```elixir route "/products" ``` or contain dynamic parameters: ```elixir route "/users/:username/posts/:post_id/comments" ``` For routes with parameters, use the `param/2` macro to specify how string parameters should be converted: ```elixir param :username, :string param :post_id, :integer ``` Supported parameter types: - `:atom` - converts to atom - `:float` - converts to float - `:integer` - converts to integer - `:string` - keeps the parameter as string ##### Route Resolution Hologram's router uses a search tree rather than ordered routing (like Phoenix or Rails). Static segments are always prioritized over parameterized ones automatically - so `/users` will always match before `/:username`, regardless of the order pages are defined. This eliminates a whole class of route shadowing bugs that are common in ordered routers, where adding a route in the wrong position can silently break another. The tradeoff is that you can't have two ambiguous parameterized routes at the same level (e.g. `/:username` and `/:post_slug`). In practice, this is a design smell in any framework - the fix is always to give them distinct prefixes, such as `/users/:username` and `/posts/:post_slug`. #### Component ID The page is always assigned a component ID (cid) of `"page"`. This is important to remember when targeting actions or commands at the page. ### Layouts Layouts are regular components that serve as the root of the component tree for pages. They don't have any specialized features - they are just components that wrap the page's content. For detailed information about components, see the Components documentation. #### Specifying a Layout for a Page Each page must specify which layout component to use. This can be done in one of the following ways. Using the `layout/1` macro to specify just the layout component: ```elixir layout MyApp.MainLayout ``` Using the `layout/2` macro to specify the layout component and its props: ```elixir layout MyApp.MainLayout, page_title: "My Page", show_sidebar?: true ``` Using the `layout/1` macro and setting props through the page's `init/3` function: ```elixir layout MyApp.MainLayout def init(_params, component, _server) do put_state(component, page_title: "My Page", show_sidebar?: true) end ``` #### Layout Template A layout template must include: - The `Hologram.UI.Runtime` component inside the `head` tag - it contains Hologram runtime and page JS bundles - The `` tag where the page content will be inserted #### Basic Example Here's a complete layout component implementation: ```elixir defmodule MyApp.MainLayout do use Hologram.Component prop :page_title, :string def template do ~HOLO""" {@page_title} """ end end ``` #### Component ID The layout component is always assigned a component ID (cid) of `"layout"`. This is important to remember when targeting actions or commands at the layout component. ### Events Events in Hologram are user interactions that can trigger actions or commands. They are the primary way to make your application interactive and responsive to user input. #### Event Types Hologram supports various event types that you can bind to elements in your templates (more are coming soon): - `$blur` - triggered when an element loses focus - `$change` - triggered when the value of an input element changes - `$click` - triggered when an element is clicked - `$click_outside` - triggered when a click occurs outside an element and its descendants - `$focus` - triggered when an element receives focus - `$key_down` - triggered when a key is pressed down - `$key_up` - triggered when a key is released - `$mouse_move` - triggered when the mouse cursor moves over an element - `$pointer_cancel` - triggered when a pointer event is cancelled (e.g., when the browser decides the pointer will no longer generate events) - `$pointer_down` - triggered when a pointer (mouse, touch, pen) is pressed down on an element - `$pointer_move` - triggered when a pointer moves while over an element - `$pointer_up` - triggered when a pointer (mouse, touch, pen) is released from an element - `$reach_bottom` - triggered when a scroll container's bottom edge comes into view - `$reach_left` - triggered when a scroll container's left edge comes into view - `$reach_right` - triggered when a scroll container's right edge comes into view - `$reach_top` - triggered when a scroll container's top edge comes into view - `$resize` - triggered when an element or the browser window is resized - `$scroll` - triggered when a scrollable element or the page is scrolled - `$select` - triggered when text is selected in an input or textarea element - `$submit` - triggered when a form is submitted - `$transition_cancel` - triggered when a CSS transition is cancelled before it completes - `$transition_end` - triggered when a CSS transition has finished - `$transition_run` - triggered when a CSS transition is created - `$transition_start` - triggered when a CSS transition has started #### Event Data When an event occurs, Hologram provides event data that you can access in the action or command that was bound to handle that event. This data is available in the `params` argument under the `:event` key. The event data structure varies depending on the event type: ##### Change Event The `$change` event data structure depends on where the event handler is placed: - **Input-level events:** include a `value` field with the specific element's value - **Form-level events:** include all form field values as a map, where keys are input names and values are current input values For detailed information about `$change` event behavior with forms, including synthetic event mapping, usage patterns, and data types for different input types, see the "Event Handling" section in the Forms documentation. ##### Keyboard Events Keyboard events (`$key_down`, `$key_up`) include: - `alt_key` - whether the Alt key was held - `code` - the physical key code (e.g. `"KeyK"`, `"Enter"`), independent of keyboard layout - `ctrl_key` - whether the Ctrl key was held - `key` - the value of the key pressed (e.g. `"k"`, `"Enter"`, `"ArrowUp"`), reflecting the layout and modifiers - `meta_key` - whether the Meta key (Command on macOS, Windows key on Windows) was held - `repeat` - whether the key is being held down and auto-repeating - `shift_key` - whether the Shift key was held To run a handler only for specific keys or key combinations, see the "Keyboard Key Filters" section below. ##### Mouse Events Mouse events (like `$mouse_move`) include: - `client_x` - X coordinate relative to the client area (viewport) - `client_y` - Y coordinate relative to the client area (viewport) - `movement_x` - horizontal movement delta since the last mouse event - `movement_y` - vertical movement delta since the last mouse event - `offset_x` - X coordinate relative to the target element - `offset_y` - Y coordinate relative to the target element - `page_x` - X coordinate relative to the page - `page_y` - Y coordinate relative to the page - `screen_x` - X coordinate relative to the screen - `screen_y` - Y coordinate relative to the screen ##### Pointer Events Pointer events (like `$click`, `$click_outside`, `$pointer_cancel`, `$pointer_down`, `$pointer_move`, `$pointer_up`) include: - `client_x` - X coordinate relative to the client area (viewport) - `client_y` - Y coordinate relative to the client area (viewport) - `movement_x` - horizontal movement delta since the last pointer event - `movement_y` - vertical movement delta since the last pointer event - `offset_x` - X coordinate relative to the target element - `offset_y` - Y coordinate relative to the target element - `page_x` - X coordinate relative to the page - `page_y` - Y coordinate relative to the page - `pointer_type` - the type of pointer that triggered the event, as an atom (`:mouse`, `:touch`, or `:pen`), or `nil` if the device type cannot be detected by the browser - `screen_x` - X coordinate relative to the screen - `screen_y` - Y coordinate relative to the screen ###### Click Event A `$click` binding fires on a plain click, but not on a modified one - the modifier signals that the user wants the browser's own behavior, such as opening a link in a new tab or window, so Hologram stays out of the way. A click counts as modified when any of these is held: - `⌥ Alt` - `⌃ Ctrl` - `⌘ Command` - `⇧ Shift` ###### Click-Outside Event The `$click_outside` event fires when a click lands anywhere outside the bound element and its descendants - the gesture behind dismissible UI like dropdowns, popovers, modals, and menus. A click on the element or inside it does nothing. It is usually rendered only while the element is open, behind a conditional, so it listens for outside clicks just then. ##### Reach Events The reach events fire when a scroll container is scrolled so one of its edges comes into view - the basis for patterns like infinite scroll, load-more, and pull-to-refresh. Bind them to the scrolling element, one per edge: - `$reach_bottom` - the container's bottom edge comes into view - `$reach_left` - the container's left edge comes into view - `$reach_right` - the container's right edge comes into view - `$reach_top` - the container's top edge comes into view A reach event carries no data - it is a pure trigger. To branch on the edge in a shared handler, pass it as an action argument. It also fires on mount when the edge is already in view - for example, a short list keeps loading until the viewport fills. To stop it firing, resolve the binding to `nil` from state, the same way any binding is switched off. Append `within(distance)` to set how far before the edge it fires - a length such as `200px` or a percentage of the container such as `50%`. The default is `100%` - the container's height for the top and bottom edges, its width for the left and right. Prepending content above the viewport (for `$reach_top` or `$reach_left`) does not yet preserve the scroll position - the view stays at the top, showing the newly loaded content. This will be addressed by keyed lists, which let the browser hold the scroll position as items are inserted above. Bind it to the scroll container - for example, a list that loads more as its bottom edge approaches: ```holo
{%for item <- @items}
{item.title}
{/for}
``` ##### Resize Event The `$resize` event fires when the bound target's size changes - bind it to an element to track that element, or to `` to react to the browser window being resized. It reports changes only: there is no initial dispatch when the element first renders. An element resize includes the element's box sizes, each a map with `inline_size` (the width, in horizontal writing mode) and `block_size` (the height): - `border_box_size` - the element's outer size, including padding and border - `content_box_size` - the element's inner size, excluding padding and border - `device_pixel_content_box_size` - the content box in device pixels, for pixel-perfect canvas and WebGL rendering, or `nil` where the browser does not support it `device_pixel_content_box_size` is part of the official Resize Observer spec, but no Safari version implements it - WebKit computes device-pixel sizes only after the paint cycle, while resize observations are delivered before it. Until that gap closes, the field is `nil` on every Apple device, including all iOS browsers - code that reads it must handle `nil`. A window resize includes no fields - the native event provides no size data. Both bindings fire rapidly during a continuous resize - a window-edge drag, a dragged splitter pane, an animated layout - so pair them with `throttle(ms)` or `debounce(ms)` to coalesce the stream. ##### Scroll Event The `$scroll` event fires when a scrollable element, or the page, is scrolled - bind it to an element to track that element, or to `` / `` to track the page. It includes: - `scroll_left` - the horizontal scroll offset of the scrolled element or page - `scroll_top` - the vertical scroll offset of the scrolled element or page Scroll fires rapidly, so pair it with `throttle(ms)` or `debounce(ms)` to coalesce the stream. ##### Select Event The `$select` event includes: - `value` - the selected text value ##### Submit Event The `$submit` event includes form field values directly under the `:event` key. For example, if your form has fields named "email" and "password", the event data will look like: ```elixir %{email: "user@example.com", password: "secret"} ``` #### Event Binding Syntax When binding events to elements in your templates, prefix the event name with a dollar sign (`$`). You can bind events using several syntax options. Note that text and shorthand syntax are only available for actions. To trigger a command, you must use the longhand syntax with the `command:` key. ##### Text Syntax (Actions Only) The simplest way to bind an event to an action: ```holo ``` ##### Expression Shorthand Syntax (Actions Only) Use this syntax when you need to pass parameters to the action: ```holo ``` ##### Expression Longhand Syntax For complete control, including targeting specific components, triggering commands, and scheduling delayed execution: ```holo ``` To trigger an action with a delay (in milliseconds), use the `delay:` key: ```holo ``` To trigger a command, use the `command:` key instead of `action:` (note that delays are not supported for commands): ```holo ``` ##### Disabling a Binding An event binding that resolves to no operation is disabled: nothing is dispatched, and the browser's native behavior is left untouched (no `preventDefault`, no `stopPropagation`). This makes bindings conditional - compute the operation from state and return `nil` to switch the binding off: ```holo ``` The rule covers every expression form: a `nil` whole value like `$click={nil}`, a `nil` shorthand name slot like `$click={nil, x: 1}`, and a `nil` longhand `action:` / `command:` key like `$click={action: nil}`. The value is read at event time from the current render, so a re-render can enable or disable the binding at any point. Note that `{if @editable, do: :save}` is invalid inside template braces (they are tuple braces, so Elixir requires parentheses or a `do...end` block for the ambiguous call) - use the `do...end` form shown above. ##### Multiple Bindings per Element An element can carry any number of event bindings, including several for the same event - each `$event` attribute is an independent binding with its own modifiers and its own operation. When the event fires, every binding for it runs through its own modifier pipeline in the order the attributes are written, so one binding can react instantly while another debounces, or several keyboard filters can route different keys to different actions. The classic use is pairing a plain binding with a debounced one: the plain `$change` keeps state current on every keystroke, while the debounced one triggers only the expensive work once typing settles: ```holo ``` The paired bindings can even differ only in their windows - `$change.debounce(250)` next to `$change.debounce(1000)` stages the work: a quick, cheap search shortly after typing pauses, then the full one. #### Event Targets By default, events are handled by the component that contains them. If the component is stateless (doesn't have a CID specified), the closest stateful component higher in the hierarchy is used. You can specify a different component for handling the event using the `target` parameter: ```holo ``` Valid targets include: - `"page"` - targets the current page - `"layout"` - targets the current layout component - a string representing a component ID (CID) - targets a specific component by its unique identifier within the application #### Window and Document Events Some events are not tied to any rendered element - a global keyboard shortcut, a paste anywhere on the page, a warning before the user leaves the page. Bind these with the `` and `` tags, which attach the event to the global `window` or the `document` rather than to an element. They render nothing and reuse the same `$event` syntax, keyboard key filters, and modifiers as element bindings: ```holo ``` Both tags follow the same targeting rules as a regular element and accept only event bindings - any other attribute fails the build. Each tag's listener lives as long as the tag is in the template, so one behind a conditional listens only while that condition holds. Reach for `` for window events such as resize and scroll, and `` for document events such as a tab becoming visible or hidden. Events that bubble, like keyboard and pointer events, reach both, so either tag works for a global shortcut. #### Keyboard Key Filters Keyboard events can be filtered to a specific key directly in the template by appending the key to the event name with a dot. The handler then fires only when that key is pressed: ```holo ``` Combine modifier keys with the key using `+` to match keyboard shortcuts: ```holo ``` Filters work on both `$key_down` and `$key_up`, and you can place several filtered bindings on one element - each fires only on its own key or combination, as described in the "Multiple Bindings per Element" section above. Keys are matched case-insensitively, so `$key_down.k` also matches `Shift`+`K`. A filter matches as long as the keys it lists are pressed - any extra modifiers held at the same time do not prevent it. So `$key_down.ctrl+enter` still fires when `Shift` is also down, and `$key_down.enter` fires whether or not a modifier is held. ##### Supported Keys - **Letters and digits** - written as the character itself, e.g. `k` or `7` - **Modifier keys** - `alt`, `ctrl`, `meta` (Command on macOS, Windows key on Windows), and `shift`, valid only when combined with a key (e.g. `ctrl+k`) - **Named keys** - `arrow_up`, `arrow_down`, `arrow_left`, `arrow_right`, `backspace`, `caps_lock`, `delete`, `end`, `enter`, `escape`, `home`, `insert`, `page_up`, `page_down`, `space`, `tab`, and `f1` through `f12` ##### Symbol Keys Symbol keys are written as alias words rather than the raw characters (which would clash with the template and filter syntax). Each alias is named after the key on a standard keyboard: - `` ` `` → `backquote` - `\` → `backslash` - `[` → `bracket_left` - `]` → `bracket_right` - `,` → `comma` - `=` → `equal` - `-` → `minus` - `.` → `period` - `'` → `quote` - `;` → `semicolon` - `/` → `slash` For example, to match `Ctrl`+`/`, write `$key_down.ctrl+slash`. Keys that require `Shift` to type (such as `?`, `+`, or `&`) do not have aliases - match those in your action handler via the `key` field. ##### Compile-Time Validation Because Hologram compiles your templates, key filters are parsed and validated at compile time - not when they run in the browser. A misspelled key name like `$key_down.entr` fails the build with a clear error that suggests the closest valid name. In most frameworks a misspelled key is a silent runtime no-op: the handler simply never fires, and you lose time discovering why. ##### Dynamic Keys Key filters are for keys known at compile time. When the key is determined at runtime - a user-configurable shortcut, or a value from state - bind the bare event and compare against the `key` field in your handler instead: ```holo ``` #### Debouncing High-frequency events - typing in a search box, moving the pointer, scrolling a list - can fire many times per second, dispatching an action on every one. Append `debounce(ms)` to the event name to coalesce a burst into a single dispatch: the handler runs once, `ms` milliseconds after the events stop, carrying the data from the final event. ```holo ``` Used without a value, `debounce` applies a default window of 250 milliseconds - long enough to coalesce a burst of events, short enough to stay responsive once they settle: ```holo ``` Debouncing works on any event and combines with key filters - `$key_down.enter.debounce(300)` debounces only the Enter key. Each binding keeps its own timer, so several debounced bindings on one element never interfere - including several for the same event, as in the typeahead pattern from the "Multiple Bindings per Element" section above. Like key filters, the window is validated at compile time: a non-positive or non-integer value such as `$change.debounce(0)` fails the build rather than misbehaving in the browser. ##### Lifecycle Boundaries A pending debounced dispatch resolves early at the boundaries where waiting would leave the app in a stale state. When the bound element loses focus, the pending dispatch fires immediately - the burst is definitively over, so nothing is gained by waiting out the rest of the window. When a form is submitted, pending debounced dispatches inside it fire first, so the submit action observes state that already reflects everything typed - and no trailing dispatch fires after the submit to resurrect a value the submit action may have cleared. Navigating to another page resolves the opposite way: the page a pending dispatch was meant for no longer exists, so it is dropped without firing. Together these rules make `debounce` a pure timing concern - it changes when the dispatch happens, never what the app observes at a boundary. ##### Debounce vs Delay Debouncing is easy to confuse with the `delay` option covered in the binding syntax above, but they address different concerns and live in different places by design. `debounce` is an event concern: it shapes the stream of events at the edge, before any action is chosen, so it attaches to the event name as a modifier, like a key filter. `delay` is an action concern: it is a property of the action being dispatched, so it lives in the action specification and can even be set programmatically with `put_action`, where there is no event for a modifier to attach to. In short, `delay` postpones an action you have already decided to dispatch, while `debounce` decides whether to dispatch at all - collapsing a rapid burst into one trailing call. #### Throttling Throttling caps how often a high-frequency event dispatches while it keeps firing. Append `throttle(ms)` to dispatch at most once per `ms`: the first event fires immediately, then at most one dispatch per window, with the latest event of each window dispatched on its trailing edge. It is built for continuous feedback - a live cursor readout, a drag preview, a scroll-position indicator. ```holo
``` Used without a value, `throttle` applies a default window of 100 milliseconds - around ten updates per second, smooth enough to feel live without flooding the action pipeline: ```holo
``` Throttling works on any event and combines with key filters, and each binding keeps its own window. Like debouncing, the value is validated at compile time. A binding cannot carry both `throttle` and `debounce` - they are opposite strategies, so combining them fails the build. Like a pending debounced dispatch, a trailing dispatch held inside an open throttle window is dropped when the user navigates to another page - it never fires into a page it was not meant for. ##### Throttle vs Debounce Both tame high-frequency events, in opposite ways. `debounce` waits for activity to settle and then dispatches once - reach for it when you only care about the final state, like a search query after the user stops typing. `throttle` dispatches at a steady capped rate while activity continues - reach for it when you want regular updates during the interaction. In short, debounce fires once after activity stops, while throttle fires repeatedly at a capped rate during it. #### Firing Once Append `once` to fire a binding a single time, then stop. It suits one-shot interactions - a confirm or dismiss button, claiming an item, or lazy-loading the first time a scroll container is reached. ```holo ``` It works on every event, including the window, document, resize, and reach events, and it combines with `debounce` and `throttle` - the single debounced or throttled dispatch fires, then the binding stops. A spent binding still handles the event as before - suppressing the native default and stopping propagation just as its modifiers do - and only stops dispatching the action. `once` is a precise guarantee, scoped to the binding's target. For an element binding the target is the element instance, so re-rendering keeps a fired binding spent however many times, and it re-arms only when the element is removed and re-added as a fresh DOM node. `$submit.once`, for example, stops a repeated click from double-submitting but comes back armed if the form is re-created. A `` or `` binding has no node to re-create - it keys on the global target, which lives for the whole page, so it fires once for the page and never re-arms. To get that same page-lifetime guarantee on an element, drive it from state: have the action record that it ran, then stop rendering or disable the control. #### Controlling the Native Default Hologram calls `preventDefault` by default only on `$submit` - so a bound form does not reload the page. Every other event is left alone, so clicks, typing, text selection, scrolling, and the rest of the browser's native behavior keep working. Two modifiers override this per binding: `allow_default` lets the native default through where Hologram would prevent it, and `prevent_default` forces `preventDefault` where Hologram would leave it alone. ##### Allowing the Native Default Append `allow_default` to a single binding so Hologram does not call `preventDefault` for it - the browser's native default then proceeds, and the action still dispatches. ```holo
``` Use it when you want both the native behavior and an action - for example a form that posts to an external endpoint you also want to track. It is meaningful only where Hologram actually prevents the native default (in practice `$submit`). It composes with key filters, `stop_propagation`, `debounce`, and `throttle`. ##### Preventing the Native Default The mirror of `allow_default` is `prevent_default`: append it and Hologram calls `preventDefault` for that binding. Since `$submit` is the only event prevented by default, `prevent_default` is how you do the same on any other event. The classic case is Enter-to-send on a keyboard binding, where the action should fire without the keystroke also inserting a newline or submitting the surrounding form: ```holo ``` It composes with key filters - so only the matched key is prevented - as well as `stop_propagation`, `debounce`, and `throttle`. It only affects cancelable events, so it is a no-op on ones the browser does not let you cancel - among them `$change`, `$input`, `$select`, `$scroll`, `$resize`, `$focus`, `$blur`, `$pointer_cancel`, and the `$transition_*` events. A binding cannot carry both `allow_default` and `prevent_default`: they are opposite intents, so combining them fails the build. #### Stopping Event Propagation DOM events bubble: an event on a nested element also fires the bindings on its ancestors. When a bound element sits inside another bound element - a delete button inside a card that opens on click - both actions dispatch. Append `stop_propagation` to the inner binding to stop the event at the bound element: its action still dispatches, and ancestor bindings no longer fire. ```holo
``` Hologram never stops propagation on its own - the modifier is the per-binding opt-in. Note that document-level features rely on bubbling: `$click_outside` bindings and `` / `` bindings only see events that bubble all the way up, so an event stopped by `stop_propagation` never reaches them - clicking such an element inside a dismissible popover will not dismiss it. Sometimes that is exactly the point, but keep it in mind when combining the two. The modifier composes with key filters, `allow_default`, `prevent_default`, `debounce`, and `throttle` - on keyboard events, propagation stops only for keys the filter matches. #### Event Flow When an event occurs, it can trigger either an action or a command: - `Actions` - client-side operations that can update component state, trigger commands, and more. See Actions for more details. - `Commands` - server-side operations that can access server resources, trigger actions, and more. See Commands for more details. #### Best Practices When working with events in Hologram, consider these best practices: - Use meaningful action and command names that describe what they do - Keep event handlers focused on a single responsibility - Use the appropriate event type for the interaction you want to handle - Be mindful of event bubbling and use event targets appropriately - Use keyboard events for accessibility and keyboard navigation - Handle form events properly to ensure good user experience ### Actions Actions in Hologram are client-side operations that allow you to: - Update component state - Trigger commands and other actions - Navigate to another page - Update emitted context They are typically executed in response to user interactions, enabling dynamic and interactive web applications. #### Defining Actions Actions are defined as functions in your page or component modules using the following syntax: ```elixir def action(name, params, component) do # Action logic here end ``` For example: ```elixir def action(:update_count, params, component) do put_state(component, :count, params.new_count) end ``` #### Action Parameters Actions receive three arguments: - `name` - the atom representing the action name - `params` - a map containing: - Custom parameters from template event attributes, other actions via `put_action/3`, or commands via `put_action/3` - Event data under the `:event` key (see Events for details about event data) - `component` - the current `%Component{}` struct #### Action Results Actions must return a `%Component{}` struct. This struct not only reflects changes to the component's state but also includes instructions for what happens next through functions that can be called on it. These functions are pure - they don't cause any side effects and simply return a new modified `%Component{}` struct. Since these functions return the modified `%Component{}` struct, they can be chained together using the pipe operator `|>`, allowing for a clean and composable way to combine multiple state changes and behaviors. Within an action, you can: ##### Update Component State By setting the value for a single key: ```elixir put_state(component, :key, value) ``` With multiple key-value pairs using a keyword list: ```elixir put_state(component, key_1: value_1, key_2: value_2) ``` With multiple key-value pairs using a map: ```elixir put_state(component, %{key_1: value_1, key_2: value_2}) ``` Update a nested value by specifying a path of keys: ```elixir put_state(component, [:path_key_1, :path_key_2], value) ``` ##### Trigger a Server Command Command with the specified name without any parameters: ```elixir put_command(component, :my_command) ``` Command with the specified name and additional parameters provided as a keyword list: ```elixir put_command(component, :my_command, param_1: value_1, param_2: value_2) ``` Command with a specified name, target, and parameters using a map for the parameters (in this longhand version, both params and target are optional): ```elixir put_command(component, name: :my_command, target: "other_component", params: %{key: value}) ``` Command using a `%Command{}` struct: ```elixir put_command(component, %Command{name: :my_command}) ``` See also: Commands. ##### Navigate to Another Page Page without params: ```elixir put_page(component, ProductsPage) ``` Page with params: ```elixir put_page(component, ProductPage, product_id: 123) ``` ##### Update Emitted Context By setting a value for a single key: ```elixir put_context(component, :key, value) ``` Using a tuple for namespacing, allowing different components to use the same key without conflict: ```elixir put_context(component, {MyModule, :key}, value) ``` See also: Context. ##### Chain Another Action Action with the specified name without any additional parameters: ```elixir put_action(component, :my_action) ``` Action with the specified name and additional parameters provided as a keyword list: ```elixir put_action(component, :my_action, param_1: value_1, param_2: value_2) ``` Action using the longhand syntax (where params, target, and delay are optional): ```elixir put_action(component, name: :my_action, target: "other_component", params: %{key: value}) ``` Using an `%Action{}` struct: ```elixir put_action(component, %Action{name: :my_action}) ``` #### Triggering Actions Actions can be triggered using event attributes in templates. For detailed information about event binding syntax and available event types, see the Events documentation. #### Action Targets By default, actions are executed on the component that contains them. If the component is stateless (doesn't have a CID specified), the closest stateful component higher in the hierarchy is used. You can specify a different component for execution using the target parameter. For more information about event targets, see the Events documentation. #### Action Delays Actions can be scheduled to execute after a specified delay using the delay parameter, which accepts a value in milliseconds. The delay can be specified in several ways: - In event bindings using the longhand syntax: `$click={action: :my_action, delay: 750}` - When chaining actions with put_action/2: `put_action(component, name: :my_action, delay: 750)` Common use cases include: - Polling for updates at regular intervals - Creating smooth animations by scheduling the next frame - Implementing auto-hide notifications - Creating timed game mechanics Note: The delay parameter is only available for actions, not commands. #### Putting It All Together Here's a simple example that demonstrates how actions work in Hologram. The counter component shows: 1. How to trigger an action from a template using event binding (see Events for details) 2. How to pass parameters to the action 3. How to chain multiple functions on the `%Component{}` struct using the pipe operator: - Updating component state with `put_state/3` - Sending an asynchronous server command with `put_command/3` ```elixir defmodule MyApp.Components.Counter do # ... def template do ~HOLO"""

Count: {@count}

""" end def action(:increment, params, component) do new_count = component.state.count + params.step component |> put_state(:count, new_count) |> put_command(:save_count, new_count) end # ... end ``` ### Commands Commands in Hologram are server-side operations that allow you to: - Execute server-side logic - Access server-only resources (like databases, files, or APIs) - Manage server-side state (cookies and session) - Perform privileged operations - Trigger client-side actions (like updating UI state) They are typically used for operations that require server processing, and are always executed asynchronously. #### Defining Commands Commands are defined as functions in your page or component modules using the following syntax: ```elixir def command(name, params, server) do # Command logic here end ``` For example: ```elixir def command(:save_user, params, server) do case MyApp.Users.create(params) do {:ok, user} -> put_action(server, :user_saved, user: user) {:error, changeset} -> put_action(server, :validation_failed, errors: changeset.errors) end end ``` #### Command Parameters Commands receive three arguments: - `name` - the atom representing the command name - `params` - a map containing: - Custom parameters that can come from: - Template event attributes - Actions via `put_command/3` - Event data under the `:event` key (see Events for details about event data) - `server` - the current `%Server{}` struct #### Command Results Commands must return a `%Server{}` struct. This struct not only manages server-side state (session and cookies) but also includes instructions for what happens next through functions that can be called on it. These functions are pure - they don't cause any side effects and simply return a new modified `%Server{}` struct. Since these functions return the modified `%Server{}` struct, they can be chained together using the pipe operator `|>`, allowing for a clean and composable way to combine multiple state changes and behaviors. Within a command, you can: ##### Trigger a Client Action Action with the specified name without any additional parameters: ```elixir put_action(server, :my_action) ``` Action with the specified name and additional parameters provided as a keyword list: ```elixir put_action(server, :my_action, param_1: value_1, param_2: value_2) ``` Action using the longhand syntax (where `params`, `target`, and `delay` are optional): ```elixir put_action(server, name: :my_action, target: "other_component", params: %{key: value}) ``` Using an `%Action{}` struct: ```elixir put_action(server, %Action{name: :my_action}) ``` ##### Update Session Data Session data can be managed using the session functions. For example, using `put_session/3`: ```elixir put_session(server, :user_id, user.id) ``` See the Session documentation for more details on managing session data. ##### Update Cookies Browser cookies can be managed using the cookie functions. For example, using `put_cookie/3`: ```elixir put_cookie(server, "remember_token", token) ``` See the Cookies documentation for more details on managing cookies. ##### Navigate to Another Page (coming soon) Future versions of Hologram will support server-side navigation, for example using `put_page/3`: ```elixir put_page(server, ProductPage, id: product.id) ``` #### Triggering Commands Commands can be triggered using event attributes in templates or from actions. For detailed information about event binding syntax and available event types, see the Events documentation. ##### From Actions Commands can also be triggered from actions using `put_command/3`: ```elixir def action(:save_form, params, component) do component |> put_state(:saving, true) |> put_command(:save_user, user: params.user) end ``` #### Command Targets By default, commands are executed on the component that contains them. If the component is stateless (doesn't have a CID specified), the closest stateful component higher in the hierarchy is used. You can specify a different component for execution using the `target` parameter. For more information about event targets, see the Events documentation. ### Navigation Hologram provides a seamless navigation experience by combining server-side rendering with client-side transitions. While each page is loaded fresh from the server, the framework uses the History PushState API and virtual DOM to ensure smooth transitions between pages. Note: For information about defining routes for pages, see the Pages documentation. #### Link Component The `Hologram.UI.Link` component is the primary way to add navigation links in your templates. It supports both simple page navigation and passing parameters to the target page. Basic usage: ```holo MyPage ``` With parameters: ```holo MyPage with params ``` #### Programmatic Navigation Besides using the `Hologram.UI.Link` component in templates, you can also navigate programmatically in your actions using the `put_page/2` and `put_page/3` functions. Basic navigation: ```elixir put_page(component, MyPage) ``` Navigation with parameters: ```elixir put_page(component, MyPage, a: 1, b: 2) ``` #### Navigation Behavior Hologram implements several optimizations to make page transitions feel instant: ##### Prefetching When a user interacts with a link, Hologram starts prefetching the target page on the `$pointer_down` event. When the `$pointer_up` event occurs, the current page is replaced with the prefetched content, making the transition feel instantaneous. ##### Browser History Hologram properly maintains the browser's history stack, ensuring that: - Back and forward navigation works as expected - URL changes are reflected in the browser's address bar - Each page is loaded fresh from the server, ensuring up-to-date content ##### Benefits This navigation approach combines the best of both worlds: - Server-side rendering ensures SEO-friendly, up-to-date content - Client-side transitions provide a smooth, app-like experience - Fresh page loads guarantee content consistency - Proper history management enables natural browser navigation ### Forms Forms in Hologram work seamlessly with the framework's declarative architecture. Hologram provides two flexible approaches for handling form inputs, allowing you to choose the best strategy for your specific use case. #### Synchronized vs Non-synchronized Inputs Hologram supports two approaches for handling form inputs: - **Synchronized inputs** - input state is synchronized with component/page state through unidirectional data flow achieved with `$change` event handler on the input element level - **Non-synchronized inputs** - without the `$change` event handler on input element level. To get the values of such inputs, `$change` or `$submit` event handlers on a form element level can be used ##### Synchronized Inputs Synchronized inputs maintain their displayed values in sync with your component's state through unidirectional data flow, ensuring the UI consistently reflects your application's data. This approach is similar to concepts like "controlled inputs" from React or "form input bindings" from Vue or Svelte, but maintains strict unidirectional flow - the component state is the single source of truth, and user input updates flow back to the state through event handlers. ###### Basic Usage To create a synchronized input, you need three things: state initialization, the input element synchronized with the state, and an event handler: ```elixir def init(_props, component, _server) do put_state(component, :email, "") end ``` ```holo ``` ```elixir def action(:email_changed, params, component) do put_state(component, :email, params.event.value) end ``` ###### Value Synchronization Hologram synchronizes your application state with form inputs using different attributes depending on the input type: - **Text-based inputs (text, email, password, etc.), textareas, and selects** use the `value` attribute (similar to React) - **Checkboxes and radio buttons** use the `checked` attribute Hologram optimizes this synchronization process: - Efficient mechanisms update form values while preserving browser behavior - Redundant DOM updates are avoided to prevent interrupting user input ##### Non-synchronized Inputs Non-synchronized inputs don't have `$change` event handlers on the input element level. Instead, you can access their values using form-level event handlers. This approach is useful when you don't need real-time state synchronization and prefer to handle form data as a whole. ###### Accessing Form Data With non-synchronized inputs, you can access form data from `params.event` in both `$change` and `$submit` event handlers on the form level. The form data is provided as a map where keys are the input names and values are the current input values. ```holo
{@validation_error}
``` ```elixir def action(:form_changed, params, component) do # params.event contains all form data: %{username: "...", email: "..."} # Validate the form whenever any field changes validation_error = cond do params.event.username == "" -> "Username is required" params.event.email == "" -> "Email is required" true -> "" end put_state(component, :validation_error, validation_error) end def action(:form_submitted, params, component) do # params.event contains all form data: %{username: "...", email: "..."} username = params.event.username email = params.event.email # Process form submission... component end ``` ##### When to Use Each Approach - **Synchronized inputs** are ideal when you need real-time state updates, form validation as the user types, or dynamic UI updates based on input values - **Non-synchronized inputs** are useful for simple forms where data is only processed on submission, when minimizing state updates and re-renders, or when you don't need real-time access to individual field values **Note:** Form-level `$change` and `$submit` event handlers can also be used with synchronized inputs. This allows you to handle both individual input changes (via input-level `$change` handlers) and form-wide operations (via form-level handlers) in the same form. #### Event Handling Hologram uses synthetic events similar to React. The `$change` event behavior depends on both the input type and where you place the event handler: ##### Input-level `$change` Events When you place `$change` directly on form inputs: - For text-based inputs and textareas, `$change` maps to the native `input` event, triggering on every keystroke - For checkboxes, radio buttons, and select elements, `$change` uses the native `change` event, triggering when the selection changes For input-level `$change` events, the event data contains a `value` field with the specific element's value. ##### Form-level `$change` Events When you place `$change` on the form element itself, it behaves like the native `change` event - typically triggering when a field loses focus, regardless of the input type. This is useful for validation workflows where you want to validate after the user finishes editing a field. The event data contains all form field values as a map, where keys are the input names and values are the current input values. ##### Event Data Types Regardless of whether you use input-level or form-level events, the values have different types depending on the input type: - For text-based inputs (text, email, password, etc.) and textareas: a string value - For checkboxes: a boolean value indicating if the element is checked - For radio buttons and select dropdowns: a string value of the selected option #### Input Types and Usage Hologram supports all standard HTML form elements, which can be used with either synchronized or non-synchronized approaches. Below are examples showing both approaches for each input type. ##### Text-based Inputs Text-based inputs (text, email, password) are the most common form elements. They use the `value` attribute for state synchronization: **Synchronized:** ```holo ``` **Non-synchronized:** ```holo ``` ##### Textareas Textareas provide multi-line text input. They use the `value` attribute for state synchronization: **Synchronized:** ```holo ``` ##### Checkboxes Checkboxes allow users to select or deselect options. They use the `checked` attribute for state synchronization: **Synchronized:** ```holo ``` **Non-synchronized:** ```holo ``` ##### Radio Buttons Radio buttons allow users to select one option from a group. They use the `checked` attribute for state synchronization: **Synchronized:** ```holo ``` **Non-synchronized:** ```holo ``` ##### Select Elements Select elements provide dropdown menus. They use the `value` attribute for state synchronization: **Synchronized:** ```holo ``` **Non-synchronized:** ```holo ``` #### Client-side Validation One of Hologram's key architectural advantages is that it runs Elixir in the browser, enabling you to perform validation client-side using the same code you'd use server-side. This means you can provide immediate feedback to users without network round-trips. ##### Isomorphic Validation Your validation logic can be truly isomorphic - the same Elixir validation code (including Ecto changesets) runs both client-side and server-side: - **Client-side:** Provides immediate user feedback and improves UX - **Server-side:** Ensures security, data integrity, and handles business rules that require server resources ##### Recommended Validation Pattern For optimal user experience, consider this validation approach: - Use form-level `$change` events to trigger validation when fields lose focus - Run your validation logic client-side in the action handler for immediate feedback - Display validation errors immediately in the UI by updating component state - Re-validate server-side when actually persisting data for security and integrity This pattern provides the best of both worlds: immediate user feedback through client-side validation, with the security and reliability of server-side validation. #### Best Practices When working with forms in Hologram: - Use descriptive `name` attributes that match your data structure for easier form processing - Leverage form-level `$change` events for validation workflows that trigger when fields lose focus - Consider using commands for form submission when server-side processing is required - Use appropriate input types (`email`, `password`, etc.) for better UX and built-in validation - Keep form state minimal - only store what you need for validation and UI updates - Take advantage of isomorphic validation - use the same Elixir validation code client-side and server-side ### Context Context in Hologram provides a way to share data between components without explicitly passing it through props. The main purpose of Context is to avoid prop drilling - the need to pass data through multiple layers of components that don't need the data themselves, just to make it available to deeply nested components that do need it. It's particularly useful for data that needs to be accessed by many components in your application, such as user authentication state, theme preferences, or global settings. #### Setting Context Values Context values can be set within actions or in init functions using the `put_context/3` function: ```elixir put_context(component, :key, value) ``` When you set a context value, it becomes available to all child components in the component tree below the component that emitted it. This means you can set context at any level and access it in any descendant component, without having to pass it through intermediate components. ##### Using Namespaced Context Keys To avoid naming conflicts between different components, you can use namespaced context keys: ```elixir put_context(component, {MyModule, :key}, value) ``` This allows different components to use the same key name without conflicts. #### Accessing Context Values To access context values in your components, define a prop with the `from_context` option: ```elixir prop :user, :map, from_context: :current_user ``` This will automatically set the `@user` prop to the value of `:current_user` from context. #### Common Use Cases Context is particularly useful for: - User authentication state - Theme preferences (light/dark mode) - Global application settings - Shared data between deeply nested components - Feature flags and configuration #### Best Practices When using Context, keep these best practices in mind: - Use context for truly global state that needs to be accessed by many components - Prefer props for component-specific data that only needs to be passed to direct children - Use namespaced keys to avoid naming conflicts - Keep context values as simple as possible - avoid storing complex objects or functions - Document the context keys your components expect to receive ### Middleware Middleware is reusable, server-side logic that runs before a page renders or a command executes. It is a natural home for cross-cutting concerns such as authentication, authorization, request enrichment (locale, theme, tenant, feature flags), rate limiting, or audit logging. Middleware is server-only. Actions run on the client and cross no trust boundary, so they have no middleware - anything privileged an action needs goes through a command, which middleware gates server-side. #### Defining Middleware Middleware lives in one of two places: an **inline function** on the page or component itself, or a **module** you reuse across pages and components. Inline suits one-off logic specific to a single page or component. A module suits anything shared, and is written as a **leaf** (does the work itself) or a **composite** (combines other middleware). The `Server` helpers (`put_status`, `put_redirect`, `put_session`, `put_stash`, and the rest) are available unqualified in all of them. ##### Inline Middleware The simplest middleware is a function defined right on the page or component, attached by name with `middleware :name`. It receives the `Server` struct and any options it was attached with (`[]` when none), and returns a `Server` struct - the same one, or one it has updated, which threads into the next middleware. ```elixir defmodule MyApp.DashboardPage do use Hologram.Page route "/dashboard" middleware :put_locale def put_locale(server, _opts) do put_stash(server, :locale, get_request_header(server, "accept-language")) end end ``` Options passed in the declaration arrive as the second argument: ```elixir middleware :put_theme, default: "dark" def put_theme(server, opts) do put_stash(server, :theme, get_cookie(server, "theme") || opts[:default]) end ``` ##### Leaf Middleware A leaf is the reusable form: a module that defines `call/2`, with the same `(server, opts)` signature as an inline function but in its own module so multiple pages can share it. ```elixir defmodule MyApp.Middleware.RequireAuth do use Hologram.Middleware @impl Hologram.Middleware def call(server, _opts) do require_auth(server, get_session(server, :user_id)) end defp require_auth(server, nil), do: put_redirect(server, MyApp.LoginPage) defp require_auth(server, _user_id), do: server end ``` ##### Composite Middleware A composite declares a sub-chain with the `middleware` macro instead of defining `call/2` - the framework generates the `call/2` that runs that sub-chain. Because a composite has a `call/2` like any other middleware, it attaches anywhere a leaf can - including inside another composite. A terminal response from any member stops the rest, and that short-circuit carries through nested composites. ```elixir defmodule MyApp.Middleware.AdminGate do use Hologram.Middleware middleware MyApp.Middleware.RequireAuth middleware MyApp.Middleware.RequireAdmin end ``` #### Attaching Middleware Attach middleware to a page or component with the `middleware` macro. The target is either a module or an inline `:function` name, and each takes optional keyword options: - `middleware SomeModule` - runs `SomeModule.call(server, opts)`. - `middleware SomeModule, max: 100` - same, with options passed as the second argument. - `middleware :some_function` - runs the host module's own inline `some_function(server, opts)`. - `middleware :some_function, role: :admin` - same, with options. Attach as many as you like, mixing inline functions and modules. Declarations run top to bottom, in the order written. #### Composition Composition is explicit. Middleware composes two ways: a base module that shares its declarations with every page or component that uses it, and a composite that bundles middleware into a single unit (shown above). Both resolve at compile time into one flat chain, with no runtime registry and nothing injected behind your back, so the full chain stays visible where it is attached - good for review. A base module whose `__using__` injects `middleware` declarations passes them on to every page or component that uses it - the base can wrap `use Hologram.Page` or `use Hologram.Component`. The child's own declarations accumulate after the inherited ones (parent first, then child), so a shared set plus a child-specific addition is just one more `middleware` line. ```elixir defmodule MyApp.AdminPage do defmacro __using__(_opts) do quote do use Hologram.Page middleware MyApp.Middleware.RequireAuth layout MyApp.AdminLayout end end end ``` ```elixir defmodule MyApp.SettingsPage do use MyApp.AdminPage route "/admin/settings" middleware MyApp.Middleware.RequireAdmin # ... end ``` App-wide concerns are assembled per page or component - a shared base module or composite plugged into the ones that need it. This avoids the anti-pattern of a global gate wrongly applying to public pages (login, signup) that need no auth. Truly-universal concerns that apply to every request - CSRF protection and telemetry, for example - are handled at the framework level, not via your middleware. #### Dispatch and Scoping Where middleware runs depends on where it is attached: - **Page middleware** runs before the page renders (before its `init/3`) and before any of that page's commands. - **Component middleware** runs only before that component's own commands. A component is always rendered as part of a page, never on its own, so there is no separate component render for middleware to gate. Component middleware lets a distributable component gate its own commands regardless of where it is embedded. **Security - a page's middleware does not cover its components' commands.** Dispatch is flat. It runs only the target module's middleware, never a tree of them. So when a command targets a component - even one nested deep inside a page you have gated - that component's middleware runs, but the page's and any ancestor component's do not. There is no inheritance up the nesting. To protect a component's commands, attach the gate to the component itself, directly or through a shared composite the page and component both use. Do not rely on the page's gate to cover them. **Ordering note:** anything that must run even on a rejected request (audit logging especially) goes early in the chain, since a terminal response stops everything after it. #### The Server Struct Middleware operates on a `Server` struct - the same struct `init/3` and `command/3` receive. It has three zones: - **Request** (read-only) - the incoming request, read as struct fields: `method`, `scheme`, `host`, `port`, `path`, `query`, `raw_query`, and `ip`. - **Response** - the response being built: its `status`, headers, and body. - **Stash** - a request-scoped scratchpad for passing data to later middleware in the chain and on to `init/3` or `command/3`. Identity, session, and cookies live here too. The functions for reading the request and writing each zone are in the Helper Reference below. Enrichment is a common middleware job - the example here resolves the current user once and stashes it, so everything downstream can read it: ```elixir defmodule MyApp.Middleware.LoadCurrentUser do use Hologram.Middleware @impl Hologram.Middleware def call(server, _opts) do load_user(server, get_session(server, :user_id)) end defp load_user(server, nil), do: server defp load_user(server, user_id) do put_stash(server, :current_user, Accounts.get_user(user_id)) end end ``` #### Terminal Responses Middleware stops the request by producing a response - that is, by setting a `status` on the server. `put_status` sets that field rather than halting on the spot - the middleware runs to completion, and once it returns with a status set, the rest of the chain and the handler are skipped, and the response is built from the status, headers, and body the chain accumulated. Decorations such as cookies and headers set before the stop still apply, so denying and setting a cookie both land. There is no separate `halt` - the status is the signal, set as an integer status code or an atom alias. A redirect sets a status, so it is inherently terminal: - `put_redirect(server, MyApp.LoginPage)` - stops and redirects (302 by default). - `put_status(server, 403)` - stops with a Forbidden response. - `put_status(server, :not_found)` - stops and renders the app's registered 404 page. ```elixir defmodule MyApp.Middleware.RequireAdmin do use Hologram.Middleware @impl Hologram.Middleware def call(server, _opts) do check_role(server, get_session(server, :role)) end defp check_role(server, :admin), do: server defp check_role(server, _role), do: put_status(server, :forbidden) end ``` #### Proxy and Trust Model The request fields reflect what the client claimed or what an upstream proxy resolved - they are **not** authenticated. - **`host` is client-supplied and spoofable**, even with no proxy in front. Validate it against an allowlist before using it for a security decision (for example, tenant routing). **Never** build security-sensitive absolute URLs - password-reset links, emails - from `host` or `request_url/1`. Use a configured canonical URL instead. - **`ip` / `scheme` resolution is an ingress concern:** - **Embedded mode** (Hologram inside your Phoenix endpoint) - add `RemoteIp` and/or `Plug.RewriteOn` to your endpoint to resolve the real client `ip` / `scheme` behind a proxy. Hologram reads the already-resolved conn and does no resolution of its own. If you do not configure these, `ip` is the proxy's address. - **Standalone mode** - Hologram resolves `ip` / `scheme` / `port` out of the box (trusted-proxy-gated when behind a proxy), while still requiring you to validate `host`. #### Key Conventions The three keyed stores differ in key type, by lifetime: - **Stash** - atom keys. In-memory, request-scoped, never serialized. - **Session** - string keys. Atom keys are accepted and converted to strings (in embedded mode the session is backed by the Phoenix session store, which keys by string - recovering atoms would need an unsafe string-to-atom conversion). See Session. - **Cookies** - string keys only. See Cookies. #### Helper Reference These helpers are imported into pages, components, and `use Hologram.Middleware` modules, so you call them unqualified. The `get_*` readers take an optional final `default`, returned when the value is absent (otherwise `nil`). ##### Reading the Request - `get_request_header(server, name)` - reads a request header by case-insensitive name. - `referrer_url(server)` - the referring URL, or `nil`. - `request_url(server)` - the full request URL assembled from the request fields. Client-claimed, so never use it for security-sensitive links (see Proxy and Trust Model). ##### Building the Response - `append_response_header(server, name, value)` - appends a value to the response header `name`, keeping that header's existing value (comma-separated) instead of replacing it like `put_response_header`. - `delete_response_header(server, name)` - removes a response header. - `get_response_header(server, name)` - reads a header already set on the response. - `put_redirect(server, url_or_page)` - a 302 redirect to either a URL string or a page module. - `put_redirect(server, page, params)` - a 302 redirect to a page module with route params. - `put_response_body(server, body)` - sets the response body (iodata). Not terminal on its own - pair it with `put_status`. - `put_response_header(server, name, value)` - sets a response header (name lower-cased), replacing any existing value. Raises on `cookie` / `set-cookie` - use the cookie helpers for those. - `put_status(server, status)` - sets the response status and stops the chain. `status` is an integer like `403` or an atom alias like `:forbidden` or `:not_found`. An unknown alias or out-of-range integer raises. The full alias set is in Status Aliases below. ##### Identity - `delete_user_id(server)` - clears the authenticated user. - `put_user_id(server, user_id)` - records the authenticated user (a string, integer, or atom), persisted to the session. ##### Session and Cookies Available here too, documented in full in their own sections: - **Session** - `get_session` / `put_session` / `delete_session`. See Session. - **Cookies** - `get_cookie` / `put_cookie` / `delete_cookie`. See Cookies. ##### Stash - `delete_stash(server, key)` - removes a stashed value. - `get_stash(server, key)` - reads a stashed value. - `put_stash(server, key, value)` - stashes a value for downstream middleware and handlers. #### Status Aliases Each status code and the atom alias `put_status` accepts for it. An unknown alias raises. - 100 - :continue - 101 - :switching_protocols - 102 - :processing - 103 - :early_hints - 200 - :ok - 201 - :created - 202 - :accepted - 203 - :non_authoritative_information - 204 - :no_content - 205 - :reset_content - 206 - :partial_content - 207 - :multi_status - 208 - :already_reported - 226 - :im_used - 300 - :multiple_choices - 301 - :moved_permanently - 302 - :found - 303 - :see_other - 304 - :not_modified - 305 - :use_proxy - 307 - :temporary_redirect - 308 - :permanent_redirect - 400 - :bad_request - 401 - :unauthorized - 402 - :payment_required - 403 - :forbidden - 404 - :not_found - 405 - :method_not_allowed - 406 - :not_acceptable - 407 - :proxy_authentication_required - 408 - :request_timeout - 409 - :conflict - 410 - :gone - 411 - :length_required - 412 - :precondition_failed - 413 - :payload_too_large - 414 - :uri_too_long - 415 - :unsupported_media_type - 416 - :range_not_satisfiable - 417 - :expectation_failed - 418 - :im_a_teapot - 421 - :misdirected_request - 422 - :unprocessable_entity - 423 - :locked - 424 - :failed_dependency - 425 - :too_early - 426 - :upgrade_required - 428 - :precondition_required - 429 - :too_many_requests - 431 - :request_header_fields_too_large - 451 - :unavailable_for_legal_reasons - 500 - :internal_server_error - 501 - :not_implemented - 502 - :bad_gateway - 503 - :service_unavailable - 504 - :gateway_timeout - 505 - :http_version_not_supported - 506 - :variant_also_negotiates - 507 - :insufficient_storage - 508 - :loop_detected - 510 - :not_extended - 511 - :network_authentication_required ### Session Session in Hologram provides secure storage that persists across page visits within a user's browsing session. Session data is stored in a secure session cookie that ensures data integrity and security. You can work with session data in three main contexts: - During page or component initialization (`init/3` functions) - In page or component commands for dynamic session operations - In page or component middleware, which runs before `init/3` and commands Sessions are ideal for storing user authentication data, temporary state, and sensitive information. The data is automatically secured by the server through the session system. Hologram also provides functions for managing regular Cookies outside of the session system. See the "Session vs Direct Cookie Management" section below for guidance on when to use which approach. #### Session Functions Hologram provides the following functions for working with sessions: - `get_session(server, key)` - reads a session value - `get_session(server, key, default)` - reads a session value with a default if the key doesn't exist - `put_session(server, key, value)` - writes a session value - `delete_session(server, key)` - deletes a session value **Note:** Session keys must be atoms or strings - other types raise. Atom keys are accepted and converted to strings, so `:user_id` and `"user_id"` address the same value. In embedded mode the session is backed by the Phoenix session store, which keys by string, and recovering the original atom would require an unsafe string-to-atom conversion. #### Reading Session Data You can read session values using the `get_session/2` and `get_session/3` functions. ##### Reading Simple Values For simple values stored in the session: ```elixir def init(_params, component, server) do user_id = get_session(server, :user_id) put_state(component, :user_id, user_id) end ``` ##### Reading Complex Data Structures Sessions can store any Elixir data type, including complex data structures like maps, lists, and tuples. You can work with these structures directly: ```elixir def init(_params, component, server) do preferences = server |> get_session(:user_profile) |> Map.get(:preferences, %{}) put_state(component, :user_preferences, preferences) end ``` ##### Handling Missing Session Values When a session key doesn't exist, `get_session/2` returns `nil`. You can provide default values using `get_session/3`: ```elixir def init(_params, component, server) do cart_items = get_session(server, :cart, []) put_state(component, :cart_items, cart_items) end ``` #### Writing Session Data You can write session values using the `put_session/3` function. Session data is automatically persisted and will be available across page visits within the same session. ##### Storing Simple Values Writing simple values to the session: ```elixir def init(_params, _component, server) do put_session(server, :user_id, 123) end ``` ##### Storing Complex Data Structures Sessions can store complex Elixir data structures: ```elixir def init(_params, _component, server) do user_profile = %{ id: 123, email: "user@example.com", role: :admin, preferences: %{ theme: "dark", notifications: true } } put_session(server, :user_profile, user_profile) end ``` ##### Updating Session Data To update existing session data, retrieve it, modify it, and store it back: ```elixir def init(_params, _component, server) do cart = get_session(server, :cart, []) updated_cart = [%{id: 456, quantity: 1} | cart] put_session(server, :cart, updated_cart) end ``` #### Deleting Session Data Remove session values using the `delete_session/2` function: ```elixir def init(_params, _component, server) do delete_session(server, :temporary_data) end ``` #### Using Sessions in Commands Session operations can also be performed in server commands, allowing for dynamic session management in response to user interactions: ##### Authentication Example ```elixir def command(:login, params, server) do case authenticate_user(params.email, params.password) do {:ok, user} -> server |> put_session(:user_id, user.id) |> put_session(:user_role, user.role) |> put_action(:redirect_to_dashboard) {:error, _reason} -> put_action(server, :show_error_message, message: "Invalid credentials") end end ``` ##### Shopping Cart Example ```elixir def command(:add_to_cart, params, server) do cart = get_session(server, :cart, []) item = %{product_id: params.product_id, quantity: params.quantity} updated_cart = [item | cart] server |> put_session(:cart, updated_cart) |> put_action(:update_cart_display, cart: updated_cart) end ``` ##### Logout Example ```elixir def command(:logout, _params, server) do server |> delete_session(:user_id) |> delete_session(:user_role) |> delete_session(:cart) |> put_action(:redirect_to_home) end ``` #### Session vs Direct Cookie Management Understanding when to use Hologram's session system versus direct cookie management: - **Session System** - Automatic security handling, data is opaque to clients, managed through session functions - **Direct Cookie Management** - Full control over cookie behavior, data encoding, and security settings - **Data Access** - Session data cannot be read by client-side code; direct cookies can be configured for client access - **Security** - Sessions provide automatic security; direct cookies require manual security configuration - **Use Cases** - Sessions for sensitive/private data; direct cookies when you need client-side access or specific cookie behavior #### Common Use Cases - **User authentication** - Store user ID, role, and authentication status - **Shopping cart** - Temporary cart contents for logged-in users - **Multi-page form data** - Form progress and temporary state that spans multiple pages - **Flash messages** - Temporary success/error messages that persist across redirects - **User preferences** - Temporary UI state and user-specific settings - **CSRF protection** - Store anti-CSRF tokens securely on the server #### Best Practices - **Minimize session data** - Store only necessary information to keep memory usage low - **Use consistent keys** - Adopt a naming convention for session keys across your application - **Handle missing values** - Always provide defaults when reading session data that might not exist - **Clean up on logout** - Remove sensitive session data when users log out ### Cookies Cookies in Hologram allow you to store and retrieve data that persists across page visits and browser sessions. While cookies are stored in the client browser, they are managed through server-side functions for security reasons. You can work with cookies in three main contexts: - During page or component initialization (`init/3` functions) - In page or component commands for dynamic cookie operations - In page or component middleware, which runs before `init/3` and commands Hologram provides built-in functions for reading, writing, and deleting cookies, with support for both simple string values and complex Elixir data structures. For most authentication and session-related needs, consider using Hologram's Session abstraction instead. #### Cookie Functions Hologram provides the following functions for working with cookies: - `get_cookie(server, key)` - reads a cookie value - `get_cookie(server, key, default)` - reads a cookie value with a default if the cookie doesn't exist - `put_cookie(server, key, value)` - writes a cookie with default settings - `put_cookie(server, key, value, opts)` - writes a cookie with custom settings - `delete_cookie(server, key)` - deletes a cookie **Note:** Cookie keys must be strings. Using atoms or other data types as keys will result in an error. #### Reading Cookies You can read cookies using the `get_cookie/2` function or `get_cookie/3` with a default value. Hologram automatically handles decoding of both string values and Hologram-encoded Elixir data structures. ##### Reading String-Encoded Cookies For simple string values stored in cookies: ```elixir def init(_params, component, server) do cookie_value = get_cookie(server, "my_cookie") put_state(component, :cookie_value, cookie_value) end ``` ##### Reading Hologram-Encoded Cookies Hologram can automatically encode and decode complex Elixir data structures (maps, lists, tuples, etc.) in cookies: ```elixir def init(_params, component, server) do user_data = server |> get_cookie("user_preferences") |> Map.put(:theme, "dark") put_state(component, :user_data, user_data) end ``` ##### Reading with Default Values Use `get_cookie/3` to provide a default value when a cookie doesn't exist: ```elixir def init(_params, component, server) do # Set default theme if no cookie exists theme = get_cookie(server, "theme", "light") # Set default user preferences if no cookie exists preferences = get_cookie(server, "user_prefs", %{language: "en", timezone: "UTC"}) component |> put_state(:theme, theme) |> put_state(:preferences, preferences) end ``` #### Writing Cookies You can write cookies using `put_cookie/3` for default settings or `put_cookie/4` for custom settings. ##### Default Settings Writing a cookie with default security settings: ```elixir def init(_params, _component, server) do put_cookie(server, "username", "abc123") end ``` Default settings include: - `http_only: true` - Cookie is only accessible via HTTP(S), not JavaScript - `path: "/"` - Cookie is available for the entire domain - `same_site: :lax` - CSRF protection with reasonable usability - `secure: true` - Cookie only sent over HTTPS connections ##### Custom Settings You can customize cookie behavior by providing options: ```elixir def init(_params, _component, server) do opts = [ http_only: false, path: "/admin", same_site: :strict, secure: false ] put_cookie(server, "ui_preference", "sidebar_collapsed", opts) end ``` ##### Available Options - `http_only` - boolean, whether cookie is accessible only via HTTP (not JavaScript) - `path` - string, URL path where cookie is available - `same_site` - `:strict`, `:lax`, or `:none` for CSRF protection among other security benefits - `secure` - boolean, whether cookie requires HTTPS - `max_age` - integer, cookie lifetime in seconds - `domain` - string, domain where cookie is available #### Deleting Cookies Remove cookies using the `delete_cookie/2` function: ```elixir def init(_params, _component, server) do delete_cookie(server, "temporary_data") end ``` #### Using Cookies in Commands Cookie operations can also be performed in server commands, allowing for dynamic cookie management in response to user interactions: ##### Command Examples ```elixir def command(:save_user_preferences, params, server) do user_prefs = %{ theme: params.theme, language: params.language, timezone: params.timezone } server |> put_cookie("user_preferences", user_prefs) |> put_action(:show_success_message) end def command(:load_user_preferences, _params, server) do preferences = get_cookie(server, "user_preferences") put_action(server, :update_ui_preferences, preferences: preferences) end def command(:logout, _params, server) do server |> delete_cookie("session_id") |> delete_cookie("user_preferences") |> put_action(:redirect_to_login) end ``` #### Data Encoding Hologram automatically handles encoding and decoding of cookie values: - **Writing cookies** - All values (including strings) are encoded using Hologram's serialization format - **Reading cookies** - Hologram can read both Hologram-encoded cookies and plain string cookies (by detecting the encoding format) - **Data types** - Complex data structures (maps, lists, tuples, atoms, etc.) are automatically preserved through the encoding/decoding process #### Security Considerations When working with cookies, consider the following security best practices: - **Use HTTPS** - Keep `secure: true` in production to prevent cookie theft - **HTTP-only by default** - Use `http_only: true` unless you need JavaScript access - **Appropriate SameSite** - Use `:strict` for sensitive cookies, `:lax` for general use - **Minimal data** - Store only necessary data in cookies to keep them lightweight and secure - **Set expiration** - Use `max_age` to limit cookie lifetime #### Common Use Cases - **User preferences** - Theme, language, layout settings - **Custom authentication** - When implementing authentication outside of Hologram's session system - **Shopping cart** - Temporary cart contents for anonymous users - **Analytics** - User tracking (with appropriate consent) - **Feature flags** - User-specific feature toggles ### Realtime Realtime lets your server-side Elixir code dispatch actions to connected clients without the client polling for changes. When something happens on the server - a new chat message, a finished background job, another user's edit - you broadcast an action, and Hologram runs the matching action handler on every subscribed client. You subscribe and broadcast from your server-side handlers - `init/3` and `command/3`. The actions you broadcast run on the client, in the same `action/3` handlers you already write for user events. A separate API exists for code that doesn't run in a handler, such as background jobs - it's covered in the "Calling Realtime Outside a Handler" section below. #### Core Concepts ##### Instances An instance is a single browser tab - one JS execution context. Each instance is identified by an `instance_id` that Hologram mints at the initial page render. It's stable across reconnects and across Hologram's in-page navigation, and it's regenerated on a hard refresh or in a new tab. You can read it as `server.instance_id`. ##### Channels A channel is the target of a broadcast and the thing a component subscribes to. Channels are structured, typed values, never raw topic strings. A channel is either a bare atom like `:notifications`, or a tuple of an atom tag followed by one or more primitive values, like `{:room, 42}` or `{:doc, "abc-123", "v2"}`. The framework encodes each channel to an internal topic for you, so you never build or see a topic string, and a malformed channel is rejected with a clear error instead of routing nowhere. There are two kinds: identity channels and application channels. ###### Identity Channels Identity channels address a recipient by who or what is on the other end. There are three, nested from narrowest to broadest: - `{:instance, instance_id}` - a single tab - `{:session, session_id}` - one session, which may span several tabs - `{:user, user_id}` - one authenticated user, which may span several sessions and devices Each level is independently addressable. A page, layout, or component that wants events on an identity channel subscribes to it explicitly (see "Subscribing to Identity Channels" below). ###### Application Channels Application channels are channels you define for your domain - a chat room, a document, a notifications feed. They're how you fan out a single broadcast to many recipients without maintaining your own membership table: - `:notifications` - `{:room, 42}` - `{:doc, "abc-123", "v2"}` Every component subscribed to an application channel receives the broadcasts sent to it. ##### Component Targeting with Cid A `cid` (component id) identifies a stateful component. For the components you place in your templates, it's the `cid` attribute you set on them. The page and the layout are components too, and Hologram assigns them the reserved cids `"page"` and `"layout"`. Subscriptions are recorded per component, and `put_subscription` always subscribes the component whose handler is running - you can't subscribe on another component's behalf. Broadcasts never name a component. The publisher names only the channel, and Hologram runs the action on every component subscribed to it - which is what lets a single broadcast update several components at once. #### How Realtime Calls Behave Inside Handlers The functions you call inside a server-side handler - `put_subscription`, `delete_subscription`, `put_broadcast`, and `put_broadcast_except` - are deferred and transactional. Nothing happens the moment you call them. Hologram records your intent and applies it only after the handler returns successfully. If the handler raises, every queued subscription change and broadcast is discarded along with the rest of the server state, exactly like Hologram's other server-side operations (`put_session`, `put_cookie`, and so on). That's why a broadcast can't leak from a handler that fails partway through. #### Realtime Functions Hologram provides the following functions for working with realtime inside a server-side handler: - `put_subscription(server, channel)` - subscribes the current component to a channel - `delete_subscription(server, channel)` - removes a subscription added earlier in the page's lifetime - `put_broadcast(server, channel, action_name)` - broadcasts an action to a channel - `put_broadcast(server, channel, action_name, params)` - broadcasts an action with params - `put_broadcast_except(server, except_identities, channel, action_name)` - broadcasts to a channel, excluding one or more identities - `put_broadcast_except(server, except_identities, channel, action_name, params)` - same, with params For code that runs outside a handler, see the "Calling Realtime Outside a Handler" section below. #### Subscribing to Channels A component only receives broadcasts for channels it's subscribed to. You subscribe inside `init/3` (or a command) with `put_subscription`. ##### Subscribing in a Handler Subscribe the current component to a channel by passing the channel to `put_subscription`: ```elixir def init(%{id: room_id}, _component, server) do put_subscription(server, {:room, room_id}) end ``` ##### Subscribing to Identity Channels Identity channels are subscribed the same way. A layout that wants user-level events declares it in its own `init/3`. Note that `user_id` may be `nil` for anonymous visitors, so guard it before subscribing: ```elixir def init(_params, _component, server) do put_subscription(server, {:user, server.user_id}) end ``` ##### Subscription Lifecycle A subscription is sticky for the page's lifetime and is cleaned up automatically - you don't unsubscribe on navigation or tab close. When the visitor navigates to another page, Hologram drops the subscriptions the old page declared. If the next page re-declares the same subscription (for example, a layout-level subscription that spans a same-layout navigation), it's preserved without any churn. Because cleanup is automatic, `delete_subscription` is only needed when you want to remove a subscription mid-page. ##### Removing a Subscription Remove a subscription declared earlier in the page's lifetime with `delete_subscription`: ```elixir def command(:leave_room, %{room_id: room_id}, server) do delete_subscription(server, {:room, room_id}) end ``` Removal is authoritative: that component stops receiving broadcasts for that channel, and the removal holds even across reconnects. ##### Inspecting Current Subscriptions `server.subscriptions` is a public field you can read at any point as you thread the struct through a handler and the helpers it calls - for example, to decide whether to subscribe based on what's already there. It's a list of `{channel, cid}` tuples for the current component - in a command it starts from that component's existing subscriptions, in `init/3` from an empty list. #### Broadcasting Actions Broadcasting dispatches an action to every component subscribed to the channel - it runs in the component's `action/3` handler, the same way a user-triggered action does, but the trigger comes from the server. From inside a server-side handler, use `put_broadcast`. ##### Broadcasting in a Handler For example, a command that persists a new chat message and broadcasts it to the room, with the action handler that adds it to the list on each subscriber: ```elixir def command(:send_message, %{room_id: room_id, text: text}, server) do message = Chat.create_message(room_id, text) put_broadcast(server, {:room, room_id}, :add_message, message: message) end def action(:add_message, %{message: message}, component) do put_state(component, messages: [message | component.state.messages]) end ``` The publisher names only the channel and the action - it never names a component. `params` is a keyword list at the call site and arrives as a map in the handler, exactly like a client-dispatched action. ##### Broadcasting Without Params `params` is optional. Omit it for signal-only broadcasts where the action name itself carries the meaning: ```elixir # With params put_broadcast(server, {:room, 42}, :add_message, text: "hi") # Signal only, no params put_broadcast(server, {:room, 42}, :refresh) ``` ##### Excluding a Recipient `put_broadcast_except` broadcasts to a channel but skips one or more identities - each identifying an instance, session, or user. A common use is broadcasting a change to a room while skipping the tab that just made it, because that tab already updated itself: ```elixir # Broadcast to room 42, but skip the tab that sent the message put_broadcast_except( server, {:instance, server.instance_id}, {:room, room_id}, :add_message, message: message ) ``` ##### Does the Sender Receive Its Own Broadcast? Yes. With `put_broadcast`, the instance that triggered the broadcast receives it too, on any of its components subscribed to the channel. In the chat example above, the sender sees their own message appear through the same `add_message` handler as everyone else. If you don't want that, exclude your own instance with `put_broadcast_except` and `{:instance, server.instance_id}`. ##### Inspecting Queued Broadcasts `server.broadcasts` is a public field you can read at any point as you thread the struct through a handler and the helpers it calls - for example, to avoid queuing a broadcast that's already there. It holds the broadcasts added with `put_broadcast` / `put_broadcast_except` so far, starting empty in each handler. ##### A Complete Example Subscribe, broadcast, and the action handler together - a complete chat room page: ```elixir defmodule MyApp.RoomPage do use Hologram.Page route "/rooms/:id" layout MyApp.Layout def init(%{id: room_id}, component, server) do messages = Chat.recent_messages(room_id) component = put_state(component, room_id: room_id, messages: messages) server = put_subscription(server, {:room, room_id}) {component, server} end def command(:send_message, %{room_id: room_id, text: text}, server) do message = Chat.create_message(room_id, text) put_broadcast(server, {:room, room_id}, :add_message, message: message) end def action(:add_message, %{message: message}, component) do put_state(component, messages: [message | component.state.messages]) end end ``` `init/3` subscribes the page to the room. Sending a message persists it and broadcasts `add_message` to the room. Every subscriber, including the sender, runs the `add_message` handler and adds the message to its list. #### Identity on the Server Struct Three fields on the `server` struct identify the current instance, session, and user. You use them to build identity-channel addresses: - `server.instance_id` - the current tab. Read-only. - `server.session_id` - the current session, managed by Hologram's session system. Read-only. - `server.user_id` - the current authenticated user, or `nil` for anonymous visitors. You may set it to signal login (assign the user's id) or logout (set it to `nil`). Hologram reacts to the change. Address an identity channel by pairing the tag with the matching field: ```elixir put_subscription(server, {:user, server.user_id}) put_subscription(server, {:session, server.session_id}) put_subscription(server, {:instance, server.instance_id}) ``` #### Calling Realtime Outside a Handler Everything above runs inside a page or component handler - the idiomatic way to use Realtime in Hologram, where calls are transactional and tied to the page lifecycle. Sometimes, though, the code that needs to broadcast or change a subscription isn't running in a handler at all: a background job finishing work, a worker, a GenServer, or existing Phoenix code in an app adopting Hologram incrementally. For those cases, `Hologram.Realtime` exposes immediate counterparts. These calls fire immediately and do not roll back - there's no handler success to defer to. Reach for them only when there's genuinely no handler to be in: - `Hologram.Realtime.broadcast_action(channel, action_name)` - the immediate counterpart to `put_broadcast` - `Hologram.Realtime.broadcast_action(channel, action_name, params)` - same, with params - `Hologram.Realtime.broadcast_action_except(except_identities, channel, action_name)` - broadcasts while excluding one or more identities - `Hologram.Realtime.broadcast_action_except(except_identities, channel, action_name, params)` - same, with params - `Hologram.Realtime.subscribe(identity, channel, cid)` - the immediate counterpart to `put_subscription` - `Hologram.Realtime.unsubscribe(identity, channel, cid)` - removes one subscription binding - `Hologram.Realtime.unsubscribe_all(identity, channel)` - removes every subscription a target has to a channel ##### Broadcasting from Outside a Handler `broadcast_action` is the immediate counterpart to `put_broadcast`, with the same channel, action, and params shape: ```elixir Hologram.Realtime.broadcast_action({:user, user_id}, :show_toast, text: "Saved") Hologram.Realtime.broadcast_action({:user, user_id}, :reload_session) ``` `broadcast_action_except` excludes one or more identities from delivery: ```elixir Hologram.Realtime.broadcast_action_except( {:instance, originator_instance_id}, {:room, 42}, :add_message, message: message ) ``` ##### Subscribing from Outside a Handler `subscribe`, `unsubscribe`, and `unsubscribe_all` are the immediate counterparts to `put_subscription` and removal. They require an explicit `cid`, since there's no handler to supply one: ```elixir Hologram.Realtime.subscribe({:user, 7}, {:room, 42}, "page") Hologram.Realtime.unsubscribe({:user, 7}, {:room, 42}, "page") Hologram.Realtime.unsubscribe_all({:user, 7}, {:room, 42}) ``` These calls act on instances that are connected right now. `subscribe` signals currently-connected tabs. It doesn't durably grant a subscription to a user who isn't connected yet. To grant access that persists across reconnects and future tabs, update your data layer (a permission or membership table) so the next time that user's page runs `init/3`, it declares the subscription with `put_subscription`. Tabs that are already open won't re-run `init/3` until they reload, so you can fire `Hologram.Realtime.subscribe` to apply the change to them right away. ##### Example: Notifying a User Across Their Tabs With the layout subscribed to the user's identity channel, a finished background job can notify every signed-in tab on every device: ```elixir Hologram.Realtime.broadcast_action( {:user, user_id}, :show_toast, kind: :success, text: "Upload complete" ) ``` Because the layout subscribed with the reserved `"layout"` cid, the action runs on the layout component - the natural place for app-wide toasts. ##### Example: Removing a User from a Channel `unsubscribe_all` is authoritative and channel-wide - the right tool for a kick or a revoked permission. It removes every subscription the user has to the channel, across all their tabs, and the removal holds across reconnects: ```elixir Hologram.Realtime.unsubscribe_all({:user, user_id}, {:room, 42}) ``` #### Authorization Realtime performs no implicit authorization. None of these functions checks whether the caller is allowed to broadcast to, subscribe to, or remove someone from a channel. That's your responsibility: check permissions before the call - "is this user allowed in `{:room, rid}`?" - wherever you make it. #### Delivery Semantics Realtime is fire-and-forget, following the standard at-most-once delivery model of pub/sub messaging - the same guarantees you'd get from Phoenix.PubSub or Phoenix Channels. There are no delivery acknowledgements, no replay buffer for clients that were offline, and no ordering or delivery guarantees. Delivery is subscription-driven: a broadcast reaches only the instances with a component subscribed to its channel at the moment it's sent, and within each, only the components that subscribed run the action - Hologram routes by subscription rather than delivering everywhere and filtering on the client. A client that isn't connected simply doesn't receive it. If a channel has no subscribers, a broadcast to it is silently dropped. Design with this in mind: treat broadcasts as live nudges to update already-loaded state, and keep the authoritative data in your data layer, so a client that missed a broadcast gets the right state the next time it loads. ### JavaScript Interop Hologram lets you call JavaScript from your Elixir client-side code, and vice versa. You can import JS modules, call functions, manipulate objects, dispatch DOM events, and work with async code. JS interop is useful when you need to: - **Use JavaScript libraries** - integrate npm packages or your own JS modules (e.g. charting libraries, rich text editors, payment SDKs) - **Interact with existing JavaScript code** - call into JS code that's already part of your project - **Access Web APIs** - as an escape hatch, use browser APIs that Hologram doesn't wrap yet (e.g. Web Audio, WebGL, Geolocation, Clipboard, etc.) - **Manipulate the DOM directly** - for cases where you need fine-grained DOM control beyond what Hologram templates provide Here is a quick example showing a page that imports an npm package and uses it via JS interop: ```elixir defmodule MyApp.CalculatorPage do use Hologram.Page use Hologram.JS js_import from: "decimal.js", as: :Decimal route "/calculator" layout MyApp.DefaultLayout def init(_params, component, _server) do put_state(component, :result, nil) end def template do ~HOLO"""

Result: {@result}

""" end def action(:calculate, _params, component) do result = :Decimal |> JS.new([100]) |> JS.call(:plus, [23]) |> JS.call(:toNumber, []) # 123 put_state(component, :result, result) end end ``` Hologram JS interop works in both directions: 1. **Elixir to JavaScript** - `JS.call`, `JS.new`, `JS.get`, `JS.set`, and friends give you direct, fine-grained control over JavaScript objects from Elixir. Calls work synchronously in event loop lock-step with your action handlers (unless you deliberately use async calls), integrate with state management, type conversion, and async handling, and keep your JS integration logic inside your Elixir modules. 2. **JavaScript to Elixir** - `Hologram.dispatchAction()` triggers Elixir action handlers from JS code. Migrating from Phoenix LiveView? The `JS.dispatch_event` function maps closely to the LiveView hooks pattern for a quick migration path. See the "Coming from LiveView" section below. #### Setup Add `use Hologram.JS` to any module that runs on the client side where you need JS interop: ```elixir defmodule MyApp.DashboardPage do use Hologram.Page use Hologram.JS # ... end ``` This imports the `js_import` macro, the `~JS` sigil, and aliases the `Hologram.JS` module as `JS`. #### Importing JavaScript Modules Use `js_import` at the top of your module to make JavaScript exports available as named bindings. Bindings are scoped to the Elixir module where they are declared, and can be used as receivers or function references in `JS.*` calls within that module. Each `js_import` maps directly to a JavaScript `import` statement: | Elixir | Resolves To | |--------|-------------| | `js_import from: "decimal.js", as: :Decimal` | `import Decimal from "decimal.js"` | | `js_import :multiply, from: "./helpers.mjs"` | `import { multiply } from "./helpers.mjs"` | | `js_import :Chart, from: "chart.js", as: :MyChart` | `import { Chart as MyChart } from "chart.js"` | ##### Default Export Import the default export of a JS module using the `:from` and `:as` options. Given this JavaScript file: ```javascript // calculator.mjs export default class Calculator { constructor(initial) { this.value = initial; } add(n) { this.value += n; return this.value; } } ``` You can import it in Elixir like this: ```elixir js_import from: "./calculator.mjs", as: :Calculator ``` Both `:from` and `:as` are required for default imports. ##### Named Export Import a specific named export. Given this JavaScript file: ```javascript // helpers.mjs export function multiply(a, b) { return a * b; } ``` You can import it in Elixir like this: ```elixir js_import :multiply, from: "./helpers.mjs" ``` The binding name defaults to the export name. Use `:as` to alias it: ```elixir js_import :Chart, from: "chart.js", as: :MyChart ``` ##### Path Resolution Import paths are resolved as follows: - **Relative paths** (`./` or `../`) are resolved relative to the Elixir source file that contains the `js_import`. This means you can colocate JS files next to your Elixir modules. - **Bare specifiers** (no `./` or `../` prefix) are resolved as npm packages. Make sure the package is installed in your `assets/package.json`. - **Paths from the assets directory** work with relative paths from the caller. ##### Duplicate Binding Names Each binding name must be unique within a module. Declaring two imports with the same `:as` name will raise a compile-time error. #### API Reference ##### JS.call/2 - Call a Function Calls a function without a receiver. The function is resolved from your `js_import` bindings first, then from `window`. ```elixir js_import :multiply, from: "./helpers.mjs" # Call an imported function JS.call(:multiply, [4, 6]) # 24 # Call a global function JS.call(:parseInt, ["42abc"]) # 42 ``` ##### JS.call/3 - Call a Method Calls a method on a receiver. The receiver can be: - An **atom** referencing an imported binding or a global object (e.g. `:Math`, `:document`) - A **native value** returned by a previous JS interop call (e.g. an object from `JS.new/2`) ```elixir js_import from: "./helpers.mjs", as: :helpers # Call a method on an imported module JS.call(:helpers, :sum, [1, 2]) # 3 # Call a method on a global object JS.call(:Math, :round, [3.7]) # 4 # Call a method on a native object reference :Calculator |> JS.new([10]) |> JS.call(:add, [5]) # 15 ``` ##### Callback Interop Elixir anonymous functions can be passed as callbacks to JavaScript functions. They are automatically converted to JavaScript functions and their return values are converted back. Given this JavaScript file: ```javascript // helpers.mjs const helpers = { mapArray(arr, fn) { return arr.map(fn); }, }; export default helpers; ``` You can pass an Elixir anonymous function as a callback: ```elixir callback = fn x -> x * 2 end JS.call(:helpers, :mapArray, [[1, 2, 3], callback]) # [2, 4, 6] ``` ##### JS.new/2 - Instantiate a Class Creates a new instance of a JavaScript class: ```elixir js_import from: "./calculator.mjs", as: :Calculator :Calculator |> JS.new([10]) |> JS.call(:add, [5]) # 15 ``` The class can be an imported binding or a global constructor. ##### JS.get/2 - Get a Property Reads a property from a JavaScript object: ```elixir value = :Calculator |> JS.new([10]) |> JS.get(:value) # 10 ``` ##### JS.set/3 - Set a Property Sets a property on a JavaScript object. Returns the receiver (for chaining): ```elixir value = :Calculator |> JS.new([10]) |> JS.set(:value, 20) |> JS.get(:value) # 20 ``` ##### JS.delete/2 - Delete a Property Deletes a property from a JavaScript object. Returns the receiver (for chaining): ```elixir result = :Calculator |> JS.new([42]) |> JS.delete(:value) |> JS.get(:value) |> JS.typeof() # "undefined" ``` ##### JS.typeof/1 - Get Type Returns the JavaScript `typeof` result as a string: ```elixir :Calculator |> JS.new([10]) |> JS.typeof() # "object" ``` ##### JS.instanceof/2 - Check Instance Checks if a value is an instance of a JavaScript class. Returns a boolean: ```elixir :Calculator |> JS.new([10]) |> JS.instanceof(:Calculator) # true ``` ##### JS.eval/1 - Evaluate an Expression Evaluates a JavaScript expression and returns the result: ```elixir JS.eval("3 + 4") # 7 ``` The expression is evaluated as a single expression (not statements). For multi-line code, use `JS.exec/1`. ##### JS.exec/1 - Execute Code Executes arbitrary JavaScript code. You can optionally use `return` to produce a value: ```elixir JS.exec(""" const x = 2; return x + 3; """) # 5 ``` ##### ~JS Sigil - Inline JavaScript The `~JS` sigil is a shorthand for `JS.exec/1`. It is convenient for DOM manipulation and fire-and-forget code: ```elixir ~JS""" const el = document.getElementById('my-element'); el.textContent = 'Updated!'; """ ``` The `~JS` sigil can also return a value: ```elixir ~JS""" const x = 7; return x + 4; """ # 11 ``` ##### JS.dispatch_event - Dispatch Events Dispatches an event on a target. There are several variants: ###### dispatch_event/2 - CustomEvent, No Options ```elixir target = JS.call(:document, :getElementById, ["my-element"]) JS.dispatch_event(target, "my:event") ``` ###### dispatch_event/3 - CustomEvent with Options ```elixir JS.dispatch_event(target, "my:event", detail: %{value: 99}) ``` ###### dispatch_event/3 - Specific Event Type ```elixir JS.dispatch_event(target, :MouseEvent, "click") ``` ###### dispatch_event/4 - Specific Event Type with Options ```elixir JS.dispatch_event(target, :CustomEvent, "my:event", cancelable: true) ``` ###### Dispatching on Global Objects Use atoms like `:document` or `:window` as the target: ```elixir JS.dispatch_event(:document, "my:event") ``` ###### Using Event Listeners with Callbacks You can combine `dispatch_event` with callback interop to set up listeners: ```elixir target = JS.call(:document, :getElementById, ["my-element"]) JS.call(target, :addEventListener, [ "my:event", fn event -> detail = JS.get(event, :detail) JS.set(:window, :__captured__, JS.get(detail, :value)) end ]) JS.dispatch_event(target, "my:event", detail: %{value: 42}) JS.get(:window, :__captured__) # 42 ``` #### Async / Promises In Hologram's client-side Elixir runtime, JavaScript Promises become Elixir `Task`s. Use `Task.await/1` to get the result, just like you would with any other Task in Elixir. ##### Async Methods and Promise-Returning Functions If a JavaScript function is `async` or returns a Promise, `JS.call/3` returns a Task: ```elixir # Async function result = :helpers |> JS.call(:asyncSum, [10, 20]) |> Task.await() # 30 # Promise-returning function result = :helpers |> JS.call(:promiseSum, [100, 200]) |> Task.await() # 300 ``` ##### Async eval and exec If the evaluated expression or executed code returns a Promise, `JS.eval/1` and `JS.exec/1` return a Task: ```elixir result = "fetch('/api/data').then(r => r.json())" |> JS.eval() |> Task.await() ``` ##### Async get If a property value is a Promise, `JS.get/2` returns a Task: ```elixir result = :promiseValue |> JS.get(:data) |> Task.await() # 77 ``` ##### Async new If a constructor returns a Promise (e.g. for async initialization), `JS.new/2` returns a Task: ```elixir obj = :AsyncCounter |> JS.new([50]) |> Task.await() result = JS.get(obj, :value) # 51 ``` #### Dispatching Actions from JavaScript You can dispatch Hologram actions directly from JavaScript code using `Hologram.dispatchAction()`. This is useful for integrating third-party JS libraries or handling events outside of Hologram's template system. ```javascript Hologram.dispatchAction("my_action", "page", { amount: 42, label: "test" }); ``` **Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `actionName` | string | The action name (e.g. `"my_action"` maps to `def action(:my_action, ...)`) | | `target` | string | `"page"` for page actions, `"layout"` for layout actions, or a component CID for component actions | | `params` | object | (Optional) A plain JS object. Keys become atom keys in the `params` map | **Corresponding action in your page, layout, or component:** ```elixir def action(:my_action, params, component) do # params.amount => 42 (integer) # params.label => "test" (string) put_state(component, :result, {params.amount, params.label}) end ``` ##### Dispatching Before Runtime Loads `Hologram.dispatchAction()` is available immediately - even before the Hologram runtime has fully loaded. Actions dispatched before initialization are queued and replayed once the page mounts. This is useful for inline ` ``` Note: Escape curly braces with backslashes (`\{`, `\}`) inside `~HOLO` templates to prevent them from being interpreted as Hologram expressions. Alternatively, wrap the script body in a `{%raw}...{/raw}` block to disable template syntax entirely. #### Type Conversion Values are automatically converted when crossing the Elixir/JavaScript boundary. ##### Elixir to JavaScript | Elixir Type | JavaScript Type | |-------------|-----------------| | integer | number / bigint* | | float | number | | boolean | boolean | | nil | null | | string | string | | list | Array | | tuple | Array | | map | Object | | anonymous function | Function | | atom | JS binding** / string | \* Integers outside the safe integer range (`Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER`) are passed as `bigint`. \*\* Atoms are resolved in order: `js_import` binding, then `window` property, then string fallback. Exception: atom keys in maps are always converted to strings. ##### JavaScript to Elixir | JavaScript Type | Elixir Type | |-----------------|-------------| | number | integer / float* | | boolean | boolean | | null | nil | | undefined | opaque native value | | string | string (bitstring) | | Array | list | | Plain Object | map (string keys) | | Promise | Task (await with Task.await/1) | | Class instance / other object | opaque native value | | bigint | opaque native value | | Function | opaque native value | | Symbol | opaque native value | \* Determined by `Number.isInteger()`: values like `42` or `3.0` become `integer`, values like `3.14` become `float`. **Opaque native values** are JavaScript values that don't have a direct Elixir equivalent. They are represented as `Hologram.JS.NativeValue` structs and can be passed back to JS interop functions. You can pattern match on the `type` field (`:bigint`, `:function`, `:object`, `:symbol`, `:undefined`). #### DOM Patching Hologram's virtual DOM patching is aware of JS interop. If you manipulate the DOM directly via JS interop (e.g. appending child elements), those changes are preserved across Hologram re-renders, as long as the container element is present in the template: ```elixir def template do ~HOLO"""

Counter: {@counter}

""" end def action(:populate, _params, component) do ~JS""" const container = document.getElementById('js_managed'); const span = document.createElement('span'); span.textContent = 'JS managed content'; container.appendChild(span); """ put_state(component, :counter, component.state.counter + 1) end def action(:increment, _params, component) do put_state(component, :counter, component.state.counter + 1) end ``` In this example, clicking "Populate JS subtree" adds a span via JavaScript and increments the counter. Clicking "Increment counter" afterwards will update the counter display without removing the JS-managed span. #### Coming from LiveView If you're migrating from Phoenix LiveView, Hologram offers two paths for JS interop. The event-based dispatching API (`JS.dispatch_event` and `Hologram.dispatchAction()`) maps directly to LiveView hooks patterns and is provided as a convenience for faster migration - it lets you port existing hook-based integrations with minimal changes. However, for new code the **low-level API is the recommended approach**. Functions like `JS.call`, `JS.new`, `JS.get`, and `JS.set` are tightly integrated with the Hologram runtime - they work synchronously in event loop lock-step with your action handlers, interact naturally with component state management, support automatic type conversion, and handle async results through `Task.await/1`. This keeps your JS integration logic inside your Elixir modules rather than split across separate hook files, and avoids the indirection of passing data through events. The low-level API has no LiveView equivalent - it's a new capability unique to Hologram. ##### JS to Elixir: pushEvent to Hologram.dispatchAction In LiveView, you use `this.pushEvent()` inside a hook to send a message to the server: ```javascript // LiveView hook Hooks.MyChart = { mounted() { this.pushEvent("chart_clicked", { point: 42 }); } }; ``` ```elixir # LiveView handler (server-side) def handle_event("chart_clicked", %{"point" => point}, socket) do {:noreply, assign(socket, :selected, point)} end ``` In Hologram, use `Hologram.dispatchAction()` - no hook boilerplate needed: ```javascript // Anywhere in your JavaScript Hologram.dispatchAction("chart_clicked", "page", { point: 42 }); ``` ```elixir # Hologram handler (runs in the browser) def action(:chart_clicked, params, component) do put_state(component, :selected, params.point) end ``` ##### Elixir to JS: push_event + handleEvent to JS.dispatch_event + addEventListener In LiveView, you push events from the server to a hook's `handleEvent` callback: ```elixir # LiveView (server-side) def handle_event("refresh", _params, socket) do {:noreply, push_event(socket, "data_updated", %{value: 99})} end ``` ```javascript // LiveView hook Hooks.MyChart = { mounted() { this.handleEvent("data_updated", ({ value }) => { this.chart.update(value); }); } }; ``` In Hologram, dispatch an event and listen for it with `addEventListener`: ```elixir # Hologram action (runs in the browser) def action(:refresh, _params, component) do target = JS.call(:document, :getElementById, ["my-chart"]) JS.dispatch_event(target, "data_updated", detail: %{value: 99}) component end ``` ```javascript // Standard listener (no hook needed) document.getElementById("my-chart").addEventListener("data_updated", (event) => { chart.update(event.detail.value); }); ``` Alternatively, you can set up the listener entirely from Elixir using the low-level API: ```elixir def action(:setup_chart, _params, component) do target = JS.call(:document, :getElementById, ["my-chart"]) JS.call(target, :addEventListener, [ "data_updated", fn event -> detail = JS.get(event, :detail) JS.call(:chart, :update, [JS.get(detail, :value)]) end ]) component end ``` ##### Key Differences from LiveView - **No hooks boilerplate** - LiveView hooks require defining a `Hooks` object, wiring it to elements with `phx-hook`, and implementing lifecycle callbacks (`mounted`, `updated`, `destroyed`). Hologram uses standard DOM APIs directly. - **No network round-trip** - LiveView's `pushEvent` and `push_event` communicate over WebSocket between browser and server. Hologram's interop runs entirely in the browser, so there's no latency. - **Beyond events** - LiveView hooks are limited to event passing. Hologram's low-level API lets you instantiate JS classes, read/write properties, chain method calls, and pass Elixir functions as JS callbacks - all from your Elixir action handlers. #### Server-Side Behaviour JS interop functions can only be used in functions reachable through action handlers. Since actions are not executed during SSR (only `init` runs server-side), JS interop code never runs on the server. On the server side, `JS.*` calls are no-ops - most return `:ok`, while `JS.set/3` and `JS.delete/2` return the receiver to preserve chaining. `js_import` is compile-time only and has no runtime effect. Since JS interop functions only work in the browser, functions that use them cannot be covered by Elixir unit tests. Use feature tests (browser-based integration tests) to verify JS interop behaviour. #### Best Practices - **Prefer Elixir over JS when possible** - Only reach for JS interop when you need browser APIs or JS libraries. Keep general logic in Elixir. - **Isolate JS interop behind facade modules** - Create dedicated Elixir modules that wrap the JavaScript interaction, rather than calling `JS.call` directly in your pages and components. This keeps your pages and components focused on UI logic and makes the JS dependency easy to swap. - **Keep JS files small and focused** - Each `.mjs` file should have a single responsibility rather than becoming a utility grab-bag. - **Prefer `JS.call` over `JS.exec` / `JS.eval`** - Structured calls to imported modules are easier to maintain and debug than inline string-based code. #### Publishing Packages Planning to publish a reusable package that wraps a JavaScript library, a Web API, or provides Hologram utilities? Check the Package Naming conventions to help distinguish official and community packages. ### Configuration Hologram is configured under the `:hologram` key in your config files, and reads a few things from environment variables. Everything is optional - the defaults are chosen so that a new app needs no configuration at all. **Note:** These settings belong in `config/config.exs`, or in an environment-specific file such as `config/prod.exs`. The compiler reads them while it builds the client bundles, so changing one requires recompiling, not just restarting - and setting one in `config/runtime.exs` has no effect on a release, whose bundles were compiled long before that file is evaluated. #### Client Diagnostics Two settings control how much an error raised on the client tells you. Neither of them changes the error itself: error messages and `rescue` semantics are identical in every environment, on the client and on the server alike. They only decide how much diagnostic detail travels with the error, and who gets to see it. ##### client_stacktraces Defaults to `true` in the `dev` and `test` environments, and `false` everywhere else. ```elixir config :hologram, client_stacktraces: true ``` When enabled, the compiler emits source metadata into the client bundle and the runtime tracks a call stack, so an error raised on the client carries the same stacktrace as one raised on the server: Elixir frames naming the module, function and arity, each pointing at the line its function had reached. `__STACKTRACE__` holds those frames inside a `rescue` or `catch` clause, and `reraise/2,3` preserves them. When disabled, an error still carries the frame it was raised in - message derivation depends on it - but nothing below that frame. Enabling it outside of `dev` and `test` has trade-offs worth weighing: - Argument values appear in stacktrace frames, and can leave the device through screenshots, support tickets, or pasted console output. - Source paths and line numbers reveal the project's file layout, on top of the module names already readable in the bundle. - Bundles grow, since every expression carries the source line it sits on, every function clause the shape it matches, and every module its source metadata. The compiled Elixir in a bundle is roughly a third larger over the wire. ##### client_error_overlay Defaults to whatever `client_stacktraces` resolves to, so turning client diagnostics on turns both on. ```elixir config :hologram, client_stacktraces: true, client_error_overlay: false ``` When enabled, an uncaught client error is rendered in the page, on top of the app, as well as printed to the browser console. The overlay can be dismissed with the Escape key or its close button. Uncaught errors reach the console in every environment - this setting only decides whether they are also shown in the page. The two reach different people: console output is read by whoever opens the devtools, whereas the overlay is shown to everyone who hits the error. Setting it to `false` alongside enabled stacktraces keeps the server-shaped report in the console, without putting an error screen in front of the app's users. #### Compilation ##### max_bundle_size Unset by default, which means no limit. ```elixir config :hologram, max_bundle_size: 5_000_000 ``` The maximum size of a generated JavaScript bundle, in bytes. A bundle larger than this fails the build, so an accidental dependency that drags a large module into a page is caught before it ships. #### Environment Variables A few things are read from the environment rather than from config, because they are needed before the application environment is available or because they differ per machine: - `HOLOGRAM_ENV` - the environment Hologram runs as, falling back to `MIX_ENV`. It decides which defaults apply, e.g. `client_stacktraces` is enabled in `dev` and `test`, and disabled elsewhere. - `HOLOGRAM_START` - set it to `1` to start Hologram in `dev` and `test`, which is exactly what `mix holo` does. Outside those environments Hologram always starts. - `SECRET_KEY_BASE` - the secret Hologram signs with, required in every environment. In **embedded mode** (Hologram inside your Phoenix endpoint), `dev` and `test` fall back to the endpoint's configured `:secret_key_base`. ## Reference ### Client Runtime Hologram automatically compiles Elixir functions to JavaScript for browser execution. However, many Elixir standard library functions rely on underlying Erlang functions that must be manually ported to JavaScript. This reference tracks the implementation status of both Elixir and Erlang functions in the client runtime. #### Quick Stats The Client Runtime reference page on the website shows live statistics for the number of Elixir and Erlang functions that are done, in progress, todo, or deferred. Visit https://hologram.page/reference/client-runtime for current numbers. #### Understanding This Reference ##### How Statistics Are Calculated A call graph of the Elixir standard library determines which Erlang functions each Elixir function depends on. Function progress is the percentage of its Erlang dependencies that have been ported to JavaScript. Module progress is the average of its functions' progress (including deferred ones). Overall Elixir progress is the average of all non-deferred function progresses. Overall Erlang progress is the percentage of non-deferred functions that are done. ##### Status Meanings - **done** - Fully implemented and ready to use - **in progress** - Partially implemented; often works in typical scenarios, with missing parts only triggered in rare code branches or edge cases - **todo** - Not yet implemented - **deferred** - Implementation postponed to future development phases #### Implementation Scope Hologram development is divided into three phases: **Phase 1** (current focus) targets full-stack web and basic local-first applications; **Phase 2** will add Elixir's process model for advanced local-first and mobile development; **Phase 3** will introduce file system and OS operations for desktop applications. For Phase 1 use cases, you need robust functions for state management, data manipulation, dates/times, strings, and collections - but not processes, file system, or OS operations. JavaScript is single-threaded, and Hologram's architecture handles workflow orchestration automatically for typical web applications. Modules like Process, System, File, Port, Code, and Node are marked as deferred because they target Phases 2-3. This reference shows all dependencies comprehensively - even for deferred functionality. Many "in progress" functions, especially those with high completion percentages, already work fine for web development, with missing dependencies only affecting rare code branches or advanced scenarios you're unlikely to encounter. ### Features Comprehensive overview of all Hologram features organized by area. Each feature shows implementation status, progress percentage, and category classification. Visit https://hologram.page/reference/features for the live, up-to-date version. #### Template Engine **UI:** Components (done), Context (done), Layouts (done), Navigation (done), Pages (done), Routing (done), Template Colocation (done) **Markup:** Component node (done), Element node (done), Interpolation (done), For Block (done), If Block (done), Public Comment (done), Raw Block (done), Text node (done) #### Framework Runtime **Client:** Actions (done), JS Interop (done), Synchronized Form Inputs (done) **Server:** Commands (done), Cookies (done), Middleware (done), Pub/Sub (done), Session (done) #### Events **Focus:** Blur (done), Focus (done) **Form:** Change (done), Select (done), Submit (done) **Keyboard:** Key Down (done), Key Up (done) **Mouse:** Mouse Move (done) **Pointer:** Click (done), Click Outside (done), Pointer Cancel (done), Pointer Down (done), Pointer Move (done), Pointer Up (done) **Resize:** Resize (done) **Scroll:** Reach Bottom (done), Reach Left (done), Reach Right (done), Reach Top (done), Scroll (done) **Transition:** Transition Cancel (done), Transition End (done), Transition Run (done), Transition Start (done) #### Tooling **Development:** Client Error Overlay (done), Incremental Compilation (done), Live Reload (done), Template Formatter (in progress, 60%), VS Code Extension (done) #### Project Setup **Deployment:** Standalone Mode (in progress, 70%) **Structure:** Umbrella Apps (done) #### Elixir Client Runtime **Data Types:** Atom (done), Bitstring (done), Float (done), Function (done), Integer (done), List (done), Map (done), PID (done), Port (done), Reference (done), Tuple (done) **Control Flow:** Case (done), Cond (done), If (done), Function (done), Unless (done), With (done) **Functions:** Anonymous Function (done), Function Capture (done), Local Function (done), Remote Function (done) **Operators:** Arithmetic (done), Boolean (done), Comparison (done), List ++/-- (done), String Concatenation <> (done), Pipe |> (done), Match = (done), Pin ^ (done), Range ../..// (done), Membership in (done), Module Attribute @ (done), Capture & (done), Type :: (done), Exponentiation ** (done), Regex Match =~ (done) **Guards:** Guards in all contexts (done) **Comprehensions:** Bitstring Generator (done), Enumerable Generator (done), Filter (done), :into Option (done), :reduce Option (done), :uniq Option (done) **Bitstrings:** Computed-Size Matching (todo), Construction (done), Dynamic-Size Matching (todo), Fixed-Size Matching (done), UTF Matching (todo), UTF16 and UTF32 Encoding (todo) **Other:** Behaviours (done), Bitwise Module Operators (done), Error Handling (done), Error Stacktraces (done), Essential Stdlib Functions (done), Processes (deferred), Protocols (done), Regular Expressions (done) ### Roadmap This roadmap outlines features that are either in progress or planned for upcoming releases. For currently available features, see the Features page. For details about Elixir standard library coverage and Erlang porting status, refer to the Client Runtime page. Our development roadmap is organized by timeline, with colors representing proximity: short-term goals focus on foundational features, medium-term goals expand capabilities and developer experience, and long-term goals represent transformative capabilities for the framework. Features are ordered alphabetically within each group. #### Short-Term Goals - Foundation & Core Features - **Component-Level Rerendering** - Implement selective rerendering of only changed components instead of entire pages, significantly improving action execution speed and overall responsiveness. - **Dynamic Components** - Support for dynamically loading and rendering components. - **Local-First Support** - Implement local-first architecture with offline-capable local database, auto-sync, and conflict resolution for resilient applications. - **Navigate Between Pages from Commands** - Enable navigation to another page from within a command. - **Standalone Mode** - Enable a standalone mode that operates without Phoenix, with installer, asset pipeline & watchers, code reloader, clustering, and deployment-ready defaults. - **Template Engine Error Reporting** - Improve error reporting in the template engine to make debugging template issues more straightforward. #### Medium-Term Goals - Advanced Features & Enhanced DX - **Advanced Bitstring Support** - Add support for the remaining bitstring capabilities: dynamic and computed segment sizes in patterns, UTF pattern matching, and UTF16/UTF32 encodings. - **Advanced Routing** - Add support for multi-tenant routing (subdomain-based) and catch-all (wildcard) routes for dynamic path segments of arbitrary depth. - **Auth** - Built-in authentication, authorization, and access control system for managing user identities and protecting resources. - **Client MFAs Whitelisting** - Create a mechanism to explicitly whitelist Module-Function-Arity combinations that should be compiled for client-side execution when not automatically detected by the compiler. - **Command Failure Handling Control** - Add more granular control over how command failures are handled, allowing developers to customize error handling strategies, retry logic, and failure recovery mechanisms. - **Internationalization** - Built-in i18n: locale detection and management, locale-aware routing, translation resolution across server and client, and locale-aware date, number, and currency formatting. - **Keyed Lists** - Reconcile rendered comprehension lists by key rather than position, auto-keying items by their `:id` with an explicit `$key` override, so reordered items reuse the correct DOM nodes. - **Props Validation** - Add a system for validating component props. - **Reactivity Layer** - Introduce derived values, prop watchers, and effects for a cohesive reactivity system. - **Render Coalescing** - Batch multiple state updates that land in the same tick into a single render pass automatically, rather than rendering once per action. - **Shorthand Prop Assignment Syntax** - Create an optional, more concise syntax for assigning props while maintaining support for the standard syntax. - **Structural Sharing** - Implement structural sharing for immutable data structures to reduce memory usage and improve performance by avoiding unnecessary data copying. - **Template Formatter** - Implement automatic formatting for templates to ensure consistent code style. - **Template Partials** - Add support for lightweight, reusable template snippets (similar to LiveView's function components) for reducing code duplication without the overhead of full components. #### Long-Term Goals - Transformative Capabilities - **Client Processes** - Port Elixir's process model to the client for concurrent and parallel programming patterns. - **Desktop Platform Support** - Enable packaging Hologram applications for desktop platforms with native integrations. - **Mobile Platform Support** - Enable building native-quality mobile applications from the same Hologram codebase. - **Secrets Protection** - Prevent sensitive information from leaking to the client side. - **Time Travel Debugger** - Develop a debugging tool that allows stepping backward through state changes. ### Contributing Thanks for your interest in contributing to Hologram! #### Contributions happen through issues, not pull requests Hologram has benefited from community pull requests in the past, and I'm grateful to everyone who sent one. For now, though, I write all the code myself. It's my full-time job, funded by sponsors, and this setup has turned out to be the most productive one by far. Once most of the design work is done, I'd like Hologram to have a core team maintaining it - but that's a later phase of the project. Until then, the door isn't closed - it just moved. Here's the short version of why. Writing code is the cheap part now. AI made it easy to produce, but no easier to verify, and with an external PR all of that verification lands on me, whether a human or an AI wrote the code. Given a good spec, it's faster for me to implement a feature than to properly review someone else's. That includes AI doing much of the typing - the difference is a tight feedback loop, where I iterate and benchmark along the way instead of auditing finished code after the fact. The architecture is also moving fast. Everything that exists today is documented on the website, and the Roadmap shows what's coming - but not how it'll be designed. Speccing that out publicly would be wasted effort, because it could change a month later. Which is why even good external code tends to make choices that won't fit where the framework is heading. I don't bolt things on top of Hologram: every feature has to fit the whole design, and that's much easier to get right when the design discussion happens before the code exists. Issues are where that discussion happens. You describe the problem, we talk through the options, and it turns into a spec. That costs you an hour, not a weekend. When the feature fits the roadmap, you get it implemented by the person who knows where the architecture is heading, shaped around your actual use case. And your name goes in the release announcement. When it doesn't fit yet, the issue still shapes priorities, and there's often a workaround in the meantime. #### What helps most Bugs, feature requests, and docs feedback go through GitHub issues. Security vulnerabilities are the exception - report those privately instead of opening a public issue, as described in Reporting Vulnerabilities. For help with your own project, the Discord and the Elixir Forum are the places to get unstuck. If a bug or missing feature is blocking you, say so in the issue and describe what it blocks - a project, a migration, a decision whether to adopt Hologram. Blockers weigh heavily when I decide what to work on next. - Bug reports with a minimal reproduction - your Hologram, Elixir, and Erlang/OTP versions, plus the smallest page or component that triggers the problem, ideally as a repo built from the Hologram skeleton app. - Feature requests that describe what you're building and where Hologram fights you, rather than a proposed implementation. - Docs feedback - typos, unclear sections, missing examples. Even one-character fixes. - Questions - ask on Discord or the Elixir Forum, whichever you prefer. I'm on both, but Discord is busier, so answers usually come faster there. When an answer uncovers a bug or a docs gap, it graduates to an issue. - Ecosystem packages - building your own package on top of Hologram is very welcome, and that code is entirely yours. Hologram aims to ship a lot of core tooling out of the box, but don't let that stop you: packages that fill a gap until official support lands are valuable even when that support is planned, and alternative approaches to what Hologram ships - extensions, integrations, different trade-offs - are always fair game. Ask on Discord or the Elixir Forum what's coming and when, so you know what you're building alongside. Just mind the package naming conventions - the `hologram_` prefix and `Hologram.*` namespace are reserved for official packages. - Spreading the word - blog posts, talks, and example apps help more than you might think. - Sponsoring the project - if Hologram is useful to you, sponsors are what make the full-time work possible. ### Package Naming When publishing a Hologram-related package, keep in mind the following reserved names. This helps the community distinguish official packages from community-maintained ones. #### Reserved Package Name Prefix: `hologram_` Hex package names starting with `hologram_` are reserved for official Hologram packages and extensions. #### Reserved Module Namespace: `Hologram.` The `Hologram.*` module namespace is reserved for the Hologram framework and official extensions. ### Sponsor Hologram Help us bring pure Elixir to the frontend and grow the ecosystem. I'm Bart Blast, the creator of Hologram. For over 5 years, I've been working on this full-stack Elixir web framework that automatically compiles Elixir to JavaScript, bringing the language to the browser. Nearly 3 of those years have been full-time Hologram development. Sponsorship keeps Hologram moving forward. Hologram is a full-time effort. Sponsorships allow me to focus on development, support the community, and push the roadmap forward. Every new sponsor helps expand what's possible. #### Endorsed by José Valim and Elixir Community Hologram has earned recognition and support from the highest levels of the Elixir ecosystem, including promotion by José Valim (Creator of Elixir), being featured on elixir-lang.org, ElixirConf EU 2025 speaker slot, active sponsorship from Zach Daniel (Ash Framework Creator), public endorsement from Adam Kirk (Jump.ai CTO), and production deployments. #### Hologram's Growing Impact **GitHub Activity (since project inception):** 1.1K+ GitHub Stars, 10K+ Commits, 1M+ Lines of Code, 50+ Contributors **Website & Social Media Engagement (last 11 months):** 50K+ Website Visits, 200K+ Pageviews, 250K+ Social Impressions, 3K+ Likes & Shares **Production Use:** While Hex downloads remain modest (2K+) due to the current involved installation process, developers are already experimenting with Hologram and even using it in production environments. The upcoming standalone mode will reduce installation to just 2 terminal commands. #### The Scope: Building an Entire Ecosystem Hologram is an ambitious project. This isn't just another framework - Hologram's scope essentially encompasses: Phoenix + Surface + ElixirScript + React + Local-First. Each of these components represents years of work by entire teams, yet Hologram aims to integrate all these capabilities into a unified, pure Elixir experience. #### Why This Matters Hologram represents a unique opportunity for the Elixir ecosystem. While LiveView revolutionized server-driven interactivity, Hologram brings Elixir to the client - enabling instant UI, offline apps, and modern frontend capabilities without JavaScript. This isn't just about avoiding JavaScript - it's about: - **Code Reuse** - Write your business logic once in Elixir and use it everywhere - **Consistent Language Guarantees** - Maintain Elixir's guarantees across the full stack - **Developer Experience** - Stay in the language you love for the entire application - **Performance** - Eliminate network round trips for UI interactions - **Local-First Applications** - Build offline-capable apps with Elixir - **Already Production-Ready** - In production use with growing adoption #### Sponsorship Opportunities Visit the GitHub Sponsors page to choose a tier and begin your sponsorship: https://github.com/sponsors/bartblast - **Early Adopter** ($14/mo) - Your name in the README.md pioneers section, GitHub Sponsor badge - **Framework Visionary** ($29/mo) - Recognition in release announcements, plus previous tier benefits - **Innovation Partner** ($250/mo) - Company logo on website homepage and README.md, plus previous tier benefits - **Strategic Ally** ($500/mo) - Prominent logo on each documentation page, large logo on homepage and README.md, plus previous tier benefits Custom amounts and creative sponsorship arrangements are also welcome. #### Other Ways to Help - **Share this page** - Help reach potential sponsors by sharing in your network, on social media, or at meetups - **Advocate for corporate sponsorship** - If your company is interested in Hologram, consider advocating within your organization - **Consider consulting** - I'm available for consulting to help explore Hologram's potential for your specific needs - **Spread the word** - Talk about Hologram at meetups, conferences, or in your local Elixir community ### Reporting Vulnerabilities Report it privately through GitHub by opening a draft advisory (https://github.com/bartblast/hologram/security/advisories/new). That's the fastest route, and it keeps the details out of sight until there's a version to upgrade to. If GitHub doesn't work for you, email bart@bartblast.com instead. Please don't use a public GitHub issue, Discord, the Elixir Forum, or Slack for this. They're all public, and a report there hands the details to attackers before anyone can patch. Send whatever you have: - your Hologram, Elixir, and Erlang/OTP versions - what an attacker can do with it - the smallest page or component that reproduces it, ideally built from the Hologram skeleton app (https://github.com/bartblast/hologram_skeleton) A rough report beats no report. If you're unsure whether something counts, send it anyway. #### What Happens Next Reports get acknowledged within 3 business days, and within a week you'll know whether it reproduces and how serious it looks. Hologram is maintained full-time, so reports don't sit in a queue. After that: 1. The fix lands on a private branch, with a test that pins the behavior. 2. It ships in a release. 3. A security advisory goes out naming the affected versions and the fixed one, along with a CVE request. You get credit in the advisory under whatever name or handle you like, or none at all if you'd rather stay anonymous. Just say which. Anything exploitable gets a patch release rather than waiting for the next minor. The advisory goes public when that release does. The fix commit becomes public with that release, so holding the advisory back any longer only keeps it from the people who need to upgrade. If something turns out to need a redesign and can't move that fast, you'll hear where it stands rather than nothing at all. #### Which Versions Get Fixes The latest release, and nothing older. Hologram is pre-1.0 and the architecture still moves fast, so there are no backports to earlier minor versions. Acting on a security fix means upgrading to the newest version. #### Scope The framework itself is in scope: the compiler, the client runtime, the server, and the code Hologram generates for your pages. If an app built the way the docs describe ends up exposed, that's worth reporting. Out of scope: - Bugs in your own application code, unless Hologram's docs told you to write it that way. - Vulnerabilities in a dependency. Report those to the dependency's maintainers. Do report it here if Hologram uses a dependency in a way that turns an otherwise harmless bug into an exploitable one. - Anything that needs the attacker to already have access to your server, your database, or your source code. - Scanner output with no working reproduction. - Packages built on top of Hologram by other people. Those belong with their own maintainers. ## Resources ### Community Connect with other Hologram developers, ask questions, share your projects, and stay up to date with the latest developments. - **Elixir Forum** (https://elixirforum.com/hologram) - In-depth discussions, questions, and announcements - **Discord** (https://discord.gg/huJWNuqt8J) - Real-time conversations, quick questions, and community chat - **Slack** (https://elixir-lang.slack.com/channels/hologram) - Chat in the #hologram channel on the Elixir Slack workspace - **X / Twitter** (https://x.com/Bart_Blast) - Release announcements and project updates - **Bluesky** (https://bsky.app/profile/bartblast.com) - Release announcements and project updates - **LinkedIn** (https://www.linkedin.com/in/bartblast/) - Release announcements and project updates ### Courses Get early access to comprehensive Hologram video courses. Learn to build real-world applications through step-by-step video tutorials that start with the Hologram basics and progress to recreating popular applications like Slack, Figma, and Spotify, as well as games like Tetris. You'll gain hands-on experience with different types of interfaces and functionality while learning frontend development patterns, component architecture, and state management. Beyond Hologram itself, you'll learn how to integrate with popular Elixir libraries like Ash, Ecto, Oban, and many others. Each course includes practical projects you can use as templates for your own work. The courses will be offered at an affordable price, with all proceeds going directly toward sustaining the Hologram project and supporting its ongoing development. Join the waiting list at https://hologram.page/courses. ### Hologram UI Get early access to the official Hologram UI component library. A polished, ready-to-use set of components designed specifically for Hologram, so you can focus on your product instead of rebuilding the same building blocks for every project. Because the library is designed around Hologram's conventions, the components are idiomatic rather than generic ones bolted on. The library will be offered at an affordable price, with all proceeds going toward sustaining the Hologram project. It is currently available through a waiting list at https://hologram.page/ui. ### Newsletter Join the growing community following Hologram's journey. Every month, you'll get updates on development milestones, ecosystem news, and insights from the Hologram world. Think of it as your monthly check-in with everything Hologram-related. Whether it's new features being worked on, interesting discussions in the community, or updates on the framework's direction, you'll get a nice overview of what's been happening. Subscribe at https://hologram.page/newsletter.