teamon

teamon

As part of work on headless I hit a bug that in certain conditions when updating the UI from LiveView an Alpine.js component crash.

This happenes on various occasions, but I reduced the problem to the smalles example possible that is also 100% reproducible.

Here’s the scene:

  • There is a URL param (?color=red) and based on the value of this param one of two components is rendered - either a blue counter or a red counter.
  • Both Alpine “componentes” are registerd using Alpine.data() function.

When things are OK: When using elixir functions to generate components, like this:

# define component as function
def blue_counter(assigns) do
  ~H"""
  <div x-data="blue_counter">
    <div style="color: blue" x-bind="ns"></div>
    <button x-bind="button">+</button>
  </div>
  """
end

# and then in render()
  <%= case @color do %>
    <% "blue" -> %> <.blue_counter/>
    <% "red" -> %> <.red_counter/>
  <% end %>

When things are not OK: When putting the code inline, like this:

<%= case @color do %>
  <% "blue" -> %>
    <div x-data="blue_counter">
      <div style="color: blue" x-bind="ns"></div>
      <button x-bind="button">+</button>
    </div>
  <% "red" -> %>
    ...
<% end %>

When using inline code, whn switching between red and blue there is a crash with Alpine Expression Error: ns is not defined error.

Here’s a video showing the issue: switching between components generated with functions is fine, switching between inline components crashes.

Gif

Link to video

I’ve looked into WebSocket messages and the only difference I’ve found is that the good ones include "r": 1 part, while the bad ones do not. Unfortunately I have no idea what that means, so here’s where my research reached a dead end.

The other thing I’ve noticed is that in the good case in onBeforeElUpdated callback the from._x_dataStack is NULL and Alpine.clone() is never called, while in the bad case it is called and crashes.

I’ve included a single-file example app that can be run with elixir file.exs and also dump of console and WebSockets logs.

If anyone has any idea what could be wrong here, either with Alpine or LiveView please let me know. Without solving this the whole headless project can’t happen.

Sing-file app example
Application.put_env(:sample, Example.Endpoint,
  http: [ip: {127, 0, 0, 1}, port: 5001],
  server: true,
  live_view: [signing_salt: "aaaaaaaa"],
  secret_key_base: String.duplicate("a", 64)
)

Mix.install([
  {:plug_cowboy, "~> 2.5"},
  {:jason, "~> 1.0"},
  {:phoenix, "~> 1.7"},
  # please test your issue using the latest version of LV from GitHub!
  {:phoenix_live_view,
   github: "phoenixframework/phoenix_live_view", branch: "main", override: true},
  {:ecto, "~> 3.0"},
  {:phoenix_ecto, "~> 4.4"}
])

# build the LiveView JavaScript assets (this needs mix and npm available in your path!)
path = Phoenix.LiveView.__info__(:compile)[:source] |> Path.dirname() |> Path.join("../")
# System.cmd("mix", ["deps.get"], cd: path, into: IO.binstream())
# System.cmd("npm", ["install"], cd: Path.join(path, "./assets"), into: IO.binstream())
# System.cmd("mix", ["assets.build"], cd: path, into: IO.binstream())

defmodule Example.ErrorView do
  def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end

