Giter Site home page Giter Site logo

geekcheng / supler Goto Github PK

View Code? Open in Web Editor NEW

This project forked from softwaremill/supler

0.0 3.0 0.0 855 KB

Functional Reactive Form Library

Home Page: http://www.softwaremill.com

License: Apache License 2.0

JavaScript 31.42% Scala 38.42% TypeScript 30.16%

supler's Introduction

supler - Functional Reactive Form Library

Build Status

Links

Introduction

Supler is a library which makes writing complex form easier. It has server-side (Scala) and client-side (JavaScript) components.

Supler does not define or mandate how the objects/entities backing the forms should work, how are they persisted, how are sessions managed or how you handle requests. It is also agnostic to other JS frameworks and libraries. The generated HTML has elements with predictable names, which can be easily customized.

On the server side Supler provides:

  • a DSL for defining forms
  • a way to generate a JSON description of a form
  • running server-side conversion and validation
  • running server-side actions
  • applying values sent from the frontend to the backing object

On the frontend side Supler provides:

  • generating HTML basing on JSON form description
  • serializing a form to HTML
  • running client-side validations
  • customizability of the HTML generation process
  • automatically refresh the form with server-side changes after a field is edited

Supler diagram

Supler diagram

Using Supler

As Supler has two parts, they are deployed in different repositories. The backend can be found in Sonatype’s OSS repository, and if you have e.g. an SBT build you just need to add:

libraryDependencies += "com.softwaremill" %% "supler" % "0.1.0"

The frontend is deployed to Bower, and you can install it simply using bower install supler. Or you can just grab supler.js directly from the GitHub tag.

Server-side

All examples assume that all members of the Supler objects are available in the current scope (import org.supler.Supler._).

Forms

You can use any object and class as the backing object for a Supler form. The fields of the class which are editable in the form must be either vars, provide a Scala-style getter/setter, or immutable vals in a case class. We recommend the latter, immutable approach.

A form consists of a list of fields belonging to a single class. Supler provides a convenience method, form, which captures the class of the object once, and can be used to quickly build forms. Here we are defining a form with three fields:

case class Person(firstName: String, lastName: String, age: Int)

val personForm = form[Person](f => List(
  f.field(_.firstName),
  f.field(_.lastName),
  f.field(_.age)
))

The fields can be further customized. Almost always you'll want to specify the label of a field'

f.field(_.firstName).label("First name")

The label can also be a key which will be looked up in the i18n component on the frontend.

Validation

Another useful customization of fields is specifying validators. There's a number of built-in validators, but you can also specify custom ones. Validators have access to the value of the field and the whole object:

val personForm = form[Person](f => List(
  f.field(_.firstName).label("First name")
    .validate(custom((e, v) => v.startsWith("A"), (e, v) => ErrorMessage("First name cannot start with an 'A'!"))),
  f.field(_.lastName).label("Last name"),
  f.field(_.age).label("Age").validate(ge(0), le(120))
))

The built-in validators include a JSON representation, and they will be checked both on the client and server side. Custom validators by default are checked only on the server, but it is possible to provide a JSON representation as well.

