{"id":24698986,"url":"https://github.com/hacklanta/lift-formality","last_synced_at":"2025-10-09T07:32:28.262Z","repository":{"id":9515152,"uuid":"11411881","full_name":"hacklanta/lift-formality","owner":"hacklanta","description":"Formality is a new way of doing form creation in Lift—type safe, CSS selector based, with minimal repetition of fields and their names and values, it also supports event callbacks and validations.","archived":false,"fork":false,"pushed_at":"2021-11-17T14:46:31.000Z","size":340,"stargazers_count":8,"open_issues_count":8,"forks_count":6,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-03-27T08:21:52.637Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hacklanta.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2013-07-15T00:53:02.000Z","updated_at":"2024-03-27T08:21:52.637Z","dependencies_parsed_at":"2022-08-26T19:01:58.803Z","dependency_job_id":null,"html_url":"https://github.com/hacklanta/lift-formality","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hacklanta%2Flift-formality","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hacklanta%2Flift-formality/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hacklanta%2Flift-formality/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hacklanta%2Flift-formality/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hacklanta","download_url":"https://codeload.github.com/hacklanta/lift-formality/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":235801795,"owners_count":19047126,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-01-27T04:35:02.193Z","updated_at":"2025-10-09T07:32:22.797Z","avatar_url":"https://github.com/hacklanta.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Formality\n\nFormality is a library that provides better, cleaner, more palatable\nform handling for Lift. Form handling without the pain, if you will.\n\nThe goal of creating this library was to eliminate as much repetition as\npossible while wielding the full power of Lift's stateful form mechanism\nand its CSS selector transforms, as well as embracing Lift's own\nerror-reporting mechanisms.\n\n## Status\n\nFormality is still in an early state. While there are tests ensuring\neverything works, use in production is likely to uncover some\nundiscovered bugs. Those bugs will be crushed as quickly as possible\nonce reported on Github.\n\n## Including\n\nYou can include Formality directly as a dependency in your `build.sbt`,\nsuffixing the major and minor Lift version you are using (3.0 thru 3.3 are\ncurrently supported).\n\nFor example to include the build for 3.3, you would write:\n\n```\nlibraryDependencies ++= Seq(\n  \"com.hacklanta\" %% \"lift-formality_3.3\" % \"1.2.0\"\n)\n```\n\nAnd for 3.2 you would write:\n\n```\nlibraryDependencies ++= Seq(\n  \"com.hacklanta\" %% \"lift-formality_3.2\" % \"1.2.0\"\n)\n```\n\nAnd soforth.\n\n## Usage\n\nFormality provides a DSL for binding forms already defined in the HTML.\nIt leaves as much of the markup as possible to the markup itself, and\nlayers on only the necessary functionality from Lift's side. Amongst\nother things, this means that the `type` of a field in the HTML is left\nto the markup.\n\nA basic form snippet would look like this:\n\n```scala\n  import com.hacklanta.formality.Formality._\n\n  val registrationForm =\n    form withFields(\n      field[String](\".name\"),\n      field[String](\".phone-number\"),\n      field[Int](\".age\"),\n      checkboxField(\"#terms-and-conditions\")\n    ) onSuccess { (name, phoneNumber, age, termsAndConditions) =\u003e\n      // Assuming a case class User(name: String, age: Int, phoneNumber: String, termsAndConditions: Boolean).\n      User(name, age, phoneNumber, termsAndConditions).save\n    }\n\n  \"form\" #\u003e registrationForm.binder\n```\n\nThe corresponding HTML5 markup could be:\n\n```html\n\u003cform data-lift=\"Registration.form\"\u003e\n  \u003cinput class=\"name\"\u003e\n  \u003cinput class=\"phone-number\" placeholder=\"(XXX) XXX-XXXX\"\u003e\n  \u003cinput class=\"age\" type=\"number\"\u003e\n\n  \u003clabel for=\"terms-and-conditions\u003e\n    \u003cinput type=\"checkbox\" id=\"terms-and-conditions\"\u003e\n    I agree to the \u003ca href=\"/terms\"\u003eTerms and Conditions\u003c/a\u003e\n  \u003c/label\u003e\n\u003c/form\u003e\n```\n\nThis is a very basic registration form with no validations or event\ncallbacks. The key here is that our success handler gets a set of\nproperly-typed values. It is only called if all of the entries can\nproperly be deserialized into their requested types (for example,\nputting in \"hello\" for the age would mean the success callback would not\nrun).\n\nBehind the scenes, Formality tries to deserialize the values. If\nthe deserialization fails, `S.error` is automatically set for the\nappropriate field. You can then customize the behavior of `S.error` as\nyou desire (see [LiftRules](http://liftweb.net/api/26/api/net/liftweb/http/LiftRules.html)\nfor more, specifically the `noticesToJsCmd` property).\n\n### Validations\n\nFormality can also do validation:\n\n```scala\n  import com.hacklanta.formality.Formality._\n  import com.hacklanta.formality.Html5Validations._\n\n  val nameField = field[String](\".name\") ? notEmpty\n  val phoneNumberField = field[String](\".phone-number\")\n  val ageField = field[Int](\".age\") ? inRange(15, 120)\n  val termsField =\n    checkboxField(\"#terms-and-conditions\") ? { incoming: Boolean =\u003e\n      if (incoming) {\n        Empty\n      } else {\n        Full(\"You must agree to the terms and conditions.\")\n      }\n    }\n\n  val registrationForm =\n    form withFields(\n      nameField,\n      phoneNumberField,\n      ageField,\n      termsField\n    ) onSuccess {\n      // same as above\n    }\n```\n\nNow we've taken it a step further. Validations can be simple\nfunctions. These functions take the deserialized value (i.e., the value\nafter it has been typed properly) and return a `Full` `Box` with an\nerror message when there is an error, or an `Empty` `Box` if the value\nis valid. In cases where a validation fails (by providing an error\nmessage), that error message is associated with the appropriate field by\ncalling `S.error`.\n\nValidations can also be more than simple functions. The built-in\nvalidations, like `inRange` and `notEmpty`, extend the `Validation`\ntrait. These `Validation`s are still functions that work as specified\nabove (i.e., they have an `apply` method that behaves as we specified\nabove). However, they also have a `binder` method that returns a\n`CssSel`. These can be used to provide client-side attributes to also\nenforce the validation client-side. Client-side validations have\nthe advantage of being able to run faster and without taxing the\nserver. `Validation`s allow you to specify client-side validations and\npair them with server-side validations to ensure that valid data is what\nmakes it into your database.\n\nThe two built-ins used above are from the `Html5Validations` object,\nwhich does client-side validation using HTML5 attributes. In particular,\nthe `notEmpty` validation applies a `required` attribute to the input,\nand the `inRange` validation applies `min` and `max` attributes to the\ninput.\n\nAlso in progress is an object, `ParsleyValidations`, meant to support\nthe set of validations that are supported by\n[Parsley.js](http://parsleyjs.org). These will have matching server-side\nvalidation implementations.\n\nIn certain very specific cases, it can be useful to run a validation even\nif the value in question isn't actually submitted in the form. Typically\nthis is used for a check that a required field was submitted, particularly\nwhen it comes from a checkbox or radio button. Either way, in these cases\na validation can take a `Box[T]` instead of `T`. It will receive an `Empty`\nif the field is not submitted, and a `Full` with the deserialized value if\nthe field is submitted. Lift-formality's own `notBlank`/`notEmpty` validators\nwill correctly handle unsubmitted fields.\n\n### Event handling\n\nFormality can also add server-side event handlers to form inputs:\n\n```scala\n  import com.hacklanta.formality.Formality._\n  import com.hacklanta.formality.Html5Validations._\n\n  val nameField = field[String](\".name\") ? notEmpty\n  val phoneNumberField = field[String](\".phone-number\")\n  val ageField =\n    field[Int](\".age\") ?\n      inRange(15, 120) -\u003e\n      on(\"change\", { incoming: Int =\u003e\n        if (incoming \u003c 15) {\n          Run(\"$('#terms-and-conditions').attr('disabled', 'disabled')\")\n        }\n      })\n  val termsField =\n    checkboxField(\"#terms-and-conditions\") ? { incoming: Boolean =\u003e\n      if (incoming) {\n        Empty\n      } else {\n        Full(\"You must agree to the terms and conditions.\")\n      }\n    }\n\n  val registrationForm =\n    form withFields(\n      nameField,\n      phoneNumberField,\n      ageField,\n      termsField\n    ) onSuccess {\n        // same as above\n    }\n```\n\n`-\u003e on(\u003cevent\u003e, \u003chandler)` is the syntax used to add an event handler to\na field. The `event` is whatever the event name will be on the client, as a\n`String`, and the `handler` takes in the deserialized incoming value and\ndoes something with it. It is expected to return a `JsCmd`, though keep\nin mind that there is an implicit conversion from `Unit` to `JsCmd`\nwhen necessary. Above, on change, we disable the terms and conditions\ncheckbox to ensure the user cannot select it if they are below\n15[*](#client-note)\u003ca name=\"client-note-return\"\u003e\u003c/a\u003e.\n\n\u003ca name=\"client-node\"\u003e\u003c/a\u003e* - Obviously we could do this on the client as\nwell, but this is merely an example :) [↩](#client-note-return)\n\n### File Uploads\n\nA complicating factor is when your form includes uploaded files.\n\nThey will work perfectly if your form is not ajaxified, but submitting files as part of an ajax upload takes a little more effort.  A classic approach is insert an iframe and submit to that, thus preventing your page from reloading.\n\nThis can be implemented clientside with the inclusion of a script along the lines of:\n(Adapted from [this example](https://github.com/Shadowfiend/lift-ajax-file-upload-example/blob/master/src/main/webapp/static/js/fileUpload.js]))\n\n```javascript\n$(function() {\n    function submitFormToIframe($form, target) {\n        $form\n            .attr('target', target)\n            .removeAttr('onsubmit')\n            .removeAttr('action')\n            .removeAttr('onclick')\n            .attr('action', '/ajax')\n            .attr('method', 'post')\n            .attr('enctype', 'multipart/form-data' )\n            .attr('encoding', 'multipart/form-data')\n            .find('input:submit,button[type=submit]')\n            .end()\n            .append($('\u003cinput type=\"hidden\" name=\"' +\n                      $form.find('input:submit').attr('name') +\n                      '\" value=\"_\" /\u003e'))\n            .after(\n                // do not use attr() to set name. IE7 will hate this: http://stackoverflow.com/questions/2105815/weird-behaviour-of-iframe-name-attribute-set-by-jquery-in-ie\n                $('\u003ciframe id=\"' + target + '\" name=\"' + target + '\" /\u003e')\n                    .addClass('form-target')\n                    .css('display','none')\n            );\n    }\n\n    submitFormToIframe($('form:has(input[type=file])'), 'fileUploadExampleForm');\n});\n```\nYou may also wish to treat your uploaded files as optional.  By default, a `fileUploadField` will fail validation if not provided. You can make the field(s) optional through a conversion into Option:\n\n```scala\nfieldGroup\n            .withFields(\n              fileUploadField(\"#anInputOfTypeFile\")\n            )\n            .withBoxedConverter { optional: Box[FileParamHolder] =\u003e\n              optional match {\n                case Full(fph) =\u003e Full(Some(fph))\n                case _         =\u003e Full(None)\n              }\n            }\n```\n\n### Operator Allergies\n\nIn case you have an aversion to using operators, the `?` operator that\nadds a validation to a field is originally named `validatingWith`, so\nyou can use that instead. Additionally, the `-\u003e` operator that adds\nan event handler is originally named `handlingEvent`.\n\n### Failure Handling\n\nFormality's default behavior is to send down deserialization and\nvalidation errors using `S.error`. However, you may want to take\nadditional action when dealing with failures. Or, you may want to\nignore `S.error` in favor of your own error handling strategy. To\ndo this, you can add a failure handler to the form:\n\n```scala\n  val registrationForm =\n    form withFields(\n      nameField,\n      phoneNumberField,\n      ageField,\n      termsField\n    ) onSuccess {\n        // same as above\n    } onFailure { failures =\u003e\n      failures.foreach {\n        case ParamFailure(message, _, _, validationErrors) =\u003e\n          logger.error(\"Got \" + message + \" with validation errors: \" + validationErrors)\n        case Failure(message, _, _) =\u003e\n          logger.error(\"Got \" + message)\n        case _ =\u003e\n      }\n    }\n```\n\nNote that the basic `Failure` message when validations fail simply says\n\"\u003cfield\u003e failed validations.\" However, in such cases, the failure is\na `ParamFailure` whose parameter is a list of `String`s representing the\nvalidation errors for that field.\n\n### Field Groups\n\nFormality provides an abstraction above fields, called field groups. This\nlets you group fields into nested groups. By default, this looks like this:\n\n```scala\n  val registrationForm =\n    form withFields(\n      fieldGroup.withFields(\n        nameField,\n        phoneNumberField,\n        ageField\n      ),\n      termsField\n    ) onSuccess {\n      case ((name :+: phoneNumber :+: age :+: HNil), terms) =\u003e\n        // Assuming a case class User(name: String, age: Int, phoneNumber: String) this time.\n        if (terms) {\n          User(name, phoneNumber, age).save\n        }\n      User(name, age, phoneNumber, termsAndConditions).save\n    }\n```\n\nBut field groups also have a very important property: they can be converted\ninto a different type using its `as` function. In particular, a field group can\nbe a placeholder for a type that involves its combined fields. Let's redo the\nabove:\n\n```scala\n  val registrationForm =\n    form withFields(\n      fieldGroup.withFields(\n        nameField,\n        phoneNumberField,\n        ageField\n      ).as(User.apply _),\n      termsField\n    ) onSuccess { (user, terms) =\u003e\n      if (terms) {\n        user.save\n      }\n    }\n```\n\nOur success handler now gets a full-on User object instead of the decomposed\nfields, and we could use the `User` constructor directly.\n\n`as` takes a function that takes the parameters from the group and produces a\ntype T. However, sometimes we want to be able to fail at the conversion step.\nIn those cases, we can use `withConverter` instead, which allows us to return a\n`Box[T]`, where a non-`Full` will cause a failed form submission:\n\n```scala\n  val registrationForm =\n    form withFields(\n      fieldGroup.withFields(\n        nameField,\n        phoneNumberField,\n        ageField\n      ).withConverter(User.createUnique _),\n      termsField\n    ) onSuccess { (user, terms) =\u003e\n      if (terms) {\n        user.save\n      }\n    }\n```\n\nAbove, `createUnique` can be considered as a version of `User` that creates the\nnew `User` object and returns a `Full` unless, say, the name is not unique. In that case it returns a `Failure`.\n\nNote that failures at the group conversion level don't trigger `S.error`, since\nthere's no concrete field to associate the failure with. You can do your own\nhandling of the failure in the `onFailure` handler.\n\n#### Field groups with boxed converters\n\nField groups can also use a converter that takes the boxed values of the\nfields that it is made up of. This can be used, for example, to make choices\nbetween two fields that supply the same data. Let's take as an example an\nupload form that accepts a document in PDF format or a pasted text file:\n\n```scala\n  val fileNameField = field[String](\".file-name\")\n  val pastedTextField = field[String](\".pasted-text\")\n  val uploadedFileField = fileUploadField(\".pasted-text\")\n\n  val uploadForm =\n    form withFields(\n      fileNameField,\n      fieldGroup.withFields(\n        pastedTextField,\n        uploadedFileField\n      ).withBoxedConverter { (text: Box[String], file: Box[FileParamHolder]) =\u003e\n        text.map(_.getBytes(\"UTF-8\")) or file.data\n      }\n    ).onSuccess { (fileName: String, fileData: Array[Data]))\n      DbFile.create(fileName, fileData)\n    }\n```\n\nAbove, we have a form with a field for the file's name, and then a group that\nencloses the pasted text and the file upload. The converter receives both as\nboxed values, which will be `Empty` if the user didn't specify anything for the\nfields in question. The converter then uses the pasted text if it was specified,\nor if not chooses the file data. The success handler then only sees two parameters:\nthe files name, and the final data array that represents its contents, which we've\nresolved in the field group converter.\n\n#### Scoping field groups\n\nYou can also add a scope to a field group. This is a CSS selector that ensures\nthe fields in the field group are bound only if they're found within elements\nthat match that selector:\n\n```scala\n  val registrationForm =\n    form withFields(\n      fieldGroup(\".user\").withFields(\n        nameField,\n        phoneNumberField,\n        ageField\n      ).withConverter(User.createUnique _),\n      termsField\n    ) onSuccess ...\n```\n\nWith the above code, the markup that we introduced at the beginning would no\nlonger successfully bind the user part of the registration form. Instead, we\nwould need something like this:\n\n```html\n\u003cform data-lift=\"Registration.form\"\u003e\n  \u003cfieldset class=\"user\"\u003e\n    \u003clegend\u003eUser Information\u003c/legend\u003e\n\n    \u003cinput class=\"name\"\u003e\n    \u003cinput class=\"phone-number\" placeholder=\"(XXX) XXX-XXXX\"\u003e\n    \u003cinput class=\"age\" type=\"number\"\u003e\n  \u003c/fieldset\u003e\n\n  \u003clabel for=\"terms-and-conditions\u003e\n    \u003cinput type=\"checkbox\" id=\"terms-and-conditions\"\u003e\n    I agree to the \u003ca href=\"/terms\"\u003eTerms and Conditions\u003c/a\u003e\n  \u003c/label\u003e\n\u003c/form\u003e\n```\n\n## License\n\n`lift-formality` is provided under the terms of the MIT License. No warranties\nare made, express or implied.  See the `LICENSE` file in this same directory.\n\n# Author/Contributors\n\n`lift-formality` is copyright [Hacklanta](http://hacklanta.com), and was\noriginally conceived and written by [Antonio Salazar\nCardozo](http://github.com/Shadowfiend).\n\nYou can find our writings on the [Hacklanta blog](http://hacklanta.com/blog).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhacklanta%2Flift-formality","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhacklanta%2Flift-formality","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhacklanta%2Flift-formality/lists"}