defmodule Example.HomeLive do
  use Phoenix.LiveView, layout: {__MODULE__, :live}

  def handle_params(params, _url, socket) do
    {:noreply,
     assign(socket,
       color: Map.get(params, "color", "blue"),
       type: Map.get(params, "type", "function")
     )}
  end

  def render("live.html", assigns) do
    ~H"""
    <script src="/assets/phoenix/phoenix.js"></script>
    <script src="/assets/phoenix_live_view/phoenix_live_view.js"></script>
    <script src="//unpkg.com/alpinejs" defer></script>
    <script type="text/javascript">
      document.addEventListener('alpine:init', () => {
        console.log("init")
        Alpine.data('red_counter', () => {
          return {
            count: 0,
            display: {
              ["x-text"](){
                return this.count;
              }
            },
            button: {
              ["x-on:click"](){
                return this.count++;
              }
            }
          }
        });

        Alpine.data('blue_counter', () => {
          return {
            n: 0,
            ns: {
              ["x-text"](){
                return this.n;
              }
            },
            button: {
              ["x-on:click"](){
                return this.n++;
              }
            }
          }
        })
      })
    </script>
    <script>
      let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket, {
        dom: {
          onBeforeElUpdated(from, to) {
            if (from._x_dataStack) {
              console.log("onBeforeElUpdated", from._x_dataStack)
              window.Alpine.clone(from, to);
            }
          },
        }
      })
      liveSocket.connect()

    </script>

    <%= @inner_content %>
    """
  end

  def render(assigns) do
    ~H"""
    <div>
      <h1>Alpine Counter Example</h1>

      <nav>
        <div>
          Function (works):
          <.link patch="/?color=blue&type=function">Blue Counter</.link> |
          <.link patch="/?color=red&type=function">Red Counter</.link>
        </div>

        <div style="padding: 10px 0">
          Inline (crash):
          <.link patch="/?color=blue&type=inline">Blue Counter</.link> |
          <.link patch="/?color=red&type=inline">Red Counter</.link>
        </div>
      </nav>

      <%= case @type do %>
        <% "function" -> %>
          <%= case @color do %>
            <% "blue" -> %> <.blue_counter/>
            <% "red" -> %> <.red_counter/>
          <% end %>

        <% "inline" -> %>
          <%= case @color do %>
            <% "blue" -> %>
              <div x-data="blue_counter">
                <div style="color: blue" x-bind="ns"></div>
                <button x-bind="button">+</button>
              </div>
            <% "red" -> %>
              <div>
                <div x-data="red_counter" x-ref="root">
                  <div style="color: red"} x-bind="display"></div>
                  <button x-bind="button">+</button>
                </div>
              </div>
          <% end %>
      <% end %>

    </div>
    """
  end

  def blue_counter(assigns) do
    ~H"""
    <div x-data="blue_counter">
      <div style="color: blue" x-bind="ns"></div>
      <button x-bind="button">+</button>
    </div>
    """
  end

  def red_counter(assigns) do
    ~H"""
    <div>
      <div x-data="red_counter" x-ref="root">
        <div style="color: red"} x-bind="display"></div>
        <button x-bind="button">+</button>
      </div>
    </div>
    """
  end
end

defmodule Example.Router do
  use Phoenix.Router
  import Phoenix.LiveView.Router

  pipeline :browser do
    plug(:accepts, ["html"])
  end

  scope "/", Example do
    pipe_through(:browser)

    live("/", HomeLive, :index)
  end
end

defmodule Example.Endpoint do
  use Phoenix.Endpoint, otp_app: :sample
  socket("/live", Phoenix.LiveView.Socket)

  plug(Plug.Static, from: {:phoenix, "priv/static"}, at: "/assets/phoenix")
  plug(Plug.Static, from: {:phoenix_live_view, "priv/static"}, at: "/assets/phoenix_live_view")

  plug(Plug.Parsers,
    parsers: [:urlencoded, :multipart, :json],
    pass: ["*/*"],
    json_decoder: Phoenix.json_library()
  )

  plug(Example.Router)
end

{:ok, _} = Example.Endpoint.start_link()
Process.sleep(:infinity)
Console log: functions (good)
phx-F9MC-AB8-YqApANh update:  -  {0: {…}}
onBeforeElUpdated called
onBeforeElUpdated called
onBeforeElUpdated called
onBeforeElUpdated called
phx-F9MC-AB8-YqApANh update:  -  {0: {…}}
onBeforeElUpdated called
onBeforeElUpdated called
onBeforeElUpdated called
onBeforeElUpdated called
phx-F9MC-AB8-YqApANh update:  -  {0: {…}}
onBeforeElUpdated called
onBeforeElUpdated called
onBeforeElUpdated called
onBeforeElUpdated called
phx-F9MC-AB8-YqApANh update:  -  {0: {…}}
onBeforeElUpdated called
onBeforeElUpdated called
onBeforeElUpdated called
onBeforeElUpdated called
WS Log: functions (good)
[
  "4",
  "8",
  "lv:phx-F9MBWHzvtDjF2AIi",
  "phx_reply",
  {
    "status": "ok",
    "response": {
      "diff": {
        "0": {
          "4": {
            "0": {
              "0": {
                "s": [
                  "<div x-data=\"blue_counter\">\n  <div style=\"color: blue\" x-bind=\"ns\"></div>\n  <button x-bind=\"button\">+</button>\n</div>"
                ],
                "r": 1
              },
              "s": [
                " ",
                "\n        "
              ]
            },
            "s": [
              "\n      ",
              "\n\n    "
            ]
          }
        }
      }
    }
  }
]



