Data Binding

data

Data Binding

Definition

Data binding is the mechanism that creates a live connection between a data source and a consumer (typically a user interface), ensuring that changes to the underlying data are automatically propagated to the presentation layer and, in bidirectional binding, that user interactions update the data source. In traditional client-side frameworks (React, Vue, Angular), data binding operates through observer patterns, virtual DOM diffing, or reactive streams. In the Prismatic Platform's Phoenix LiveView architecture, data binding is implemented server-side through socket assigns, where state changes on the server trigger minimal DOM patches sent over WebSocket connections.

This server-side approach eliminates the need for client-side state management libraries, API serialization layers, and complex synchronization logic. The server holds the canonical state and pushes diffs to the browser, creating a fundamentally simpler architecture that retains real-time responsiveness. The tradeoff: server memory usage scales with connected users, and network latency affects interaction responsiveness.

Overview

Data Binding Paradigms

flowchart TD
    subgraph Client["Client-Side Binding (React, Vue)"]
        A[User Action] --> B[Client State Update]
        B --> C[Virtual DOM Diff]
        C --> D[DOM Patch]
        B --> E[API Call to Server]
        E --> F[Server Response]
        F --> B
    end

    subgraph Server["Server-Side Binding (Phoenix LiveView)"]
        G[User Action] --> H[WebSocket Event]
        H --> I[Server handle_event]
        I --> J[assign update]
        J --> K[HEEx Re-render]
        K --> L[Diff Computation]
        L --> M[Minimal DOM Patch via WS]
    end
AspectClient-Side (React/Vue)Server-Side (LiveView)
State locationBrowser (Redux, Vuex)Server (socket assigns)
Sync mechanismHTTP/REST + local stateWebSocket + server diff
Initial loadJS bundle + API callsHTML + WS connection
Offline supportPossible (service worker)Not possible (server required)
SEORequires SSRNative (server-rendered)
ComplexityHigh (state management, sync)Low (single source of truth)
MemoryClient-sideServer-side (per connection)
LatencyInstant (local state)Network round-trip
SecurityLogic exposed in JSLogic stays on server

Why Server-Side Binding Works

The key insight behind LiveView's data binding model: most web applications are inherently server-connected. They need to read/write databases, enforce business rules, and coordinate across users. Client-side frameworks duplicate state between server and client, then spend enormous effort keeping them synchronized. LiveView eliminates this duplication by keeping state in one place.

The bandwidth cost is surprisingly low. LiveView's diffing engine sends only changed DOM fragments -- typically 50-500 bytes per update, compared to full API responses of 5-50KB in traditional SPAs.

Technical Deep Dive

Socket Assigns: The Core Mechanism

Phoenix LiveView's data binding operates through three interconnected mechanisms:

  1. Socket assigns (assign/3): Key-value state stored on the server socket
  2. HEEx templates: Declarative rendering that reads assigns
  3. Diffing engine: Computes minimal DOM patches when assigns change
# Each assign change triggers a re-render of the affected template region
socket
|> assign(:tools, tools)           # Replaces :tools, triggers render
|> assign(:loading, false)         # Replaces :loading, triggers render
|> assign(:results, new_results)   # Replaces :results, triggers render

When a socket assign changes, LiveView:

  1. Re-evaluates the HEEx template (only the parts referencing changed assigns)
  2. Compares the new rendered output against the previous fingerprint
  3. Computes a minimal set of DOM operations (text changes, attribute updates, element insertions/removals)
  4. Serializes the diff as a compact binary message
  5. Sends it over the WebSocket
  6. Client-side JS applies the patch to the live DOM

Binding Types

