Git Product home page Git Product logo

formex_ecto's Introduction

Formex Ecto

Library that integrates Ecto with Formex.

It also has an Ecto.Changeset validator adapter for those who wants to use validation functions from Ecto.Changeset.

Instalation

def deps do
  [{:formex_ecto, "~> 0.2.0"}]
end

config/config.exs

config :formex,
  repo: App.Repo

web/web.ex

def model do
  quote do
    use Formex.Ecto.Schema
  end
end

def controller do
  quote do
    use Formex.Ecto.Controller
  end
end

In every form type that uses Ecto:

defmodule App.ArticleType do
  use Formex.Type
  use Formex.Ecto.Type # <- add this

Optional Ecto.Changeset validator

config/config.exs

config :formex,
  validator: Formex.Ecto.ChangesetValidator

More info about this validator

Usage

Model

We have models Article, Category and Tag:

schema "articles" do
  field :title, :string
  field :content, :string
  field :hidden, :boolean

  belongs_to :category, App.Category
  many_to_many :tags, App.Tag, join_through: "articles_tags" #...
end
schema "categories" do
  field :name, :string
end
schema "tags" do
  field :name, :string
end

Form Type

Let's create a form for Article using Formex. For validation we will use Ecto.Changeset validator

# /web/form/article_type.ex
defmodule App.ArticleType do
  use Formex.Type
  alias Formex.Ecto.CustomField.SelectAssoc

  def build_form(form) do
    form
    |> add(:title, :text_input, label: "Title", validation: [:required])
    |> add(:content, :textarea, label: "Content", phoenix_opts: [
      rows: 4
    ], validation: [:required])
    |> add(:category_id, SelectAssoc, label: "Category", phoenix_opts: [
      prompt: "Choose a category"
    ], validation: [:required])
    |> add(:tags, SelectAssoc, label: "Tags", validation: [:required])
    |> add(:hidden, :checkbox, label: "Is hidden?", required: false)
    |> add(:save, :submit, label: "Submit", phoenix_opts: [
      class: "btn-primary"
    ])
  end
end

Controller

def new(conn, _params) do
  form = create_form(App.ArticleType, %Article{})
  render(conn, "new.html", form: form)
end

def create(conn, %{"article" => article_params}) do
  App.ArticleType
  |> create_form(%Article{}, article_params)
  |> insert_form_data
  |> case do
    {:ok, _article} ->
      conn
      |> put_flash(:info, "Article created successfully.")
      |> redirect(to: article_path(conn, :index))
    {:error, form} ->
      render(conn, "new.html", form: form)
  end
end

def edit(conn, %{"id" => id}) do
  article = Repo.get!(Article, id)
  form = create_form(App.ArticleType, article)
  render(conn, "edit.html", article: article, form: form)
end

def update(conn, %{"id" => id, "article" => article_params}) do
  article = Repo.get!(Article, id)

  App.ArticleType
  |> create_form(article, article_params)
  |> update_form_data
  |> case do
    {:ok, article} ->
      conn
      |> put_flash(:info, "Article updated successfully.")
      |> redirect(to: article_path(conn, :show, article))
    {:error, form} ->
      render(conn, "edit.html", article: article, form: form)
  end
end

Template

form.html.eex

<%= formex_form_for @form, @action, fn f -> %>
  <%= if @form.submitted? do %>Oops, something went wrong!<% end %>

  <%= formex_row f, :name %>
  <%= formex_row f, :content %>
  <%= formex_row f, :category_id %>
  <%= formex_row f, :tags %>
  <%= formex_row f, :hidden %>
  <%= formex_row f, :save %>

  <%# or generate all fields at once: formex_rows f %>
<% end %>

Also replace changeset: @changeset with form: @form in new.html.eex and edit.html.eex

The final effect after submit:

Collections of forms

Every schema used in collections of forms should call formex_collection_child:

schema "user_addresses" do
  field       :street, :string
  field       :postal_code, :string
  field       :city, :string
  belongs_to  :user, App.User

  formex_collection_child() # <- add this
end

This macro adds :formex_id and :formex_delete virtual fields.

Automation

This library does few things automatically.

Nested forms and collections

def build_form(form) do
  form
  |> add(:user_info, App.UserInfoType, struct_module: App.UserInfo)
end

You don't need to pass :struct_module option, it is taken from schema information.

Method

<%= formex_form_for @form, article_path(@conn, :create), [method: :post], fn f -> %>

You don't need to pass :method option, it's set basing on struct.id value.

Changeset modification

There is a callback modify_changeset. Examples:

Add something to an user during registration

You can add additional changes while user creation, such as hash of a password.

def build_form(form) do
  form
  |> add(:email, :text_input)
  |> add(:password, :password_input)
  |> add(:save, :submit, label: "Register")
end

