Basic Search Form has me flummoxed: no function clause matching

I’m new to Elixir & Phoenix and I’ve been struggling to find a solution to the following problem.

I’m creating an inventory app to keep track of my Magic: The Gathering cards. I used a generator to get started, creating everything I need to create, edit and view cards.

I want to add a search feature where I can enter a set id and a card number. Using the code to add new card as a guide, I’ve added the following:

lib/card_inventory_app/cards.ex

def get_card_by_set_and_number(set, collection_number) do

    Repo.get_by(Card, set: set, collection_number: collection_number)

end

lib/card_inventory_app_web/router.ex


scope "/", CardInventoryAppWeb do 

get "/cards/search", CardController, :search
end

CardController.ex

 def search(conn, %{"set" => set, "collection_number" => collection_number} = _params) do

    card = Cards.get_card_by_set_and_number(set, collection_number)

    render(conn, :search, card: card)

end

controllers/card_html.ex

  attr :card, :map, default: nil

  def search_form(assigns)

controller/card_html/search.html.heex

<Layouts.app flash={@flash}>
<.header>
Search Cards
<:subtitle>Use this form to search for card records in your database.</:subtitle>
</.header>

<.search_form card={@card} action={~p"/cards/search"} return_to={~p"/cards/search"} />
</Layouts.app>

controller/card_html/search_form.html.heex

<.form :let={f} for={@card} action={@action}>
<.input field={f[:set]} type=“text” label=“Set” />
<.input field={f[:collection_number]} type=“number” label=“Collection number” />

<footer>
    <.button variant="primary">Search for Card</.button>
    <.button :if={@return_to} href={@return_to}>Cancel</.button>
  </footer>
</.form>

The Cards.get_card_by_set_and_number function works in iex.
I’ve played around with the forms, changes etc but I still can’t get the search form to display.

The current error is as follows:

no function clause matching in CardInventoryAppWeb.CardController.search/2

