`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.