Auto generated CRUD forms with Knockout

For the last two years I’ve been working for the asset management at a major Swedish bank. At such a institution CRUD is inevitable and we all know that most devs prefer working with rich domain systems over CRUD. CRUD also generate a lot of similar or duplicated code. In my latest project I wanted to do something about this and decided to create a convention based API to auto generate these forms.

I want to point out that this blog post wont cover the actual code from that project, instead I recreated the functionality from scratch in a stand alone Durandal project.

The idea is that the form can be completely auto generated out of the JSON data sent from server but with the option to configure the individual fields. We also had a complex authorization model were users could edit fields on a role basis. The authorization logic wont be covered in this example, instead I have replaced that with a canEdit member.

Value editors and readonly presenters

The API is built around a collection of ViewModels, each ViewModel is either a value editor or a read only value presenter. Both the editors and the presenters has a default ViewModel which handles data of type text. Each non default ViewModel has a can function that determines if it can handle the data type. This is the Number presenter ViewModel.

define(["fields/fieldClosures"], function (fields) {
    var ctor = function (opt) {
        this.value = ko.observable(opt.value).extend({ isNumeric: opt.options.format || "N" });
    };

    ctor.can = function (value) {
        return typeof value === "number";
    };

    return fields.readonly.number = ctor;
});

First I create a constructor that holds the correctly formatted number using a Knockout extension. I then add a can function to that constructor which checks if the value is a number. We also add the constructor to a object closure that the API can iterate over to determine which editor or presenter model that should be used for the specific value type.

The API has a factory function looking like this.

function factory(closure, opt) {
    for (var index in closure) {
        var model = closure[index];
        if (model.can && model.can(opt == null ? null : ko.unwrap(opt.value), opt || {})) {
            return model;
        }
    }

    return closure.default;
}

It iterates over the closure and finds the correct ViewModel, if none is found using the can function it returns the default.

The factory is called from a computed like.

this.field = ko.computed(function () {
    var closure = ko.unwrap(this.canEdit) ? fields.editors : fields.readonly;
    var model = factory(closure, opt);
    var field = new model(opt);
    field.id = this.id;
    return field;
}, this);

It checks the canEdit state and supplies the factory with either a editor or presenter closure. We wrap the functionally in a KO computed, this way we can change the canEdit at any time and the View will reflect it. Note that in the real project we didn’t use a computed and thus this code hasn’t been properly tested. After that its just a matter of Knockout.BindingConventions coupled with Durandal magic to render the correct View depending on selected ViewModel.

Field configuration

We also need a way to specify field configuration or let the API auto generate the configuration using the supplied data. A configuration can look like this

{
    myFieldOne: true,
    myFieldTwo: {
        canEdit: true,  
        label: "This is a custom label"
    }  
}

The first field will auto generate its label to “My field one” while the second is explicit set. The second field will also be in editor mode. Another example.

{
    autoGenerate: true,
    canEdit: true,
    fields: {
        myFieldTwo: {          
            label: "This is a custom label"
        }
    }  
}

In this example all fields will be in edit mode, field two is explicit declared. The rest of the fields will be auto generated from the supplied data.

A data object for above examples could look like.

{
    myFieldOne: 6.0,
    myFieldTwo: "Text data"
}

Serializing the result

The API has built in validation using Knockout Validation. By default a field is not required, to make it required you need specify that in the options like.

{
    myFieldOne: {
        required: true
    }
}

Once everything is valid you can call the serialize function, it will create a new object mirroring the original data object but with the new edited values.

You can download a fully working example here. Like I said before, this is reproduced code of the actual production code, it misses a lot of features and its not tested like the production code.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s