← back to the blog


Simple Angular Form at Keystone.js

Posted in Node.js, Angular.js, Express.js, Jade by dake

This is a tutorial how to create angular app at keystone.js. Keystone.js is CMS build on top of node.js, express.js at server side. Use Jade for translate template and translate server side data to the UI side. First we need to modify the server side so client side will be able to call.

Define API Routing at server side Keystone.js

Here we define a http post /api/contact for our angular form to use later. the second and third parameter is for define api routing at keystone.js.routes.views.contact.create is point to a file contains the actual api code.

// index.js
exports = module.exports = function (app) {
app.post('/api/contact', keystone.initAPI, routes.views.contact.create);
}

API code

Here, we call our Enquiry model to add data and return error to the client or successful message, in this case is empty. Now at this point your server side code is finish, I also attached the keystne.js Enquiry Model class.


exports.create = function (req, res) {
    var application = new Enquiry.model(),
        updater = application.getUpdateHandler(req);

    updater.process(req.body, {
        flashErrors: true,
        fields: 'name, email, phone, enquiryType, message',
        errorMessage: 'There was a problem submitting your enquiry:'
    }, function (err) {
        if (err) {
            res.apiResponse(err.errors);
        } else {
            res.apiResponse();
        }
    });
}

Enquiry Model - MongoDB document

Keystone.js for database storage by default is using MongoDB, you have host mongodb your own or use other MongoDB clould service. Keystone.js is using Mongoose to access MongoDB. Following is the code from keystone.js, I only modify the couple things.


var keystone = require('keystone'),
	Types = keystone.Field.Types;

var Enquiry = new keystone.List('Enquiry', {
	nocreate: true,
	noedit: true
});

Enquiry.add({
	name: { type: Types.Name },
	email: { type: Types.Email },
	phone: { type: String },
	enquiryType: { type: Types.Select, options: [
		{ value: 'message', label: "Just leaving a message" },
		{ value: 'question', label: "I've got a question" },
		{ value: 'other', label: "Something else..." }
	] },
	message: { type: Types.Markdown, required: true },
	createdAt: { type: Date, default: Date.now }
});

Enquiry.schema.pre('save', function(next) {
	this.wasNew = this.isNew;
	next();
})

Enquiry.schema.post('save', function() {
	if (this.wasNew) {
		this.sendNotificationEmail();
	}
});

Enquiry.schema.methods.sendNotificationEmail = function(callback) {
	
	var enqiury = this;
	
	keystone.list('User').model.find().where('isAdmin', true).exec(function(err, admins) {
		
		if (err) return callback(err);
		
		new keystone.Email('enquiry-notification').send({
			to: admins,
			from: {
				name: 'dake tech',
				email: 'contact@dake-tech.com'
			},
			subject: 'New Enquiry for me',
			enquiry: enqiury
		}, callback);
		
	});
	
}

Enquiry.defaultSort = '-createdAt';
Enquiry.defaultColumns = 'name, email, enquiryType, createdAt';
Enquiry.register();


Test the API

Before create any client side code, you need to make sure the server side API is available. Following, I'm using Postman, a chrome extension tool for testing API. Now I'm ready to create angular form app since server is ready.

Angular Form App Structure

As you can see, I've defined my angular form app as following. Angular app contains two pages, one is a page for user to submit contact information. Another is the confirmation page.

app.js

app.js, I define my pages, create and confirm page's angular controller and html. Also, define my module's name and this angular app's dependencies, which so far is only ngRoute.


var contactUsApp = angular.module('dhe.contactUs', ['ngRoute']);
contactUsApp.config(["$routeProvider", function ($routeProvider) {
        'use strict';
        $routeProvider.
      when('/', {
            templateUrl: '/apps/contactUs/views/create.html',
            controller: 'createCtrl'
        }).
      when('/confirm', {
            templateUrl: '/apps/contactUs/views/confirm.html',
            controller: 'confirmCtrl'
        }).
      otherwise({
            redirectTo: '/'
        });
    }]);

Create Controller setup

Here at the create controller, I only defined ui submit function, which is posting the UI form to the server side api and redirect to the confirm page. For share scope information I create anthoer service as scopeServcie.


/* globals contactUsApp */
contactUsApp.controller('createCtrl', 
    function ($scope, $http, $window, scopeService) {
    'use strict';
    function formPost() {
        if ($scope.contactForm.$valid) {
            $http.post("/api/contact", $scope.cu).success(function () {
                scopeService.cu = $scope.cu;
                $window.location.href = "#confirm";
            });

        }
        else {
                
        }
    }
    
    $scope.ui = {
        submit: formPost
    };
});

Factory for share scope globally at the app

Simple, but just a object shared between controllers.


