Friday, January 31, 2014

SharePoint 2013 Client Side Contact Form Using knockout.js and Twitter Bootstrap

Here I am going to implement a simple contact form which will add a contact details to a SharePoint list. I’m using knockout to get user input data and SharePoint 2013 REST service to insert contact details to the list as list item. For the fine UI purpose let’s use twitter bootstrap 3.0 to keep contact form’s user interface responsive and clean.

Following list shows js/css references we need to add.

  • sp.runtime.debug.js and sp.debug.js : for SharePoint 2013 REST service
  • knockout-3.0.0.js : for Knockout
  • jquery-1.10.2.min.js, bootstrap.min.js and bootstrap.min.css : for Twitter Bootstrap

I have created a SharePoint 2013 Custom list named “Contact Us” with following columns.

  • FirstName – Single Line of Text
  • LastName – Single Line of Text
  • Email – Single Line of Text
  • Districts – Choice (CheckBoxes)
  • Message – Multiple Line of Text

Following js snippet shows the structure of viewModel object.


var viewModel = function() {

    // These properties hold TextBoxes content.
    this.firstName = ko.observable("");
    this.lastName = ko.observable("");
    this.email = ko.observable("");
    this.message = ko.observable("");

    // We need an array to map with districts column
    // since it's a choice type.
    this.districts = ko.observableArray();

    // This function will call on submit button click.
    this.submitValues = function() {};
};

Let me roughly explain how this works. On document ready I am getting the Contact Us list object. When user updates the Contact form, those changes get reflected to the viewModel. When user click the submit button, I am creating a list item and assign viewModel properties value to the relevant list column. Finally I am adding the list item to the list and update the list.

Piece of cake right ?

Now let me show how knockout helps us here.

  • to keep a centralized object with fresh form data.
  • to validate the form easily with the help of knockout observables.
  • to keep checkboxes values in an array.

Bellow you can find self-explanatory code with form validations and bootstrap applied.


<div id="contact-form">
    <div class="form-horizontal" role="form">
        <div class="form-group">
            <div class="col-sm-3"></div>
            <div class="col-sm-6">
                <h3 class="text-center">Provide your feedback</h3>
            </div>
            <div class="col-sm-3"></div>
        </div>
        <div class="form-group">
            <div class="col-sm-3"></div>
            <label class="col-sm-2 control-label">First Name</label>
            <div class="col-sm-4">
                <input data-bind="value:firstName,valueUpdate:'afterkeydown'" type="text" class="form-control" />
                <span data-bind="visible:isFirstNameEmpty" style="color:red">*</span>
            </div>
            <div class="col-sm-3"></div>
        </div>
        <div class="form-group">
            <div class="col-sm-3"></div>
            <label class="col-sm-2 control-label">Last Name</label>
            <div class="col-sm-4">
                <input data-bind="value:lastName,valueUpdate:'afterkeydown'" type="text" class="form-control" />
                <span data-bind="visible:isLastNameEmpty" style="color:red">*</span>
            </div>
            <div class="col-sm-3"></div>
        </div>
        <div class="form-group">
            <div class="col-sm-3"></div>
            <label class="col-sm-2 control-label">Email</label>
            <div class="col-sm-4">
                <input data-bind="value:email,valueUpdate:'afterkeydown'" type="text" class="form-control" />
                <span data-bind="visible:isEmailEmpty" style="color:red">*</span>
            </div>
            <div class="col-sm-3"></div>
        </div>
        <div class="form-group">
            <div class="col-sm-3"></div>
            <label class="col-sm-2 control-label">Districts</label>
            <div class="col-sm-4">
                <div class="checkbox"><label><input type="checkbox" data-bind="checked:districts" value="Colombo" />Colombo</label></div>
                <div class="checkbox"><label><input type="checkbox" data-bind="checked:districts" value="Galle" />Galle</label></div>
                <div class="checkbox"><label><input type="checkbox" data-bind="checked:districts" value="Kandy" />Kandy</label></div>
                <div class="checkbox"><label><input type="checkbox"data-bind="checked:districts" value="Jaffna" />Jaffna</label></div>
                <div class="checkbox"><label><input type="checkbox"data-bind="checked:districts" value="Other" />Other</label></div>
            </div>
            <div class="col-sm-3"></div>
        </div>
        <div class="form-group">
            <div class="col-sm-3"></div>
            <label class="col-sm-2 control-label">Message</label>
            <div class="col-sm-4">
                <textarea data-bind="value:message,valueUpdate:'afterkeydown'" class="form-control" form-groups="4"></textarea>
                <span data-bind="visible:isMessageEmpty" style="color:red">*</span>
            </div>
            <div class="col-sm-3"></div>
        </div>
        <div class="form-group">
            <div class="col-sm-3"></div>
            <label class="col-sm-2 control-label"></label>
            <div class="col-sm-4">
                <input data-bind="click:submitValues" type="button" class="btn btn-default" value="Submit" />
            </div>
            <div class="col-sm-3"></div>
        </div>
    </div>
</div>  
  

var context,
    web,
    list,
    emailRegex = /^\s*[\w\-\+_]+(\.[\w\-\+_]+)*\@[\w\-\+_]+\.[\w\-\+_]+(\.[\w\-\+_]+)*\s*$/,
    thankyouMsg = "Your feedback was successfully submitted.",
    errorMsg = "Something went wrong. Please try again";

$(document).ready(function() {
    context = SP.ClientContext.get_current();
    web = context.get_web();
    list = web.get_lists().getByTitle("Contact Us");
});

var viewModel = function() {
    var self = this;
    self.firstName = ko.observable("");
    self.lastName = ko.observable("");
    self.email = ko.observable("");
    self.districts = ko.observableArray();
    self.message = ko.observable("");

    self.isFirstNameEmpty = ko.computed(function() {
        if ($.trim(self.firstName()).length > 0) return false;
        return true;
    }, self);

    self.isLastNameEmpty = ko.computed(function() {
        if ($.trim(self.lastName()).length > 0) return false;
        return true;
    }, self);

    self.isEmailEmpty = ko.computed(function() {
        if ($.trim(self.email()).length > 0 && emailRegex.test(self.email())) return false;
        return true;
    }, self);

    self.isMessageEmpty = ko.computed(function() {
        if ($.trim(self.message()).length > 0) return false;
        return true;
    }, self);

    self.submitValues = function() {

        if (self.isFirstNameEmpty() || self.isLastNameEmpty() || self.isEmailEmpty() || self.isMessageEmpty()) {
            alert('Please fill mandatory fields');
            return false;
        }

        try {
            var ici = new SP.ListItemCreationInformation();
            var item = list.addItem(ici);
            item.set_item("FirstName", self.firstName());
            item.set_item("LastName", self.lastName());
            item.set_item("Email", self.email());
            item.set_item("District", self.districts());
            item.set_item("Message", self.message());
            item.update();

            context.executeQueryAsync(function() {
                self.firstName("");
                self.lastName("");
                self.email("");
                self.districts([]);
                self.message("");
                alert(thankyouMsg);
            }, function() {
                alert(errorMsg);
            });

        } catch (e) {
            alert('Something went wrong. Please try again\n');
        }

    }; //End of submit values.
};

ko.applyBindings(new viewModel());