Improving UX in web pages (ASP.MVC 4, jQuery Accordion and Knockout.JS) | ASSIST Software Romania
get in touch
>

LIKE

SHARE

Facebook Share Tweet LinkedIn Share

FOLLOW

LinkedIn Follow Xing Follow

I. Existing problem

In one of the pages in a Web Application project, an admin user has the option to set up a list of questions for a form template, which will later be used in the application by regular users, to submit a list of answers specific to that form. The question configuration was looking like below:

The first problem with that configuration, as it can be seen, is the fact that we can have plenty of questions in that form (in this case 30). In the screenshot above only 3 can be seen, imagine the page/form with 30 questions. It’s 10 times bigger.

Another problem is the fact that you must pay a fair amount of attention to notice where a question ends and where the next one starts.

Although there is no clear indication anywhere, each question has an order, which makes this the third problem. Closely related to this, the fourth problem, is the fact that if you decide to change the order of a question in the form template, you have to do that by manually deleting the questions (by clicking the X button next to the question name) and create them again in the order you want.

II. Solution

  The Accordion

As you can see, most of the problems are solved. Accordion enforces only one/none active item/tab open at a given moment in time, for a great UX. Header information is comprised of the order index and the text of the question. If needed, these can be further enhanced with other information.

III. The best thing of all

The drag and drop question reordering 

The watch before drag and drop:

 Improving UX in web pages. ASSIST Software - Ion Balan - The watch before drag and drop

Drag

Drop

The watch after drag and drop:

IV. Technical solution

1. jQuery accordion

var newOrder = new Array();

$(document).ready(function () {
    $("#accordion").accordion({
        collapsible: true,
        header: "> div > h3",
        icons: false,
        autoFill: true,
        active: false,
        header: 'h3'
    }).sortable({
        axis: "y",
        handle: "h3",
        stop: function (event, ui) {
            newOrder = new Array();
            $('div.questions > div.accordionPanel > input.orderId').each(function () {
                //get the id
                var id = $(this).attr("value");
                newOrder.push(id);
            });
            appViewModel.refresh(newOrder)
        }
    });

    $('#addQuestionLink').click(function () {
        appViewModel.addQuestion(uuidv4(), "", newQuestionsJSON.Type);
        $('.questions').accordion("refresh");
        $("#accordion").accordion({ active: -1 });
        return false;
    });
});
$('#accordion').on('accordionactivate', function (event, ui) {
    if (ui.newPanel.length) {
        $('#accordion').sortable('disable');
    } else {
        $('#accordion').sortable({
            enable: 'enable',

        });
    }
});

 

2. Knockout view model

function AppViewModel() {
    var self = this;

    self.questionList = ko.observableArray();

    self.addQuestion = function (qid, qOrder, title, qIsMandatory) {

        var question = {
            id: ko.observable(qid),
            order: ko.observable(qOrder),
            isMandatory: ko.observable(qIsMandatory),
            questionText: ko.observable(title),
            questionType: ko.observable(qType)

        };
        var delayedQuestionType = ko.pureComputed(question.questionType).extend({ throttle: 50 });
        delayedQuestionType.subscribe(function (value) {
            $('.questions').accordion("refresh");
        });
        self.questionList.push(question);
    };

    self.removeQuestion = function () {
        self.questionList.remove(this);
    }
    self.refresh = function (newOrder) {
        var newList = [];
        newOrder.forEach(function (id, index) {
            var item = ko.utils.arrayFirst(self.questionList(), function (item) {
                return item.id() === id;
            });
            if (item != null) {
                item.order(index);
                newList.push(item);
            }
        });
        self.questionList(newList);
    }
    self.setQuestions = function (questions) {
        questions.forEach(function (question, index) {
            self.addQuestion(question.Id, question.Order, question.Text,question.IsMandatory, question.Type)
        });
    }
}
 
3. The ASP .NET MVC display templates for the list of questions (partially included, as an example only)

 The list itself:
@model List<MyWebApplication.ViewModels.QuestionViewModel>

@{
    Layout = null;
}

<div class="panel-body">
    <div id="accordion" class="questions form-template-questions">
        @Html.EditorFor(model=>model[0],"Question")
    </div>
</div>

 The list item (QuestionViewModel):

<!-- ko foreach: questionList -->
    <div class="accordionPanel namePathRoot">
        @Html.HiddenFor(model => model.Id, new { data_bind = "value: id(), namePath: true" ,@class = "orderId"})
        @Html.HiddenFor(model => model.Order, new { data_bind = "value: order(), namePath: true" })
        <h3 data-bind="text: $index() + 1 + '. ' + questionText()"><span class="required" data-bind="visible: isRequired() ">*</span></h3>
        <div style="-ms-align-content: stretch; -webkit-align-content:stretch; align-content:stretch">
            <div class="input-group">
                @Html.TextBoxFor(x => x.Text, new { maxLength = 400, @class = "form-control", data_bind = "value: questionText, namePath: true" })
                <div class="action input-group-addon">
                    <a href="#" data-bind="click: $parent.removeQuestion" class="remove glyphicon glyphicon-remove" title="@MyWebApplication.Resources. Manage.Tooltip_Question_Delete"></a>
                </div>
                @Html.ValidationMessageFor(x => x.Text)
            </div>
            <div class="form-group">
                @Html.LabelFor(x=>x.IsMandatory)
                @Html.CheckBoxWithNamePathBinding("IsMandatory",Model.IsMandatory, new { data_bind = "checked: isMandatory, namePath: true" } )
            </div>
            <div class="questionType form-group">
                @Html.LabelFor(x=>x.Type)
                @{
                    @Html.EnumDropDownListFor(x => x.Type, typeof(MyWebApplication. Domain.QuestionType), new { @class = "form-control", data_bind = "value: questionType, namePath: true" }, typeof(MyWebApplication.Resources.Enums), false)
                }
            </div>

V. Some of the challenges

a. The items reordering – as it can be seen, it’s done when an item stops being dragged, by calling appViewModel.refresh(newOrder). Then the Knockout binding makes sure that the new order is bound into the ASP.NET MVC model.

b. Knockout support for collection of objects – this is not supported, instead it’s done through custom binding ‘namePath’(namePath :true). You can find more information if you access this page.
 

VI. Conclusion

It was a good exercise to combine all these 3 technologies, ASP.NET MVC, jQuery and Knockout.JS. Many other things can be accomplished in a similar manner, however if you have complex objects or if Knockout.JS was not one of your original choices while your application evolved, then things can easily get complicated. However, if you do have simple objects, or you are working on new features, then Knockout.JS can help provide a significant improvement to the UX, as well as helping keep your code clean and sane, by taking away some uninteresting concerns (like binding – or passing values from client to server side) on client side.  
Finally, each piece of technology can make our lives easier only if used correctly. It is up to us to make sure that that happens.

 

If you enjoyed reading this, you can also check my previous article:

Vous souhaitez nous contacter ? 

Si vous êtes intéressés par nos services de développement de logiciel, si vous souhaitez rejoindre notre équipe, ou si vous souhaitez tout simplement en savoir plus sur nous, nous sommes à votre disposition. Contactez-nous ! Écrivez-nous et un membre de l'équipe ASSIST vous répondra dans les plus brefs délais. Nous pourrons certainement vous ASSISTer.

CONTACTEZ-NOUS