[
  "4",
  "9",
  "lv:phx-F9MBWHzvtDjF2AIi",
  "phx_reply",
  {
    "status": "ok",
    "response": {
      "diff": {
        "0": {
          "4": {
            "0": {
              "0": {
                "s": [
                  "<div>\n  <div x-data=\"red_counter\" x-ref=\"root\">\n    <div style=\"color: red\" } x-bind=\"display\"></div>\n    <button x-bind=\"button\">+</button>\n  </div>\n</div>"
                ],
                "r": 1
              },
              "s": [
                " ",
                "\n      "
              ]
            }
          }
        }
      }
    }
  }
]

[
  "4",
  "10",
  "lv:phx-F9MBWHzvtDjF2AIi",
  "phx_reply",
  {
    "status": "ok",
    "response": {
      "diff": {
        "0": {
          "4": {
            "0": {
              "0": {
                "s": [
                  "<div x-data=\"blue_counter\">\n  <div style=\"color: blue\" x-bind=\"ns\"></div>\n  <button x-bind=\"button\">+</button>\n</div>"
                ],
                "r": 1
              },
              "s": [
                " ",
                "\n        "
              ]
            }
          }
        }
      }
    }
  }
]

Console log: inline (bad)
phx-F9MDEe4D9aSenQAF update:  -  {0: {…}}
onBeforeElUpdated called
onBeforeElUpdated called
onBeforeElUpdated called
onBeforeElUpdated called
onBeforeElUpdated called
onBeforeElUpdated stack [Proxy(Object)]
onBeforeElUpdated called
phx-F9MDEe4D9aSenQAF update:  -  {0: {…}}
onBeforeElUpdated called
onBeforeElUpdated called
onBeforeElUpdated called
onBeforeElUpdated called
onBeforeElUpdated called
onBeforeElUpdated stack []
onBeforeElUpdated called
onBeforeElUpdated stack [Proxy(Object)]
Alpine Expression Error: ns is not defined  <- crash
Alpine Expression Error: button is not defined
WS Log: inline (bad)
[
  "4",
  "13",
  "lv:phx-F9MBWHzvtDjF2AIi",
  "phx_reply",
  {
    "status": "ok",
    "response": {
      "diff": {
        "0": {
          "4": {
            "0": {
              "s": [
                "\n          <div x-data=\"blue_counter\">\n            <div style=\"color: blue\" x-bind=\"ns\"></div>\n            <button x-bind=\"button\">+</button>\n          </div>\n        "
              ]
            },
            "s": [
              "\n      ",
              "\n  "
            ]
          }
        }
      }
    }
  }
]


[
  "4",
  "14",
  "lv:phx-F9MBWHzvtDjF2AIi",
  "phx_reply",
  {
    "status": "ok",
    "response": {
      "diff": {
        "0": {
          "4": {
            "0": {
              "s": [
                "\n          <div>\n            <div x-data=\"red_counter\" x-ref=\"root\">\n              <div style=\"color: red\" } x-bind=\"display\"></div>\n              <button x-bind=\"button\">+</button>\n            </div>\n          </div>\n      "
              ]
            }
          }
        }
      }
    }
  }
]

[
  "4",
  "15",
  "lv:phx-F9MBWHzvtDjF2AIi",
  "phx_reply",
  {
    "status": "ok",
    "response": {
      "diff": {
        "0": {
          "4": {
            "0": {
              "s": [
                "\n          <div x-data=\"blue_counter\">\n            <div style=\"color: blue\" x-bind=\"ns\"></div>\n            <button x-bind=\"button\">+</button>\n          </div>\n        "
              ]
            }
          }
        }
      }
    }
  }
]

Showing Posts 1 to 4

teamon

teamon OP

Update 1

I’ve found out that the same issue occurs when using inline x-data={...} too.

Single-file app example
Application.put_env(:sample, Example.Endpoint,
  http: [ip: {127, 0, 0, 1}, port: 5001],
  server: true,
  live_view: [signing_salt: "aaaaaaaa"],
  secret_key_base: String.duplicate("a", 64)
)

