May 18, 2013

CRUD Operations using jQuery Dialog with WYSIWYG HTML Editor, Knockout and ASP.NET Web API

In this article, We will implement CRUD (Create, Read, update and delete) operations with Knockout.js, ASP.NET Web API, Entity Framework 5.0 Database First Approach. For Add or Edit operation, A modal dialog having WYSIWYG Html editor is used. User can enter information and submit it. It should look like the image below:

edit comment

Creating Project:

Open VS2012

File > New Project > ASP.NET MVC 4 Web Application > Enter name & Path > OK

Select Web API as template > OK

DB Structure:

Consider the following table structure:

edit comment

Models:

Right Click on Model Folder > Add > New Item > Ado.Net Entity Data Model > Enter Name > Add

Generate from database > Next

Select Database > Enter Entity Name (DBEntities) > Next

Select Comments Table > Enter Model Name > Finish

Web API Controller:

Right Click on Controllers folder > Add > Controller

Enter Name "CommentController" > Select Template "API Controller with Read/Write Actions ...", Select Model (Comment) and Data Context Class (DBEntities)> Add

HTML Editor:

For simplicity, We are going to use same app to consume Web API.

We are going to use jHtmlArea Editor built on top of jQuery.

To install jHtmlArea, run the following command in the Package Manager Console

Install-Package jHtmlArea

Client-Side JavaScript:

If you see default SPA(Single Page Application) template, there are three layers:

1. todo.datacontext.js: responsible for AJAX requests to the Web API controllers.

2. todo.model.js: Defines the client side models.

3. todo.viewmodel.js: Defines the view model.

There is one more file:

4. todo.bindings.js: for knockout custom bindings

To implement all above layers, add a new script file say 'Comment.js' and reference it in the view.

1. Creating Client Side Model:


// Model
function Comment(data) {
    var self = this;
    data = data || {};

    // Persisted properties
    self.CommentID = data.CommentID;
    self.Name = ko.observable(data.Name || "");
    self.Email = ko.observable(data.Email || "");
    self.HTMLComment = ko.observable(data.HTMLComment || "");
    self.error = ko.observable();
    //persist edits to real values on accept
    self.acceptChanges = function () {
        self.HTMLComment($('.htmlEditor').htmlarea('toHtmlString'));
        dataContext.saveComment(self);
        window.commentViewModel.selectedComment(null);
    };

    //reset to originals on cancel
    self.cancelChanges = function () {
        window.commentViewModel.selectedComment(null);
    };

    self.deleteComment = function () {
        if (confirm('Do you really want to delete this comment?')) {
            dataContext.deleteComment(self).done(function () {
                window.commentViewModel.comments.remove(self);
            });            
        }
    }

}

In above model, three methods are implemented to save comment(acceptChanges), to cancel the changes(cancelChanges) and to delete comment(deleteComment).

We will add a class 'htmlEditor' to comment textarea and here it is used to get user entered value.

2. Data Context Layer:


// DataContext 
var commentApiUrl = '/api/comment';
var dataContext = {
    saveComment: function (comment) {
        var InsertMode = comment.CommentID ? false : true;
        comment.error(null);

        if (InsertMode) {
            var options = {
                dataType: "json",
                contentType: "application/json",
                cache: false,
                type:  'POST',
                data: ko.toJSON(comment)
            };
            return $.ajax(commentApiUrl, options)
                 .done(function (result) {
                     comment.CommentID = result.CommentID;                    
                     window.commentViewModel.comments.push(comment);                     
                 })
                 .fail(function () {
                     alert('Error on adding comment');
                 });

        }
        else {
            var options = {
                dataType: "json",
                contentType: "application/json",
                cache: false,
                type: 'PUT',
                data: ko.toJSON(comment)
            };
            return $.ajax(commentApiUrl+"/" + comment.CommentID, options)                 
                 .fail(function () {
                     comment.error('Error on adding comment');
                 });
        }
    },

    getComments: function (commentObservable, errorObservable) {
        var options = {
            dataType: "json",
            contentType: "application/json",
            cache: false,
            type: 'GET'
        };

        return $.ajax(commentApiUrl, options)
             .done(getSucceeded)
             .fail(getFailed);

        function getSucceeded(data) {
            var mappedComments = $.map(data, function (list) { return new Comment(list); });
            commentObservable(mappedComments);
        }

        function getFailed() {
            errorObservable("Error retrieving comments.");
        }
    },

    deleteComment: function (comment) {
        var options = {
            dataType: "json",
            contentType: "application/json",
            cache: false,
            type: 'DELETE'           
        };
        return $.ajax(commentApiUrl + "/" + comment.CommentID, options)
             .fail(function () {
                 comment.error('Error on adding comment');
             });

    }
}

It handles AJAX calls to the Web API controllers to get all comments and add, edit or delete comment.

