Application Development with Rapid Application Development Framework AllcountJS

The idea of Rapid Application Development (RAD) was born as a response to traditional waterfall development models. Many variations of RAD exist; for example, Agile development and the Rational Unified Process. However, all such models have one thing in common: they aim to yield maximum business value with minimal development time through prototyping and iterative development. To accomplish this, the Rapid Application Development model relies on tools that ease the process. In this article, we shall explore one such tool, and how it can be used to focus on business value and optimization of the development process.

AllcountJS is an emerging open source framework built with rapid application development in mind. It is based on the idea of declarative application development using JSON-like configuration code that describes the structure and behavior of the application. The framework has been built on top of Node.js, Express, MongoDB and relies heavily on AngularJS and Twitter Bootstrap. Although it is reliant on declarative patterns, the framework still allows further customization through direct access to API where needed.

Why AllcountJS as Your RAD Framework?

According to Wikipedia, there are at least one hundred tools that promise rapid application development, but this raises the question: how rapid is “rapid.” Do these tools allow a particular data-centric application to be developed in a few hours? Or, perhaps, it is “rapid” if the application can be developed in a few days or a few weeks. Some of these tools even claim that a few minutes is all it takes to produce a working application. However, it is unlikely that you could build a useful application in under five minutes and still claim to have satisfied every business need. AllcountJS doesn’t claim to be such a tool; what AllcountJS offers is a way to prototype an idea in a short period of time.

With AllcountJS framework, it is possible to build an application with a themeable auto-generated user interface, user management features, RESTful API and a handful of other features with minimal effort and time. It is possible to use AllcountJS for a wide variety of use cases, but it best suits applications where you have different collections of objects with different views for them. Typically, business applications are a good fit for this model.

AllcountJS has been used to build allcountjs.com, plus a project tracker for it. It is worth noting that allcountjs.com is a customized AllcountJS application, and that AllcountJS allows both static and dynamic views to be combined with little hassle. It even allows dynamically loaded parts to be inserted into static content. For example, AllcountJS manages a collection of demo application templates. There is a demo widget on the main page of allcountjs.com that loads a random application template from that collection. A handful of other sample applications are available in the gallery at allcountjs.com.

Getting Started

To demonstrate some of the capabilities of RAD framework AllcountJS, we will create a simple application for Toptal, which we will call Toptal Community. If you follow our blog you may already know that a similar application was built using Hoodie as part of one of our earlier blog posts. This application will allow community members to sign up, create events and apply to attend them.

In order to set up the environment, you should install Node.jsMongoDB and Git. Then, install AllcountJS CLI by invoking an “npm install” command and perform project init:

npm install -g allcountjs-cli
allcountjs init toptal-community-allcount
cd toptal-community-allcount
npm install

AllcountJS CLI will ask you to enter some info about your project in order to pre-fill package.json.

AllcountJS can be used as standalone server or as a dependency. In our first example we aren’t going to extend AllcountJS, so a standalone server should just work for us.

Inside this newly created app-config directory, we will replace contents of main.js JavaScript file with the following snippet of code:

A.app({
  appName: "Toptal Community",
  onlyAuthenticated: true,
  allowSignUp: true,
  appIcon: "rocket",
  menuItems: [{
    name: "Events",
    entityTypeId: "Event",
    icon: "calendar"
  }, {
    name: "My Events",
    entityTypeId: "MyEvent",
    icon: "calendar"
  }],
  entities: function(Fields) {
    return {
      Event: {
        title: "Events",
        fields: {
          eventName: Fields.text("Event").required(),
          date: Fields.date("Date").required(),
          time: Fields.text("Starts at").masked("99:99").required(),
          appliedUsers: Fields.relation("Applied users", "AppliedUser", "event")
        },
        referenceName: "eventName",
        sorting: [['date', -1], ['time', -1]],
        actions: [{
          id: "apply",
          name: "Apply",
          actionTarget: 'single-item',
          perform: function (User, Actions, Crud) {
            return Crud.actionContextCrud().readEntity(Actions.selectedEntityId()).then(function (eventToApply) {
              var userEventCrud = Crud.crudForEntityType('UserEvent');
              return userEventCrud.find({filtering: {"user": User.id, "event": eventToApply.id}}).then(function (events) {
                if (events.length) {
                  return Actions.modalResult("Can't apply to event", "You've already applied to this event");
                } else {
                  return userEventCrud.createEntity({
                    user: {id: User.id},
                    event: {id: eventToApply.id},
                    date: eventToApply.date,
                    time: eventToApply.time
                  }).then(function () { return Actions.navigateToEntityTypeResult("MyEvent") });
                }
              });
            })
          }
        }]
      },
      UserEvent: {
        fields: {
          user: Fields.fixedReference("User", "OnlyNameUser").required(),
          event: Fields.fixedReference("Event", "Event").required(),
          date: Fields.date("Date").required(),
          time: Fields.text("Starts at").masked("99:99").required()
        },
        filtering: function (User) { return {"user.id": User.id} },
        sorting: [['date', -1], ['time', -1]],
        views: {
          MyEvent: {
            title: "My Events",
            showInGrid: ['event', 'date', 'time'],
            permissions: {
              write: [],
              delete: null
            }
          },
          AppliedUser: {
            permissions: {
	          write: []
            },
            showInGrid: ['user']
          }
        }
      },
      User: {
        views: {
          OnlyNameUser: {
            permissions: {
              read: null,
              write: ['admin']
            }
          },
          fields: {
            username: Fields.text("User name")
          }
        }
      }
    }
  }
});

