How to Create a Simple Angular Contact Form at Keystone.js
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