Using form_for without ecto

Hi there,

I’m using Couchdb in my phoenix application. I have a create.html page which is used fo creating or updating data. This form is defined with the form_for/4 function. As I’m not using Ecto, what is the best way of filling the form inputs with all the properties of my model? The create part is working great, but I don’t know how to proceed for the update part without ecto and changeset…

Here is my model :

defmodule Dbuniverse.Character do

    @derive [Poison.Encoder]
    defstruct [:_id, :category, :name, :image_url, :description, :type]

end

Here is my form :

<%= form_for @conn, character_path(@conn, :add), [as: :character], fn f -> %>
    <div class="form-group">
        <%= label f, :name, "Name" %>
        <%= text_input f, :name, class: "form-control" %>
    </div>
    <div class="form-group">
        <%= label f, :description, "Description" %>
        <%= textarea f, :description, class: "form-control", rows: 5 %>
    </div>

    <%= submit "Add", class: "btn btn-primary pull-right" %>
<% end %>

And finally, my controller functions :

def create(conn, _params) do

        render conn, "create.html", conn: conn

end

def add(conn, %{"character" => character}) do

        # IO.inspect(params)
        c = Poison.encode! character
        json = Dbuniverse.Repo.insert c
        redirect conn, to: character_path(conn, :show, json["id"])

end

Thanks for your time!
Didier

2 Likes

You can still use Ecto.Changeset for this if you want to. This is an article about MongoDB but it would equally apply to Couch: https://tomjoro.github.io/2017-02-09-ecto3-mongodb-phoenix/

You can also implement the Phoenix.HTML.FormData protocol, which is what Phoenix.Ecto does.

2 Likes

Thanks Jose for your time. Do you have any links on how to implement the Phoenix.HTML.FormData protocol? I’m pretty new to phoenix and elixir…

What I did is the following :

I created an ecto model in order to create a changeset and use it with the form_for function :

defmodule Dbuniverse.EctoCharacter do
    
    use Ecto.Schema
    import Ecto.Changeset

    @required_fields [:name, :description]
    @optional_fields [:_id, :category, :image_url, :type]

    schema "ectocharacters" do
        
        field :_id,         :string
        field :category,    :string
        field :name,        :string
        field :image_url,   :string
        field :description, :string
        field :type,        :string

        timestamps()

    end

    def changeset(character, params \\ %{}) do
        character
        |> cast(params, @required_fields ++ @optional_fields)
        |> validate_required(@required_fields)
    end

end 

I also have a couchdb model :

 defmodule Dbuniverse.Character do

    @derive [Poison.Encoder]
    defstruct [:_id, :category, :name, :image_url, :description, :type]

end

In the character controller, I create two actions : edit and update. The first one gets a character by its id from couchdb, creates a changeset and renders the view, like this :

 def edit(conn, %{"id" => id}) do

        character = Dbuniverse.Repo.get_by_id id
        changeset = Dbuniverse.EctoCharacter.changeset(%Dbuniverse.EctoCharacter{}, %{_id: id, name: character["name"], description: character["description"], image_url: character["image_url"], category: character["category"]})
        # IO.inspect changeset
        render conn, "edit.html", changeset: changeset

    end

In the update method of the controller, I pass the changeset and the character’s id as arguments of my repo.update function :

def update(conn, %{"ecto_character" => character, "id" => id}) do
        
        # json = Poison.encode! character

        IO.inspect character
        # IO.inspect json

        Dbuniverse.Repo.update character, id
        redirect conn, to: character_path(conn, :show, id)

    end

In the edit template, I can now use form_for with the ecto changeset. Is this the right way of doing or am I completely wrong?

Thanks for your help,
Didier

1 Like

I finally came up with a solution without the need of two models (for ecto and my app). I have only one model that I implemented like this :

 defmodule Dbuniverse.Character do
    
    use Ecto.Schema
    import Ecto.Changeset

    @required_fields [:name, :description]
    @optional_fields [:category, :image_url, :type]

    schema "character" do
        
        field :category,    :string
        field :name,        :string
        field :image_url,   :string
        field :description, :string
        field :type,        :string

        timestamps()

    end

    def create_new_character(character, params \\ %{}) do
        character
        |> cast(params, @required_fields ++ @optional_fields)
        |> validate_required(@required_fields)
    end

end

I then created, in both edit and create actions of my controller, a changeset and passed it to my edit page :