Mix.install([
  {:plug_cowboy, "~> 2.5"},
  {:jason, "~> 1.0"},
  {:phoenix, "~> 1.7"},
  # please test your issue using the latest version of LV from GitHub!
  {:phoenix_live_view,
   github: "phoenixframework/phoenix_live_view", branch: "main", override: true},
  {:ecto, "~> 3.0"},
  {:phoenix_ecto, "~> 4.4"}
])

# build the LiveView JavaScript assets (this needs mix and npm available in your path!)
path = Phoenix.LiveView.__info__(:compile)[:source] |> Path.dirname() |> Path.join("../")
# System.cmd("mix", ["deps.get"], cd: path, into: IO.binstream())
# System.cmd("npm", ["install"], cd: Path.join(path, "./assets"), into: IO.binstream())
# System.cmd("mix", ["assets.build"], cd: path, into: IO.binstream())

defmodule Example.ErrorView do
  def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end

defmodule Example.HomeLive do
  use Phoenix.LiveView, layout: {__MODULE__, :live}

  def handle_params(params, _url, socket) do
    {:noreply,
     assign(socket,
       color: Map.get(params, "color", "blue"),
       type: Map.get(params, "type", "function")
     )}
  end

  def render("live.html", assigns) do
    ~H"""
    <script src="/assets/phoenix/phoenix.js"></script>
    <script src="/assets/phoenix_live_view/phoenix_live_view.js"></script>
    <script src="//unpkg.com/alpinejs" defer></script>

    <script>
      let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket, {
        dom: {
          onBeforeElUpdated(from, to) {
            console.log("onBeforeElUpdated called")
            if (from._x_dataStack) {
              console.log("onBeforeElUpdated stack", from._x_dataStack)
              window.Alpine.clone(from, to);
            }
          },
        }
      })
      liveSocket.connect()

    </script>

    <%= @inner_content %>
    """
  end

  def render(assigns) do
    ~H"""
    <div>
      <h1>Alpine Counter Example</h1>

      <nav>
        <div>
          Function (works):
          <.link patch="/?color=blue&type=function">Blue Counter</.link> |
          <.link patch="/?color=red&type=function">Red Counter</.link>
        </div>

        <div style="padding: 10px 0">
          Inline (crash):
          <.link patch="/?color=blue&type=inline">Blue Counter</.link> |
          <.link patch="/?color=red&type=inline">Red Counter</.link>
        </div>
      </nav>

      <%= case @type do %>
        <% "function" -> %>
          <%= case @color do %>
            <% "blue" -> %> <.blue_counter/>
            <% "red" -> %> <.red_counter/>
          <% end %>

        <% "inline" -> %>
          <%= case @color do %>
            <% "blue" -> %>
              <div x-data="{n: 0}">
                <div style="color: blue" x-text="n"></div>
                <button x-on:click="n++">+</button>
              </div>
            <% "red" -> %>
              <div>
                <div x-data="{count: 0}" x-ref="root">
                  <div style="color: red"} x-text="count"></div>
                  <button x-on:click="count++">+</button>
                </div>
              </div>
          <% end %>
      <% end %>

    </div>
    """
  end

  def blue_counter(assigns) do
    ~H"""
    <div x-data="{n: 0}">
      <div style="color: blue" x-text="n"></div>
      <button x-on:click="n++">+</button>
    </div>
    """
  end

  def red_counter(assigns) do
    ~H"""
    <div>
      <div x-data="{count: 0}" x-ref="root">
        <div style="color: red"} x-text="count"></div>
        <button x-on:click="count++">+</button>
      </div>
    </div>
    """
  end
end

defmodule Example.Router do
  use Phoenix.Router
  import Phoenix.LiveView.Router

  pipeline :browser do
    plug(:accepts, ["html"])
  end

  scope "/", Example do
    pipe_through(:browser)

    live("/", HomeLive, :index)
  end
end

defmodule Example.Endpoint do
  use Phoenix.Endpoint, otp_app: :sample
  socket("/live", Phoenix.LiveView.Socket)

  plug(Plug.Static, from: {:phoenix, "priv/static"}, at: "/assets/phoenix")
  plug(Plug.Static, from: {:phoenix_live_view, "priv/static"}, at: "/assets/phoenix_live_view")

  plug(Plug.Parsers,
    parsers: [:urlencoded, :multipart, :json],
    pass: ["*/*"],
    json_decoder: Phoenix.json_library()
  )

  plug(Example.Router)