Although AllcountJS works with Git repositories, for sake of simplicity we will not use it in this tutorial. To run the Toptal Community application, all we have to do is invoke AllcountJS CLI run command in the toptal-community-allcount directory.

allcountjs run

It is worth noting that MongoDB should be running when this command is executed. If all goes well, the application should be up and running at http://localhost:9080.

To login please use the username “admin” and password “admin”.

Less Than 100 Lines

You may have noticed that the application defined in main.js took only 91 lines of code. These lines include the declaration of all the behaviors that you may observe when you navigate to http://localhost:9080. So, what exactly is happening, under the hood? Let us take a closer look at each aspect of the application, and see how the code relates to them.

Sign in & Sign up

The first page you see after opening the application is a sign in. This doubles as a sign up page, assuming that the checkbox – labelled “Sign Up” – is checked before submitting the form.

Sign in & Sign up

This page is shown because the main.js file declares that only authenticated users may use this application. Moreover, it enables the ability for users to sign up from this page. The following two lines are all that was necessary for this:

A.app({
  ...,
  onlyAuthenticated: true,
  allowSignUp: true,
  ...
})

Welcome Page

After signing in, you’ll be redirected to a welcome page with an application menu. This portion of the application is generated automatically, based on the menu items defined under the “menuItems” key.

welcome page example

Along with a couple of other relevant configurations, the menu is defined in the main.js file as follows:

A.app({
  ...,
  appName: "Toptal Community",
  appIcon: "rocket",
  menuItems: [{
    name: "Events",
    entityTypeId: "Event",
    icon: "calendar"
  }, {
    name: "My Events",
    entityTypeId: "MyEvent",
    icon: "calendar"
  }],
  ...
});

AllcountJS uses Font Awesome icons, so all icon names referenced in the configuration are mapped to Font Awesome icon names.

Browsing & Editing Events

After clicking on “Events” from the menu, you’ll be taken to the Events view shown in the screenshot below. It is a standard AllcountJS view that provides some generic CRUD functionalities on the corresponding entities. Here, you may search for events, create new events and edit or delete existing ones. There are two modes of this CRUD interface: list and form. This portion of the application is configured through the following few lines of JavaScript code.

A.app({
  ...,
  entities: function(Fields) {
    return {
      Event: {
        title: "Events",
        fields: {
          eventName: Fields.text("Event").required(),
          date: Fields.date("Date").required(),
          time: Fields.text("Starts at").masked("99:99").required(),
          appliedUsers: Fields.relation("Applied users", "AppliedUser", "event")
        },
        referenceName: "eventName",
        sorting: [['date', -1], ['time', -1]],
        ...
      }
    }
  }
});

This example shows how entity descriptions are configured in AllcountJS. Notice how we are using a function to define the entities; every property of AllcountJS configuration can be a function. These functions can request dependencies to be resolved through its argument names. Before the function is called, appropriate dependencies are injected. Here, “Fields” is one of the AllcountJS configuration APIs used to describe entity fields. The property “Entities” contains name-value pairs where the name is an entity-type identifier and value is its description. An entity-type for events is described, in this example, where the title is “Events.” Other configurations, such as default-sort-ordering, reference name, and the like, may also be defined here. Default-sort-order is defined through an array of field names and directions, while the reference name is defined through a string (read more here).

allcountJS function

This particular entity-type has been defined as having four fields: “eventName,” “date,” “time” and “appliedUsers,” the first three of which are persisted in the database. These fields are mandatory, as indicated by the use of “required().” Values in these fields with such rules are validated before the form is submitted on the front-end as shown in the screenshot below. AllcountJS combines both client-side and server-side validations to provide the best user experience. The fourth field is a relationship that bears a list of users who have applied to attend the event. Naturally, this field is not persisted in the database, and is populated by selecting only those AppliedUser entities relevant to the event.

allcountjs development rules

Applying to Attend Events

