Simple Angular Form at Keystone.js

11/2/2015·11 min read

This is a tutorial on how to create a contact form with AngularJS at keystone.js. It is for the keystone-classic, not the latest keystone. The keystone-classic is built on top of node.js, express.js on server-side, and database using MongoDB. Use Jade to translate the template and translate server-side data to the UI side. It has the contact form, but this blog shows how to build Angularjs form inside the keystone-classic. Don't ask why, it is just something comes into your mind, and want to give it a try, but hopefully this would help you if this is what you're looking for.

Define API Routing

We will create a contact form, so first we'll need to define the API route at keystone.js server side./p>

We gonna define an HTTP post /api/contact for our angular form to use later to post contact form data. The second and third parameter is to define API routing at keystone.js.routes.views.contact.create is a point to a file that contains the actual API code.

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

API backend code

At the backend of the API code, we call our Enquiry model to create data then return either error or success message based on the logic.


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

Define MongoDB schema, so Keystone.js is using MongoDB for database storage. You can host your own MongoDB or use any MongoDB cloud service. Keystone.js is also using npm package Mongoose to access MongoDB. Following is the code from keystone.js, I only modify a couple of 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: 'someone is sending message',
				email: 'contact@mytech.com'
			},
			subject: 'New Enquiry for me',
			enquiry: enqiury
		}, callback);
		
	});
	
}

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

Test the API

Before creating any client-side code, you need to make sure the server-side API is available. Following this, I'm using Postman, a chrome extension tool for testing API. After you verified the result, now I'm ready to create an angular form app since the server is ready.

Angular Form App Structure

First, let's define the app structure, I'll define my angular form app as following. It'll contain two pages, one is a page for users to submit contact information. Another is the confirmation page.

Create Angular Javascript Code

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


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 an Angular Controller

Here we create an Angular controller, I only defined ui submit function, which is posting the UI form to the server-side API and redirect to the confirm page. To share scope information I create another 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

This is just a code snippet for creating a scopeService that shared objects 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 the user submits 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 the following CSS class for style in a different case. Each following CSS class shows a different state of the ng-model.

Detailed description checkout this link.

ng-valid
the model contains a valid value
ng-invalid
the model contains an invalid value
ng-valid-[key]
the model contains a valid value, the key can be added as $setValidity
ng-invalid-[key]
the model contains an 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 the user had entered something into 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 the previous page, createController


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

ConfirmController's HTML

The confirmController's HTML just binds 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 the 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 to 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 of that. You only need to be aware of which one should be put 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')

That's, the following is a sample image of how our two angular page contact form page will look like.

CreateController

ConfirmController