Changelog for v1.1
View SourceQuick update guide
Here is a quick summary of the changes necessary to upgrade to LiveView v1.1:
In your
mix.exs
, updatephoenix_live_view
to latest and addlazy_html
as a dependency:{:phoenix_live_view, "~> 1.1"}, {:lazy_html, ">= 0.0.0", only: :test},
Note you may remove
floki
as a dependency if you don't use it anywhere.Still in your
mix.exs
, prepend:phoenix_live_view
to your list of compilers insidedef project
, such as:compilers: [:phoenix_live_view] ++ Mix.compilers(),
(optional) In your
config/dev.exs
, finddebug_heex_annotations
, and also adddebug_tags_location
for improved annotations:config :phoenix_live_view, debug_heex_annotations: true, debug_tags_location: true, enable_expensive_runtime_checks: true
(optional) To enable colocated hooks, you must update
esbuild
withmix deps.update esbuild
and then update yourconfig/config.exs
accordingly. In particular, append--alias:@=.
to theargs
list and pass a list of paths to the"NODE_PATH"
env var, as shown below:your_app_name: [ args: ~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.), env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]},
Colocated hooks
When writing hooks for a specific component, the need to place the JavaScript code in a whole separate file often feels inconvenient. LiveView 1.1 introduces colocated hooks to allow writing the hook's JavaScript code in the same file as your regular component code.
A colocated hook is defined by placing the special :type
attribute on a <script>
tag:
alias Phoenix.LiveView.ColocatedHook
def input(%{type: "phone-number"} = assigns) do
~H"""
<input type="text" name={@name} id={@id} value={@value} phx-hook=".PhoneNumber" />
<script :type={ColocatedHook} name=".PhoneNumber">
export default {
mounted() {
this.el.addEventListener("input", e => {
let match = this.el.value.replace(/\D/g, "").match(/^(\d{3})(\d{3})(\d{4})$/)
if(match) {
this.el.value = `${match[1]}-${match[2]}-${match[3]}`
}
})
}
}
</script>
"""
end
Important: LiveView now supports the phx-hook
attribute to start with a dot (.PhoneNumber
above) for namespacing. Any hook name starting with a dot is prefixed at compile time with the module name of the component. If you named your hooks with a leading dot in the past, you'll need to adjust this for your hooks to work properly on LiveView 1.1.
Colocated hooks are extracted to a phoenix-colocated
folder inside your _build/$MIX_ENV
directory (Mix.Project.build_path()
). See the quick update section at the top of the changelog on how to adjust your esbuild
configuration to handle this. With everything configured, you can import your colocated hooks inside of your app.js
like this:
...
import {LiveSocket} from "phoenix_live_view"
+ import {hooks as colocatedHooks} from "phoenix-colocated/my_app"
import topbar from "../vendor/topbar"
...
const liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
+ params: {_csrf_token, csrfToken},
+ hooks: {...colocatedHooks}
})
The phoenix-colocated
folder has subfolders for each application that uses colocated hooks, therefore you'll need to adjust the my_app
part of the import depending on the name of your project (defined in your mix.exs
). You can read more about colocated hooks in the module documentation of Phoenix.LiveView.ColocatedHook
. There's also a more generalized version for colocated JavaScript, see the documentation for Phoenix.LiveView.ColocatedJS
.
We're planning to make the private Phoenix.Component.MacroComponent
API that we use for those features public in a future release.
Keyed Comprehensions
One pitfall when rendering collections in LiveView is that they are not change tracked. If you have code like this:
<ul>
<li :for={item <- @items}>{item.name}</li>
</ul>
When changing @items
, all elements are re-sent over the wire. LiveView still optimizes the static and dynamic parts of the template, but if you have 100 items in your list and only change a single one (or append, prepend, etc.), LiveView still sends the dynamic parts of all items.
To improve this, LiveView prior to version 1.1 had two solutions:
- Use streams. Streams are not kept in memory on the server and if you
stream_insert
a single item, only that item is sent over the wire. But because the server does not keep any state for streams, this also means that if you update an item in a stream, all the dynamic parts of the updated item are sent again. - Use a LiveComponent for each entry. LiveComponents perform change tracking on their own assigns. So when you update a single item, LiveView only sends a list of component IDs and the changed parts for that item.
So LiveComponents allow for more granular diffs and also a more declarative approach than streams, but require more memory on the server. Thus, when memory usage is a concern, especially for very large collections, streams should be your first choice. Another downside of LiveComponents is that they require you to write a whole separate module just to get an optimized diff.
LiveView 1.1 introduces a new :key
attribute that can be used with :for
:
<ul>
<li :for={item <- @items} :key={item.id}>{item.name}</li>
</ul>
Under the hood, this has the same diff over the wire as a LiveComponent for each entry, but it allows you to define the template inline. LiveView optimizes a :key
ed comprehension into a special LiveComponent under the hood. Therefore, this optimization can only be used on regular HTML tags, while :for
without :key
also works on components and slots. In the future, we might introduce further optimizations that would allow this to also work on components and slots using the same :key
syntax.
Types for public interfaces
LiveView 1.1 adds official types to the JavaScript client. This allows IntelliSense to work in editors that support it and is a massive improvement to the user experience when writing JavaScript hooks.
If you're not using TypeScript, you can also add the necessary JSDoc hints to your hook definitions, assuming your editor supports them.
Example when defining a hook object that is meant to be passed to the LiveSocket
constructor:
/**
* @type {import("phoenix_live_view").HooksOptions}
*/
let Hooks = {}
Hooks.PhoneNumber = {
mounted() {
this.el.addEventListener("input", e => {
let match = this.el.value.replace(/\D/g, "").match(/^(\d{3})(\d{3})(\d{4})$/)
if(match) {
this.el.value = `${match[1]}-${match[2]}-${match[3]}`
}
})
}
}
let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, ...})
...
Example when defining a hook on its own:
/**
* @type {import("phoenix_live_view").Hook}
*/
Hooks.InfiniteScroll = {
page() { return this.el.dataset.page },
mounted(){
this.pending = this.page()
window.addEventListener("scroll", e => {
if(this.pending == this.page() && scrollAt() > 90){
this.pending = this.page() + 1
this.pushEvent("load-more", {})
}
})
},
updated(){ this.pending = this.page() }
}
Also, hooks can now be defined as a subclass of ViewHook
, if you prefer native classes:
import { LiveSocket, ViewHook } from "phoenix_live_view"
class MyHook extends ViewHook {
mounted() {
...
}
}
let liveSocket = new LiveSocket(..., {
hooks: {
MyHook
}
})
Using @types/phoenix_live_view
(not maintained by the Phoenix team) is not necessary any more.
Phoenix.Component.portal
When designing reusable HTML components for UI elements like tooltips or dialogs, it is sometimes necessary to render a part of a component's template outside of the regular DOM hierarchy of that component, for example to prevent clipping due to CSS rules like overflow: hidden
that are not controlled by the component itself. Modern browser APIs for rendering elements in the top layer can help in many cases, but if you cannot use those for whatever reasons, LiveView previously did not have a solution to solve that problem. In LiveView 1.1, we introduce a new Phoenix.Component.portal/1
component:
<%!-- in some nested LiveView or component --%>
<.portal id="my-element" target="body">
<%!-- any content here will be teleported into the body tag --%>
</.portal>
Any element can be teleported, even LiveComponents and nested LiveViews, and any phx-*
events from inside a portal will still be handled by the correct LiveView. This is similar to <Teleport>
in Vue.js or createPortal
in React.
As a demo, we created an example for implementing tooltips using Phoenix.Component.portal
as a single-file Elixir script. When saved as portal.exs
, you can execute it as elixir portal.exs
and visit http://localhost:5001
in your browser.
JS.ignore_attributes
Sometimes it is useful to prevent some attributes from being patched by LiveView. One example where this frequently came up is when using a native <dialog>
or <details>
element that is controlled by the open
attribute, which is special in that it is actually set (and removed) by the browser. Previously, LiveView would remove those attributes on update and required additional patching, now you can simply call JS.ignore_attributes
in a phx-mounted
binding:
<details phx-mounted={JS.ignore_attributes(["open"])}>
<summary>...</summary>
...
</details>
Moving from Floki to LazyHTML
LiveView 1.1 moves to LazyHTML as the HTML engine used by LiveViewTest
. LazyHTML is based on lexbor and allows the use of modern CSS selector features, like :is()
, :has()
, etc. to target elements. Lexbor's stated goal is to create output that "should match that of modern browsers, meeting industry specifications".
This is a mostly backwards compatible change. The only way in which this affects LiveView projects is when using Floki specific selectors (fl-contains
, fl-icontains
), which will not work any more in selectors passed to LiveViewTest's element/3
function. In most cases, the text_filter
option of element/3
should be a sufficient replacement, which has been a feature since LiveView v0.12.0.
Note that in Phoenix versions prior to v1.8, the phx.gen.auth
generator used the Floki specific fl-contains
selector in its generated tests in two instances, so if you used the phx.gen.auth
generator to scaffold your authentication solution, those tests will need to be adjusted when updating to LiveView v1.1. In both cases, changing to use the text_filter
option is enough to get you going again:
{:ok, _login_live, login_html} =
lv
- |> element(~s|main a:fl-contains("Sign up")|)
+ |> element("main a", "Sign up")
|> render_click()
|> follow_redirect(conn, ~p"<%= schema.route_prefix %>/register")
If you're using Floki itself in your tests through its API (Floki.parse_document
, Floki.find
, etc.), you are not required to rewrite them when you update to LiveView v1.1.
Slot and line annotations
When :debug_heex_annotations
is enabled, LiveView will now annotate the beginning and end of each slot. A new :debug_tags_location
has also been added, which adds the starting line of each tag. The goal is to provide more precise information to tools.
To enable this, a new callback called annotate_slot/4
was added. Custom implementations of Phoenix.LiveView.TagEngine
must implement it accordingly.
v1.1.0-rc.1 (2025-06-20)
Bug fixes
- Fix variable tainting which could cause some template parts to not be re-rendered (#3856).
v1.1.0-rc.0 (2025-06-17)
Enhancements
- Add type annotations to all public JavaScript APIs (#3789)
- Add
Phoenix.LiveView.JS.ignore_attributes/1
to allow marking specific attributes to be ignored when LiveView patches an element (#3765) - Add
Phoenix.LiveView.Debug
module with functions for inspecting LiveViews at runtime (#3776) - Add
Phoenix.LiveView.ColocatedHook
andPhoenix.LiveView.ColocatedJS
(#3810) - Add
:update_only
option toPhoenix.LiveView.stream_insert/4
(#3573) - Use
LazyHTML
instead of Floki internally for LiveViewTest - Normalize whitespace in LiveViewTest's text filters (#3621)
- Raise by default when LiveViewTest detects duplicate DOM or LiveComponent IDs. This can be changed by passing
on_error
toPhoenix.LiveViewTest.live/3
/Phoenix.LiveViewTest.live_isolated/3
- Raise an exception when trying to bind a single DOM element to multiple views (this could happen when accidentally loading your app.js twice) (#3805)
- Ensure promise rejections include stack traces (#3738)
- Treat form associated custom elements as form inputs (3823)
- Add
:inline_matcher
option toPhoenix.LiveView.HTMLFormatter
which can be configured as a list of strings and regular expressions to match against tag names to treat them as inline (#3795)
v1.0
The CHANGELOG for v1.0 and earlier releases can be found in the v1.0 branch.