Live Pane - Resizable pane components for LiveView

Hello I’m working on Live Pane. It’s basically a pure javascript, dependency-free, port of paneforge / react-resizable-panels (for svelte and react resp.).

It’s still pretty much a work in progress and it’s my first time making a library!

It provides 3 hooks and 3 liveview components to create resizable panels:

<LivePane.group id="demo">
  <LivePane.pane group_id="demo" id="demo_pane_1" >
    ...
  </LivePane.pane>

  <LivePane.resizer id="demo-resizer" group_id="demo"></LivePane.resizer>
  <LivePane.pane group_id="demo" id="demo_pane_2>
    ...
  </LivePane.pane>
</LivePane.group>

The basic mechanism works and you could already use it. It’s just missing keyboard and JS ↔ LV events support (e.g. send an event from server to collapse a pane, and viceversa send an event from client when a pane is collapsed). And of course I need a ton of documentation as well.

Thanks a ton to live_toast as well cause I looked at that repo a lot to setup mine.

Demo: https://live-pane.fly.dev/
Hex Docs: Live Pane v0.2.0
GitHub:

13 Likes

This is great, nice work and congrats on your first Elixir lib!

For some feedback: It doesn’t seem there is any value in having these as LiveComponents. They are all static and don’t encapsulate any behaviour. They would also be simpler, for example the resizer:

defmodule LivePane.Resizer do
  @moduledoc """
  TODO
  """
  use Phoenix.LiveComponent

  @impl true
  def update(assigns, socket) do
    direction = assigns[:direction] || "horizontal"
    active = assigns[:active] || "pointer"
    enabled = assigns[:disabled] || true
    tab_index = assigns[:tab_index] || 0

    socket =
      socket
      |> assign(assigns)
      |> assign(:direction, direction)
      |> assign(:active, active)
      |> assign(:enabled, enabled)
      |> assign(:tab_index, tab_index)

    {:ok, socket}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div
      id={@id}
      role="separator"
      phx-update="ignore"
      phx-hook="live_pane_resizer"
      data-pane-resizer=""
      data-pane-resizer-id={@id}
      data-pane-group-id={@group_id}
      data-pane-direction={@direction}
      data-pane-active={@active}
      data-pane-enabled={@enabled}
      tabindex={@tab_index}
      class={@class}
    >
      {render_slot(@inner_block)}
    </div>
    """
  end
end

This could be:

defmodule LivePane.Resizer do
  use Phoenix.Component

  attr :direction, :string, values: ["horizontal", "vertical"], default: "horizontal"
  attr :active, :string, default: "pointer" # (not sure what the other values are) 
  attr :enabled, :boolean, default: true
  attr :tab_index, :integer, default: 0

  slot :inner_block, required: true

  def resizer(assigns) do
    ~H"""
    <div
      id={@id}
      role="separator"
      phx-update="ignore"
      phx-hook="live_pane_resizer"
      data-pane-resizer=""
      data-pane-resizer-id={@id}
      data-pane-group-id={@group_id}
      data-pane-direction={@direction}
      data-pane-active={@active}
      data-pane-enabled={@enabled}
      tabindex={@tab_index}
      class={@class}
    >
      {render_slot(@inner_block)}
    </div>
    """
  end
end

It’s actually quite a bit simpler having them as function components.

3 Likes

Thanks!

I kinda left the Elixir code behind after a quick draft cause I dove into the hooks part instead :sweat_smile:

I have to get back to it and refactor!

I think he mentioned wanting to be able to send events back and forth and maybe that’s why it’s a LV, still work in progress. But either way, really cool library and interesting idea :slight_smile:

2 Likes

I think it’s better to keep attribute names like tabindex the same rather than renaming them in the attr list (:tab_index). You can even use attr :rest, :global which will include tabindex and others by default.

1 Like

Definitely! Good callout.

And yes, I read so quickly while taking a little break from work and was more interested in looking at the code.

Makes sense if there are going to be events, though it still feels a bit heavy-handed if it’s simply for showing and hiding. You could generally have a generic component for this in your app that you could wrap any component with. But I’m not exactly thinking this through, mostly that I would like to use this library and would like a simple function component that I can resize :slight_smile:

3 Likes

TBH I also don’t see that much value to send events from the panes back to liveview, but perhaps someone will want to react to a panel that gets collapsed :sweat_smile:

My personal use case was to use this as a draggable sidebar with a button to collapse/expand it, which will only use client-side events.

I will turn them into simple function components like you suggested.

If/when someone will need to have that extra functionality I can always add another set of components for LiveView so the lib can be used both with vanilla Phoenix and with LiveView (and lib’s name keeps making sense).

1 Like

You can do this with options that take JS commands:

<.panel on_drag_start={JS.push("some-event") on_drag_end={JS.push("some-other-event"}) ...>

Then people are free to store the state where they want be it a LiveView or LiveComponent.

1 Like

Just published 0.3.0!

  • Now the 3 components are simple function components. Now it works without liveview as well!
  • Added keyboard events support so when focusing on a handle, the panes can be resized using the arrow keys.
  • Worked on the docs site to better showcase the library, check it out!
3 Likes

Hello!

0.4.0 is out! What I’ve added:

  • Now you can push_event("collapse/expand", %{pane_id: "the_pane_id"}) from your liveview to the pane to programmatically collapse/expand a collapsible pane (the collapsible example in the website has a button that does that).
  • I’ve changed the attribute name default_size to starting_size cause it was more clear to me.
  • Removed phx-update="ignore" from the components. I was making a “conditional panes” example and was losing my mind why my <%= if hide_pane do %> wasn’t doing anything.
  • Updated the docs site again!

Now I need to add a way to persist the sizes of the panes and the lib should more or less have feature parity with the others mentioned.

3 Likes

Published 0.5.0!

  • Finally added persistent panes that keep the same sizes as you left them with the new auto_save attr on the groups.
  • I’ve also ported the dynamic aria attributes on the resizing handles. They get updated on resizing.
  • Also added a persistent panes example in the docs site.

Now the porting should be more or less completed. I will slowly improve the docs site cause it sucks on mobile and add more examples in the future.

0.6.0 is out!

This time I’ve added tracking to the pane state change: collapsed, collapsing to expanding, expanded when programmatically changed. When dragging with mouse it’s just collapsed to expanded and viceversa.

The data attribute data-pane-state gets updated with these values so you can use it to add custom styling during these changes.

Now you can animate the collapsing/expanding with:

data-[pane-state=collapsing]:transition-[flex-grow] 
data-[pane-state=expanding]:transition-[flex-grow] 

In addition there are 2 new attributes on panes: on_collapse and on_expand which accept JS commands so you can send events to the server with JS.push (or any other command).

Also added a new example to showcase this stuff.

1 Like