surface-ui / surface

A server-side rendering component library for Phoenix

Home Page:https://surface-ui.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`class` prop on `Form` crashes when written as a string within `<.focus_wrap>`

simonmcconnell opened this issue · comments

Describe the bug

A Form with class="some string" crashes when rendered inside a <.focus_wrap>. It looks like value_to_opts(:class, value) normally receives a list of classes regardles of whether the class is defined as a list or string in the template. In this case described below, it is getting a string, which upsets it.

** (Protocol.UndefinedError) protocol Enumerable not implemented for "h-full flex flex-col bg-white shadow-xl" of type BitString
    (elixir 1.14.3) lib/enum.ex:1: Enumerable.impl_for!/1
    (elixir 1.14.3) lib/enum.ex:166: Enumerable.reduce/3
    (elixir 1.14.3) lib/enum.ex:4307: Enum.join/2
    (surface 0.9.4) lib/surface/type_handler/css_class.ex:65: Surface.TypeHandler.CssClass.value_to_opts/2
    (surface 0.9.4) lib/surface/type_handler.ex:188: Surface.TypeHandler.attr_to_opts!/3
    (surface 0.9.4) lib/surface/components/form.ex:82: anonymous fn/3 in Surface.Components.Form."render (overridable 1)"/1
    (elixir 1.14.3) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
    (surface 0.9.4) lib/surface/components/form.ex:82: Surface.Components.Form."render (overridable 1)"/1
    (phoenix_live_view 0.18.15) lib/phoenix_live_view/html_engine.ex:35: Phoenix.LiveView.HTMLEngine.component/3
    (logacy 0.6.1) lib/logacy_web/components/side_drawer.ex:25: anonymous fn/2 in LogacyWeb.Components.SideDrawer."render (overridable 1)"/1
    (phoenix_live_view 0.18.15) lib/phoenix_live_view/diff.ex:388: Phoenix.LiveView.Diff.traverse/7
    (phoenix_live_view 0.18.15) lib/phoenix_live_view/diff.ex:528: anonymous fn/4 in Phoenix.LiveView.Diff.traverse_dynamic/7
    (elixir 1.14.3) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
    (phoenix_live_view 0.18.15) lib/phoenix_live_view/diff.ex:388: Phoenix.LiveView.Diff.traverse/7
    (phoenix_live_view 0.18.15) lib/phoenix_live_view/diff.ex:528: anonymous fn/4 in Phoenix.LiveView.Diff.traverse_dynamic/7
    (elixir 1.14.3) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
    (phoenix_live_view 0.18.15) lib/phoenix_live_view/diff.ex:388: Phoenix.LiveView.Diff.traverse/7
    (phoenix_live_view 0.18.15) lib/phoenix_live_view/diff.ex:528: anonymous fn/4 in Phoenix.LiveView.Diff.traverse_dynamic/7
    (elixir 1.14.3) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
    (phoenix_live_view 0.18.15) lib/phoenix_live_view/diff.ex:388: Phoenix.LiveView.Diff.traverse/7
Last message: {:phoenix, :send_update, {LogacyWeb.Components.SideDrawer, "sidedrawer", %{id: "sidedrawer", show: true}}}

How to reproduce it