3. View Model Layer:


//ViewModel
window.commentViewModel = (function () {
    var comments = ko.observableArray(),
    error = ko.observable(),
    addComment = function () {
        selectedComment(new Comment());
        loadEditor();
    },
    selectedComment = ko.observable(''),
    editComment = function (comment) {
        selectedComment(comment);
        loadEditor();
    };
    dataContext.getComments(comments, error);

    function loadEditor() {

        $(".htmlEditor").htmlarea({          
            // Override/Specify the Toolbar buttons to show
            toolbar: [
                ["bold", "italic", "h1", "link", //"image", 
                 "orderedList", "unorderedList", "horizontalrule", {
                     // Add a custom Toolbar Button
                     css: "justifyleft",
                     text: "Code Sample <pre><code>",
                     action: function (btn) {
                         // 'this' = jHtmlArea object                         
                         this.pasteHTML('<pre><code>' + this.getSelectedHTML() + '</code></pre>');
                     }
                 }]
            ],
            toolbarText: $.extend({}, jHtmlArea.defaultOptions.toolbarText, {
                h1: "Heading 1", horizontalrule: "Insert Horizontal Rule"
            })
        });
    }
    return {
        comments: comments,
        error: error,
        addComment: addComment,
        selectedComment: selectedComment,
        editComment: editComment
    }
})();


In above viewmodel, loadEditor method is used to initialize jHtmlArea editor and a custom toolbar button is added to insert the code.

4. Custom Bindings:

To initialize jquery UI dialog we use Knockout custom binding.


//custom binding to initialize a jQuery UI dialog
ko.bindingHandlers.jqDialog = {
    init: function (element, valueAccessor) {
        var options = ko.utils.unwrapObservable(valueAccessor()) || {};

        //handle disposal
        ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
            $(element).dialog("destroy");
        });

        $(element).dialog(options);
    }
};

//custom binding handler that opens/closes the dialog
ko.bindingHandlers.openDialog = {
    update: function (element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor());
        if (value) {
            $(element).dialog("open");
        } else {
            $(element).dialog("close");
        }
    }
}

5. Activating Knockout:

to bind the view model to the view


ko.applyBindings(window.commentViewModel);

View:

First reference jHtmlArea css in the head tag.


    @Styles.Render("~/Content/css")
    @Styles.Render("~/Content/themes/base/css")
    @Scripts.Render("~/bundles/modernizr")
    <link href="~/Content/jHtmlArea/jHtmlArea.css" rel="stylesheet" />

For "Add Comment" button


   <a type="button" class="linkButton"
            data-bind="click: addComment">Add Comment</a>

To display all existing comments with Edit Delete options:


 <p class="error" data-bind="text: error"></p>
            <div style="" data-bind="foreach: { data: comments }, visible: comments().length > 0">
                <div class="content">
                    <p class="error" data-bind="text: error"></p>
                    <span class="cname" data-bind="text: Name"></span>- <span class="cemail" data-bind="    text: Email"></span>
                    <div data-bind="html: HTMLComment"></div>
                    <a data-bind="click: $root.editComment" href="javascript:void(0)">Edit</a> | 
                     <a data-bind="click: deleteComment" href="javascript:void(0)">Delete</a>
                </div>
            </div>

To display Comment Form in jQuery UI dialog for Add, Edit operations:


<div id="dialog" title="Comment"
                data-bind="jqDialog: { 'autoOpen': false, 'resizable': false, 'modal': true, 'width': '60%' }, template: { 'name': 'descTmpl', 'data': selectedComment, if: selectedComment }, openDialog: selectedComment">
            </div>
            <script id="descTmpl" type="text/html">
                <p>
                    Name: 
                    <input type="text" data-bind="value: Name" />
                </p>
                <p>
                    Email: 
                    <input type="text" data-bind="value: Email" />
                </p>
                <p>
                    Comment: 
                    <textarea id="dialogEditor" rows="10" style="width: 100%" class="htmlEditor"  data-bind="value: HTMLComment"></textarea>
                </p>
                <input type="button" class="linkButton" value="OK" data-bind="click: acceptChanges" />
                <input type="button" class="linkButton" value="Cancel" data-bind="click: cancelChanges" />
            </script>

Make sure to add jHtmlArea and Comment.js script references.


  @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/jqueryui")
    <script src="~/Scripts/knockout-2.2.0.js"></script>
    <script src="~/Scripts/jHtmlArea-0.7.5.min.js"></script>
    <script src="~/Scripts/Comment.js"></script>

Conclusion:

We have implemented CRUD operations with knockout.js and asp.net web api. The main tricky part is to load html editor in jquery UI dialog on Add,Edit operations. You can download source code from GitHub. The next step is to validate html input to prevent XSS attack, Check out my next post for this.

Hope, you enjoy it.