def create(conn, _params) do

        changeset = Character.create_new_character(%Character{}, %{:name => "", :description => ""})
        render conn, "edit.html", [changeset: changeset, options: %{}]

    end

 def edit(conn, %{"id" => id}) do

        character = Dbuniverse.Repo.get_by_id id
        changeset = Character.create_new_character(
                %Character{}, 
                %{
                    :name => character["name"], 
                    :description => character["description"], 
                    :category => character["category"],
                    :image_url => character["image_url"]
                }
            )
        
        # IO.inspect changeset
        render conn, "edit.html", [changeset: changeset, options: %{id: id, rev: character["_rev"]}]

    end

For both add and update actions, I get the changeset back from the form and then pass it to my repo methods :

def add(conn, %{"character" => character}) do

        character = Map.put(character, :type, "character")
        character = Poison.encode! character
        json = Dbuniverse.Repo.insert character
        redirect conn, to: character_path(conn, :show, json["id"])

    end

def update(conn, %{"character" => character, "id" => id, "rev" => rev}) do
        
        IO.inspect character

        character = Map.put(character, :_rev, rev)
        character = Map.put(character, :type, "character")
        
        IO.inspect character

        Dbuniverse.Repo.update(Poison.encode!(character), id)
        redirect conn, to: character_path(conn, :show, id)

    end

My form finally looks like this :

<h1>Edit character</h1>
<%= form_for @changeset, create_or_update_action(@conn, @options), fn f -> %>
    <div class="form-group">
        <%= label f, :name, "Nom" %>
        <%= text_input f, :name, class: "form-control" %>
    </div>
    <div class="form-group">
        <%= label f, :description, "Description" %>
        <%= textarea f, :description, class: "form-control", rows: 5 %>
    </div>
    <div class="form-group">
        <%= label f, :image_url, "Url" %>
        <%= text_input f, :image_url, class: "form-control" %>
    </div>
    <div class="form-group">
        <%= label f, :category, "category" %>
        <%= text_input f, :category, class: "form-control" %>
    </div>

    <%= submit "Add", class: "btn btn-primary pull-right" %>
<% end %>

The create_or_update_action comes from my character’s view and determines to which action should the form post the data :

defmodule DbuniverseWeb.CharacterView do

    use DbuniverseWeb.Web, :view
    
    def create_or_update_action(conn, %{"id": id, "rev": rev}) do

        character_path(conn, :update, id, rev)
    
    end
    
    def create_or_update_action(conn, %{}) do

        character_path(conn, :create)

    end

end
2 Likes

Hey,

in my setup, I don’t use a database but I do use embedded_schema for my data structures. At this point, I used the generated code from mix phx.gen.html and try to make it work with my own data store.

Everything fine so far until it comes to creating and updating a resource.
I didn’t touch any code for editing a resource yet. This is all how it was generated:

context module:

def change_video(%Video{} = video) do
    Video.changeset(video, %{})
  end

resource module:

  @primary_key false
  embedded_schema do
    field :id, :string
    ...
  end

  @doc false
  def changeset(video, attrs) do
    video
    |> cast(attrs, [:id, ...])
    |> validate_required([:id, ...])
  end

controller:

  def edit(conn, %{"id" => id}) do
    video = Media.get_video!(id)
    changeset = Media.change_video(video)
    render(conn, "edit.html", video: video, changeset: changeset)
  end

And when I visit the edit page for a resource I get the following error:

Request: GET /videos/8f2d7f88-d6a6-31be-bcad-438316614d86/edit
** (exit) an exception was raised:
    ** (Protocol.UndefinedError) protocol Phoenix.HTML.FormData not implemented for #Ecto.Changeset<action: nil, changes: %{}, errors: [], data: #Videothek.Media.Video<>, valid?: true>. This protocol is implemented for: Plug.Conn
        (phoenix_html) deps/phoenix_html/lib/phoenix_html/form_data.ex:1: Phoenix.HTML.FormData.impl_for!/1
        (phoenix_html) deps/phoenix_html/lib/phoenix_html/form_data.ex:15: Phoenix.HTML.FormData.to_form/2
        (phoenix_html) lib/phoenix_html/form.ex:325: Phoenix.HTML.Form.form_for/3
        (phoenix_html) lib/phoenix_html/form.ex:376: Phoenix.HTML.Form.form_for/4
        (videothek) lib/videothek_web/templates/video/form.html.eex:1: VideothekWeb.VideoView."form.html"/1
        (videothek) lib/videothek_web/templates/video/edit.html.eex:3: VideothekWeb.VideoView."edit.html"/1
        (videothek) lib/videothek_web/templates/layout/app.html.eex:26: VideothekWeb.LayoutView."app.html"/1
        (phoenix) lib/phoenix/view.ex:399: Phoenix.View.render_to_iodata/3
        (phoenix) lib/phoenix/controller.ex:729: Phoenix.Controller.__put_render__/5
        (phoenix) lib/phoenix/controller.ex:746: Phoenix.Controller.instrument_render_and_send/4
        (videothek) lib/videothek_web/controllers/video_controller.ex:1: VideothekWeb.VideoController.action/2
        (videothek) lib/videothek_web/controllers/video_controller.ex:1: VideothekWeb.VideoController.phoenix_controller_pipeline/2
        (videothek) lib/videothek_web/endpoint.ex:1: VideothekWeb.Endpoint.instrument/4
        (phoenix) lib/phoenix/router.ex:275: Phoenix.Router.__call__/1
        (videothek) lib/videothek_web/endpoint.ex:1: VideothekWeb.Endpoint.plug_builder_call/2
        (videothek) lib/plug/debugger.ex:122: VideothekWeb.Endpoint."call (overridable 3)"/2
        (videothek) lib/videothek_web/endpoint.ex:1: VideothekWeb.Endpoint.call/2
        (phoenix) lib/phoenix/endpoint/cowboy2_handler.ex:34: Phoenix.Endpoint.Cowboy2Handler.init/2
        (cowboy) /mnt/c/Users/Phillipp/Code/videothek/videothek/deps/cowboy/src/cowboy_handler.erl:41: :cowboy_handler.execute/2
        (cowboy) /mnt/c/Users/Phillipp/Code/videothek/videothek/deps/cowboy/src/cowboy_stream_h.erl:296: :cowboy_stream_h.execute/3

I am using Phoenix 1.4 with the --no-ecto flag but with ecto installed for the embedded schemas and changesets.

Any ideas? I am really just using the generated code but with my own data store. Maybe embedded_schema is missing something that schema has?

I believe the issue is you are not using :phoenix_ecto dependency, can you verify?

3 Likes

You are right, I totally missed it. This is my first time working with Phoenix 1.4 and even though I read about the changes for the ecto integration, I completely forgot that I need {:phoenix_ecto, "~> 4.0"}.

It’s working now :slight_smile:

2 Likes

this solution saved my day…

1 Like

Thanks for sharing this; I’ve been following along and have been having some degree of success. I created a schema which I turn into a changeset using cast()

defmodule Instructor.Task do

 use Ecto.Schema
 import Ecto.Changeset
[...]

schema "tasks" do
    field :answer, :string
    field :choices, {:array, :string}
    field :course, :string
    field :false_text, :string, default: "False"
    field :language, :string, default: "Javascript"
    field :name, :string
    field :question, :string
    field :schema, :map
    field :points, :integer, default: 0
    field :true_text, :string, default: "True"
  end

def changeset(task, attrs) do
    task
    |> cast(attrs, [
      :answer,
      :choices,
      :course,
      :false_text,
      :language,
      :name,
      :question,
      :schema,
      :points,
      :true_text
    ])
  end

I call Task.changeset() in a controller like so:

changeset = Task.changeset(
      %Task{},
      %{
        :answer => task["answer"],
        :choices => task["choices"],
        :course => task["course"],
        :false_text => task["false_text"] || "False",
        :language => task["language"] || "javascript",
        :name => task["name"],
        :question => task["question"],
        :schema => task["schema"],
        :points => task["points"] || :true_text => task["true_text"] || "True"
      }
    )

Which I guess isn’t kosher, because Phoenix.Map.to_param expects changeset.params to be a struct where it’s a map. Using String.atom on the keys of task doesn’t seem to work. I’m very new to Elixir and Phoenix so I’m kind of stumped…

E: I’ve been able to get past this; evidently it wasn’t the changeset at issue, but the task I was retrieving from Couchbase, which was being returned as a map. I subsequently ran up against an issue where Phoenix didn’t know how to render a form based on a changeset, which I resolved using mix compile.protocols --force A credit to everyone involved in building Phoenix and Elixir; debugging is simple and error messages are far less cryptic than they are in many other ecosystems.