The following arguments were given to CardInventoryAppWeb.CardController.search/2:

    # 1
    %Plug.Conn{adapter: {Bandit.Adapter, :...}, assigns: %{flash: %{}}, body_params: %{}, cookies: %{"_card_inventory_app_key" => "SFMyNTY.g3QAAAABbQAAAAtfY3NyZl90b2tlbm0AAAAYWWcydmVZVUpIUFV4NXBrSjRzVUxjZG9S.vga988mp5AKDefNL_iBoB8y4E-SN7YUH4ssIDHSqiXI"}, halted: false, host: "localhost", method: "GET", owner: #PID<0.6597.0>, params: %{}, path_info: ["cards", "search"], path_params: %{}, port: 4000, private: %{:phoenix_live_reload => true, :phoenix_view => %{"html" => CardInventoryAppWeb.CardHTML, "json" => CardInventoryAppWeb.CardJSON}, :phoenix_endpoint => CardInventoryAppWeb.Endpoint, CardInventoryAppWeb.Router => [], :phoenix_action => :search, :phoenix_layout => %{}, :phoenix_controller => CardInventoryAppWeb.CardController, :phoenix_format => "html", :phoenix_root_layout => %{"html" => {CardInventoryAppWeb.Layouts, :root}}, :phoenix_router => CardInventoryAppWeb.Router, :plug_session_fetch => :done, :plug_session => %{"_csrf_token" => "Yg2veYUJHPUx5pkJ4sULcdoR"}, :before_send => [#Function<0.122245838/1 in Plug.CSRFProtection.call/2>, #Function<4.109347978/1 in Phoenix.Controller.fetch_flash/2>, #Function<0.76585640/1 in Plug.Session.before_send/2>, #Function<0.24950767/1 in Plug.Telemetry.call/2>, #Function<1.66527966/1 in Phoenix.LiveReloader.before_send_inject_reloader/3>], :phoenix_request_logger => {"request_logger", "request_logger"}}, query_params: %{}, query_string: "", remote_ip: {127, 0, 0, 1}, req_cookies: %{"_card_inventory_app_key" => "SFMyNTY.g3QAAAABbQAAAAtfY3NyZl90b2tlbm0AAAAYWWcydmVZVUpIUFV4NXBrSjRzVUxjZG9S.vga988mp5AKDefNL_iBoB8y4E-SN7YUH4ssIDHSqiXI"}, req_headers: [{"host", "localhost:4000"}, {"accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}, {"sec-fetch-site", "none"}, {"upgrade-insecure-requests", "1"}, {"sec-fetch-mode", "navigate"}, {"user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.1 Safari/605.1.15"}, {"referer", "``http://localhost:4000/cards/search``"}, {"sec-fetch-dest", "document"}, {"cache-control", "max-age=0"}, {"accept-language", "en-GB,en;q=0.9"}, {"priority", "u=0, i"}, {"accept-encoding", "gzip, deflate"}, {"cookie", "_card_inventory_app_key=SFMyNTY.g3QAAAABbQAAAAtfY3NyZl90b2tlbm0AAAAYWWcydmVZVUpIUFV4NXBrSjRzVUxjZG9S.vga988mp5AKDefNL_iBoB8y4E-SN7YUH4ssIDHSqiXI"}, {"connection", "keep-alive"}], request_path: "/cards/search", resp_body: nil, ...}

I get the feeling I’m missing something basic, as it doesn’t feel like it should be this hard!

Any help would be greatly appreciated!

Welcome to Elixir and Phoenix. And thank you for the care in providing as much info as you did to help people help you.

As you’ve probably worked out, pattern matching is one of the great strengths of the language (in fact, of the BEAM platform). But it does take some time to become familiar with what some exceptions are trying to tell you.

In the case of “no function clause matching” errors, you can think about it this way:

  1. The function you’re trying to call is known (CardInventoryAppWeb.CardController.search exists)
  2. There is at least one function clause that expects 2 parameters (CardInventoryAppWeb.CardController.search/2 tells you that)
  3. The parameters being applied to the function don’t match any of the function clauses that are defined.

In short, your search function expects that params will have the match shape of %{"set" => set, "collection_number" => collection_number} and it doesn’t.

Unfortunately, the exception message is longer than than the inspected output so its hard to know what is actually being passed.

One thing you can try (and it’s useful in all kinds of situations like this) is to define a catch-all clause that is guaranteed to match - and then output whatever debug information you need. (Don’t forget to delete it when you’re done).

In your case, add after your current search/2 clause (since clauses are matched top to bottom):

def search(_conn, params) do
  IO.inspect(params, limit: :infinity)
  .....
end

Hope this at least gets you moving forward to the next issue :slight_smile:

5 Likes

Thank you! You’re suggestion helped me solve it.

I needed to handle loading the search page without params initially so that the page displays:

# When first loading /cards/search without params

  def search(conn, _params) do

    render(conn, :search, card: nil, set: nil, collection_number: nil)

end

Once I added the above, I could see my form, which led to my next problem. When i submit a set and collection number I got the following message:

protocol Phoenix.HTML.FormData not implemented for CardInventoryApp.Cards.Card (a struct). This protocol is implemented for: Ecto.Changeset, Map
 
 Got value:
 
     %CardInventoryApp.Cards.Card{
       __meta__: #Ecto.Schema.Metadata<:loaded, "cards">,
       id: 1,
       set: "CLB",
       collection_number: 659,
       image_url: "https://cards.scryfall.io/normal/front/6/b/6bb131fd-d96f-43e0-a40d-fa6394a4b75c.jpg?1705436707",
       oracle: "\"Flying Whenever this creature attacks, look at the top four cards of your library. You may reveal a Cleric card, a Rogue card, a Warrior card, and/or a Wizard card from among them and put those cards into your hand. Put the rest on the bottom of your library in a random order.\"",
       cardmarket: "661373",
       title: "Harper Recruiter",
       type: "Creature",
       rarity: "Rare",
       scryfall: "https://api.scryfall.com/cards/CLB/659",
       colour: "White",
       inserted_at: ~U[2025-11-08 09:07:54Z],
       updated_at: ~U[2025-11-08 09:07:54Z]
     }

This was helpful as it made me realise that the page I was trying to render expected a form, not the results.
I changed the original search action in the controller to render show.html.heex and the results are displayed as desired.

Thanks again!

1 Like

That error comes up when you try to call to_form/1 with a schema rather than with the params/changeset. The point of a form is to render “in progress” changes. That is, the form needs to include the user’s latest input so it can show errors. If the form was updating a %Card{}, you would do something like this:

changeset = Card.changeset(current_card, %{title: "New title"})
form = to_form(changeset)

The form bindings docs are pretty good, definitely check those out.

1 Like

Don’t forget to set the action of the changeset to show errors. It’s shown in the docs but can be easy to miss

changeset = Card.changeset(current_card, %{title: "New title"})
form = to_form(changeset, action: :validate)
3 Likes

Oh yeah, I still think of the old style where you would manually set the :action on the Changeset. That was pretty awful, to_form/2 is much better.

If anyone else is curious where the action takes effect I think it’s here. Took me a minute to track it down. I expected it to actually set the action on the Changeset but it doesn’t appear to.

It’s either set by the various Repo functions or by Ecto.Changeset.apply_action. This is an explicit step, so that an initial changeset for a form (which could already have errors, depending on how it’s constructed) can be told appart from one that was used to attempt an insert/update/whatever needs to be done.

1 Like

Sorry, what I meant was I expected to_form(..., action: :foo) to set the action on the changeset you pass in but it doesn’t. It just uses the action option directly to show/hide errors. Which makes perfect sense! I just thought it was interesting.

But of course it does still take the action from the changeset if present.