The logo taken from: https://www.udemy.com/elixir-for-beginners/

Well, let’s begin with explaining the title:

Form Object is a common pattern in web development which wraps user input into a convenient structure to be processed in the application.

It’s basically an aggregation of an incoming payload, for the program’s convenience, with some basic casting and validation.

Why did I put objects in quotes then? Because in Elixir we actually don’t have objects themselves, we use modules and structs. And that’s why we will leverage them to mimic the Form Object pattern.

Prerequisites

To start building form objects, you may want to get familiar with Ecto library. It’s required to use schemas with casting, and to not reinvent the wheel again. Make sure you have it in your dependencies:

defp deps do

[

{:ecto, "~> 2.1"},

# ...

]

end

Embedded schemas

Ecto introduces a concept called Embedded Schemas which is nothing more than embedding Elixir structs in Ecto schemas. Ecto 2.1 delivers even more convenient solution, from Form Objects perspective, which is Inline Embeds. It applies for both embeds_one and embeds_many .

Payload

I’ll explain the idea briefly here. Imagine you have the following payload, a set of params incoming to your API:

{

"object": "page",

"entry": [

{

"time": 1481873073,

"id": "134217098310966",

"changes": [

{

"value": {

"verb": "add",

"sender_name": "Kamil Lelonek",

"sender_id": 1704703389844168,

"post_id": "124717008330964",

"parent_id": "154517128434964",

"message": "This a comment.",

"item": "comment",

"created_time": 1481873072,

"comment_id": "156424298170245"

},

"field": "feed"

}

]

}

]

}

Note we have two top level attributes:

object — string

— string entry — array of objects

As we can see, entry object also contains some nested values:

time — integer

— integer id — string

— string changes — array of objects

Moreover, a single change is also quite complex:

value — object

— object field — string

Schema

OK, enough nesting. Once we know the general structure, we can model our schema. Let’s start from defining something like that:

defmodule MyApp.Form do

use Ecto.Schema # we need this to use e.g. `embedded_schema` @primary_key false # we don't use autogenerated primary key `id` embedded_schema do # we don't specify any relation (table name)

field :object, :string # we have many entry structures without autogenerated ID

embeds_many :entry, Entry, primary_key: false do

field :id, :string

field :time, :decimal # we have many changes without autogenerated ID

embeds_many :changes, Change, primary_key: false do

field :field, :string

field :value, :map

end

end

end

end

I believe that everything is very self-explanatory, especially after what we’ve just discussed above. The form is defined, now it’s time to define logic for building the form from incoming params.

Changeset

What is it exactly? Well, the documentation explains it pretty clearly:

Changesets allow filtering, casting, validation and definition of constraints when manipulating structs.

As you can read, filtering and casting is something what we’ve already mentioned and it’s exactly what we need here.

When we continue reading the documentation:

The function cast/3 is the usual entry points for creating changesets. It is used to cast and validate external parameters, such as parameters sent through a form, API, command line, etc.

Once again — it’s all about creating final schemas based on incoming parameters. This perfectly fits our needs.

We need to extend our Form module. Let’s firstly create new/1 function as an entry point accepting user input, and then a bunch of changeset functions to build every embedded Schema we defined previously.

def new(params), # we will be able to call `Form.new(params)` then

do: changeset(params) defp changeset(params) do

# this expands to `Form` module (we are inside it)

%__MODULE__{}

# this is the only "standalone" attribute

|> cast(params, ~w(object)a)

# we need to map `entry` with its own changeset function

|> cast_embed(:entry, with: &entry_changeset/2)

# once we're done, we finish building our changeset

|> apply_changes()

end defp entry_changeset(schema, params) do

schema

# we filter `id` and `time` attributes

# we don't care about the rest (if there are any)

|> cast(params, ~w(id time)a)

# we build "changes" array with it's own attributes

|> cast_embed(:changes, with: &changes_changeset/2)

end



defp changes_changeset(schema, params),

# here we take only `field` and `value` attribute

# we don't dive into `value` details

# and treat it is as a regular map

do: cast(schema, params, ~w(field value)a)

Struct

It seems we are done, right? We can call Form.new(%{}) with any given params that come from a client.

We could finish here, however I suggest to do one more thing as a good practice. After calling new/1 we would have something like:

%Form{

object: "page",

entry: [

%Form.Entry{

id: "134217098310966",

time: 1481873073,

changes: [

%Form.Entry.Change{

field: "feed",

value: %{

"comment_id" => "156424298170245_158606271285960",

"created_time" => 1481873072,

"item" => "comment",

"message" => "This a comment.",

"parent_id" => "154517128434964_157424168093244",

"post_id" => "124717008330964_157454198070243",

"sender_id" => 1704703389844168,

"sender_name" => "Kamil Lelonek",

"verb" => "add"

}

}

]

}

]

}

The thing is that we don’t want to have the knowledge about particular schemas leaking to our services. We would prefer to have a regular map here, instead of nested schemas.

Map

To do that, we need to convert our changeset composition into a regular map. We could leverage Map.from_struct/1 for that but we want to be explicit about that and sure what attributes we want to take out. Let’s do that in the following way:

def to_map(form) do

%{

object: form.object,

entries: Enum.map(form.entry, &new_entry/1),

}

end def new_entry(%{id: id, time: time, changes: changes}) do

%{

entry_id: id,

time: time,

changes: Enum.map(changes, &new_change/1),

}

end def new_change(%{value: value} = change) do

%{

field: change.field,

created_time: value["created_time"],

sender_id: value["sender_id"],

sender_name: value["sender_name"],

post_id: value["post_id"],

item: value["item"],

verb: value["verb"]

}

end

and then, extend our new/1 function like:

def new(params) do

params

|> changeset()

|> to_map()

end

Thanks to that, the final result of creating a new Form is:

%{

object: "page",

entry: [

%{

id: "134217098310966",

time: 1481873073,

changes: [

%{

field: "feed",

value: %{

comment_id: "156424298170245_158606271285960",

created_time: 1481873072,

item: "comment",

message: "This a comment.",

parent_id: "154517128434964_157424168093244",

post_id: "124717008330964_157454198070243",

sender_id: 1704703389844168,

sender_name: "Kamil Lelonek",

verb: "add"

}

}

]

}

]

}

Which is exactly what we need to use in our application later on!

Subscribe to get the latest content immediately

https://tinyletter.com/KamilLelonek

Summary

As you can see, with a few simple steps, we covered the topic of Form Objects in Elixir. You can leverage that pattern to create convenient structures to operate on in your applications.

I believe it’s a very useful approach to help you passing a handy structure across your services. Remember you can always use poison library and decode your form and probably a couple more solutions for that.

I encourage you to try especially this pattern in your code, specifically Phoenix apps, and let me know if you find it useful.