Binding TypeMechanismDirectionExample
Assign Bindingassign/3 on socketServer -> Clientassign(socket, :tools, tools)
Event Bindingphx-click, phx-changeClient -> Serverphx-click="filter"
Form Bindingphx-change + changesetsBidirectionalphx-change="validate"
PubSub BindingPhoenix.PubSubExternal -> Server -> Clientbroadcast("osint:results", data)
Stream Bindingstream/4 (LiveView 0.18+)Server -> Client (append/prepend)stream(:results, results)
Hook Bindingphx-hook + JS interopBidirectionalphx-hook="ChartHook"

PubSub-Driven Binding

The most powerful binding pattern in the Prismatic Platform: external events (OSINT results, DD pipeline updates, security alerts) flow through PubSub into LiveView assigns:

flowchart LR
    subgraph External["External Events"]
        O[OSINT Tool] --> P1[PubSub: osint:results]
        D[DD Pipeline] --> P2[PubSub: dd:pipeline]
        S[Security Scan] --> P3[PubSub: alerts:security]
    end

    subgraph LV["LiveView Process"]
        P1 --> H1[handle_info]
        P2 --> H2[handle_info]
        P3 --> H3[handle_info]
        H1 --> A[assign :results]
        H2 --> A2[assign :dd_status]
        H3 --> A3[assign :alerts]
    end

    subgraph Browser["Browser"]
        A --> DOM1[Results list updates]
        A2 --> DOM2[Pipeline status updates]
        A3 --> DOM3[Alert banner appears]
    end

Temporary Assigns for Streaming Data

For high-volume data (OSINT results, log entries), storing all items in assigns would consume excessive server memory. Temporary assigns solve this:

defmodule PrismaticWeb.OsintResultsLive do
  use PrismaticWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    if connected?(socket) do
      Phoenix.PubSub.subscribe(Prismatic.PubSub, "osint:results")
    end

    {:ok,
      assign(socket, results: [], result_count: 0),
      temporary_assigns: [results: []]}
    # After each render, :results is reset to []
    # The DOM retains previously rendered items
    # New items are appended without keeping all in memory
  end

  @impl true
  def handle_info({:osint_result, result}, socket) do
    {:noreply,
      socket
      |> assign(:results, [result])
      |> update(:result_count, &(&1 + 1))}
  end
end

LiveView Streams (Modern Approach)

LiveView 0.18+ introduced streams as the preferred pattern for large collections:

defmodule PrismaticWeb.DdEntitiesLive do
  @moduledoc """
  DD entity list using LiveView streams for efficient
  large-collection rendering with insert/delete support.
  """
  use PrismaticWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    entities = PrismaticDd.list_entities(limit: 50)

    {:ok,
      socket
      |> stream(:entities, entities)
      |> assign(:page, 1)}
  end

  @impl true
  def handle_event("load_more", _params, socket) do
    page = socket.assigns.page + 1
    entities = PrismaticDd.list_entities(limit: 50, offset: (page - 1) * 50)

    {:noreply,
      socket
      |> stream(:entities, entities)
      |> assign(:page, page)}
  end

  @impl true
  def handle_event("delete", %{"id" => id}, socket) do
    {:ok, entity} = PrismaticDd.delete_entity(id)
    {:noreply, stream_delete(socket, :entities, entity)}
  end
end

Streams vs temporary assigns:

FeatureTemporary AssignsStreams
Insert positionAppend onlyInsert at any position
DeleteNot supportedstream_delete/3
UpdateNot supportedRe-stream same ID
DOM IDManualAutomatic (streams-{id})
MemoryReset after renderServer tracks IDs only

Usage in Prismatic Platform

OSINT Toolbox Real-Time Binding

The OSINT toolbox at /osint/toolbox demonstrates sophisticated data binding where tool execution results stream in real-time:

defmodule PrismaticWeb.OsintToolboxLive do
  @moduledoc """
  Interactive OSINT toolbox with real-time result streaming.
  Demonstrates multi-source data binding: tool registry (ETS),
  execution results (PubSub), and user interactions (events).
  """
  use PrismaticWeb, :live_view

  @impl Phoenix.LiveView
  def mount(_params, _session, socket) do
    if connected?(socket) do
      Phoenix.PubSub.subscribe(Prismatic.PubSub, "osint:results")
    end

    tools = PrismaticOsintCore.ToolRegistry.list_tools()
    categories = PrismaticOsintCore.ToolRegistry.categories()

    {:ok, assign(socket,
      tools: tools,
      categories: categories,
      selected_tool: nil,
      search_query: "",
      selected_category: "all",
      results: [],
      loading: false,
      view_mode: :grid
    )}
  end

  # Event binding: user selects a tool
  @impl Phoenix.LiveView
  def handle_event("select_tool", %{"slug" => slug}, socket) do
    tool = PrismaticOsintCore.ToolRegistry.get_tool(slug)
    {:noreply, assign(socket, selected_tool: tool, results: [])}
  end

  # Form binding: search input with debounce
  @impl Phoenix.LiveView
  def handle_event("search", %{"query" => query}, socket) do
    filtered = PrismaticOsintCore.ToolRegistry.search(query)
    {:noreply, assign(socket, tools: filtered, search_query: query)}
  end

  # Event binding: execute tool
  @impl Phoenix.LiveView
  def handle_event("execute", params, socket) do
    tool = socket.assigns.selected_tool
    Task.start(fn -> PrismaticOsintCore.execute(tool.slug, params) end)
    {:noreply, assign(socket, loading: true)}
  end

  # PubSub binding: results arrive asynchronously
  @impl Phoenix.LiveView
  def handle_info({:osint_result, result}, socket) do
    {:noreply, assign(socket,
      results: [result | socket.assigns.results],
      loading: false
    )}
  end
end

Dashboard Multi-Source Binding

defmodule PrismaticWeb.DashboardLive do
  @moduledoc """
  Real-time dashboard demonstrating multi-source data binding
  where multiple PubSub topics feed into a unified LiveView state.
  """
  use PrismaticWeb, :live_view

  @topics ["dd:pipeline", "osint:results", "alerts:security", "quality:metrics"]

  @impl Phoenix.LiveView
  def mount(_params, _session, socket) do
    if connected?(socket) do
      Enum.each(@topics, &Phoenix.PubSub.subscribe(Prismatic.PubSub, &1))
    end

    {:ok, assign(socket,
      dd_status: %{running: 0, completed: 0},
      osint_findings: [],
      security_alerts: [],
      quality_score: 0,
      connected_at: DateTime.utc_now()
    )}
  end

  @impl Phoenix.LiveView
  def handle_info({:dd_update, status}, socket) do
    {:noreply, assign(socket, dd_status: status)}
  end

  @impl Phoenix.LiveView
  def handle_info({:osint_finding, finding}, socket) do
    findings = [finding | Enum.take(socket.assigns.osint_findings, 49)]
    {:noreply, assign(socket, osint_findings: findings)}
  end

  @impl Phoenix.LiveView
  def handle_info({:security_alert, alert}, socket) do
    alerts = [alert | socket.assigns.security_alerts]
    {:noreply, assign(socket, security_alerts: alerts)}
  end

  @impl Phoenix.LiveView
  def handle_info({:quality_update, score}, socket) do
    {:noreply, assign(socket, quality_score: score)}
  end
end

JavaScript Hook Binding

For client-side interactivity (Chart.js, Mermaid, D3.js), LiveView hooks bridge server assigns to JavaScript:

# Server-side: push data to hook
def handle_info({:metrics_update, metrics}, socket) do
  {:noreply, push_event(socket, "chart-update", %{data: metrics})}
end
// Client-side: hook receives server events
Hooks.MetricsChart = {
  mounted() {
    this.chart = new Chart(this.el, { type: 'line', data: {} });

    this.handleEvent("chart-update", ({ data }) => {
      this.chart.data.datasets[0].data = data;
      this.chart.update();
    });
  },

  destroyed() {
    this.chart.destroy();
  }
};