contactUsApp.factory('scopeService', 
    function () {
    'use strict';
    return {};
});

Create Html page

Compare to the controller part, html is a little more code. I need to add ng-model to the text box so angular validation will work. Also when user submit the button add the ng-click to the createController's submit function.


<div class="container">
    <h1>Contact</h1>
    <p>Do you need to contact me? You can just go to the message and write to me, rest of fields are all optional.</p>
    <div class="row">
        <div class="col-sm-8 col-md-6">
            <form name="contactForm" novalidate>
                <input type="hidden" name="action" value="contact">
                <div class="form-group">
                    <label>Name</label>
                    <input type="text" name="name.full" class="form-control" placeholder="(optional)" ng-model="cu.name">
                </div>
                <div class="form-group">
                    <label>Email</label>
                    <input type="email" name="email" class="form-control" placeholder="(optional)" ng-model="cu.email">
                </div>
                <div class="form-group">
                    <label>Phone</label>
                    <input type="text" name="phone" placeholder="(optional)" class="form-control" ng-model="cu.phone">
                </div>
                <div class="form-group">
                    <label>What are you contacting about?</label>
                    <select name="enquiryType" class="form-control" ng-model="cu.enquiryType">
                        <option value="">(select one)</option>
                        <option value="message">Just leaving a message</option>
                        <option value="question">I've got a question</option>
                        <option value="other">Something else...</option>
                    </select>
                </div>
                <div class="form-group">
                    <label>Message</label>
                    <textarea name="message" placeholder="Leave us a message..." rows="4" class="form-control" ng-model="cu.message" required></textarea>
                </div>
                <div class="form-actions">
                    <a class="btn btn-primary" ng-disabled="contactForm.$invalid && contactForm.$dirty" ng-click="ui.submit()">Send</a>
                </div>
            </form>
        </div>
    </div>
</div>

Angular Validation class

Angular had added following css class for style in different case. Each following css class shows different state of the ng-model.

Detail description checkout this link.

ng-valid
the model contains valid value
ng-invalid
the model contains invalid value
ng-valid-[key]
the model contains valid value, the key can be added as $setValidity
ng-invalid-[key]
the model contains invalid value, the key can be added as $setValidity
ng-pristine
user has not used the control yet
ng-dirty
user already used the control.
ng-touched
the control has been blurred
ng-untouched
the control hasn't been blurred
ng-pending
any $asyncValidators are unfulfilled

Angular Validation Style

Following style is only when user had enter something to the form and the form contains invalid input.


input.ng-dirty.ng-invalid, select.ng-dirty.ng-invalid, textarea.ng-dirty.ng-invalid{
    border: 1px solid #dd4b39;
}

ConfirmController

ConfirmController almost like just one line of code, basically using the scopeService create above to get the scope from previous page, createController


contactUsApp.controller('confirmCtrl',  
    function ($scope, scopeService) {
    'use strict';
    $scope.cu = scopeService.cu;
});

ConfirmController's html

The confirmController's html is just bind the scope to the UI.


<article class="container">
    <h1>Contact</h1>
    <h3>Thanks, I got your message...</h3>
    <div class="form-group" ng-show="cu.name">
        <label>Name: </label>
        {{cu.name}}
    </div>
    <div class="form-group" ng-show="cu.email">
        <label>Email: </label>
        {{cu.email}}
    </div>
    <div class="form-group" ng-show="cu.phone">
        <label>Phone: </label>
        {{cu.phone}}
    </div>
    <div class="form-group" ng-show="cu.enquiryType">
        <label>Enquiry Type</label>
        {{cu.enquiryType}}
    </div>
    <div class="form-group" ng-show="cu.message">
        <label>Message: </label>
        {{cu.message}}
    </div>
</article>

Load Script for Angular App

I'm using jade, so following is how I defined the angular script to my jade page. In production mode, should bundle all the script together. Also, for angular script loader and the order of how script load. You only need to be aware of the first there javascript here, angular.js, angular-route, app.js. If you need to load any other angular app to your own app, you have to put that app before your app.js. That's the only rule for loading angular javascript in this way. But the rest of the script, controllers, services, views, directives, angular will take care that. You only need to be aware which one should be put the first.


block js
	script(src='/js/vendor/angular.js')
	script(src='/js/vendor/angular-route.js')
	script(src='/apps/contactUs/app.js')
	script(src='/apps/contactUs/controllers/confirmController.js')
	script(src='/apps/contactUs/controllers/createController.js')
	script(src='/apps/contactUs/services/scopeService.js')

Source Code

I don't have a open source code for this one yet, but you can check the live demo site at here.

What next, I'm planning to pull request at keystone.js and add this example.

keystone.js code at github

CreateController

ConfirmController