When a user selects a particular event, the toolbar shows a button labelled “Apply.” Clicking on it adds the event to the user’s schedule. In AllcountJS, actions similar to this can be configured by simply declaring them in the configuration:

actions: [{
  id: "apply",
  name: "Apply",
  actionTarget: 'single-item',
  perform: function (User, Actions, Crud) {
    return Crud.actionContextCrud().readEntity(Actions.selectedEntityId()).then(function (eventToApply) {
      var userEventCrud = Crud.crudForEntityType('UserEvent');
      return userEventCrud.find({filtering: {"user": User.id, "event": eventToApply.id}}).then(function (events) {
        if (events.length) {
          return Actions.modalResult("Can't apply to event", "You've already applied to this event");
        } else {
          return userEventCrud.createEntity({
            user: {id: User.id},
            event: {id: eventToApply.id},
            date: eventToApply.date,
            time: eventToApply.time
          }).then(function () { return Actions.navigateToEntityTypeResult("MyEvent") });
        }
      });
    })
  }
}]

The property “actions” of any entity type takes an array of objects that describe the behavior of each custom action. Each object has an “id” property which defines a unique identifier for the action, the property “name” defines the display name and the property “actionTarget” is used to define the action context. Setting “actionTarget” to “single-item” indicates that the action should be performed with a particular event. A function defined under the property “perform” is the logic executed when this action is performed, typically when the user clicks on the corresponding button.

Dependencies may be requested by this function. For instance, in this example the function depends on “User,” “Actions” and “Crud.” When an action occurs, a reference to the user, invoking this action, can be obtained by requiring the “User” dependency. The “Crud” dependency, which allows the manipulation of database state for these entities, is also requested here. The two methods that return an instance of Crud object are: The method “actionContextCrud()” – returns CRUD for “Event” entity-type since the action “Apply” belongs to it, while the method “crudForEntityType()” – returns CRUD for any entity type identified by its type ID.

CRUD dependencies

The implementation of the action begins by checking if this event is already scheduled for the user, and if not, it creates one. If it is already scheduled, a dialog box is shown by returning the value from calling “Actions.modalResult()”. Besides showing a modal, an action may perform different types of operations in a similar way, such as “navigate to view,” “refresh view,” “show dialog,” and so on.

implementation of the action

User Schedule of Applied Events

After successfully applying to an event, the browser is redirected to the “My Events” view, which shows a list of events the user has applied to. The view is defined by the following configuration:

UserEvent: {
	fields: {
		user: Fields.fixedReference("User", "OnlyNameUser").required(),
		event: Fields.fixedReference("Event", "Event").required(),
		date: Fields.date("Date").required(),
		time: Fields.text("Starts at").masked("99:99").required()
	},
	filtering: function (User) { return {"user.id": User.id} },
	sorting: [['date', -1], ['time', -1]],
	views: {
		MyEvent: {
		title: "My Events",
		showInGrid: ['event', 'date', 'time'],
		permissions: {
			write: [],
			delete: null
		}
		},
		AppliedUser: {
		permissions: {
			write: []
		},
		showInGrid: ['user']
		}
	}
},

In this case, we are using a new configuration property, “filtering.” As with our earlier example, this function also relies on the “User” dependency. If the function returns an object, it is treated as a MongoDB query; the query filters the collection for events that belong only to the current user.

Another interesting property is “Views.” “View” is a regular entity-type, but it’s MongoDB collection is the same as for parent entity-type. This makes it possible to create visually different views for the same data in the database. In fact, we used this feature to create two different views for “UserEvent:” “MyEvent” and “AppliedUser.” Since the prototype of the sub-views is set to the parent entity type, properties that are not overridden are “inherited” from the parent type.

views

Listing Event Attendees

After applying to an event, other users may see a list of all the users planning to attend. This is generated as a result of the following configuration elements in main.js:

AppliedUser: {
    permissions: {
        write: []
    },
    showInGrid: ['user']
}
// ...
appliedUsers: Fields.relation("Applied users", "AppliedUser", "event")

“AppliedUser” is a read-only view for a “MyEvent” entity-type. This read-only permission is enforced by setting an empty array to the “Write” property of the permissions object. Also, as the “Read” permission isn’t defined, by default, reading is permitted for all users.

applieduser for myevent type

Extending Default Implementations

The typical backdraw of RAD frameworks is the lack of flexibility. Once you’ve built your app and you need to customize it, you may encounter significant obstacles. AllcountJS is developed with extensibility in mind and allows replacement of every building block inside.

To achieve that AllcountJS uses it’s own Dependency Injection (DI) implementation. DI allows the developer to override the default behaviors of the framework through extension points, and, at the same time, allows it through the reuse of existing implementations. Many aspects of RAD framework extension are described in the documentations. In this section, we will explore how we may extend two of the many components in the framework, server side logic and views.

