I’m creating my first form with Ash and cannot get past this error. Here’s my quick setup.
I have a Project
resource in a Projects
domain. The domain is defined:
defmodule Crochet.Projects do
use Ash.Domain,
extensions: [AshPhoenix]
resources do
resource Crochet.Projects.Project do
define :create_project, action: :create, args: [:name, :user_id]
# other stuff
end
end
end
Following along in the AshPhoenix.Form
documentation it shows a way to render a form. When I try to follow along with those examples, I get a function AshPhoenix.Form.fetch/2 is undefined
error:
function AshPhoenix.Form.fetch/2 is undefined (AshPhoenix.Form does not implement the Access behaviour
You can use the "struct.field" syntax to access struct fields. You can also use Access.key!/1 to access struct fields dynamically inside get_in/put_in/update_in)
Here is my code that attempts to render the form:
defmodule CrochetWeb.ProjectsLive.Form do
use CrochetWeb, :live_component
alias Crochet.Projects
def mount(socket) do
socket = assign(socket, :form, Projects.form_to_create_project())
{:ok, socket}
end
def render(assigns) do
~H"""
<div>
<.form for={@form} phx-change="validate">
<.input field={@form[:name]} />
</.form>
</div>
"""
end
end
I feel like that is pretty basic and matches really closely to the documentation examples. Specifically, it errors on @form[:name]
. What am I doing wrong?
Full stacktrace
UndefinedFunctionError at GET /projects
Exception:
** (UndefinedFunctionError) function AshPhoenix.Form.fetch/2 is undefined (AshPhoenix.Form does not implement the Access behaviour
You can use the "struct.field" syntax to access struct fields. You can also use Access.key!/1 to access struct fields dynamically inside get_in/put_in/update_in)
(ash_phoenix 2.1.12) AshPhoenix.Form.fetch(#AshPhoenix.Form<resource: Crochet.Projects.Project, action: :create, type: :create, params: %{}, source: #Ash.Changeset<domain: Crochet.Projects, action_type: :create, action: :create, attributes: %{}, relationships: %{}, errors: [%Ash.Error.Changes.Required{field: :name, type: :attribute, resource: Crochet.Projects.Project, splode: nil, bread_crumbs: [], vars: [], path: [], stacktrace: #Splode.Stacktrace<>, class: :invalid}], data: #Crochet.Projects.Project<user: #Ash.NotLoaded<:relationship, field: :user>, __meta__: #Ecto.Schema.Metadata<:built, "projects">, id: nil, name: nil, inserted_at: nil, updated_at: nil, user_id: nil, aggregates: %{}, calculations: %{}, ...>, valid?: false>, name: "form", data: nil, form_keys: [], forms: %{}, domain: Crochet.Projects, method: "post", submit_errors: nil, id: "form", transform_errors: nil, original_data: nil, transform_params: nil, prepare_params: nil, prepare_source: nil, raw_params: %{}, warn_on_unhandled_errors?: true, any_removed?: false, added?: false, changed?: false, touched_forms: MapSet.new([]), valid?: false, errors: nil, submitted_once?: false, just_submitted?: false, ...>, :name)
(elixir 1.17.0) lib/access.ex:322: Access.get/3
(crochet 0.1.0) lib/crochet_web/live/projects/form.ex:15: anonymous fn/2 in CrochetWeb.ProjectsLive.Form.render/1
(phoenix_live_view 1.0.1) lib/phoenix_live_view/diff.ex:414: Phoenix.LiveView.Diff.traverse/7
(phoenix_live_view 1.0.1) lib/phoenix_live_view/diff.ex:555: anonymous fn/4 in Phoenix.LiveView.Diff.traverse_dynamic/7
(elixir 1.17.0) lib/enum.ex:2531: Enum."-reduce/3-lists^foldl/2-0-"/3
(phoenix_live_view 1.0.1) lib/phoenix_live_view/diff.ex:412: Phoenix.LiveView.Diff.traverse/7
(phoenix_live_view 1.0.1) lib/phoenix_live_view/diff.ex:555: anonymous fn/4 in Phoenix.LiveView.Diff.traverse_dynamic/7
(elixir 1.17.0) lib/enum.ex:2531: Enum."-reduce/3-lists^foldl/2-0-"/3
(phoenix_live_view 1.0.1) lib/phoenix_live_view/diff.ex:412: Phoenix.LiveView.Diff.traverse/7
(phoenix_live_view 1.0.1) lib/phoenix_live_view/diff.ex:782: Phoenix.LiveView.Diff.render_component/8
(phoenix_live_view 1.0.1) lib/phoenix_live_view/diff.ex:720: Phoenix.LiveView.Diff.zip_components/5
(phoenix_live_view 1.0.1) lib/phoenix_live_view/diff.ex:703: anonymous fn/4 in Phoenix.LiveView.Diff.render_pending_components/6
(stdlib 6.2) maps.erl:860: :maps.fold_1/4
(phoenix_live_view 1.0.1) lib/phoenix_live_view/diff.ex:648: Phoenix.LiveView.Diff.render_pending_components/6
(phoenix_live_view 1.0.1) lib/phoenix_live_view/diff.ex:143: Phoenix.LiveView.Diff.render/3
(phoenix_live_view 1.0.1) lib/phoenix_live_view/static.ex:288: Phoenix.LiveView.Static.to_rendered_content_tag/4
(phoenix_live_view 1.0.1) lib/phoenix_live_view/static.ex:171: Phoenix.LiveView.Static.do_render/4
(phoenix_live_view 1.0.1) lib/phoenix_live_view/controller.ex:39: Phoenix.LiveView.Controller.live_render/3
(phoenix 1.7.18) lib/phoenix/router.ex:484: Phoenix.Router.__call__/5
Code:
nofile
No code available.
Called with 2 arguments
#AshPhoenix.Form<resource: Crochet.Projects.Project, action: :create, type: :create, params: %{}, source: #Ash.Changeset<domain: Crochet.Projects, action_type: :create, action: :create, attributes: %{}, relationships: %{}, errors: [%Ash.Error.Changes.Required{field: :name, type: :attribute, resource: Crochet.Projects.Project, splode: nil, bread_crumbs: [], vars: [], path: [], stacktrace: #Splode.Stacktrace<>, class: :invalid}], data: #Crochet.Projects.Project<user: #Ash.NotLoaded<:relationship, field: :user>, __meta__: #Ecto.Schema.Metadata<:built, "projects">, id: nil, name: nil, inserted_at: nil, updated_at: nil, user_id: nil, aggregates: %{}, calculations: %{}, ...>, valid?: false>, name: "form", data: nil, form_keys: [], forms: %{}, domain: Crochet.Projects, method: "post", submit_errors: nil, id: "form", transform_errors: nil, original_data: nil, transform_params: nil, prepare_params: nil, prepare_source: nil, raw_params: %{}, warn_on_unhandled_errors?: true, any_removed?: false, added?: false, changed?: false, touched_forms: MapSet.new([]), valid?: false, errors: nil, submitted_once?: false, just_submitted?: false, ...>
:name
lib/access.ex
No code available.
lib/crochet_web/live/projects/form.ex
10
11 def render(assigns) do
12 ~H"""
13 <div>
14 <.form for={@form} phx-change="validate">
15> <.input field={@form[:name]} />
16 </.form>
17 </div>
18 """
19 end
20 end
lib/phoenix_live_view/diff.ex
409 changed?
410 ) do
411 {_counter, diff, children, pending, components, template} =
412 traverse_dynamic(
413 socket,
414> invoke_dynamic(rendered, false),
415 %{},
416 pending,
417 components,
418 template,
419 changed?
lib/phoenix_live_view/diff.ex
550 Enum.reduce(dynamic, {0, %{}, children, pending, components, template}, fn
551 entry, {counter, diff, children, pending, components, template} ->
552 child = Map.get(children, counter)
553
554 {serialized, child_fingerprint, pending, components, template} =
555> traverse(socket, entry, child, pending, components, template, changed?)
556
557 # If serialized is nil, it means no changes.
558 # If it is an empty map, then it means it is a rendered struct
559 # that did not change, so we don't have to emit it either.
560 diff =
lib/enum.ex
No code available.
lib/phoenix_live_view/diff.ex
407 components,
408 template,
409 changed?
410 ) do
411 {_counter, diff, children, pending, components, template} =
412> traverse_dynamic(
413 socket,
414 invoke_dynamic(rendered, false),
415 %{},
416 pending,
417 components,
lib/phoenix_live_view/diff.ex
550 Enum.reduce(dynamic, {0, %{}, children, pending, components, template}, fn
551 entry, {counter, diff, children, pending, components, template} ->
552 child = Map.get(children, counter)
553
554 {serialized, child_fingerprint, pending, components, template} =
555> traverse(socket, entry, child, pending, components, template, changed?)
556
557 # If serialized is nil, it means no changes.
558 # If it is an empty map, then it means it is a rendered struct
559 # that did not change, so we don't have to emit it either.
560 diff =
lib/enum.ex
No code available.
lib/phoenix_live_view/diff.ex
407 components,
408 template,
409 changed?
410 ) do
411 {_counter, diff, children, pending, components, template} =
412> traverse_dynamic(
413 socket,
414 invoke_dynamic(rendered, false),
415 %{},
416 pending,
417 components,
lib/phoenix_live_view/diff.ex
777
778 {changed?, linked_cid, prints} =
779 maybe_reuse_static(rendered, socket, component, cids, components)
780
781 {diff, component_prints, pending, components, nil} =
782> traverse(socket, rendered, prints, %{}, components, nil, changed?)
783
784 children_cids =
785 for {_component, list} <- pending,
786 entry <- list,
787 do: elem(entry, 0)
lib/phoenix_live_view/diff.ex
715 {pending, diffs, components}
716 ) do
717 diffs = maybe_put_events(diffs, socket)
718
719 {new_pending, diffs, components} =
720> render_component(socket, component, id, cid, new?, cids, diffs, components)
721
722 pending = Map.merge(pending, new_pending, fn _, v1, v2 -> v2 ++ v1 end)
723 zip_components(sockets, metadata, component, cids, {pending, diffs, components})
724 end
725
lib/phoenix_live_view/diff.ex
698
699 {sockets, Map.put(telemetry_metadata, :sockets, sockets)}
700 end)
701
702 metadata = Enum.reverse(metadata)
703> triplet = zip_components(sockets, metadata, component, cids, {pending, diffs, components})
704 {triplet, seen_ids}
705 end)
706
707 render_pending_components(socket, pending, seen_ids, cids, diffs, components)
708 end
maps.erl
No code available.
lib/phoenix_live_view/diff.ex
643
644 defp render_pending_components(socket, pending, seen_ids, cids, diffs, components) do
645 acc = {{%{}, diffs, components}, seen_ids}
646
647 {{pending, diffs, components}, seen_ids} =
648> Enum.reduce(pending, acc, fn {component, entries}, acc ->
649 {{pending, diffs, components}, seen_ids} = acc
650 update_many? = function_exported?(component, :update_many, 1)
651 entries = maybe_preload_components(component, Enum.reverse(entries))
652
653 {assigns_sockets, metadata, components, seen_ids} =
lib/phoenix_live_view/diff.ex
138 # cid_to_component is used by maybe_reuse_static and it must be a copy before changes.
139 # However, given traverse does not change cid_to_component, we can read it now.
140 {cid_to_component, _, _} = components
141
142 {cdiffs, components} =
143> render_pending_components(socket, pending, cid_to_component, %{}, components)
144
145 socket = %{socket | fingerprints: prints}
146 diff = maybe_put_title(diff, socket)
147 {diff, cdiffs} = extract_events({diff, cdiffs})
148 {socket, maybe_put_cdiffs(diff, cdiffs), components}
lib/phoenix_live_view/static.ex
283 content_tag(tag, attrs, "")
284 end
285
286 defp to_rendered_content_tag(socket, tag, view, attrs) do
287 rendered = Phoenix.LiveView.Renderer.to_rendered(socket, view)
288> {_, diff, _} = Diff.render(socket, rendered, Diff.new_components())
289 content_tag(tag, attrs, Diff.to_iodata(diff))
290 end
291
292 defp content_tag(tag, attrs, content) do
293 tag = to_string(tag)
lib/phoenix_live_view/static.ex
166 {:data, data_attrs}
167 | extended_attrs
168 ]
169
170 try do
171> {:ok, to_rendered_content_tag(socket, tag, view, attrs), socket.assigns}
172 catch
173 :throw, {:phoenix, :child_redirect, redirected, flash} ->
174 {:stop, Utils.replace_flash(%{socket | redirected: redirected}, flash)}
175 end
176
lib/phoenix_live_view/controller.ex
34 end
35 end
36
37 """
38 def live_render(%Plug.Conn{} = conn, view, opts \\ []) do
39> case LiveView.Static.render(conn, view, opts) do
40 {:ok, content, socket_assigns} ->
41 conn
42 |> Plug.Conn.fetch_query_params()
43 |> ensure_format()
44 |> Phoenix.Controller.put_view(LiveView.Static)
lib/phoenix/router.ex
479 :telemetry.execute([:phoenix, :router_dispatch, :stop], measurements, metadata)
480 halted_conn
481
482 %Plug.Conn{} = piped_conn ->
483 try do
484> plug.call(piped_conn, plug.init(opts))
485 else
486 conn ->
487 measurements = %{duration: System.monotonic_time() - start}
488 metadata = %{metadata | conn: conn}
489 :telemetry.execute([:phoenix, :router_dispatch, :stop], measurements, metadata)
Connection details
Params
%{}
Request info
- URI: http://localhost:4000/projects
- Query string:
Headers
- accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7
- accept-encoding: gzip, deflate, br, zstd
- accept-language: en-US,en;q=0.9
- cache-control: max-age=0
- connection: keep-alive
- cookie: my-cookie
- dnt: 1
- host: localhost:4000
- referer: http://localhost:4000/projects
- sec-ch-ua: “Not A(Brand”;v=“8”, “Chromium”;v=“132”
- sec-ch-ua-mobile: ?0
- sec-ch-ua-platform: “macOS”
- sec-fetch-dest: document
- sec-fetch-mode: navigate
- sec-fetch-site: same-origin
- upgrade-insecure-requests: 1
- user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36
Session
%{"_csrf_token" => "mV3lhtFCZtwVmZF-Bd-N4ERi", "user" => "user?id=f2063d7f-fe65-42e4-88f8-3b3bfd12a7e5"}