You can validate any object at any time using the doValidate method, which returns an optional list of validation errors found (the object doesn't have to come from the Supler-frontend):

val validationErrors: Option[FormErrors] = personForm.doValidate(Person("Adam", "Smith", 18))

Narrowing possible values

Very often a field can take a value from a restricted domain. In that case, it is possible to tell Supler what the possible values for a field are. The values can depend on the object (can be different for each instance of a class):

case class Car(make: String, year: Int)

val carForm = form[Car](f => List(
  f.field(_.make).label("Make").possibleValues(c => List("Ford", "Toyota", "KIA", "Lada")),
  f.field(_.year).label("Year")
))

If a field specifies the list of possible values, it will be rendered by default as a dropdown.

Supported types, type transformers

Fields of basic types (String, Int, Long, Float, Double and Boolean) are supported out-of-the-box, and can be directly edited in form fields.

If you have a more complex type, you need to provide an implicit implementation of a Transformer[U, S], where U is your type, and S is one of the basic types. For convenience, you can extend StringTransformer[U] etc.

In the transformer, you need to implement a method which serializes your type to a basic type, and another method which deserializes a basic type into your type, or returns a form error.

Subforms

Fields can also correspond to other forms. Already defined forms can be freely re-used multiple times (forms are immutable). Currently this is limited to a list of sub-objects/sub-forms.

case class Car(make: String, year: Int)
case class Person(name: String, cars: List[Var])

val carForm = ...

val personForm = form[Person](f => List(
  f.field(_.name).label("Name"),
  f.subform(_.cars, carForm).label("Cars")
))

Collection-valued fields

Multi-valued fields of basic types are also supported. Currently this is limited to sets, and rendered as checkboxes.

case class Person(name: String, favoriteColors: Set[String])

val personForm = form[Person](f => List(
  f.field(_.name).label("Name"),
  f.setField(_.favoriteColors).label("Favorite colors").possibleValues(_ => Set("red", "green", "blue", "black"))
))

Label (static) fields

Fields can also be non-editable and display static content - a label. Note that the value of such fields will not be included when the form is serialized on the frontend, and sent back to the server.

case class Person(name: String, registrationId: String)

val personForm = form[Person](f => List(
  f.field(_.name).label("Name"),
  f.staticField(_.registrationId).label("Registration id")
))

Actions

Forms can contain buttons which invoke actions on the server side (see also the section on reloading the form below). Each action must have a unique name (just as fields have names, but these are inferred). An action name can only contain letters, digits and _ (no spaces or other characters which would form an invalid JSON object key).

In its simplest form, an action can modify the object that is backing the form, and needs to return an ActionResult:

case class Person(name: String)

val personForm = form[Person](f => List(
  f.field(_.name).label("Name"),
  f.action("duplicateName")(p => ActionResult(p.copy(name = s"${p.name} ${p.name}"))
    .label("Duplicate name")
))

To implement some operations on subforms, such as removing a subform element, or moving the elements around, it is needed to have access to the parent object. This is possible by using parentActions. The subform is in such case parametrised by the action (so it can be reused in different contexts), which is provided in the parent form:

case class Address(street: String)
case class Person(name: String, addresses: List[Address]) {
  def removeAddress(a: Address) = this.copy(addresses = this.addresses diff List(a))
}

def addressForm(removeAction: Address => ActionResult[Address]) = form[Address](f => List(
  f.field(_.street).label("Street"),
  f.action("remove")(removeAction).label("Remove")
))

val personForm = form[Person](f => List(
  f.field(_.name).label("Name"),
  f.subform(_.addresses, addressForm(
    f.parentAction((person, index, address) => ActionResult(person.removeAddress(address)))))
    .label("Addresses")
))

Render hints

In some cases there are a couple possible rendering of a field. In such case, you can specify a render hint, which will influence how the field is rendered. It will also be possible to specify custom rendering (not yet implemented).

For example, to render a password field as a password:

case class Login(username: String, password: String)

val loginForm = form[Login](f => List(
  f.field(_.username).label("Username"),
  f.field(_.password).label("Password").renderHint(asPassword())
))

Supported render hints:

  • for subforms: asTable() (default), asList()
  • for text fields: asPassword(), asTextarea(rows = 10)
  • for single-select fields: asRadio()

Stand-alone fields

Fields can be created without a form, and later used to compose other forms. This may help to centralize the definition of some common fields. For example:

case class Person(name: String, age: Int)

val nameField = field[Person, String](_.name).label("Name")
val ageField = field[Person, Int](_.age).label("Age").validate(gt(0), le(120))

val personForm = form(_ => List(nameField, ageField))

Serializing a form to JSON

To generate a JSON representation of a form, simply call its generateJSON method, passing in an object, basing on which a form will be generated:

val personFormJson = personForm(person).generateJSON

The resulting JSON can be then sent to the client. Supler uses the Scala-standard json4s to generate the JSON.

The JSON contains both the form structure and the form values. It is a custom format, however it's very easy to understand, and self-explanatory; the fields in the JSON correspond closely to the DSL-based definition.

Applying values and validating

After receiving a JSON representing an updated state, the form can be used to apply the values to an object:

personForm(person).applyValuesFromJSON(receivedJson)

// with validation:
personForm(person).applyValuesFromJSON(receivedJson).doValidate()

The resulting type of each method is a FormWithObject[Person], which contains potential conversion/validation errors and the current state of the object.

If there are errors, the json generated by generateJSON will contain them, and the errors will be displayed to the client if the data is sent back to the client and rendered.

Client-side (frontend)

The frontend side has to load the form description, render it and later serialize and send the values back. How the form is loaded from the server, what technology is used to transmit the JSON etc., is not handled by Supler and can be done in any way.

Rendering the form

At the minimum, you need a designated container on your page, where the form will be rendered, and when the form JSON is available, use SuplerForm:

<div>
  <div id="form-container"></div>
  <a href="#" class="btn btn-primary btn-lg" id="submit" role="button">Submit</a>
  <p id="feedback"></p>
</div>
var formContainer = document.getElementById('form-container');
var form =  = new SuplerForm(formContainer, {});
form.render(formJson); // formJson is received from the server

Client-side validation

To perform and display client side validation, use the SuplerForm.validate() method. It will return true if there are any validation errors.

Any existing errors will be cleared upon next invocation of validate().

Serializing the form

To read the value of a form as a JSON object, simply use the SuplerForm.getValue() method. The resulting JSON can be sent to the server.

The resulting JSON is what you might expect, mirroring the form's structure through objects, JSON arrays, nested objects and primitive types.

In fact, to apply a JSON to an object on the server-side you don't need to use Supler-frontend. Because there's nothing special about the format, it is easy to generate such a JSON yourself.

Handling server responses

When a modified form is received (potentially with conversion/validation errors), it can be rendered using the same method: SuplerForm.render(refreshedFormJson).

Customizing the rendering process

The rendering process is fully customizable. By default, Bootstrap-based HTML is rendered, but this can be changed either by providing HTML templates, or by overriding any of the rendering functions using the options.

For details, see the readme on rendering customization.

i18n

Both the labels and the conversion/validation errors may be i18n keys. Some default keys are provided for the standard validators, but custom ones can be provided as well simply by specifying them as keys in the options to SuplerForm:

var formContainer = document.getElementById('form-container');
var form =  = new SuplerForm(formContainer, {
  error_custom_lastNameLongerThanFirstName: "Last name must be longer than first name!",
  error_custom_illegalDateFormat: function(detail) { return "Illegal date format: " + detail; }
});
form.render(formJson); // formJson is received from the server

The values can be either strings, or functions which format the message using the error message's arguments.

Reloading the form basing on server-side form changes

The form can be automatically reloaded after each field edit (value change), and when actions are performed. To do that, two things are necessary. Firstly, a reload_form_function option must be specified. This should be a javascript function, accepting the serialized form representation and a success function, to be called when the form is successfully refreshed. For example, when using JQuery, this can be:

function reloadForm(formJson, successFn) {
    $.ajax({
        url: '/refresh_form.json',
        type: 'POST',
        data: JSON.stringify(formJson),
        dataType: 'json',
        contentType: 'application/json; charset=utf-8',
        success: successFn
    });
}

Secondly, we need to provide a server-side endpoint which will refresh the form with the given values, validate and generate back the response. When validating, there is a special mode which runs the validations only for fields with filled-in values, not to show the user validation errors for fields which haven't been yet edited at all. This can be done with the convenience personForm(person).reload(receivedJson) method. This simply invokes apply, validate, run action and generate JSON in succession.

Adding custom behavior to the form

By setting the after_render_function option to a no-argument function, it is possible to get notified after a form is rendered (or refreshed), and customize the form or add some custom dynamic behavior.

Version history

0.1.0 - 16/12/2014

  • initial release

Contributors

supler's People

Contributors

adamw avatar szimano avatar

Watchers

 avatar  avatar  avatar

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.