# Put additional changes that will be saved to database.
def modify_changeset(changeset, _form) do
  changeset
  |> User.put_pass_hash
end

Assign current logged user to a data which he creates

Controller

Get the current user and pass it to a form

user = Guardian.Plug.current_resource(conn) # or similar

ArticleType
|> create_form(%Article{}, article_params, author: user) # store current logged user in opts
|> insert_form_data
|> case do
  {:ok, _user_employee} ->
    #
  {:error, form} ->
    #
end

Form type

Assign user to a new article (and don't do it if it's an update action)

def build_form(form) do
  #
end

def modify_changeset(changeset, form) do
  # check if it's a create action
  if !form.struct.id do
    changeset
    |> Ecto.Changeset.put_assoc(:author, form.opts[:author]) # access author via form.opts[:author]
  else
    changeset
  end
end

Tests

Test database

Use config/test.secret.example.exs to create config/test.secret.exs

Run this command to migrate:

MIX_ENV=test mix ecto.migrate -r Formex.Ecto.TestRepo

Now you can use tests via mix test.

Creating a new migration

MIX_ENV=test mix ecto.gen.migration migration_name -r Formex.Ecto.TestRepo

Troubleshooting

Repo is nil

Do you have some weird "nil.insert/1 is undefined or private" error?

It happens when you forgot about the repo option in the configuration or you set it after package compilation. To recompile the whole package use: mix deps.compile formex_ecto --force

Docs

Custom fields

Guides

formex_ecto's People

Contributors

jakub-zawislak avatar stareintothebeard avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

formex_ecto's Issues

Collection child with file input

When I use file input fields in a collection, I get following error:

file_input/3 requires the enclosing form_for/4 to be configured with multipart: true

However, the enclosing form does have multipart true.

Doesn't set form method :put for edit controller action

The documentation sais that form method is set automatically but I have edit action in the controller:

  def edit(conn, %{"id" => id}) do
    resume = Hr.get_resume!(id)
    form = create_form(ResumeType, resume)
    IO.inspect form.struct.id
    IO.inspect form.method
    render(conn, "edit.html", resume: resume, form: form)
  end

And in the console it gives me form.struct.id=1 and form.method=nil and of course the form is sent by post method and is rejected by the router.

For now I add the method :put to the form manually:
form = %{form | method: :put}

It helps but why it's not set automatically?

Multiple selects are broken for array columns

When a schema has an array column, the multiple select does not need to be cast to associated schema. You should be able to save multiple values back to an array column. From what I can tell, the bug is in lib/changeset.ex #cast_multiple_selects.

Using formex_ecto with abstract tables

Hi,

I'm trying to use formex and formex_ecto with a polymorphic association as discribed in the ecto docs here.

When update_form_data is called after create_changeset, @repo.insert fails with

 Postgrex.Error at PUT /task/1
ERROR 42P01 (undefined_table) relation "abstract table: comments" does not exist

    query: INSERT INTO "abstract table: comments" (...)

Is there a way (either with modify_changeset or by explicitly setting :struct_module) to specify the correct table?

If not I'd be happy to work on PR to add the ability to check for abstract table which is stored in __meta__ (i.e. __meta__: #Ecto.Schema.Metadata<:built, "abstract table: comments">) if you could give me some guidance on the best place to change it.

Finally I'm not actually using has_many but has_one but I don't think that matters as formex_collection_child() wouldn't change this issue.

SelectAssoc options custom sort

Looks like the option labels are sorted alphabetically.

|> Enum.sort(fn {name1, _}, {name2, _} ->

How do I sort by a column?

** Context
I have a multi level/nested set of categories. These can be selected at any level using a SelectAssoc field.

Data

Deserts
-- Gluten Free
-- Vegetarian
Mains
-- Chicken

Form

|> add(
  :categories,
  SelectAssoc,
  label: "Categories",
  choice_label: fn category ->
    if category.parent_id do
      "-- #{category.name}"
    else
      category.name
    end
  end,
  query: fn query ->
    from c in query,
    order_by: [asc: :sort_value]
  end,
  validation: [:required]
)

However, it displays like this:

-- Gluten Free
-- Vegetarian
-- Chicken
Deserts
Mains

Raising on Repo.insert/update when changeset contains validation breaks constraints usage

    if errors != %{} do
      raise "Your changeset has errors. Since Formex 0.5, errors added in
      `Type.changeset_after_create_callback` are not being validated. You have to use
      new validator functionality. Errors found:\n
      "<>inspect(errors)
    end

This code breaks the usage of formex_ecto with Ecto.Changeset.check_constraint/2 since it only validates after a DB roundtrip.

Why did this error was introduced in formex_ecto? I understand that it’s better to use changeset_validation since it is not tied to the DB. But constraint (like uniqueness) validation is a nice feature of Ecto that provides integrity of data at the database level.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.