DalaDev.Enable (dala_dev v0.0.9)

Copy Markdown View Source

Pure helpers for mix dala.enable — extracted for testability.

LiveView bridge architecture

Enabling LiveView mode involves three coordinated patches. Understanding why all three are necessary prevents subtle bugs when setting up projects manually.

The two bridges

The native WebView (iOS WKWebView / Android WebView) injects a window.dala JavaScript object into every page it loads. This object routes calls through the NIF bridge:

window.dala.send(data)      // JS  NIF  Elixir handle_info
window.dala.onMessage(fn)   // registers handler for NIF  JS messages
window.dala._dispatch(json) // called by the NIF to deliver messages to JS

In LiveView mode you want a different routing: JS messages should travel over the LiveView WebSocket so that handle_event/3 in your LiveView receives them and push_event/3 delivers server messages to JS. The DalaHook replaces window.dala with a LiveView-backed version on mount:

window.dala.send(data)      // JS  pushEvent("dala_message")  handle_event/3
window.dala.onMessage(fn)   // registers handler for handleEvent("dala_push")
window.dala._dispatch       // no-op: server messages arrive via handleEvent

Why a DOM element is required (the non-obvious part)

Phoenix LiveView hooks only execute their mounted() callback when an element carrying phx-hook="DalaHook" is present in the rendered HTML and the LiveView WebSocket has connected. Registering DalaHook in the hooks: map in app.js is necessary but not sufficient — the hook is dormant until LiveView finds a matching DOM element.

Without the element:

  • DalaHook never mounts
  • window.dala is never replaced with the LiveView version
  • window.dala.send() routes through the native NIF bridge instead of LiveView
  • handle_event/3 never fires; your LiveView cannot receive JS messages

The element is a hidden <div> placed immediately after the opening <body> tag in root.html.heex:

<div id="dala-bridge" phx-hook="DalaHook" style="display:none"></div>

Placing it at the top of <body> ensures the hook mounts as early as possible, so window.dala is overridden before any page-specific JS runs.

Android timing note

iOS injects the native window.dala shim via WKUserScript at .atDocumentStart — before any page JS runs. Android injects it via evaluateJavascript in onPageFinished — after the page has loaded. Between page load and onPageFinished on Android, window.dala is undefined. In practice LiveView connects after onPageFinished, so both shims are available by the time the DalaHook mounts. If you call window.dala during DOMContentLoaded, guard with if (window.dala).

Summary

Functions

Builds a plist <key>/<value> entry for Info.plist injection.

Returns the hidden bridge <div> element that must appear in root.html.heex.

Returns the DalaHook JS constant to inject into app.js.

Finds root.html.heex in a Phoenix project rooted at project_dir.

Adds android:networkSecurityConfig="@xml/network_security_config" to the <application> tag in an AndroidManifest.xml string.

Injects the hidden bridge <div> into content (a root.html.heex file).

Injects the DalaHook definition and registration into content (the full text of assets/js/app.js).

Returns the XML content for the Android network security config.

Reads the app: atom from the given mix.exs path and returns the app name as a string, or raises.

Functions

build_plist_entry(key, value, opts \\ [])

@spec build_plist_entry(String.t(), term(), keyword()) :: String.t()

Builds a plist <key>/<value> entry for Info.plist injection.

Options:

  • type: :bool — emits <true/> or <false/> instead of <string>

dala_bridge_element()

@spec dala_bridge_element() :: String.t()

Returns the hidden bridge <div> element that must appear in root.html.heex.

See the module doc for why this element is required.

dala_hook_js()

@spec dala_hook_js() :: String.t()

Returns the DalaHook JS constant to inject into app.js.

find_root_html(project_dir, app_name)

@spec find_root_html(String.t(), String.t()) :: String.t() | nil

Finds root.html.heex in a Phoenix project rooted at project_dir.

Checks both the Phoenix 1.7+ convention:

lib/<app_name>_web/components/layouts/root.html.heex

and the pre-1.7 convention:

lib/<app_name>_web/templates/layout/root.html.heex

Returns the path string or nil if neither file exists.

inject_android_network_security_config(manifest_content)

@spec inject_android_network_security_config(String.t()) :: String.t()

Adds android:networkSecurityConfig="@xml/network_security_config" to the <application> tag in an AndroidManifest.xml string.

Idempotent — returns the content unchanged if the attribute is already present.

inject_dala_bridge_element(content)

@spec inject_dala_bridge_element(String.t()) :: String.t()

Injects the hidden bridge <div> into content (a root.html.heex file).

The element is placed immediately after the opening <body> tag. This is the mount point for DalaHook — without it the hook never executes and window.dala is never replaced with the LiveView version. See the module doc for the full explanation.

Returns the patched HTML string unchanged if id="dala-bridge" is already present.

inject_dala_hook(content)

@spec inject_dala_hook(String.t()) :: String.t()

Injects the DalaHook definition and registration into content (the full text of assets/js/app.js).

  • Inserts the hook constant after the last top-level import line.
  • Registers DalaHook in the hooks: option passed to LiveSocket.

Returns the patched JS string. Idempotency (skip if already present) is handled by the calling task, not by this function.

network_security_config_xml()

@spec network_security_config_xml() :: String.t()

Returns the XML content for the Android network security config.

read_app_name_from(mix_exs_path)

@spec read_app_name_from(String.t()) :: String.t()

Reads the app: atom from the given mix.exs path and returns the app name as a string, or raises.