Given some page

      ...
      <SideDrawer changeset={@changeset}>
        <Field name={:name} class="flex flex-col gap-y-1">
          <Label class="label">Name</Label>
          <TextInput opts={phx_debounce: "1000"} />
        </Field>
      ...
      </SideDrawer>
    </AppLayout>
    """

and the SideDrawer

 ~F"""
    <section :show={@show}>
      <div :if={@show} class="absolute inset-0 overflow-hidden">
        <div
          class="absolute inset-0 bg-gray-800 bg-opacity-50 transition-opactiy"
          aria-hidden="true"
          :on-capture-click={@close}
          :on-window-keydown={@close}
          phx-key="escape"
          phx-page-loading
        />
        <.focus_wrap id="sidedrawer-content">
          <div>
            <Form
              :if={!is_nil(@changeset)}
              for={@changeset}
              as={@as}
              change={@change}
              submit={@submit}
              class="h-full flex flex-col bg-white shadow-xl"
            >
              <div>
                <#slot />
              </div>
            </Form>
          </div>
        </.focus_wrap>
      </div>
    </section>
    """

If I change the forms class to {[class="h-full flex flex-col bg-white shadow-xl"]} it works.
If I remove the focus_wrap it works.

The behavior you expected

Form's class prop works for string or list when contained within a <.focus_wrap>.

Your Environment

Surface: v0.9.4
LiveView: v0.18.15
Elixir: v1.14.3
Phoenix: 1.6.16

Did you ever find a resolution to this?

One workaround was to change the forms class to {[class="h-full flex flex-col bg-white shadow-xl"]}.

I forked Surface and changed something deep down but I can't recall where I was at with it because I haven't touched that project for a few months.

It might just work on the newer version?

Yeah, that's my short-term solution :-/ It definitely makes it a little confusing for some of our newer folks.

I was able to recreate the issue without relying on any CoreComponents code, and just render_component which almost immediately delegates to the render.

Phoenix.LiveViewTest.render_component(&Surface.Components.Form.Submit.render/1,
  class: "test-class"
)
1) test reproduce failure .modal and Surface.Form modules bug (MyAppWeb.PropsToAttrsTest)
     test/my_app_web/live/props_to_attrs_test.exs:13
     ** (Protocol.UndefinedError) protocol Enumerable not implemented for "test-class" of type BitString. This protocol is implemented for the following type(
s): DBConnection.PrepareStream, DBConnection.Stream, Date.Range, Ecto.Adapters.SQL.Stream, File.Stream, Floki.HTMLTree, Function, GenEvent.Stream, HashDict, H
ashSet, IO.Stream, Jason.OrderedObject, List, Map, MapSet, Phoenix.LiveView.LiveStream, Postgrex.Stream, Range, SFTPClient.Stream, Scrivener.Page, Stream, Tim
ex.Interval
     code: Phoenix.LiveViewTest.render_component(&Surface.Components.Form.Submit.render/1,
     stacktrace:
       (elixir 1.13.4) lib/enum.ex:1: Enumerable.impl_for!/1
       (elixir 1.13.4) lib/enum.ex:143: Enumerable.reduce/3
       (elixir 1.13.4) lib/enum.ex:4144: Enum.join/2
       (surface 0.10.0) lib/surface/type_handler/css_class.ex:65: Surface.TypeHandler.CssClass.value_to_opts/2
       (surface 0.10.0) lib/surface/type_handler.ex:188: Surface.TypeHandler.attr_to_opts!/3
       (surface 0.10.0) lib/surface/components/form/submit.ex:26: Surface.Components.Form.Submit."render (overridable 1)"/1
       (phoenix_live_view 0.18.18) lib/phoenix_live_view/test/live_view_test.ex:488: Phoenix.LiveViewTest.__render_component__/4
       test/my_app_web/live/props_to_attrs_test.exs:27: (test)

Interestingly, a call to render_surface does not cause this issue.

For people familiar with the compiler internals:

I did a bunch of digging, into the TypeHandler, TypeHandler.Default, TypeHandler.CssClass, and the compiler, as well as comparing to render_component, and I think it's something with somehow the normalize_expr not getting called at compile-time, or getting called differently?

I am not sure how the different layers between Surface and LV work at compile-time, but it seems to be when:
A Phoenix.Component with a slot of a Surface.Component that has a prop of :css_class, and the prop is a literal.

Something like this (not tested, just for illustration).

~F"""
<SomeOuterSurface>
  <.lv_middle>
    <InnerSurface class="some-class"/>
  </.lv_middle>
</SomeOuterSurface>
"""

If I understand correctly, this isn't just a bug with any of the Form or out-of-the-box components, and not specific to .focus_wrap but with all usage of the :css_class property on a Surface.Component when it's a "child" of a .HEEX component.


I am on the newest package version at this time.

Surface: v0.10.0
LiveView: v0.18.18
Elixir: v1.13.4-otp-25
Phoenix: 1.7.2

Thank you all for the detailed report. ❤️

Fixed in b2af2d0.