end

{:ok, _} = Example.Endpoint.start_link()
Process.sleep(:infinity)

Update 2

Adding id attributes seems to solve the issue. :thinking: :thinking:

<%= case @color do %>
  <% "blue" -> %>
    <div id="b" x-data="blue_counter">
      <div style="color: blue" x-bind="ns"></div>
      <button x-bind="button">+</button>
    </div>
  <% "red" -> %>
    <div id="r">
      <div x-data="red_counter" x-ref="root">
        <div style="color: red"} x-bind="display"></div>
        <button x-bind="button">+</button>
      </div>
    </div>
<% end %>
TunkShif

TunkShif

I’ve been doing some projects with Alpine and LiveView recently. And here’s my experimenting and debugging process for your problem. Hope that this can help you.

First, let’s take a closer look at the differences of the rendered html between the function components and thje inline content.

From the rendered results shown in the images below, you can see that the rendered component has a data-phx-id attribute in the component root element, while the rendered inline content doesn’t have this attribute. Here I’ve also added a data-debug attribute to the root element of the Alpine component (the element that has x-data attribute) to easily identify it later.



Next, we need to have a general idea of how LiveView DOM patch works. It uses the morphdom library under the hood for doing DOM mutations.

Given an existing DOM tree that is already rendered in the browser and a new updated HTML fragment sent from the server, morphdom will morph the existing DOM to match the content of the newly updated HTML fragment.

During the process, morphdom will try to reuse existing DOM element and only create new DOM nodes when needed, rather than simply replacing the whole old DOM tree with a completely newly created one.

It supports custom lifecycle hooks so that we can inspect the morphing process. Add the following code for debugging the morph process.

let liveSocket = new LiveSocket("/live", Socket, {
	dom: {
		onNodeAdded(node) {
			console.log("node added: ", node);
		},
		onBeforeElUpdated(from, to) {
                        // check if the element is an Alpine component root
			if (from.dataset.debug != null) {
				console.log("node updated: ");
				console.log(from.outerHTML);
				console.log(to.outerHTML);
			}

			if (from._x_dataStack) {
				console.log("datastack cloned");
				Alpine.clone(from, to);
			}
		},
	},
});

For the component type, switch between blue and red, and you can see that the whole component is always recreated. Each DOM node of the component is created and added to replace the old DOM content.

Switch from the component type to inline type, and you can see that all nodes are recreated too.

For the inline type, switch from blue to red, and you can see that the outermost div element is reused through the morph process. It is morphed from <div x-data="blue_counter" data-debug=""> to <div> with only attributes changed, there’s no new node created for it. And so far, everything works fine.

However, if we switch from red to blue, then error happens. To better demonstrate the morphing process, here I’ve also added the data-debug attribute to the outermost div element of the red counter component.

From the logging results above, we can see that two div elements were reused, and the error happens when dealing with the second node updates. From the trace stacks, the error happens when calling Alpine.clone() at the second DOM node update.


Then let’s have a look at what Alpine.clone() actually did. Alpine stores reactive states in the _x_dataStack property of a DOM element. So it first copy all the reactive states from the old element to the new element. After that, it’s calling initTree() on the new element. This function basically scans and initializes all the x- prefixed Alpine directives throughout the whole DOM tree.

And then let’s get back to the debugging results where error happened.

During the first node update, the outermost <div> element (the from param of onBeforeElUpdated ) is morphed to <div x-data="blue_counter">. But the from element is not an Alpine component root, which has no _x_dataStack property so that it fails if (from._x_dataStack) check. And Alpine.clone() is not called, the x-data="blue_counter" directive in the new element is not initialized.

During the second node update, the element is morphed to <div x-bind="ns">. But its parent element <div x-data="blue_counter"> didn’t get a chance to initialize the Alpine data, so it cannot find the ns binding from its root. And that’s why the error says Alpine Expression Error: ns is not defined.

The cause of the Alpine error should be clear now. Then let’s talk about why the component and inline rendering has different behavior when doing DOM patching. Why does morphdom always create new nodes when rendering component and why does it try to reuse existing nodes when rendering inline content?

morphdom also has a getNodeKey() option:

getNodeKey (Function(node) ) - Called to get the Node 's unique identifier. This is used by morphdom to rearrange elements rather than creating and destroying an element that already exists. This defaults to using the Node ‘s id property. (Note that form fields must not have a name corresponding to forms’ DOM properties, e.g. id .)

If you have experience in React, Vue or other frontend libraries, you may be familiar with the key attribute when doing list rendering. And the getNodeKey() function for morphdom works in a similar way.

And the implementation of this option in LiveView is shown below. It is using the element id or an attribute named PHX_MAGIC_ID as the node key. And PHX_MAGIC_ID is a constant with value data-phx-id. And that’s just what we saw in the rendered component in the beginning. Also, this is why it works if you manually add an id attribute to the root element.
https://github.com/phoenixframework/phoenix_live_view/blob/v0.20.14/assets/js/phoenix_live_view/dom_patch.js#L105

As for the "r": 1 field from the diff message, it is marked as reply in the source code. And I’m not sure what this field means, either. But I think it is not related to your problem, I guess?
https://github.com/phoenixframework/phoenix_live_view/blob/v0.20.14/lib/phoenix_live_view/diff.ex#L13

And this is all the explanations to your problem. Hope that I made it clear enough for you to understand. :no_mouth:

TunkShif

TunkShif

Also I want to write more notes and thoughts on Alpine and LiveView.
When the two are used together, they’re both trying to control DOM updates. And the code below is almost every tutorial does to integrate Alpine with LiveView:

let liveSocket = new LiveSocket("/live", Socket, {
	dom: {
		onBeforeElUpdated(from, to) {
			if (from._x_dataStack) {
				Alpine.clone(from, to);
			}
		},
	},
});

The source of this integration is from the author of Alpine in this issue: Alpine.js stopped working from LiveView Component on state change. · Issue #809 · phoenixframework/phoenix_live_view · GitHub
Alpine was originally created for Laravel LiveWire, a framework that is kind of similar to Phoenix LiveView. At the time when the issue was posted, LiveWire was also using morphdom for DOM changes and then use Alpine.clone() inside morphdom lifecycle hooks to integrate with Alpine.

But as the author stated in the issue above:

Something that may actually be a deal-breaker for the integration:

Livewire is simple. When an update happens, the server sends an entire chunk of HTML and uses morphdom to patch the existing DOM.

I’ve seen that LiveView has a much more advanced system where the server only sends the dynamic pieces of HTML that have changed.

It’s possible that you are doing something so advanced that you can’t use the simple morphdom hook as I have.

And indeed, with the current way to integrate Alpine with LiveView, you can encounter some unexpected weird behaviors like yours. And some other related problems can also be found in the forum, like what this post mentioned: Proper AlpineJS Integration with Liveview .

But now, LiveWire doesn’t use morphdom for DOM changes anymore. It is using the Alpine Morph plugin, which has the same functionality as morphdom, but it is Alpine-aware. So LiveWire can have seamless integration with Alpine.

And the Alpine.clone() function is marked as deprecated in the latest version. I’m not sure if there’s plan to completely remove it in future releases.
https://github.com/alpinejs/alpine/blob/v3.14.0/packages/alpinejs/src/clone.js#L44

Also, there used to be an instruction on how to integrate Alpine with LiveView in the JavaScript interoperability section of the LivewView doc. But I don’t know when and why it was removed.

teamon

teamon OP

@TunkShif Whoa, that’s an amazingly detailed response, thank you! :purple_heart::purple_heart::purple_heart::purple_heart::purple_heart:

So far adding ids where necessary did solve my immediate issues.

I wonder what are the rules to include data-phx-id or not :thinking:

I guess Alpine’s morph plugin duplicates the functionality of morphdom. I can only hope that Alpine won’t remove the clone() functionaly completely :sweat_smile:

— All posts loaded —

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
achenet
Hello, I’m trying to build a basic Phoenix web-app, and I’d like to use Tailwind. However, when I launch mix phx.server, I get an error...
New
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
New
asweet-confluent
I recently noticed that Elixir’s Logger defaults its primary log level to :debug when no :logger, :level application configuration is pre...
New
Cxx-mlr
I’m working on a small exercise involving update_in/3, and I came up with this solution: data = %{ name: "Periodic Table", category:...
New
ChrisAmelia
I’ve got trouble wrapping my head around the order in which functions are called in this snippet (from Phoenix’s authentication): toke...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews