defmodule Corex.Carousel do @moduledoc ~S''' Phoenix implementation of [Zag.js Carousel](https://zagjs.com/components/react/carousel). ## Anatomy | Goal | API | | ---- | --- | | Image gallery | `items={[Corex.Image.new("/images/a.jpg", alt: "A"), ...]}` — renders `` per slide | | Custom slides | `items={@posts}` + `<:item :let={post}>` — your markup per slide | | Full structure | `compound` + `carousel_*` subcomponents | ## Items Pass `items` for simple-mode slides. Image galleries use [`Corex.Image`](Corex.Image.html) — see [`Corex.Image.new/2`](Corex.Image.html#new/2). Custom slides use any list plus the `<:item>` slot. | `items` | `<:item>` slot | Result | | ------- | -------------- | ------ | | `[%Corex.Image{}, ...]` | omitted | `` per slide | | any list | `<:item :let={item}>` | your markup via `render_slot/2` | | — | — | compound mode: set `item_count` and use `carousel_item` | Without `<:item>`, every entry must be `%Corex.Image{}`; other values raise at render time. Slides may include links and other controls; off-view slides are marked `inert` on the client so they stay out of the tab order. ### Images ```heex <.carousel items={[ Corex.Image.new("/images/beach.jpg", alt: "Beach"), Corex.Image.new("/images/fall.jpg", alt: "Fall"), Corex.Image.new("/images/sand.jpg", alt: "Sand"), Corex.Image.new("/images/star.jpg", alt: "Star"), Corex.Image.new("/images/winter.jpg", alt: "Winter") ]} class="carousel" > <:prev_trigger><.heroicon name="hero-arrow-left" /> <:next_trigger><.heroicon name="hero-arrow-right" /> ``` ### Custom content ```heex <.carousel items={@posts} class="carousel"> <:item :let={post}>

{post.title}

{post.description}

<.navigate to="#" class="link">Read more
<:prev_trigger><.heroicon name="hero-arrow-left" /> <:next_trigger><.heroicon name="hero-arrow-right" /> ``` ### Compound ```heex <.carousel compound :let={ctx} item_count={2} class="carousel"> <.carousel_root ctx={ctx}> <.carousel_item_group ctx={ctx}> <.carousel_item ctx={ctx} index={0}>First slide <.carousel_item ctx={ctx} index={1}>Second slide <.carousel_control ctx={ctx}> <.carousel_prev_trigger ctx={ctx}><.heroicon name="hero-arrow-left" /> <.carousel_indicator_group ctx={ctx}> <.carousel_indicator ctx={ctx} index={0} /> <.carousel_indicator ctx={ctx} index={1} /> <.carousel_next_trigger ctx={ctx}><.heroicon name="hero-arrow-right" /> ``` ## API Requires a stable `id` on `<.carousel>`. | Function | Action | Returns | | -------- | ------ | ------- | | [`play/1`](#play/1) | Start or resume autoplay (client) | `%Phoenix.LiveView.JS{}` | | [`play/2`](#play/2) | Start or resume autoplay (server) | `socket` | | [`pause/1`](#pause/1) | Pause autoplay (client) | `%Phoenix.LiveView.JS{}` | | [`pause/2`](#pause/2) | Pause autoplay (server) | `socket` | | [`scroll_next/1`](#scroll_next/1) | Next page (client) | `%Phoenix.LiveView.JS{}` | | [`scroll_next/2`](#scroll_next/2) | Next page with `instant` (client) | `%Phoenix.LiveView.JS{}` | | [`scroll_next/3`](#scroll_next/3) | Next page (server) | `socket` | | [`scroll_prev/1`](#scroll_prev/1) | Previous page (client) | `%Phoenix.LiveView.JS{}` | | [`scroll_prev/2`](#scroll_prev/2) | Previous page with `instant` (client) | `%Phoenix.LiveView.JS{}` | | [`scroll_prev/3`](#scroll_prev/3) | Previous page (server) | `socket` | ## Events Pick an event name and pass it to `on_*` on `<.carousel>`. ### Server events | Event | When | Payload | | ----- | ---- | ------- | | `on_page_change="carousel_page_changed"` | Active page changes | `%{"id" => id, "page" => page, "pageSnapPoint" => snap}` | ### on_page_change ```heex <.carousel class="carousel" on_page_change="carousel_page_changed" items={[ Corex.Image.new("/images/beach.jpg", alt: "Beach"), Corex.Image.new("/images/fall.jpg", alt: "Fall"), Corex.Image.new("/images/sand.jpg", alt: "Sand"), Corex.Image.new("/images/star.jpg", alt: "Star"), Corex.Image.new("/images/winter.jpg", alt: "Winter") ]} > <:prev_trigger><.heroicon name="hero-arrow-left" /> <:next_trigger><.heroicon name="hero-arrow-right" /> ``` ```elixir def handle_event("carousel_page_changed", %{"id" => _id, "page" => page}, socket) do {:noreply, assign(socket, :carousel_page, page)} end ``` ### Client events | Event | When | `event.detail` | | ----- | ---- | -------------- | | `on_page_change_client="carousel-page-changed"` | Active page changes | `id`, `page`, `pageSnapPoint` | ### on_page_change_client ```heex <.carousel id="carousel-events-client" class="carousel" on_page_change_client="carousel-page-changed" items={[ Corex.Image.new("/images/beach.jpg", alt: "Beach"), Corex.Image.new("/images/fall.jpg", alt: "Fall"), Corex.Image.new("/images/sand.jpg", alt: "Sand"), Corex.Image.new("/images/star.jpg", alt: "Star"), Corex.Image.new("/images/winter.jpg", alt: "Winter") ]} > <:prev_trigger><.heroicon name="hero-arrow-left" /> <:next_trigger><.heroicon name="hero-arrow-right" /> ``` ```javascript const el = document.getElementById("carousel-events-client"); el?.addEventListener("carousel-page-changed", (e) => console.log(e.detail)); ``` ## Style Target parts with `data-scope` and `data-part`, or use Corex Design: import tokens and `carousel.css`, then set `class="carousel"` on `<.carousel>`. ```css [data-scope="carousel"][data-part="root"] {} [data-scope="carousel"][data-part="control"] {} [data-scope="carousel"][data-part="item-group"] {} [data-scope="carousel"][data-part="item"] {} [data-scope="carousel"][data-part="prev-trigger"] {} [data-scope="carousel"][data-part="next-trigger"] {} [data-scope="carousel"][data-part="indicator-group"] {} [data-scope="carousel"][data-part="indicator"] {} ``` ```css @import "../corex/main.css"; @import "../corex/tokens/themes/neo/light.css"; @import "../corex/components/carousel.css"; ``` Stack modifiers on the host (`class` on `<.carousel>`). ### Color | Modifier | Classes | | -------- | ------- | | Default | `carousel` | | Accent | `carousel carousel--accent` | | Brand | `carousel carousel--brand` | | Alert | `carousel carousel--alert` | | Info | `carousel carousel--info` | | Success | `carousel carousel--success` | ```heex <.carousel items={[ Corex.Image.new("/images/beach.jpg", alt: "Beach"), Corex.Image.new("/images/fall.jpg", alt: "Fall"), Corex.Image.new("/images/sand.jpg", alt: "Sand"), Corex.Image.new("/images/star.jpg", alt: "Star"), Corex.Image.new("/images/winter.jpg", alt: "Winter") ]} class="carousel" > <:prev_trigger><.heroicon name="hero-arrow-left" /> <:next_trigger><.heroicon name="hero-arrow-right" /> <.carousel items={[ Corex.Image.new("/images/beach.jpg", alt: "Beach"), Corex.Image.new("/images/fall.jpg", alt: "Fall"), Corex.Image.new("/images/sand.jpg", alt: "Sand"), Corex.Image.new("/images/star.jpg", alt: "Star"), Corex.Image.new("/images/winter.jpg", alt: "Winter") ]} class="carousel carousel--accent" > <:prev_trigger><.heroicon name="hero-arrow-left" /> <:next_trigger><.heroicon name="hero-arrow-right" /> ``` ### Size Layout density for the root, control bar, prev/next triggers, autoplay control, and indicators. | Modifier | Classes | | -------- | ------- | | SM | `carousel carousel--sm` | | MD | `carousel carousel--md` | | LG | `carousel carousel--lg` | | XL | `carousel carousel--xl` | ```heex <.carousel items={[ Corex.Image.new("/images/beach.jpg", alt: "Beach"), Corex.Image.new("/images/fall.jpg", alt: "Fall"), Corex.Image.new("/images/sand.jpg", alt: "Sand"), Corex.Image.new("/images/star.jpg", alt: "Star"), Corex.Image.new("/images/winter.jpg", alt: "Winter") ]} class="carousel carousel--sm" > <:prev_trigger><.heroicon name="hero-arrow-left" /> <:next_trigger><.heroicon name="hero-arrow-right" /> <.carousel items={[ Corex.Image.new("/images/beach.jpg", alt: "Beach"), Corex.Image.new("/images/fall.jpg", alt: "Fall"), Corex.Image.new("/images/sand.jpg", alt: "Sand"), Corex.Image.new("/images/star.jpg", alt: "Star"), Corex.Image.new("/images/winter.jpg", alt: "Winter") ]} class="carousel carousel--lg" > <:prev_trigger><.heroicon name="hero-arrow-left" /> <:next_trigger><.heroicon name="hero-arrow-right" /> ``` ### Radius Corner radius for the slide area and prev/next triggers. | Modifier | Classes | | -------- | ------- | | None | `carousel carousel--rounded-none` | | SM | `carousel carousel--rounded-sm` | | MD | `carousel carousel--rounded-md` | | LG | `carousel carousel--rounded-lg` | | XL | `carousel carousel--rounded-xl` | | Full | `carousel carousel--rounded-full` | ```heex <.carousel items={[ Corex.Image.new("/images/beach.jpg", alt: "Beach"), Corex.Image.new("/images/fall.jpg", alt: "Fall"), Corex.Image.new("/images/sand.jpg", alt: "Sand"), Corex.Image.new("/images/star.jpg", alt: "Star"), Corex.Image.new("/images/winter.jpg", alt: "Winter") ]} class="carousel carousel--rounded-md" > <:prev_trigger><.heroicon name="hero-arrow-left" /> <:next_trigger><.heroicon name="hero-arrow-right" /> ``` ''' @doc type: :component use Phoenix.Component import Corex.Api.Doc alias Corex.Carousel.Anatomy.{ Control, Indicator, IndicatorGroup, Item, ItemGroup, NextTrigger, PrevTrigger, Props, Root } alias Corex.Carousel.Connect alias Corex.Carousel.Utils alias Phoenix.LiveView alias Phoenix.LiveView.JS attr(:id, :string, required: false) attr(:aria_label, :string, default: nil) attr(:items, :list, default: nil, doc: "List of `%Corex.Image{}` for image slides, or arbitrary data when `<:item>` is set. Omit in compound mode; use `item_count` when children do not pass through `items`." ) attr(:item_count, :integer, default: nil, doc: "When set, overrides the slide count used for the hook and compound context (use in compound mode without `items`)." ) attr(:page, :integer, default: 1, doc: "Active page (1-based, same as pagination; first page is 1)" ) attr(:dir, :string, default: nil, values: [nil, "ltr", "rtl"]) attr(:orientation, :string, default: "horizontal", values: ["horizontal", "vertical"]) attr(:slides_per_page, :integer, default: 1) attr(:slides_per_move, :any, default: nil, doc: "Number of slides to move per step, or \"auto\"" ) attr(:loop, :boolean, default: false) attr(:autoplay, :boolean, default: false) attr(:autoplay_delay, :integer, default: 4000) attr(:allow_mouse_drag, :boolean, default: false) attr(:spacing, :string, default: "0px") attr(:padding, :string, default: nil) attr(:in_view_threshold, :float, default: 0.6) attr(:snap_type, :string, default: "mandatory", values: ["proximity", "mandatory"]) attr(:auto_size, :boolean, default: false) attr(:on_page_change, :string, default: nil) attr(:on_page_change_client, :string, default: nil) attr(:compound, :boolean, default: false, doc: "Enable compound mode with :let={ctx} and carousel_root, carousel_item_group, carousel_item, carousel_control, carousel_prev_trigger, carousel_next_trigger, carousel_indicator_group, carousel_indicator." ) attr(:rest, :global) slot(:inner_block, required: false, doc: "Compound mode. Use compound and :let={ctx}." ) slot :prev_trigger, required: false do attr(:class, :string, required: false) end slot :next_trigger, required: false do attr(:class, :string, required: false) end slot :item, required: false, doc: "Custom markup for each slide. Use :let={item} to receive each entry from `items`. Required when `items` are not `%Corex.Image{}` structs." do attr(:class, :string, required: false) end def carousel(assigns) do assigns = Utils.merge_attr_defaults(assigns) {items, slide_count, total_pages, prev_disabled, next_disabled, slides_per_page} = Utils.compute_slide_metrics(assigns) has_item_slot = Utils.item_slot?(assigns) Utils.validate_items!(items, has_item_slot) assigns = assigns |> assign_new(:id, fn -> "carousel-#{System.unique_integer([:positive])}" end) |> assign_new(:dir, fn -> "ltr" end) |> assign(:items, items) |> assign(:slide_count, slide_count) |> assign(:total_pages, total_pages) |> assign(:prev_disabled, prev_disabled) |> assign(:next_disabled, next_disabled) |> assign(:slides_per_page, slides_per_page) |> assign(:has_item_slot, has_item_slot) ctx = %{ id: assigns.id, items: assigns.items, slide_count: assigns.slide_count, total_pages: assigns.total_pages, page: assigns.page, prev_disabled: assigns.prev_disabled, next_disabled: assigns.next_disabled, orientation: assigns.orientation, dir: assigns.dir, slides_per_page: assigns.slides_per_page, spacing: assigns.spacing, aria_label: assigns.aria_label } assigns = assign(assigns, :ctx, ctx) ~H"""
{if @compound do render_slot(@inner_block, @ctx) end}
<%= if @has_item_slot do %> {render_slot(@item, item)} <% else %> {item.alt <% end %>
""" end @doc type: :compound attr(:ctx, :map, required: true) attr(:rest, :global) slot(:inner_block, required: true) def carousel_root(assigns) do root = %Root{ id: assigns.ctx.id, dir: assigns.ctx.dir, orientation: assigns.ctx.orientation, slides_per_page: assigns.ctx.slides_per_page, spacing: assigns.ctx.spacing || "0px", aria_label: assigns.ctx.aria_label } assigns = assign(assigns, :root, root) ~H"""
{render_slot(@inner_block)}
""" end @doc type: :compound attr(:ctx, :map, required: true) attr(:rest, :global) slot(:inner_block, required: true) def carousel_item_group(assigns) do ig = %ItemGroup{ id: assigns.ctx.id, orientation: assigns.ctx.orientation, dir: assigns.ctx.dir } assigns = assign(assigns, :ig, ig) ~H"""
{render_slot(@inner_block)}
""" end @doc type: :compound attr(:ctx, :map, required: true) attr(:index, :integer, required: true) attr(:rest, :global) slot(:inner_block, required: true) def carousel_item(assigns) do item = %Item{ id: assigns.ctx.id, index: assigns.index, orientation: assigns.ctx.orientation, slide_count: assigns.ctx.slide_count } assigns = assign(assigns, :item, item) ~H"""
{render_slot(@inner_block)}
""" end @doc type: :compound attr(:ctx, :map, required: true) attr(:rest, :global) slot(:inner_block, required: true) def carousel_control(assigns) do ctl = %Control{id: assigns.ctx.id, orientation: assigns.ctx.orientation} assigns = assign(assigns, :ctl, ctl) ~H"""
{render_slot(@inner_block)}
""" end @doc type: :compound attr(:ctx, :map, required: true) attr(:rest, :global) slot(:inner_block, required: true) def carousel_prev_trigger(assigns) do pt = %PrevTrigger{id: assigns.ctx.id, disabled: assigns.ctx.prev_disabled} assigns = assign(assigns, :pt, pt) ~H""" """ end @doc type: :compound attr(:ctx, :map, required: true) attr(:rest, :global) slot(:inner_block, required: true) def carousel_next_trigger(assigns) do nt = %NextTrigger{id: assigns.ctx.id, disabled: assigns.ctx.next_disabled} assigns = assign(assigns, :nt, nt) ~H""" """ end @doc type: :compound attr(:ctx, :map, required: true) attr(:rest, :global) slot(:inner_block, required: true) def carousel_indicator_group(assigns) do g = %IndicatorGroup{ id: assigns.ctx.id, orientation: assigns.ctx.orientation, dir: assigns.ctx.dir } assigns = assign(assigns, :g, g) ~H"""
{render_slot(@inner_block)}
""" end @doc type: :compound attr(:ctx, :map, required: true) attr(:index, :integer, required: true) attr(:rest, :global) slot(:inner_block, required: false) def carousel_indicator(assigns) do ind = %Indicator{ id: assigns.ctx.id, index: assigns.index, orientation: assigns.ctx.orientation, dir: assigns.ctx.dir, page: assigns.ctx.page } assigns = assign(assigns, :ind, ind) ~H""" """ end api_doc(~S""" Start or resume autoplay from the client. Dispatches `corex:carousel:play` on the carousel root. ```heex <.action phx-click={Corex.Carousel.play("my-carousel")}>Play <.action phx-click={Corex.Carousel.pause("my-carousel")}>Pause <.carousel id="my-carousel" autoplay loop class="carousel" items={[ Corex.Image.new("/images/beach.jpg", alt: "Beach"), Corex.Image.new("/images/fall.jpg", alt: "Fall") ]} > <:prev_trigger><.heroicon name="hero-arrow-left" /> <:next_trigger><.heroicon name="hero-arrow-right" /> ``` ```javascript document.getElementById("my-carousel")?.dispatchEvent( new CustomEvent("corex:carousel:play", { bubbles: false, detail: {} }) ); ``` """) def play(carousel_id) when is_binary(carousel_id) do JS.dispatch("corex:carousel:play", to: "##{carousel_id}", detail: %{}, bubbles: false) end api_doc(~S""" Pause autoplay from the client. Dispatches `corex:carousel:pause` on the carousel root. ```heex <.action phx-click={Corex.Carousel.pause("my-carousel")}>Pause <.carousel id="my-carousel" autoplay loop class="carousel" items={[Corex.Image.new("/images/beach.jpg", alt: "Beach")]} > <:prev_trigger><.heroicon name="hero-arrow-left" /> <:next_trigger><.heroicon name="hero-arrow-right" /> ``` ```javascript document.getElementById("my-carousel")?.dispatchEvent( new CustomEvent("corex:carousel:pause", { bubbles: false, detail: {} }) ); ``` """) def pause(carousel_id) when is_binary(carousel_id) do JS.dispatch("corex:carousel:pause", to: "##{carousel_id}", detail: %{}, bubbles: false) end api_doc(~S""" Start or resume autoplay from the server. `push_event("carousel_play", %{"id" => carousel_id})`. ```heex <.action phx-click="carousel_play">Play <.carousel id="my-carousel" autoplay loop class="carousel" items={[Corex.Image.new("/images/beach.jpg", alt: "Beach")]} > <:prev_trigger><.heroicon name="hero-arrow-left" /> <:next_trigger><.heroicon name="hero-arrow-right" /> ``` ```elixir def handle_event("carousel_play", _params, socket) do {:noreply, Corex.Carousel.play(socket, "my-carousel")} end ``` """) def play(socket, carousel_id) when is_struct(socket, Phoenix.LiveView.Socket) and is_binary(carousel_id) do LiveView.push_event(socket, "carousel_play", %{"id" => carousel_id}) end api_doc(~S""" Pause autoplay from the server. `push_event("carousel_pause", %{"id" => carousel_id})`. ```heex <.action phx-click="carousel_pause">Pause <.carousel id="my-carousel" autoplay loop class="carousel" items={[Corex.Image.new("/images/beach.jpg", alt: "Beach")]} > <:prev_trigger><.heroicon name="hero-arrow-left" /> <:next_trigger><.heroicon name="hero-arrow-right" /> ``` ```elixir def handle_event("carousel_pause", _params, socket) do {:noreply, Corex.Carousel.pause(socket, "my-carousel")} end ``` """) def pause(socket, carousel_id) when is_struct(socket, Phoenix.LiveView.Socket) and is_binary(carousel_id) do LiveView.push_event(socket, "carousel_pause", %{"id" => carousel_id}) end api_doc_short("Same as [`scroll_next/2`](#scroll_next/2) with `instant: false`.") def scroll_next(carousel_id) when is_binary(carousel_id), do: scroll_next(carousel_id, false) api_doc(~S""" Scroll to the next page from the client. Dispatches `corex:carousel:scroll-next`. Pass `true` as the second argument for an instant jump (no animation). ```heex <.action phx-click={Corex.Carousel.scroll_next("my-carousel")}>Next <.action phx-click={Corex.Carousel.scroll_next("my-carousel", true)}>Next (instant) <.carousel id="my-carousel" loop class="carousel" items={[ Corex.Image.new("/images/beach.jpg", alt: "Beach"), Corex.Image.new("/images/fall.jpg", alt: "Fall") ]} > <:prev_trigger><.heroicon name="hero-arrow-left" /> <:next_trigger><.heroicon name="hero-arrow-right" /> ``` ```javascript document.getElementById("my-carousel")?.dispatchEvent( new CustomEvent("corex:carousel:scroll-next", { bubbles: false, detail: { instant: true } }) ); ``` """) def scroll_next(carousel_id, instant) when is_binary(carousel_id) and is_boolean(instant) do JS.dispatch("corex:carousel:scroll-next", to: "##{carousel_id}", detail: scroll_detail(instant), bubbles: false ) end def scroll_next(socket, carousel_id) when is_struct(socket, Phoenix.LiveView.Socket) and is_binary(carousel_id) do scroll_next(socket, carousel_id, false) end api_doc(~S""" Scroll to the next page from the server. `push_event("carousel_scroll_next", %{"id" => id, "instant" => boolean})`. ```heex <.action phx-click="carousel_next">Next <.carousel id="my-carousel" loop class="carousel" items={[Corex.Image.new("/images/beach.jpg", alt: "Beach")]} > <:prev_trigger><.heroicon name="hero-arrow-left" /> <:next_trigger><.heroicon name="hero-arrow-right" /> ``` ```elixir def handle_event("carousel_next", _params, socket) do {:noreply, Corex.Carousel.scroll_next(socket, "my-carousel")} end ``` """) def scroll_next(socket, carousel_id, instant) when is_struct(socket, Phoenix.LiveView.Socket) and is_binary(carousel_id) and is_boolean(instant) do LiveView.push_event(socket, "carousel_scroll_next", scroll_payload(carousel_id, instant)) end api_doc_short("Same as [`scroll_prev/2`](#scroll_prev/2) with `instant: false`.") def scroll_prev(carousel_id) when is_binary(carousel_id), do: scroll_prev(carousel_id, false) api_doc(~S""" Scroll to the previous page from the client. Dispatches `corex:carousel:scroll-prev`. Pass `true` as the second argument for an instant jump (no animation). ```heex <.action phx-click={Corex.Carousel.scroll_prev("my-carousel")}>Prev <.action phx-click={Corex.Carousel.scroll_prev("my-carousel", true)}>Prev (instant) <.carousel id="my-carousel" loop class="carousel" items={[ Corex.Image.new("/images/beach.jpg", alt: "Beach"), Corex.Image.new("/images/fall.jpg", alt: "Fall") ]} > <:prev_trigger><.heroicon name="hero-arrow-left" /> <:next_trigger><.heroicon name="hero-arrow-right" /> ``` ```javascript document.getElementById("my-carousel")?.dispatchEvent( new CustomEvent("corex:carousel:scroll-prev", { bubbles: false, detail: {} }) ); ``` """) def scroll_prev(carousel_id, instant) when is_binary(carousel_id) and is_boolean(instant) do JS.dispatch("corex:carousel:scroll-prev", to: "##{carousel_id}", detail: scroll_detail(instant), bubbles: false ) end def scroll_prev(socket, carousel_id) when is_struct(socket, Phoenix.LiveView.Socket) and is_binary(carousel_id) do scroll_prev(socket, carousel_id, false) end api_doc(~S""" Scroll to the previous page from the server. `push_event("carousel_scroll_prev", %{"id" => id, "instant" => boolean})`. ```heex <.action phx-click="carousel_prev">Prev <.carousel id="my-carousel" loop class="carousel" items={[Corex.Image.new("/images/beach.jpg", alt: "Beach")]} > <:prev_trigger><.heroicon name="hero-arrow-left" /> <:next_trigger><.heroicon name="hero-arrow-right" /> ``` ```elixir def handle_event("carousel_prev", _params, socket) do {:noreply, Corex.Carousel.scroll_prev(socket, "my-carousel")} end ``` """) def scroll_prev(socket, carousel_id, instant) when is_struct(socket, Phoenix.LiveView.Socket) and is_binary(carousel_id) and is_boolean(instant) do LiveView.push_event(socket, "carousel_scroll_prev", scroll_payload(carousel_id, instant)) end defp scroll_detail(false), do: %{} defp scroll_detail(true), do: %{instant: true} defp scroll_payload(carousel_id, instant) do base = %{"id" => carousel_id} if instant, do: Map.put(base, "instant", true), else: base end end