Performance Optimization

Assign Size Management

# ❌ BAD: Storing large derived data in assigns
def handle_event("filter", %{"category" => cat}, socket) do
  filtered = Enum.filter(socket.assigns.all_tools, &(&1.category == cat))
  stats = compute_statistics(filtered)
  chart_data = build_chart_data(filtered)

  {:noreply, assign(socket,
    filtered_tools: filtered,    # Large list
    stats: stats,                # Derived data
    chart_data: chart_data       # More derived data
  )}
end

# ✅ GOOD: Minimal assigns, derive in render
def handle_event("filter", %{"category" => cat}, socket) do
  {:noreply, assign(socket, selected_category: cat)}
end

# Derive in the template or component
defp filtered_tools(tools, "all"), do: tools
defp filtered_tools(tools, category) do
  Enum.filter(tools, &(&1.category == category))
end

Render Optimization Table

TechniqueWhenBenefit
assign_new/3Data that never changesPrevents re-fetch on reconnect
temporary_assignsStreaming append-only listsConstant server memory
stream/4Large collections with CRUDEfficient insert/delete
push_event/3Client-side JS updatesNo server re-render
Derived values in renderComputed dataFewer assigns, less memory
Component boundariesIndependent UI sectionsIsolated re-renders

Best Practices

  1. Minimize assign sizes -- only store data the template actually renders. Derive computed values in render callbacks rather than storing them in assigns.
  2. Use temporary assigns for large lists -- temporary_assigns: [results: []] prevents memory accumulation for streaming data.
  3. Subscribe to PubSub only when connected -- the connected?(socket) guard prevents duplicate subscriptions during static mount.
  4. Avoid unnecessary re-renders -- assigning the same value to a key does not trigger a re-render, but computing and assigning derived data on every event does.
  5. Use assign_new/3 for data that should not change -- prevents re-fetching on reconnection.
  6. Prefer streams over temporary assigns for collections with CRUD operations.
  7. Use push_event/3 for pure client-side updates -- chart updates, animations, and notifications that don't need server-side DOM rendering.
  8. Keep assigns serializable -- avoid storing PIDs, references, or closures in assigns.
  9. Debounce rapid events -- use phx-debounce="300" on search inputs to reduce server load.

Common Mistakes

MistakeImpactSolution
Storing entire DB result set in assignsMemory exhaustionPaginate, use streams
Missing connected? guard on PubSub subscribeDouble subscriptionsAlways guard with if connected?(socket)
Deriving large data in handle_eventUnnecessary re-rendersDerive in template/component
Using Map.get for assign access in HEExCrashes if key missingUse @assign_name syntax
Not using temporary_assigns for streaming dataServer memory grows unboundedAdd temporary_assigns option
PubSub topic leaks (no unsubscribe)Memory leak on disconnectLiveView handles cleanup automatically
  • LiveView -- Phoenix's server-side rendering framework implementing data binding
  • Phoenix -- web framework providing the data binding infrastructure
  • PubSub -- publish-subscribe system for external event binding
  • WebSocket -- transport protocol for real-time binding updates
  • HEEx -- template engine that renders bound data
  • GenServer -- OTP behaviour managing stateful data that binds to LiveView
  • ETS -- in-memory storage providing sub-millisecond data access for binding sources
  • Assigns -- the key-value state mechanism underlying LiveView binding
  • Reactive Programming -- paradigm that data binding implements
  • Event Sourcing -- pattern where data changes drive binding updates
  • Immutable Data -- functional principle enabling reliable diff computation
  • Pattern Matching -- Elixir feature used in handle_event/handle_info binding handlers
  • Telemetry -- observability for binding performance metrics
  • Plug -- request pipeline that initializes LiveView connections

See Also


Connect & Contribute

Created by Tomas Korcak (korczis) | Open Source under GHL