{"action":"created","comment":{"url":"https://api.github.com/repos/DockYard/reefpoints/pulls/comments/74594005","id":74594005,"diff_hunk":"@@ -0,0 +1,218 @@\n+---\n+layout: post\n+title: \"Using Ecto Changesets for HTML input validations\"\n+social: true\n+author: Nico Mihalich\n+github: \"nicomihalich\"\n+summary: \"Leverage the power of Phoenix to utilize your Ecto changeset validations as input validations in your HTML forms\"\n+published: true\n+tags: elixir, phoenix, HTML, engineering\n+---\n+\n+All web applications with user submitted input have some constraints on what input is acceptable.  We as developers have two methods to make sure what the user entered falls within those constraints.\n+\n+*Client Side Validation* where your application checks form data prior to a network call and prevents the call from happening if it finds the data invalid.\n+\n+*Server Side Validation* where your application sends data to the server and waits for it to tell you if the data is valid or not.\n+\n+Both are means to the same end but have their advantages and disadvantages.\n+\n+## Server Side Validation (Necessary)\n+\n+### Pros\n+- Source of truth / \"Last line of defense\"\n+- Can be tied to DB logic\n+- Knows context of the user, session, or other data\n+- More powerful and secure\n+\n+### Cons\n+- Slow to get feedback due to network latency\n+- Sending the entire form just to get one error\n+\n+## Client Side Validations (Optional)\n+\n+### Pros\n+- Immediate validation\n+- Preventative\n+- Semantically accurate\n+- Nicer feeling feedback due to styling with CSS selectors\n+\n+### Cons\n+- Have to keep it in sync with server side\n+- Brittle\n+- Not a substitute for server side validation\n+\n+Generally client side validations are optional, faster, and provide better UX, while server side validations are necessary, stronger, and better tied to your data model.\n+\n+Ideally you utilize both, but they're a pain to keep in sync.  In a perfect world your application's back end validations automatically apply to the client. We're going to explore how Phoenix and Ecto give us the power to help us do exactly that.\n+\n+We can leverage [Phoenix](http://www.phoenixframework.org/), [Ecto.Changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html) on our front end with just a few lines of code. This doesn't work for everything (uniqueness constraints for example), but there are some nice things we can validate for: min/max, length, and required fields.\n+Ecto changesets within Phoenix support [validate_length](https://github.com/phoenixframework/phoenix_ecto/blob/master/lib/phoenix_ecto/html.ex#L143), [validate_number](https://github.com/phoenixframework/phoenix_ecto/blob/master/lib/phoenix_ecto/html.ex#L161), [validate_required](https://github.com/phoenixframework/phoenix_ecto/blob/master/lib/phoenix_ecto/html.ex#L114) which correspond to the [HTML input validations](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Forms/Data_form_validation) `minlength`/`maxlength`, `min`/`max`, and `required`.\n+\n+Our goal is to have the validations defined in a model's changeset function automatically apply the correct HTML input validation to our form.\n+\n+Let's write some code.\n+\n+---\n+\n+## The Code\n+\n+Let's work with a model named `foo` with the following changeset function:\n+\n+```elixir\n+def changeset(struct, params \\\\ %{}) do\n+  struct\n+  |> cast(params, [:name])\n+  |> validate_required([:name])\n+  |> validate_length(:name, min: 2, max: 4)\n+end\n+```\n+\n+By default our form should have something like this:\n+\n+```\n+<%= text_input f, :name, class: \"form-control\" %>\n+```\n+\n+Which generates this markup:\n+\n+```html\n+<input class=\"form-control\" id=\"foo_name\" name=\"foo[name]\" type=\"text\">\n+```\n+\n+It works, but we have to wait for a server round trip to get any validations. We can add client side validation by appending opts by hand like this:\n+\n+```\n+<%= text_input f, :name, class: \"form-control\", required: true, minlength: 2, maxlength: 4 %>\n+```\n+\n+Which generates this markup:\n+\n+```html\n+<input class=\"form-control\" id=\"foo_name\" maxlength=\"4\" minlength=\"2\" name=\"foo[name]\" required=\"required\" type=\"text\">\n+```\n+\n+This is better, but if we ever changed the max to something else we would have to remember to change it in two different places!\n+\n+We can do better by using [input_validations](https://github.com/phoenixframework/phoenix_ecto/blob/master/lib/phoenix_ecto/html.ex#L113). This function generates the HTML validation attributes from our Ecto changeset for us.\n+\n+Now we can define our own functions which simply add on those generated input validations to our text and number inputs...\n+\n+```elixir\n+alias Phoenix.HTML.Form\n+\n+def text_input(form, field, opts \\\\ []) do\n+  Form.text_input(form, field, opts ++ Form.input_validations(form, field))\n+end\n+\n+def number_input(form, field, opts \\\\ []) do\n+  Form.number_input(form, field, opts ++ Form.input_validations(form, field))\n+end\n+```\n+\n+Keep the same markup we had initially...\n+\n+```\n+<%= text_input f, :name, class: \"form-control\" %>\n+```\n+... and get the semantically correct markup with no changes to the template!\n+\n+```html\n+<input class=\"form-control\" id=\"foo_name\" maxlength=\"4\" minlength=\"2\" name=\"foo[name]\" required=\"required\" type=\"text\">\n+```\n+\n+## Other validations\n+\n+This will also work for number validations.  Say our changeset function had a line like\n+\n+```\n+|> validate_number(:count, greater_than: 2, less_than: 9)\n+```\n+\n+That would give us this markup\n+\n+```\n+<input class=\"form-control\" id=\"foo_count\" max=\"8\" min=\"3\" name=\"foo[count]\" required=\"required\" step=\"1\" type=\"number\">\n+```\n+\n+---\n+\n+## Using this in your application\n+\n+To leverage this in your own Phoenix application, we'll use a module that we will automatically import in all our views.\n+\n+First define the module with our custom `text_input` and `number_input` functions in `web/views/valid_inputs.ex`\n+\n+```elixir\n+defmodule HelloPhoenix.ValidInputs do\n+  alias Phoenix.HTML.Form\n+\n+  def text_input(form, field, opts \\\\ []) do\n+    Form.text_input(form, field, opts ++ Form.input_validations(form, field))\n+  end\n+\n+  def number_input(form, field, opts \\\\ []) do\n+    Form.number_input(form, field, opts ++ Form.input_validations(form, field))\n+  end\n+end\n+```\n+\n+Then in `web/web.ex` just have Phoenix make it available for all our views.\n+\n+```\n+def view do\n+  quote do\n+    use Phoenix.View, root: \"web/templates\"\n+\n+    # Import convenience functions from controllers\n+    import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]\n+\n+    # Use all HTML functionality (forms, tags, etc)\n+    use Phoenix.HTML\n+\n+    # vvvv BEGIN OUR CODE vvvv\n+    import Phoenix.HTML.Form, except: [number_input: 3, text_input: 3]","path":"source/posts/2016-08-11-changeset-form-validation-bindings.md","position":174,"original_position":174,"commit_id":"ab80a73e2505eaf824d72b18e57079e9e0ab3f67","original_commit_id":"ab80a73e2505eaf824d72b18e57079e9e0ab3f67","user":{"login":"chrismccord","id":576796,"avatar_url":"https://avatars.githubusercontent.com/u/576796?v=3","gravatar_id":"","url":"https://api.github.com/users/chrismccord","html_url":"https://github.com/chrismccord","followers_url":"https://api.github.com/users/chrismccord/followers","following_url":"https://api.github.com/users/chrismccord/following{/other_user}","gists_url":"https://api.github.com/users/chrismccord/gists{/gist_id}","starred_url":"https://api.github.com/users/chrismccord/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/chrismccord/subscriptions","organizations_url":"https://api.github.com/users/chrismccord/orgs","repos_url":"https://api.github.com/users/chrismccord/repos","events_url":"https://api.github.com/users/chrismccord/events{/privacy}","received_events_url":"https://api.github.com/users/chrismccord/received_events","type":"User","site_admin":false},"body":"You need to exclude the 2-artiy versions as well","created_at":"2016-08-12T13:54:09Z","updated_at":"2016-08-12T13:54:09Z","html_url":"https://github.com/DockYard/reefpoints/pull/543#discussion_r74594005","pull_request_url":"https://api.github.com/repos/DockYard/reefpoints/pulls/543","_links":{"self":{"href":"https://api.github.com/repos/DockYard/reefpoints/pulls/comments/74594005"},"html":{"href":"https://github.com/DockYard/reefpoints/pull/543#discussion_r74594005"},"pull_request":{"href":"https://api.github.com/repos/DockYard/reefpoints/pulls/543"}}},"pull_request":{"url":"https://api.github.com/repos/DockYard/reefpoints/pulls/543","id":81028463,"html_url":"https://github.com/DockYard/reefpoints/pull/543","diff_url":"https://github.com/DockYard/reefpoints/pull/543.diff","patch_url":"https://github.com/DockYard/reefpoints/pull/543.patch","issue_url":"https://api.github.com/repos/DockYard/reefpoints/issues/543","number":543,"state":"open","locked":false,"title":"Post: Ecto Changeset/HTML validations in Phoenix","user":{"login":"nicomihalich","id":748245,"avatar_url":"https://avatars.githubusercontent.com/u/748245?v=3","gravatar_id":"","url":"https://api.github.com/users/nicomihalich","html_url":"https://github.com/nicomihalich","followers_url":"https://api.github.com/users/nicomihalich/followers","following_url":"https://api.github.com/users/nicomihalich/following{/other_user}","gists_url":"https://api.github.com/users/nicomihalich/gists{/gist_id}","starred_url":"https://api.github.com/users/nicomihalich/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nicomihalich/subscriptions","organizations_url":"https://api.github.com/users/nicomihalich/orgs","repos_url":"https://api.github.com/users/nicomihalich/repos","events_url":"https://api.github.com/users/nicomihalich/events{/privacy}","received_events_url":"https://api.github.com/users/nicomihalich/received_events","type":"User","site_admin":false},"body":"WIP\r\n\r\n[Rendered](https://github.com/DockYard/reefpoints/blob/7370be4f025e9e77cf8269f60035ef6ee4c3b1a6/source/posts/2016-08-11-changeset-form-validation-bindings.md)","created_at":"2016-08-11T20:50:08Z","updated_at":"2016-08-12T13:54:09Z","closed_at":null,"merged_at":null,"merge_commit_sha":"24de7a8d81176171135e16875b4837650b82b0e1","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/DockYard/reefpoints/pulls/543/commits","review_comments_url":"https://api.github.com/repos/DockYard/reefpoints/pulls/543/comments","review_comment_url":"https://api.github.com/repos/DockYard/reefpoints/pulls/comments{/number}","comments_url":"https://api.github.com/repos/DockYard/reefpoints/issues/543/comments","statuses_url":"https://api.github.com/repos/DockYard/reefpoints/statuses/ab80a73e2505eaf824d72b18e57079e9e0ab3f67","head":{"label":"DockYard:changeset-html-validations","ref":"changeset-html-validations","sha":"ab80a73e2505eaf824d72b18e57079e9e0ab3f67","user":{"login":"DockYard","id":873981,"avatar_url":"https://avatars.githubusercontent.com/u/873981?v=3","gravatar_id":"","url":"https://api.github.com/users/DockYard","html_url":"https://github.com/DockYard","followers_url":"https://api.github.com/users/DockYard/followers","following_url":"https://api.github.com/users/DockYard/following{/other_user}","gists_url":"https://api.github.com/users/DockYard/gists{/gist_id}","starred_url":"https://api.github.com/users/DockYard/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/DockYard/subscriptions","organizations_url":"https://api.github.com/users/DockYard/orgs","repos_url":"https://api.github.com/users/DockYard/repos","events_url":"https://api.github.com/users/DockYard/events{/privacy}","received_events_url":"https://api.github.com/users/DockYard/received_events","type":"Organization","site_admin":false},"repo":{"id":2262874,"name":"reefpoints","full_name":"DockYard/reefpoints","owner":{"login":"DockYard","id":873981,"avatar_url":"https://avatars.githubusercontent.com/u/873981?v=3","gravatar_id":"","url":"https://api.github.com/users/DockYard","html_url":"https://github.com/DockYard","followers_url":"https://api.github.com/users/DockYard/followers","following_url":"https://api.github.com/users/DockYard/following{/other_user}","gists_url":"https://api.github.com/users/DockYard/gists{/gist_id}","starred_url":"https://api.github.com/users/DockYard/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/DockYard/subscriptions","organizations_url":"https://api.github.com/users/DockYard/orgs","repos_url":"https://api.github.com/users/DockYard/repos","events_url":"https://api.github.com/users/DockYard/events{/privacy}","received_events_url":"https://api.github.com/users/DockYard/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/DockYard/reefpoints","description":"DockYard Blog","fork":false,"url":"https://api.github.com/repos/DockYard/reefpoints","forks_url":"https://api.github.com/repos/DockYard/reefpoints/forks","keys_url":"https://api.github.com/repos/DockYard/reefpoints/keys{/key_id}","collaborators_url":"https://api.github.com/repos/DockYard/reefpoints/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/DockYard/reefpoints/teams","hooks_url":"https://api.github.com/repos/DockYard/reefpoints/hooks","issue_events_url":"https://api.github.com/repos/DockYard/reefpoints/issues/events{/number}","events_url":"https://api.github.com/repos/DockYard/reefpoints/events","assignees_url":"https://api.github.com/repos/DockYard/reefpoints/assignees{/user}","branches_url":"https://api.github.com/repos/DockYard/reefpoints/branches{/branch}","tags_url":"https://api.github.com/repos/DockYard/reefpoints/tags","blobs_url":"https://api.github.com/repos/DockYard/reefpoints/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/DockYard/reefpoints/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/DockYard/reefpoints/git/refs{/sha}","trees_url":"https://api.github.com/repos/DockYard/reefpoints/git/trees{/sha}","statuses_url":"https://api.github.com/repos/DockYard/reefpoints/statuses/{sha}","languages_url":"https://api.github.com/repos/DockYard/reefpoints/languages","stargazers_url":"https://api.github.com/repos/DockYard/reefpoints/stargazers","contributors_url":"https://api.github.com/repos/DockYard/reefpoints/contributors","subscribers_url":"https://api.github.com/repos/DockYard/reefpoints/subscribers","subscription_url":"https://api.github.com/repos/DockYard/reefpoints/subscription","commits_url":"https://api.github.com/repos/DockYard/reefpoints/commits{/sha}","git_commits_url":"https://api.github.com/repos/DockYard/reefpoints/git/commits{/sha}","comments_url":"https://api.github.com/repos/DockYard/reefpoints/comments{/number}","issue_comment_url":"https://api.github.com/repos/DockYard/reefpoints/issues/comments{/number}","contents_url":"https://api.github.com/repos/DockYard/reefpoints/contents/{+path}","compare_url":"https://api.github.com/repos/DockYard/reefpoints/compare/{base}...{head}","merges_url":"https://api.github.com/repos/DockYard/reefpoints/merges","archive_url":"https://api.github.com/repos/DockYard/reefpoints/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/DockYard/reefpoints/downloads","issues_url":"https://api.github.com/repos/DockYard/reefpoints/issues{/number}","pulls_url":"https://api.github.com/repos/DockYard/reefpoints/pulls{/number}","milestones_url":"https://api.github.com/repos/DockYard/reefpoints/milestones{/number}","notifications_url":"https://api.github.com/repos/DockYard/reefpoints/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/DockYard/reefpoints/labels{/name}","releases_url":"https://api.github.com/repos/DockYard/reefpoints/releases{/id}","deployments_url":"https://api.github.com/repos/DockYard/reefpoints/deployments","created_at":"2011-08-24T16:55:40Z","updated_at":"2016-08-03T01:00:29Z","pushed_at":"2016-08-11T20:50:08Z","git_url":"git://github.com/DockYard/reefpoints.git","ssh_url":"git@github.com:DockYard/reefpoints.git","clone_url":"https://github.com/DockYard/reefpoints.git","svn_url":"https://github.com/DockYard/reefpoints","homepage":"https://dockyard.com/blog","size":23747,"stargazers_count":39,"watchers_count":39,"language":"CSS","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":64,"mirror_url":null,"open_issues_count":12,"forks":64,"open_issues":12,"watchers":39,"default_branch":"master"}},"base":{"label":"DockYard:master","ref":"master","sha":"a9cbdba97f5b35d684ac80c3e4191c784b2fddc2","user":{"login":"DockYard","id":873981,"avatar_url":"https://avatars.githubusercontent.com/u/873981?v=3","gravatar_id":"","url":"https://api.github.com/users/DockYard","html_url":"https://github.com/DockYard","followers_url":"https://api.github.com/users/DockYard/followers","following_url":"https://api.github.com/users/DockYard/following{/other_user}","gists_url":"https://api.github.com/users/DockYard/gists{/gist_id}","starred_url":"https://api.github.com/users/DockYard/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/DockYard/subscriptions","organizations_url":"https://api.github.com/users/DockYard/orgs","repos_url":"https://api.github.com/users/DockYard/repos","events_url":"https://api.github.com/users/DockYard/events{/privacy}","received_events_url":"https://api.github.com/users/DockYard/received_events","type":"Organization","site_admin":false},"repo":{"id":2262874,"name":"reefpoints","full_name":"DockYard/reefpoints","owner":{"login":"DockYard","id":873981,"avatar_url":"https://avatars.githubusercontent.com/u/873981?v=3","gravatar_id":"","url":"https://api.github.com/users/DockYard","html_url":"https://github.com/DockYard","followers_url":"https://api.github.com/users/DockYard/followers","following_url":"https://api.github.com/users/DockYard/following{/other_user}","gists_url":"https://api.github.com/users/DockYard/gists{/gist_id}","starred_url":"https://api.github.com/users/DockYard/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/DockYard/subscriptions","organizations_url":"https://api.github.com/users/DockYard/orgs","repos_url":"https://api.github.com/users/DockYard/repos","events_url":"https://api.github.com/users/DockYard/events{/privacy}","received_events_url":"https://api.github.com/users/DockYard/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/DockYard/reefpoints","description":"DockYard Blog","fork":false,"url":"https://api.github.com/repos/DockYard/reefpoints","forks_url":"https://api.github.com/repos/DockYard/reefpoints/forks","keys_url":"https://api.github.com/repos/DockYard/reefpoints/keys{/key_id}","collaborators_url":"https://api.github.com/repos/DockYard/reefpoints/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/DockYard/reefpoints/teams","hooks_url":"https://api.github.com/repos/DockYard/reefpoints/hooks","issue_events_url":"https://api.github.com/repos/DockYard/reefpoints/issues/events{/number}","events_url":"https://api.github.com/repos/DockYard/reefpoints/events","assignees_url":"https://api.github.com/repos/DockYard/reefpoints/assignees{/user}","branches_url":"https://api.github.com/repos/DockYard/reefpoints/branches{/branch}","tags_url":"https://api.github.com/repos/DockYard/reefpoints/tags","blobs_url":"https://api.github.com/repos/DockYard/reefpoints/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/DockYard/reefpoints/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/DockYard/reefpoints/git/refs{/sha}","trees_url":"https://api.github.com/repos/DockYard/reefpoints/git/trees{/sha}","statuses_url":"https://api.github.com/repos/DockYard/reefpoints/statuses/{sha}","languages_url":"https://api.github.com/repos/DockYard/reefpoints/languages","stargazers_url":"https://api.github.com/repos/DockYard/reefpoints/stargazers","contributors_url":"https://api.github.com/repos/DockYard/reefpoints/contributors","subscribers_url":"https://api.github.com/repos/DockYard/reefpoints/subscribers","subscription_url":"https://api.github.com/repos/DockYard/reefpoints/subscription","commits_url":"https://api.github.com/repos/DockYard/reefpoints/commits{/sha}","git_commits_url":"https://api.github.com/repos/DockYard/reefpoints/git/commits{/sha}","comments_url":"https://api.github.com/repos/DockYard/reefpoints/comments{/number}","issue_comment_url":"https://api.github.com/repos/DockYard/reefpoints/issues/comments{/number}","contents_url":"https://api.github.com/repos/DockYard/reefpoints/contents/{+path}","compare_url":"https://api.github.com/repos/DockYard/reefpoints/compare/{base}...{head}","merges_url":"https://api.github.com/repos/DockYard/reefpoints/merges","archive_url":"https://api.github.com/repos/DockYard/reefpoints/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/DockYard/reefpoints/downloads","issues_url":"https://api.github.com/repos/DockYard/reefpoints/issues{/number}","pulls_url":"https://api.github.com/repos/DockYard/reefpoints/pulls{/number}","milestones_url":"https://api.github.com/repos/DockYard/reefpoints/milestones{/number}","notifications_url":"https://api.github.com/repos/DockYard/reefpoints/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/DockYard/reefpoints/labels{/name}","releases_url":"https://api.github.com/repos/DockYard/reefpoints/releases{/id}","deployments_url":"https://api.github.com/repos/DockYard/reefpoints/deployments","created_at":"2011-08-24T16:55:40Z","updated_at":"2016-08-03T01:00:29Z","pushed_at":"2016-08-11T20:50:08Z","git_url":"git://github.com/DockYard/reefpoints.git","ssh_url":"git@github.com:DockYard/reefpoints.git","clone_url":"https://github.com/DockYard/reefpoints.git","svn_url":"https://github.com/DockYard/reefpoints","homepage":"https://dockyard.com/blog","size":23747,"stargazers_count":39,"watchers_count":39,"language":"CSS","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":64,"mirror_url":null,"open_issues_count":12,"forks":64,"open_issues":12,"watchers":39,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/DockYard/reefpoints/pulls/543"},"html":{"href":"https://github.com/DockYard/reefpoints/pull/543"},"issue":{"href":"https://api.github.com/repos/DockYard/reefpoints/issues/543"},"comments":{"href":"https://api.github.com/repos/DockYard/reefpoints/issues/543/comments"},"review_comments":{"href":"https://api.github.com/repos/DockYard/reefpoints/pulls/543/comments"},"review_comment":{"href":"https://api.github.com/repos/DockYard/reefpoints/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/DockYard/reefpoints/pulls/543/commits"},"statuses":{"href":"https://api.github.com/repos/DockYard/reefpoints/statuses/ab80a73e2505eaf824d72b18e57079e9e0ab3f67"}}}}
{"actor":{"display_login":"chrismccord"}}