Packages
phoenix_live_view
0.17.3
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.2.0-rc.3
1.2.0-rc.2
1.2.0-rc.1
1.2.0-rc.0
1.1.32
1.1.31
1.1.30
1.1.29
1.1.28
1.1.27
1.1.26
1.1.25
1.1.24
1.1.23
1.1.22
1.1.21
1.1.20
1.1.19
1.1.18
1.1.17
1.1.16
1.1.15
1.1.14
1.1.13
1.1.12
1.1.11
1.1.10
1.1.9
1.1.8
1.1.7
1.1.6
retired
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.1.0-rc.4
1.1.0-rc.3
1.1.0-rc.2
1.1.0-rc.1
1.1.0-rc.0
1.0.18
1.0.17
1.0.16
1.0.15
1.0.14
1.0.13
1.0.12
1.0.11
1.0.10
1.0.9
1.0.8
retired
1.0.7
1.0.6
retired
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
1.0.0-rc.9
1.0.0-rc.8
1.0.0-rc.7
1.0.0-rc.6
1.0.0-rc.5
1.0.0-rc.4
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.20.17
0.20.16
0.20.15
0.20.14
0.20.13
0.20.12
0.20.11
0.20.10
0.20.9
0.20.8
0.20.7
0.20.6
0.20.5
0.20.4
0.20.3
0.20.2
0.20.1
0.20.0
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.18
0.18.17
0.18.16
0.18.15
0.18.14
0.18.13
0.18.12
0.18.11
0.18.10
0.18.9
0.18.8
0.18.7
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.14
0.17.13
0.17.12
0.17.11
0.17.10
0.17.9
0.17.8
0.17.7
0.17.6
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.8
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.1
0.11.0
0.10.0
0.9.0
0.8.1
0.8.0
0.7.1
0.7.0
0.6.0
0.6.0-dev
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.1
0.3.0
0.2.1
0.2.0
0.1.1
0.1.0
Rich, real-time user experiences with server-rendered HTML
Current section
Files
Jump to
Current section
Files
lib/phoenix_live_view/js.ex
defmodule Phoenix.LiveView.JS do
@moduledoc ~S'''
Provides commands for executing JavaScript utility operations on the client.
JS commands support a variety of utility operations for common client-side
needs, such as adding or removing css classes, showing or hiding content, and transitioning
in and out with animations. While these operations can be accomplished via
client-side hooks, JS commands are DOM patch aware, so operations applied
by the JS APIs will stick to elements across patches from the server.
In addition to purely client-side utilities, the JS command incluces a
rich `push` API, for extending the default `phx-` binding pushes with
options to customize targets, loading states, and additional payload values.
## Enhanced Push Events
The `push/3` command allows you to extend the built-in pushed event handling
when a `phx-` event is pushed to the server. For example, you may wish to
target a specific component, specify additional payload values to include
with the event, apply loading states to external elements, etc. For example,
given this basic `phx-click` event:
<div phx-click="inc">+</div>
Imagine you need to target your current component, and apply a loading state
to the parent container while the client awaits the server acknowledgement:
alias Phoenix.LiveView.JS
<div phx-click={JS.push("inc", loading: ".thermo", target: @myself)}>+</div>
Push commands also compose with all other utilities. For example, to add
a class when pushing:
<div phx-click={
JS.push("inc", loading: ".thermo", target: @myself)
|> JS.add_class(".warmer", to: ".thermo")
}>+</div>
## Client Utility Commands
The following utilities are included:
* `add_class` - Add classes to elements, with optional transitions
* `remove_class` - Remove classes from elements, with optional transitions
* `show` - Show elements, with optional transitions
* `hide` - Hide elements, with optional transitions
* `toggle` - Shows or hides elements based on visiblity, with optional transitions
* `transition` - Apply a temporary transition to elements for animations
* `dispatch` - Dispatch a DOM event to elements
For example, the following modal component can be shown or hidden on the
client without a trip to the server:
alias Phoenix.LiveView.JS
def hide_modal(js \\ %JS{}) do
js
|> JS.hide(transition: "fade-out", to: "#modal")
|> JS.hide(transition: "fade-out-scale", to: "#modal-content")
end
def modal(assigns) do
~H"""
<div id="modal" class="phx-modal" phx-remove={hide_modal()}>
<div
id="modal-content"
class="phx-modal-content"
phx-click-away={hide_modal()}
phx-window-keydown={hide_modal()}
phx-key="escape"
>
<button class="phx-modal-close" phx-click={hide_modal()}>✖</button>
<p><%= @text %></p>
</div>
</div>
"""
end
'''
alias Phoenix.LiveView.JS
defstruct ops: []
@default_transition_time 200
defimpl Phoenix.HTML.Safe, for: Phoenix.LiveView.JS do
def to_iodata(%Phoenix.LiveView.JS{} = cmd) do
Phoenix.HTML.Engine.html_escape(Phoenix.json_library().encode!(cmd.ops))
end
end
@doc """
Pushes an event to the server.
## Options
* `:target` - The selector or component ID to push to
* `:loading` - The selector to apply the phx loading classes to
* `:page_loading` - Boolean to trigger the phx:page-loading-start and
phx:page-loading-stop events for this push. Defaults to `false`
* `:value` - The map of values to send to the server
## Examples
<button phx-click={JS.push("clicked")}>click me!</button>
<button phx-click={JS.push("clicked", value: %{id: @id})}>click me!</button>
<button phx-click={JS.push("clicked", page_loading: true)}>click me!</button>
"""
def push(event) when is_binary(event) do
push(%JS{}, event, [])
end
def push(event, opts) when is_binary(event) and is_list(opts) do
push(%JS{}, event, opts)
end
def push(%JS{} = cmd, event) when is_binary(event) do
push(cmd, event, [])
end
def push(%JS{} = cmd, event, opts) when is_binary(event) and is_list(opts) do
opts =
opts
|> validate_keys(:push, [:target, :loading, :page_loading, :value])
|> put_target()
|> put_value()
put_op(cmd, "push", Enum.into(opts, %{event: event}))
end
@doc """
Dispatches an event to the DOM.
* `event` - The string event name to dispatch.
## Options
* `:to` - The optional DOM selector to dispatch the event to.
Defaults to the interacted element.
* `:detail` - The optional detail map to dispatch along
with the client event. The details will be available in the
`event.detail` attribute for event listeners.
## Examples
window.addEventListener("click", e => console.log("clicked!", e.detail))
<button phx-click={JS.dispatch("click", to: ".nav")}>Click me!</button>
"""
def dispatch(cmd \\ %JS{}, event, opts) do
opts = validate_keys(opts, :dispatch, [:to, :detail])
args = %{event: event, to: opts[:to]}
args =
case Keyword.fetch(opts, :detail) do
{:ok, detail} -> Map.put(args, :detail, detail)
:error -> args
end
put_op(cmd, "dispatch", args)
end
@doc """
Toggles elements.
## Options
* `:to` - The optional DOM selector to toggle.
Defaults to the interacted element.
* `:in` - The string of classes to apply when toggling in, or
a 3-tuple containing the transition class, the class to apply
to start the transition, and the ending transition class, such as:
`{"ease-out duration-300", "opacity-0", "opacity-100"}`
* `:out` - The string of classes to apply when toggling out, or
a 3-tuple containing the transition class, the class to apply
to start the transition, and the ending transition class, such as:
`{"ease-out duration-300", "opacity-100", "opacity-0"}`
* `:time` - The time to apply the transition `:in` and `:out` classes.
Defaults #{@default_transition_time}
* `:display` - The optional display value to set when toggling in. Defaults `"block"`.
## Examples
<div id="item">My Item</div>
<button phx-click={JS.toggle(to: "#item")}>
toggle item!
</button>
<button phx-click={JS.show(to: "#item", in: "fade-in-scale", out: "fade-out-scale")}>
toggle fancy!
</button>
"""
def toggle(opts \\ [])
def toggle(%JS{} = cmd), do: toggle(cmd, [])
def toggle(opts) when is_list(opts), do: toggle(%JS{}, opts)
def toggle(cmd, opts) when is_list(opts) do
opts = validate_keys(opts, :toggle, [:to, :in, :out, :display, :time])
in_classes = transition_class_names(opts[:in])
out_classes = transition_class_names(opts[:out])
time = opts[:time] || @default_transition_time
put_op(cmd, "toggle", %{
to: opts[:to],
display: opts[:display],
ins: in_classes,
outs: out_classes,
time: time
})
end
@doc """
Shows elements.
## Options
* `:to` - The optional DOM selector to show.
Defaults to the interacted element.
* `:transition` - The string of classes to apply before showing or
a 3-tuple containing the transition class, the class to apply
to start the transition, and the ending transition class, such as:
`{"ease-out duration-300", "opacity-0", "opacity-100"}`
* `:time` - The time to apply the transition from `:transition`.
Defaults #{@default_transition_time}
* `:display` - The optional display value to set when showing. Defaults `"block"`.
## Examples
<div id="item">My Item</div>
<button phx-click={JS.show(to: "#item")}>
show!
</button>
<button phx-click={JS.show(to: "#item", transition: "fade-in-scale")}>
show fancy!
</button>
"""
def show(opts \\ [])
def show(%JS{} = cmd), do: show(cmd, [])
def show(opts) when is_list(opts), do: show(%JS{}, opts)
def show(cmd, opts) when is_list(opts) do
opts = validate_keys(opts, :show, [:to, :transition, :display, :time])
transition = transition_class_names(opts[:transition])
time = opts[:time] || @default_transition_time
put_op(cmd, "show", %{
to: opts[:to],
display: opts[:display],
transition: transition,
time: time
})
end
@doc """
Hides elements.
## Options
* `:to` - The optional DOM selector to hide.
Defaults to the interacted element.
* `:transition` - The string of classes to apply before hiding or
a 3-tuple containing the transition class, the class to apply
to start the transition, and the ending transition class, such as:
`{"ease-out duration-300", "opacity-0", "opacity-100"}`
* `:time` - The time to apply the transition from `:transition`.
Defaults #{@default_transition_time}
## Examples
<div id="item">My Item</div>
<button phx-click={JS.hide(to: "#item")}>
hide!
</button>
<button phx-click={JS.hide(to: "#item", transition: "fade-out-scale")}>
hide fancy!
</button>
"""
def hide(opts \\ [])
def hide(%JS{} = cmd), do: hide(cmd, [])
def hide(opts) when is_list(opts), do: hide(%JS{}, opts)
def hide(cmd, opts) when is_list(opts) do
opts = validate_keys(opts, :hide, [:to, :transition, :time])
transition = transition_class_names(opts[:transition])
time = opts[:time] || @default_transition_time
put_op(cmd, "hide", %{
to: opts[:to],
transition: transition,
time: time
})
end
@doc """
Adds classes to elements.
## Options
* `:to` - The optional DOM selector to add classes to.
Defaults to the interacted element.
* `:transition` - The string of classes to apply before adding classes or
a 3-tuple containing the transition class, the class to apply
to start the transition, and the ending transition class, such as:
`{"ease-out duration-300", "opacity-0", "opacity-100"}`
* `:time` - The time to apply the transition from `:transition`.
Defaults #{@default_transition_time}
## Examples
<div id="item">My Item</div>
<button phx-click={JS.add_class("highlight underline", to: "#item")}>
highlight!
</button>
"""
def add_class(names) when is_binary(names), do: add_class(%JS{}, names, [])
def add_class(%JS{} = js, names) when is_binary(names) do
add_class(js, names, [])
end
def add_class(names, opts) when is_binary(names) and is_list(opts) do
add_class(%JS{}, names, opts)
end
def add_class(%JS{} = js, names, opts) when is_binary(names) and is_list(opts) do
opts = validate_keys(opts, :add_class, [:to, :transition, :time])
time = opts[:time] || @default_transition_time
put_op(js, "add_class", %{
to: opts[:to],
names: class_names(names),
transition: transition_class_names(opts[:transition]),
time: time
})
end
@doc """
Removes classes from elements.
## Options
* `:to` - The optional DOM selector to remove classes from.
Defaults to the interacted element.
* `:transition` - The string of classes to apply before removing classes or
a 3-tuple containing the transition class, the class to apply
to start the transition, and the ending transition class, such as:
`{"ease-out duration-300", "opacity-0", "opacity-100"}`
* `:time` - The time to apply the transition from `:transition`.
Defaults #{@default_transition_time}
## Examples
<div id="item">My Item</div>
<button phx-click={JS.remove_class("highlight underline", to: "#item")}>
remove highlight!
</button>
"""
def remove_class(names) when is_binary(names), do: remove_class(%JS{}, names, [])
def remove_class(%JS{} = js, names) when is_binary(names) do
remove_class(js, names, [])
end
def remove_class(names, opts) when is_binary(names) and is_list(opts) do
remove_class(%JS{}, names, opts)
end
def remove_class(%JS{} = js, names, opts) when is_binary(names) and is_list(opts) do
opts = validate_keys(opts, :remove_class, [:to, :transition, :time])
time = opts[:time] || @default_transition_time
put_op(js, "remove_class", %{
to: opts[:to],
names: class_names(names),
transition: transition_class_names(opts[:transition]),
time: time
})
end
@doc """
Transitions elements.
Transitions are useful for temporarily adding an animation class
to element(s), such as for highlighting content changes.
## Options
* `:to` - The optional DOM selector to remove classes from.
Defaults to the interacted element.
* `:transition` - The string of classes to apply before removing classes or
a 3-tuple containing the transition class, the class to apply
to start the transition, and the ending transition class, such as:
`{"ease-out duration-300", "opacity-0", "opacity-100"}`
* `:time` - The time to apply the transition from `:transition`.
Defaults #{@default_transition_time}
## Examples
<div id="item">My Item</div>
<button phx-click={JS.transition("shake", to: "#item")}>Shake!</button>
"""
def transition(names) when is_binary(names) or is_tuple(names) do
transition(%JS{}, names, [])
end
def transition(names, opts) when (is_binary(names) or is_tuple(names)) and is_list(opts) do
transition(%JS{}, names, opts)
end
def transition(%JS{} = cmd, names) when is_binary(names) or is_tuple(names) do
transition(cmd, names, [])
end
def transition(%JS{} = cmd, names, opts) when (is_binary(names) or is_tuple(names)) and is_list(opts) do
opts = validate_keys(opts, :transition, [:to, :time])
time = opts[:time] || @default_transition_time
put_op(cmd, "transition", %{
time: time,
to: opts[:to],
transition: transition_class_names(names)
})
end
defp put_op(%JS{ops: ops} = cmd, kind, %{} = args) do
%JS{cmd | ops: ops ++ [[kind, args]]}
end
defp class_names(nil), do: []
defp class_names(names) do
String.split(names, " ")
end
defp transition_class_names(nil), do: [[], [], []]
defp transition_class_names(transition) when is_binary(transition),
do: [class_names(transition), [], []]
defp transition_class_names({transition, tstart, tend})
when is_binary(tstart) and is_binary(transition) and is_binary(tend) do
[class_names(transition), class_names(tstart), class_names(tend)]
end
defp validate_keys(opts, kind, allowed_keys) do
for key <- Keyword.keys(opts) do
if key not in allowed_keys do
raise ArgumentError, """
invalid option for #{kind}
Expected keys to be one of #{inspect(allowed_keys)}, got: #{inspect(key)}
"""
end
end
opts
end
defp put_value(opts) do
case Keyword.fetch(opts, :value) do
{:ok, val} when is_map(val) -> Keyword.put(opts, :value, val)
{:ok, val} -> raise ArgumentError, "push :value expected to be a map, got: #{inspect(val)}"
:error -> opts
end
end
defp put_target(opts) do
case Keyword.fetch(opts, :target) do
{:ok, %Phoenix.LiveComponent.CID{cid: cid}} -> Keyword.put(opts, :target, cid)
{:ok, selector} -> Keyword.put(opts, :target, selector)
:error -> opts
end
end
end