### Presenter View Setup Source: https://github.com/launchscout/live_state/blob/main/live_state_intro.html Initializes the presenter view by creating and appending necessary DOM elements and setting up event listeners for navigation and state updates. ```javascript function V(e){const{title:t}=document;document.title="[Presenter view]"+(t?` - ${t}`:"");const n={},r=e=>(n[e]=n[e]||document.querySelector(`.${e}`),n[e]);document.body.appendChild((e=>{const t=document.createElement("div");return t.className=$,t.appendChild(e),t.insertAdjacentHTML("beforeend",N(F,null,N("div",{class:q},N("iframe",{class:O,src:"?view=next"})),N("div",{class:C}),N("div",{class:T},N("div",{class:A},N("button",{class:j,tabindex:"-1",title:"Previous"},"Previous"),N("span",{class:K}),N("button",{class:R,tabindex:"-1",title:"Next"},"Next")),N("time",{class:U,title:"Current time"}),N("div",{class:D})))),t})(e.parent)),(e=>{r(q).addEventListener("click",(()=>e.next()));const t=r(O),n=(s=t,(e,t)=>{var n;return null===(n=s.contentWindow)||void 0===n?void 0:n.postMessage(`navigate:${e},${t}`,"null"===window.origin?"*":window.origin)});var s;t.addEventListener("load",((=>{r(q).classList.add("active"),n(e.slide(),e.fragmentIndex),e.on("fragment",(({index:e,fragmentIndex:t})=>n(e,t)))}));const a=document.querySelectorAll(".bespoke-marp-note");a.forEach((e=>{e.addEventListener("keydown",(e=>e.stopPropagation())),r(C).appendChild(e)})),e.on("activate",(()=>a.forEach((t=>t.classList.toggle("active",t.dataset.index==e.slide()))))),e.on("activate",(({index:t})=>{r(K).textContent=`${t+1} / ${e.slides.length}`}));const i=r(j),o=r(R);i.addEventListener("click",(t=>{i.blur(),e.prev({fragment:!t.shiftKey})})),o.addEventListener("click",(t=>{o.blur(),e.next({fragment:!t.shiftKey})})),e.on("fragment",(({index:t,fragments:n,fragmentIndex:r})=>{i.disabled=0===t&&0===r,o.disabled=t===e.slides.length-1&&r===n.length-1}));const l=()=>r(U).textContent=(new Date).toLocaleTimeString();l(),setInterval(l,250)})(e)} ``` -------------------------------- ### Create New Phoenix App Source: https://github.com/launchscout/live_state/blob/main/docs/tutorial_start.md Use this command to generate a new Phoenix application. Follow the on-screen instructions for database setup. ```bash mix phx.new pipe_spot ``` -------------------------------- ### Define Initial State with init Callback Source: https://github.com/launchscout/live_state/blob/main/README.md Implement the c:LiveState.Channel.init/3 callback to return the initial state for the channel after it joins. This example returns a map with a 'foo' key. ```elixir def init(_channel, _payload, _socket), do: {:ok, %{foo: "bar"}} ``` -------------------------------- ### Test LiveState Channel with Phoenix.ChannelTest Source: https://context7.com/launchscout/live_state/llms.txt Use `LiveState.TestHelpers` to test LiveState channels. Ensure your endpoint and PubSub are started and the socket is subscribed and joined to the channel. ```elixir defmodule MyAppWeb.TodoChannelTest do use ExUnit.Case import Phoenix.ChannelTest import LiveState.TestHelpers @endpoint MyAppWeb.Endpoint setup do start_supervised(@endpoint) start_supervised(Phoenix.PubSub.child_spec(name: MyApp.PubSub)) {:ok, _, socket} = socket(MyAppWeb.UserSocket, "test_user", %{}) |> subscribe_and_join(MyAppWeb.TodoChannel, "todos:all", %{"token" => "test-token"}) {:ok, %{socket: socket}} end test "initial state is pushed on join" do # assert_state_change matches against state:change message assert_state_change %{todos: [], token: "test-token"} end test "add_todo event updates state", %{socket: socket} do # send_event/3 pushes "lvs_evt:" over the channel send_event(socket, "add_todo", %{"description" => "Buy milk"}) assert_state_change %{todos: [%{"description" => "Buy milk"}]} end test "state:patch is sent on incremental update", %{socket: socket} do send_event(socket, "add_todo", %{"description" => "First"}) send_event(socket, "add_todo", %{"description" => "Second"}) # assert_state_patch matches the JSON patch array assert_state_patch([%{"op" => "add", "path" => "/todos/0"}]) end test "PubSub messages update state" do Phoenix.PubSub.broadcast(MyApp.PubSub, "todos", {:todo_added, %{"description" => "From pubsub"}}) assert_push "state:change", %{state: %{todos: [%{"description" => "From pubsub"}]}} end end ``` -------------------------------- ### use LiveState.Channel — Defining a Channel Source: https://context7.com/launchscout/live_state/llms.txt This section details how to use the `LiveState.Channel` behaviour to define a channel module. It includes examples of implementing `init/3` and `handle_event/3` callbacks for managing state and responding to client events. ```APIDOC ## `use LiveState.Channel` — Defining a Channel Brings the LiveState behaviour into a Phoenix Channel module. Accepts `web_module:`, `message_builder:`, and `max_version:` options. ```elixir defmodule MyAppWeb.TodoChannel do use LiveState.Channel, web_module: MyAppWeb, max_version: 500 # optional; resets version counter at 500 (default 1000) alias LiveState.Event # Required: return initial state after a client joins @impl true def init(_channel, %{"user_id" => user_id}, _socket) do todos = Repo.all(from t in Todo, where: t.user_id == ^user_id) {:ok, %{todos: todos, user_id: user_id}} end # Handle a client event — return {:noreply, new_state} @impl true def handle_event("add_todo", %{"description" => desc}, %{todos: todos} = state) do new_todo = %{id: System.unique_integer([:positive]), description: desc, done: false} {:noreply, %{state | todos: [new_todo | todos]}} end # Handle a client event — return {:reply, event_or_events, new_state} to push CustomEvents back @impl true def handle_event("complete_todo", %{"id" => id}, %{todos: todos} = state) do updated = Enum.map(todos, fn t -> if t.id == id, do: %{t | done: true}, else: t end) {:reply, %Event{name: "todo-completed", detail: %{id: id}}, %{state | todos: updated}} end end ``` ``` -------------------------------- ### Configure Endpoint for LiveState Socket Source: https://github.com/launchscout/live_state/blob/main/README.md Set up a socket for your LiveState channel in your Phoenix Endpoint module. This example uses PgLiveHeroWeb.Channels.LiveStateSocket. ```elixir defmodule MyAppWeb.Endpoint do socket "/socket", PgLiveHeroWeb.Channels.LiveStateSocket ... ``` -------------------------------- ### Handle Client Events with Socket Access Source: https://context7.com/launchscout/live_state/llms.txt Use the four-arity `handle_event/4` when you need access to the socket, for example, to read assigns set during `authorize/3` or `init/3`. It returns `{:noreply, new_state, socket}` or `{:reply, events, new_state, socket}`. ```elixir defmodule MyAppWeb.ProfileChannel do use LiveState.Channel, web_module: MyAppWeb @impl true def authorize(_channel, %{"token" => token}, socket) do case MyApp.Auth.verify(token) do {:ok, user} -> {:ok, Phoenix.Socket.assign(socket, :current_user, user)} _ -> {:error, "unauthorized"} end end @impl true def init(_channel, _params, %{assigns: %{current_user: user}} = _socket) do {:ok, %{profile: user, edits: 0}} end # 4-arity: receives (event_name, payload, state, socket) # Must return {:noreply, new_state, socket} or {:reply, events, new_state, socket} @impl true def handle_event("update_bio", %{"bio" => bio}, state, %{assigns: %{current_user: user}} = socket) do MyApp.Accounts.update_user(user, %{bio: bio}) {:noreply, %{state | profile: %{user | bio: bio}, edits: state.edits + 1}, socket} end end ``` -------------------------------- ### Handle Client Event with State Update Source: https://github.com/launchscout/live_state/blob/main/README.md Implement the c:LiveState.Channel.handle_event/3 callback to process events from the client and update the state. This example adds a todo item to a list. ```elixir def handle_event("add_todo", todo, %{todos: todos}) do {:noreply, %{todos: [todo | todos]}} end ``` -------------------------------- ### Generate LiveState Lit-based Custom Element Source: https://context7.com/launchscout/live_state/llms.txt Use the `mix live_state.gen.element` task to generate a Lit-based custom element. This task also installs necessary npm dependencies. ```bash mix live_state.gen.element ContactForm contact-form # Creates: assets/js/contact-form.ts # Installs npm dependencies (phx-live-state, lit) ``` -------------------------------- ### LocalStorage Utility Source: https://github.com/launchscout/live_state/blob/main/live_state_intro.html Provides a utility object for interacting with localStorage, including checking availability, getting, setting, and removing items. Includes a warning if localStorage is restricted. ```javascript const w={available:( ()=>{try{return localStorage.setItem("bespoke-marp","bespoke-marp"),localStorage.removeItem("bespoke-marp"),!0}catch(e){return console.warn("Warning: Using localStorage is restricted in the current host so some features may not work."),!1}})(),get:e=>{try{return localStorage.getItem(e)}catch(e){return null}},set:(e,t)=>{try{return localStorage.setItem(e,t),!0}catch(e){return!1}},remove:e=>{try{return localStorage.removeItem(e),!0}catch(e){return!1}}} ``` -------------------------------- ### Configure LiveState Events and Topic Source: https://github.com/launchscout/live_state/blob/main/docs/tutorial_start.md Configure the `@liveState` decorator to specify which events to send, like 'create-contact', and the topic for state synchronization. This setup links frontend events to backend channel communication. ```typescript @liveState({ events: { send: ['create-contact'] } topic: 'contact_form:all' }) ``` -------------------------------- ### Define Elixir Module for Greetings Source: https://github.com/launchscout/live_state/blob/main/live_state_intro.html Defines an Elixir module named 'What do' with a function 'howdy' that returns the string 'howdy'. This is a basic example of module and function definition in Elixir. ```elixir defmodule What do def howdy(), do: "howdy" end ``` -------------------------------- ### Generate ContactForm Element Source: https://github.com/launchscout/live_state/blob/main/docs/tutorial_start.md Generate a LiveState element for the contact form using the provided mix task. This command creates `assets/js/contact-form.ts` and installs necessary npm packages. ```bash mix live_state.gen.element ContactForm contact-form ``` -------------------------------- ### Custom Message Builder with LiveState.MessageBuilder Source: https://context7.com/launchscout/live_state/llms.txt Replace the default message builder by passing `message_builder:` to `use LiveState.Channel`. This example shows a custom builder that always sends the full state instead of a patch. ```elixir # Custom message builder that always sends full state (no patching) defmodule MyApp.FullStateBuilder do alias LiveState.MessageBuilder # Always send full state:change instead of a patch def update_state_message(_old_state, new_state, version, _opts) do MessageBuilder.new_state_message(new_state, version) end def new_state_message(new_state, version, _opts) do MessageBuilder.new_state_message(new_state, version) end end ``` ```elixir # Use the custom builder in a channel defmodule MyAppWeb.SimpleChannel do use LiveState.Channel, web_module: MyAppWeb, message_builder: {MyApp.FullStateBuilder, []} @impl true def init(_ch, _params, _socket), do: {:ok, %{counter: 0}} @impl true def handle_event("increment", _payload, %{counter: n} = state), do: {:noreply, %{state | counter: n + 1}} end ``` -------------------------------- ### Initialize State with `init/3` Source: https://context7.com/launchscout/live_state/llms.txt The `init/3` callback is invoked after a client joins a channel. It returns the initial state pushed to the client. It can also return an updated socket. ```elixir defmodule MyAppWeb.DashboardChannel do use LiveState.Channel, web_module: MyAppWeb @impl true def init(_channel, payload, socket) do # Can also return {:ok, state, updated_socket} or {:error, reason} case authenticate(payload) do {:ok, user} -> socket = Phoenix.Socket.assign(socket, :current_user, user) {:ok, %{metrics: load_metrics(user), notifications: []}, socket} :error -> {:error, "unauthorized"} end end defp authenticate(%{"token" => token}), do: MyApp.Auth.verify(token) defp authenticate(_), do: :error defp load_metrics(user), do: MyApp.Metrics.for_user(user.id) end ``` -------------------------------- ### Initialize Live State Management Source: https://github.com/launchscout/live_state/blob/main/live_state_intro.html Sets up the necessary event listeners and variables for managing live state, including handling zoom factor updates from messages. ```javascript !function(){"use strict";const t="marpitSVGPolyfill:setZoomFactor,",e=Symbol();let r,o;function n(n){const i="object"==typeof n&&n.target||document,a="object"==typeof n?n.zoom:n;window[e]||(Object.defineProperty(window,e,{configurable:!0,value:!0}),window.addEventListener("message",(({data:e,origin:r})=>{if(r===window.origin)try{if(e&&"string"==typeof e&&e.startsWith(t)){const[,t]=e.split(","),r=Number.parseFloat(t);Number.isNaN(r)||(o=r)}}catch(t){console.error(t)}})));let l=!1;Array.from(i.querySelectorAll("svg[data-marpit-svg]"),(t=>{var e,n,i,s;t.style.transform||(t.style.transform="translateZ(0)");const c=a||o||t.currentScale||1;r!==c&&(r=c,l=c);const d=t.getBoundingClientRect(),{length:u}=t.children;for(let r=0;r{null==e||e.postMessage(`${t}${l}`,"null"===window.origin?"*":window.origin)}))}r=1,o=void 0;const i=(t,e,r)=>{if(t.getAttribute(e)!==r)return t.setAttribute(e,r),!0};function a({once:t=!1,target:e=document}={}){const r="Apple Computer, Inc."===navigator.vendor?[ ]:[];let o=!t;const a=()=>{for(const t of r)t({target:e});!function(t=document){Array.from(t.querySelectorAll('svg[data-marp-fitting="svg"]'),(t=>{var e;const r=t.firstChild,o=r.firstChild,{scrollWidth:n,scrollHeight:a}=o;let l,s=1;if(t.hasAttribute("data-marp-fitting-code")&&(l=null===(e=t.parentElement)||void 0===e?void 0:e.parentElement),t.hasAttribute("data-marp-fitting-math")&&(l=t.parentElement),l){const t=getComputedStyle(l),e=Math.ceil(l.clientWidth-parseFloat(t.paddingLeft||"0")-parseFloat(t.paddingRight||"0"));e&&(s=e)}const c=Math.max(n,s),d=Math.max(a,1),u=`0 0 ${c} ${d}`;i(r,"width",`${c}`),i(r,"height",`${d}`),i(t,"preserveAspectRatio",getComputedStyle(t).getPropertyValue("--preserve-aspect-ratio")||"xMinYMin meet"),i(t,"viewBox",u)&&t.classList.toggle("__reflow__")}))}(e),o&&window.requestAnimationFrame(a)};return a(),()=>{o=!1}}const l=Symbol(),s=document.currentScript;((t=document)=>{if("undefined"==typeof window)throw new Error("Marp Core's browser script is valid only in browser context.");if(t[l])return t[l];const e=a({target:t}),r=()=>{e(),delete t[l]};Object.defineProperty(t,l,{configurable:!0,value:r})})(s?s.getRootNode():document)}(); ``` -------------------------------- ### Implement Channel Initialization Source: https://github.com/launchscout/live_state/blob/main/docs/tutorial_start.md The `init` callback in the LiveState channel is responsible for setting the initial state of the application. It returns the initial state map, here setting `complete` to false. ```elixir @impl true def init(_channel, _params, _socket) do {:ok, %{complete: false}} end ``` -------------------------------- ### Authorize Connection with `authorize/3` Source: https://context7.com/launchscout/live_state/llms.txt The `authorize/3` callback runs before `init/3` during channel join. Return `{:ok, socket}` to permit the connection or `{:error, reason}` to reject it. The default implementation always allows. ```elixir defmodule MyAppWeb.PrivateChannel do use LiveState.Channel, web_module: MyAppWeb @impl true def authorize(_channel, %{"password" => "secret"}, socket) do {:ok, socket} end def authorize(_channel, _payload, _socket) do {:error, "Access denied"} # Client receives an error and the join is rejected end @impl true def init(_channel, _payload, _socket) do {:ok, %{authorized: true}} end end ``` -------------------------------- ### c:LiveState.Channel.init/3 — Initialize State Source: https://context7.com/launchscout/live_state/llms.txt This callback is invoked when a client successfully joins the channel. It is responsible for returning the initial state of the application, which is then sent to the client as a `state:change` message. It can also return an updated socket. ```APIDOC ## `c:LiveState.Channel.init/3` — Initialize State Called immediately after a client joins the channel. Returns the initial application state that is pushed to the client as a `state:change` message. ```elixir defmodule MyAppWeb.DashboardChannel do use LiveState.Channel, web_module: MyAppWeb @impl true def init(_channel, payload, socket) do # Can also return {:ok, state, updated_socket} or {:error, reason} case authenticate(payload) do {:ok, user} -> socket = Phoenix.Socket.assign(socket, :current_user, user) {:ok, %{metrics: load_metrics(user), notifications: []}, socket} :error -> {:error, "unauthorized"} end end defp authenticate(%{"token" => token}), do: MyApp.Auth.verify(token) defp authenticate(_), do: :error defp load_metrics(user), do: MyApp.Metrics.for_user(user.id) end ``` ``` -------------------------------- ### Marp Bespoke Plugin Initialization Source: https://github.com/launchscout/live_state/blob/main/live_state_intro.html Initializes the Marp Bespoke plugin, enabling presenter view, next slide preview, and other live state features based on the current view mode. ```javascript function X(e){const t=g();return t===p.Next&&e.appendChild(document.createElement("span")),e=>{t===p.Normal&&function(e){if(!(e=>e.syncKey&&"string"==typeof e.syncKey)(e))throw new Error("The current instance of Bespoke.js is invalid for Marp bespoke presenter plugin.");Object.defineProperties(e,{openPresenterView:{enumerable:!0,value:x},presenterUrl:{enumerable:!0,get:k}}),w.available&&document.addEventListener("keydown",(t=>{80!==t.which||t.altKey||t.ctrlKey||t.metaKey||(t.preventDefault(),e.openPresenterView())}))}(e),t===p.Presenter&&V(e),t===p.Next&&function(e){const t=t=>{if(t.origin!==window.origin)return;const [n,r]=t.data.split(":");if("navigate"===n){const[t,n]=r.split(",");let s=Number.parseInt(t,10),a=Number.parseInt(n,10)+1;a>=e.fragments[s].length&&(s+=1,a=0),e.slide(s,{fragment:a})}};window.addEventListener("message",t),e.on("destroy",(()=>window.removeEventListener("message",t)))}(e)}} ``` -------------------------------- ### c:LiveState.Channel.authorize/3 — Connection Authorization Source: https://context7.com/launchscout/live_state/llms.txt This callback is executed during the channel join process, prior to `init/3`. It allows for connection authorization by returning `{:ok, socket}` to permit the connection or `{:error, reason}` to reject it. The default implementation always permits the connection. ```APIDOC ## `c:LiveState.Channel.authorize/3` — Connection Authorization Called during channel join before `init/3`. Return `{:ok, socket}` to allow or `{:error, reason}` to reject the connection. Default implementation always allows. ```elixir defmodule MyAppWeb.PrivateChannel do use LiveState.Channel, web_module: MyAppWeb @impl true def authorize(_channel, %{"password" => "secret"}, socket) do {:ok, socket} end def authorize(_channel, _payload, _socket) do {:error, "Access denied"} # Client receives an error and the join is rejected end @impl true def init(_channel, _payload, _socket) do {:ok, %{authorized: true}} end end ``` ``` -------------------------------- ### Add LiveState and CORSPlug Dependencies Source: https://github.com/launchscout/live_state/blob/main/docs/tutorial_start.md Add `live_state` and `cors_plug` to your `mix.exs` file to include them in your project. Ensure you run `mix deps.get` after modifying the file. ```elixir def deps do [ ... {:live_state, ">~ 0.7"}, {:cors_plug, ">= 0.0.0"} ] end ``` -------------------------------- ### Initialize Bespoke.js Presentation Source: https://github.com/launchscout/live_state/blob/main/live_state_intro.html Initializes a Bespoke.js presentation with custom plugins. The `t` function applies Marp-specific styling and ARIA attributes for accessibility, and the `n` function enhances fragment management. ```javascript !function(){"use strict";var e=function(e,t){var n,r=1===(e.parent||e).nodeType?e.parent||e:document.querySelector(e.parent||e),s=\[\]\.filter.call("string"==typeof e.slides?r.querySelectorAll(e.slides):e.slides||r.children,(function(e){return"SCRIPT"!==e.nodeName})),a={},i=function(e,t){return(t=t||{}).index=s.indexOf(e),t.slide=e,t},o=function(e,t){a\[e\]=(a\[e\]||\[\])\.filter((function(e){return e!==t}))},l=function(e,t){return(a\[e\]||\[\])\.reduce((function(e,n){return e&&!1!==n(t)}),!0)},c=function(e,t){s\[e\]&&(n&&l("deactivate",i(n,t))),n=s\[e\] ,l("activate",i(n,t))},d=function(e,t){var r=s.indexOf(n)+e;l(e>0?"next":"prev",i(n,t))&&c(r,t)},u={off:o,on:function(e,t){return(a\[e\]||(a\[e\]=\[\])).push(t),o.bind(null,e,t)},fire:l,slide:function(e,t){if(!arguments.length)return s.indexOf(n);l("slide",i(s\[e\] ,t))&&c(e,t)},next:d.bind(null,1),prev:d.bind(null,-1),parent:r,slides:s,destroy:function(e){l("destroy",i(n,e)),a={}}};return(t||\[\]).forEach((function(e){e(u)})),n||c(0),u};function t(e){e.parent.classList.add("bespoke-marp-parent"),e.slides.forEach((e=>e.classList.add("bespoke-marp-slide"))),e.on("activate",(t=>{const n=t.slide,r=!n.classList.contains("bespoke-marp-active");e.slides.forEach((e=>{e.classList.remove("bespoke-marp-active"),e.setAttribute("aria-hidden","true")})),n.classList.add("bespoke-marp-active"),n.removeAttribute("aria-hidden"),r&&(n.classList.add("bespoke-marp-active-ready"),document.body.clientHeight,n.classList.remove("bespoke-marp-active-ready"))}))}function n(e){let t=0,n=0;Object.defineProperty(e,"fragments",{enumerable:!0,value:e.slides.map((e=>\[null,...e.querySelectorAll("[data-marpit-fragment]")\]))});const r=r=>void 0!==e.fragments\[t\]\[n+r\] ,s=(r,s)=>{t=r,n=s,e.fragments.forEach(((e,t)=>{e.forEach(((e,n)=>{if(null==e)return;const a=t{if(a){if(r(1))return s(t,n+1),!1;const a=t+1;e.fragments\[a\]&&s(a,0)}else{const r=e.fragments\[t\]\.length;if(n+1{if(r(-1)&&a)return s(t,n-1),!1;const i=t-1;e.fragments\[i\]&&s(i,e.fragments\[i\]\.length-1)})),e.on("slide",(({index:t,fragment:n})=>{let r=0;if(void 0!==n){const s=e.fragments\[t\];if(s){const{length:e}=s;r=-1===n?e-1:Math.min(Math.max(n,0),e-1)}}s(t,r)})),s(0,0)} /! * screenfull * v5.1.0 - 2020-12-24 * (c) Sindre Sorhus; MIT License */ var r,s=(function(e){!function(){var t="undefined"!=typeof window&&void 0!==window.document?window.document:{},n=e.exports,r=function(){for(var e,n=\[\["requestFullscreen","exitFullscreen","fullscreenElement","fullscreenEnabled","fullscreenchange","fullscreenerror"\],\["webkitRequestFullscreen","webkitExitFullscreen","webkitFullscreenElement","webkitFullscreenEnabled","webkitfullscreenchange","webkitfullscreenerror"\],\["webkitRequestFullScreen","webkitCancelFullScreen","webkitCurrentFullScreenElement","webkitCancelFullScreen","webkitfullscreenchange","webkitfullscreenerror"\],\["mozRequestFullScreen","mozCancelFullScreen","mozFullScreenElement","mozFullScreenEnabled","mozfullscreenchange","mozfullscreenerror"\],\["msRequestFullscreen","msExitFullscreen","msFullscreenElement","msFullscreenEnabled","MSFullscreenChange","MSFullscreenError" \]\] ,r=0,s=n.length,a={};r{s.isEnabled&&s.togg ``` -------------------------------- ### Handle Client Events with LiveState.Channel Source: https://context7.com/launchscout/live_state/llms.txt Use `handle_event/3` to process client-initiated events. It returns `{:noreply, new_state}` for simple mutations or `{:reply, events, new_state}` to dispatch custom events back to the client. ```elixir defmodule MyAppWeb.CartChannel do use LiveState.Channel, web_module: MyAppWeb alias LiveState.Event @impl true def init(_channel, _params, _socket), do: {:ok, %{items: [], total: 0}} # Simple state mutation — no reply event needed @impl true def handle_event("remove_item", %{"sku" => sku}, %{items: items} = state) do updated_items = Enum.reject(items, &(&1.sku == sku)) {:noreply, %{state | items: updated_items, total: calc_total(updated_items)}} end # Reply with a single CustomEvent dispatched on the client element @impl true def handle_event("checkout", _payload, state) do case MyApp.Orders.place(state.items) do {:ok, order} -> {:reply, %Event{name: "order-placed", detail: %{order_id: order.id}}, %{state | items: [], total: 0}} {:error, reason} -> {:reply, %Event{name: "checkout-failed", detail: %{reason: reason}}, state} end end # Reply with multiple CustomEvents @impl true def handle_event("apply_coupon", %{"code" => code}, state) do {:reply, [ %Event{name: "coupon-applied", detail: %{code: code, discount: 10}}, %Event{name: "analytics-track", detail: %{event: "coupon_used"}} ], %{state | total: state.total * 0.9}} end defp calc_total(items), do: Enum.sum(Enum.map(items, & &1.price)) end ``` -------------------------------- ### Add LiveState Dependency to mix.exs Source: https://github.com/launchscout/live_state/blob/main/README.md Add the LiveState library and optionally cors_plug to your project's dependencies in mix.exs. cors_plug is recommended for client connections. ```elixir def deps do [ {:live_state, "~> 0.8.1"}, {:cors_plug, "~> 3.0"} ] end ``` -------------------------------- ### Open Presenter View Window Source: https://github.com/launchscout/live_state/blob/main/live_state_intro.html Opens a new window for the presenter view with specified dimensions. This function is intended to be used within a context where 'this.presenterUrl' and 'this.syncKey' are defined. ```javascript function x(){const e=Math.max(Math.floor(.85*window.innerWidth),640),t=Math.max(Math.floor(.85*window.innerHeight),360);return window.open(this.presenterUrl,`bespoke-marp-presenter-${this.syncKey}`,`wid`)} ``` -------------------------------- ### Define LiveState Socket Module Source: https://github.com/launchscout/live_state/blob/main/README.md Create a socket module that uses Phoenix.Socket and defines the channel topic to listen to. Includes connect and id callbacks. ```elixir defmodule MyAppWeb.Socket do use Phoenix.Socket channel "topic", MyAppWeb.Channel @impl true def connect(_params, socket), do: {:ok, socket} @impl true def id(_), do: "random_id" ``` -------------------------------- ### Create LiveState Channel Module Source: https://github.com/launchscout/live_state/blob/main/README.md Define your channel module using the LiveState.Channel behaviour, specifying the web_module option. ```elixir defmodule MyAppWeb.Channel do use LiveState.Channel, web_module: MyAppWeb ... ``` -------------------------------- ### Handle 'create-contact' Event in Channel Source: https://github.com/launchscout/live_state/blob/main/docs/tutorial_start.md The `handle_event` callback processes the 'create-contact' event from the frontend. It uses a context module to create the contact and updates the state to set `complete` to true upon success. ```elixir @impl true def handle_event("create-contact", contact_attrs, state) do case Contacts.create_contact(contact_attrs) do {:ok, _contact} -> {:noreply, Map.put(state, :complete, true)} {:error, _} -> {:noreply, state} end end ``` -------------------------------- ### Keyboard Navigation Plugin Source: https://github.com/launchscout/live_state/blob/main/live_state_intro.html Provides keyboard navigation for slides, including next/previous slide, fragment navigation, and jumping to the first/last slide. Supports Shift key for fragment control. ```javascript var d;function u({interval:e=200}={}){return t=>{document.addEventListener("keydown",(e=>{if(32===e.which&&e.shiftKey)t.prev();else if(33===e.which||37===e.which||38===e.which)t.prev({fragment:!e.shiftKey});else if(32!==e.which||e.shiftKey)if(34===e.which||39===e.which||40===e.which)t.next({fragment:!e.shiftKey});else if(35===e.which)t.slide(t.slides.length-1,{fragment:-1});else{if(36!==e.which)return;t.slide(0)}else t.next();e.preventDefault()}));let n,r,s=0;t.parent.addEventListener("wheel",(a=>{let i=!1;const o=(e,t)=>{e&&(i=i||function(e,t){return function(e,t){const n=t===d.X?"Width":"Height";return e[``client${n}``]{n=0}),e);const l=Date.now()-s0||a.deltaY>0)&&(f="next"),(a.deltaX<0||a.deltaY<0)&&(f="prev"),f&&(t[f](),s=Date.now())}))}}!function(e){e.X="X",e.Y="Y"}(d||(d={})) ``` -------------------------------- ### Add LiveState to Project Dependencies Source: https://context7.com/launchscout/live_state/llms.txt Add the `live_state` and `cors_plug` dependencies to your Phoenix project's `mix.exs` file. ```elixir defp deps do [ {:phoenix, ">= 1.5.7"}, {:live_state, "~> 0.9.0"}, {:cors_plug, "~> 3.0"} # recommended for cross-origin custom elements ] end ``` -------------------------------- ### Configure esbuild for Custom Elements Source: https://github.com/launchscout/live_state/blob/main/docs/tutorial_start.md Update your `config/config.exs` to include a new build target for custom elements using esbuild. This ensures your custom element JavaScript is bundled correctly. ```elixir config :esbuild, version: "0.17.11", pipe_spot: args: ~w(js/app.js --bundle --target=es2020 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), cd: Path.expand("../assets", __DIR__), env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}, # Add this target bellow custom_elements: args: ~w(js/custom_elements.js --bundle --target=es2020 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), cd: Path.expand("../assets", __DIR__), env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} ``` -------------------------------- ### Generate LiveState Channel Source: https://github.com/launchscout/live_state/blob/main/docs/tutorial_start.md Use the `mix live_state.gen.channel` command to generate a new LiveState channel for managing application state on the backend. This command scaffolds the necessary channel files. ```bash mix live_state.gen.channel ContactForm ``` -------------------------------- ### Enable esbuild Watchers for Custom Elements Source: https://github.com/launchscout/live_state/blob/main/docs/tutorial_start.md Modify your development configuration in `config/config.exs` to include a watcher for custom elements. This enables live reloading when changes are made to your custom element files. ```elixir config :pipe_spot, PipeSpotWeb.Endpoint, # Binding to loopback ipv4 address prevents access from other machines. # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. http: [ip: {127, 0, 0, 1}, port: 4000], check_origin: false, code_reloader: true, debug_errors: true, secret_key_base: "yyy/xxx", watchers: esbuild: {Esbuild, :install_and_run, [:pipe_spot, ~w(--sourcemap=inline --watch)]}, # add this watcher esbuild_custom_elements: {Esbuild, :install_and_run, [:custom_elements, ~w(--sourcemap=inline --watch)]}, tailwind: {Tailwind, :install_and_run, [:pipe_spot, ~w(--watch)]} ``` -------------------------------- ### Handle Event with Multiple Reply Events Source: https://github.com/launchscout/live_state/blob/main/README.md Implement handle_event to return multiple reply events along with the state update. Each reply event is dispatched on the calling DOM Element. ```elixir def handle_event("add_todo_with_two_replies", todo, %{todos: todos}) do {:reply, [ %Event{name: "reply_event1", detail: %{foo: "bar"}}, %Event{name: "reply_event2", detail: %{bing: "baz"}} ], %{todos: [todo | todos]}} end ``` -------------------------------- ### Handle PubSub Messages with Socket Access Source: https://context7.com/launchscout/live_state/llms.txt Use the three-arity `handle_message/3` when socket assigns are needed to process PubSub messages. It returns `{:noreply, new_state, socket}`. ```elixir defmodule MyAppWeb.NotificationsChannel do use LiveState.Channel, web_module: MyAppWeb @impl true def authorize(_ch, %{"user_id" => uid}, socket), do: {:ok, Phoenix.Socket.assign(socket, :user_id, uid)} @impl true def init(_ch, _params, %{assigns: %{user_id: uid}} = _socket) do Phoenix.PubSub.subscribe(MyApp.PubSub, "user:#{uid}") {:ok, %{notifications: []}} end # 3-arity: (message, state, socket) → {:noreply, state, socket} @impl true def handle_message({:notification, notif}, state, %{assigns: %{user_id: uid}} = socket) do IO.puts("Push notification for user #{uid}") {:noreply, %{state | notifications: [notif | state.notifications]}, socket} end end ``` -------------------------------- ### Marp Core Browser Initialization Source: https://github.com/launchscout/live_state/blob/main/live_state_intro.html Initializes the Marp Core browser script, ensuring it runs only once and provides a cleanup function. ```javascript const l=Symbol(),s=document.currentScript;((t=document)=>{if("undefined"==typeof window)throw new Error("Marp Core's browser script is valid only in browser context.");if(t[l])return t[l];const e=a({target:t}),r=()=>{e(),delete t[l]};Object.defineProperty(t,l,{configurable:!0,value:r})})(s?s.getRootNode():document) ``` -------------------------------- ### Generate LiveState Phoenix Channel Source: https://context7.com/launchscout/live_state/llms.txt Use the `mix live_state.gen.channel` task to generate a Phoenix channel module. It can optionally create a LiveState socket if one is missing. ```bash mix live_state.gen.channel Todo # Creates: lib/my_app_web/channels/todo_channel.ex # Prompts to create lib/my_app_web/channels/live_state_socket.ex if missing ``` -------------------------------- ### Define a LiveState Channel Source: https://context7.com/launchscout/live_state/llms.txt Use `LiveState.Channel` to bring the LiveState behavior into a Phoenix Channel module. Implement `init/3` for initial state and `handle_event/3` for client event handling. ```elixir defmodule MyAppWeb.TodoChannel do use LiveState.Channel, web_module: MyAppWeb, max_version: 500 # optional; resets version counter at 500 (default 1000) alias LiveState.Event # Required: return initial state after a client joins @impl true def init(_channel, %{"user_id" => user_id}, _socket) do todos = Repo.all(from t in Todo, where: t.user_id == ^user_id) {:ok, %{todos: todos, user_id: user_id}} end # Handle a client event — return {:noreply, new_state} @impl true def handle_event("add_todo", %{"description" => desc}, %{todos: todos} = state) do new_todo = %{id: System.unique_integer([:positive]), description: desc, done: false} {:noreply, %{state | todos: [new_todo | todos]}} end # Handle a client event — return {:reply, event_or_events, new_state} to push CustomEvents back @impl true def handle_event("complete_todo", %{"id" => id}, %{todos: todos} = state) do updated = Enum.map(todos, fn t -> if t.id == id, do: %{t | done: true}, else: t end) {:reply, %Event{name: "todo-completed", detail: %{id: id}}, %{state | todos: updated}} end end ``` -------------------------------- ### History and Navigation Plugin Source: https://github.com/launchscout/live_state/blob/main/live_state_intro.html A Bespoke.js plugin that synchronizes the slide and fragment index with the browser's history API, allowing for back/forward navigation and deep linking. ```javascript function z(e={}){const t=Object.assign({history:!0},e);return e=>{let n=!0;const r=e=>{const t=n;try{return n=!0,e()}finally{n=t}};s=(t={fragment:!0})=>{((t,n)=>{const{fragments:r,slides:s}=e,a=Math.max(0,Math.min(t,s.length-1)),i=Math.max(0,Math.min(n||0,r[a].length-1));a===e.slide()&&i===e.fragmentIndex||e.slide(a,{fragment:i})})((B(location.hash.slice(1))||1)-1,t.fragment?B(v("f")||""):null)};e.on("fragment",(({index:e,fragmentIndex:r})=>{n||b({f:0===r||r.toString()},{location:Object.assign(Object.assign({},location),{hash:`#${e+1}`}),setter:(...e)=>t.history?history.pushState(...e):history.replaceState(...e)})})),setTimeout ``` -------------------------------- ### Handle Event with Single Reply Event Source: https://github.com/launchscout/live_state/blob/main/README.md Implement handle_event to return a single reply event along with the state update. The reply event is dispatched on the calling DOM Element. ```elixir def handle_event("add_todo_with_one_reply", todo, %{todos: todos}) do {:reply, %Event{name: "reply_event", detail: %{foo: "bar"}}, %{todos: [todo | todos]}} end ``` -------------------------------- ### Import Custom Element Source: https://github.com/launchscout/live_state/blob/main/docs/tutorial_start.md Add this import statement to `assets/js/custom_elements.js` to register your custom element. Create the file if it does not exist. ```javascript import './contact-form.js' ```