Continuing with our Toptal Community example, let us integrate an external data source to aggregate event data. Let’s imagine that there are Toptal Blog posts discussing plans for events the day before each event. With Node.js, it should be be possible to parse the blog’s RSS feed and extract such data. In order to do this, we will need some extra npm dependencies, such as “request,” “xml2js” (to load Toptal Blog RSS feed), “q” (to implement promises) and “moment” (to parse dates). These dependencies can be installed by invoking the following set of commands:

npm install xml2js
npm install request
npm install q
npm install moment

Let’s create another JavaScript file, name it “toptal-community.js” in the toptal-community-allcount directory and populate it with the following:

var request = require('request');
var Q = require('q');
var xml2js = require('xml2js');
var moment = require('moment');
var injection = require('allcountjs');
injection.bindFactory('port', 9080);
injection.bindFactory('dbUrl', 'mongodb://localhost:27017/toptal-community');
injection.bindFactory('gitRepoUrl', 'app-config');

injection.bindFactory('DiscussionEventsImport', function (Crud) {
    return {
        importEvents: function () {
            return Q.nfcall(request, "http://www.toptal.com/blog.rss").then(function (responseAndBody) {
                var body = responseAndBody[1];
                return Q.nfcall(xml2js.parseString, body).then (function (feed) {
                    var events = feed.rss.channel[0].item.map(function (item) { return {
                        eventName: "Discussion of " + item.title, 
                        date: moment(item.pubDate, "DD MMM YYYY").add(1, 'day').toDate(), 
                        time: "12:00"
                    }});
                    var crud = Crud.crudForEntityType('Event');
                    return Q.all(events.map(function (event) {
                        return crud.find({query: {eventName: event.eventName}}).then(function (createdEvent) {
                            if (!createdEvent[0]) {
                                return crud.createEntity(event);
                            }
                        });
                    } ));
                });
            })
        }
    };
});

var server = injection.inject('allcountServerStartup');
server.startup(function (errors) {
    if (errors) {
        throw new Error(errors.join('\n'));
    }
});

In this file, we are defining a dependency called “DiscussionEventsImport,” which we can use in our main.js file by adding an import action on the “Event” entity-type.

{
    id: "import-blog-events",
    name: "Import Blog Events",
    actionTarget: "all-items",
    perform: function (DiscussionEventsImport, Actions) {
        return DiscussionEventsImport.importEvents().then(function () { return Actions.refreshResult() });
    }
}

Since it is important to restart the server after making some changes to the JavaScript files, you may kill the previous instance and start it again by executing the same command as before:

node toptal-community.js

If all goes right, you will see something like the screenshot below after running the “Import Blog Events” action.

Import Blog Events action

So far so good, but let us not stop here. Default views work, but they can be boring at times. Let us customize them a little.

Do you like cards? Everyone likes cards! To make a card view, put the following in a file named events.jade inside the app-config directory:

extends main
include mixins

block vars
    - var hasToolbar = true
block content
    .refresh-form-controller(ng-app='allcount', ng-controller='EntityViewController')
        +defaultToolbar()
        .container.screen-container(ng-cloak)
            +defaultList()
                .row: .col-lg-4.col-md-6.col-xs-12(ng-repeat="item in items") 
                    .panel.panel-default
                        .panel-heading
                            h3 {{item.date | date}} {{item.time}}
                            div
                                button.btn.btn-default.btn-xs(ng-if="!isInEditMode", lc-tooltip="View", ng-click="navigate(item.id)"): i.glyphicon.glyphicon-chevron-right
                                |  
                                button.btn.btn-danger.btn-xs(ng-if="isInEditMode", lc-tooltip="Delete", ng-click="deleteEntity(item)"): i.glyphicon.glyphicon-trash
                        .panel-body
                            h3 {{item.eventName}}
            +noEntries()
            +defaultEditAndCreateForms()

block js
    +entityJs()

After that, simply reference it from the “Event” entity in main.js as “customView: “events”.” Run your app and you should see a card-based interface instead of the default tabular one.

Event entity in main.js

Conclusion

Nowadays, the development flow of web applications is similar across many web technologies, where some operations are repeated over and over again. Is it really worth it? Maybe, it is time to rethink the way your web applications are developed?

AllcountJS provides an alternative approach to rapid application development frameworks; you start by creating a skeleton for the application by defining entity descriptions, and then add views and behavior customizations around it. As you can see, with AllcountJS, we created a simple, yet fully functional application, in less than a hundred lines of code. Maybe, it doesn’t meet all production requirements, but it is customizable. All of this makes AllcountJS a good tool for rapidly bootstrapping web applications.

The post originally appeared on the Toptal Engineering Blog

  • Ask Question