Umbraco, knockout js and forms part 1

This post is about how to implement Umbraco MVC forms with Unobtrusive bootstrap validation and use knockout js to perform data binding. It also covers adding additional functions and properties to mapped observables without having to add them to your c# view models.

The problem

I needed to develop a product page that would show product details, allow the user to submit a review and show already submitted reviews. Each review needs to allow users to comment on it, rate the review and potentially share it. Review and comment submissions need to be added as content nodes below the relevant node. Then, site editors can go in, moderate and publish them. However, all this functionality needed to happen in the page, without lots of page refreshes or redirects off to other pages. I also wanted to use my existing c# view models and mvc forms so I could make use of unobtrusive validation and avoid having to duplicate the validation functionality on the client.

Solution overview

I decided to use knockout js to handle all the in-page dynamic stuff and bind to/from an Umbraco api controller. The final solution uses c# view models and data annotations along with jquery bootstrap unobtrusive validation to handle client side form validation. I used the knockoutjs mapping plugin to map from c# view models to javascript view models.

The Product Review Form C# side

In my main template, I’m calling a partial and passing the current page’s Id to it so the review can be added as a child of the right page.

@Html.Partial("_ProductReviewForm", new ProductReviewViewModel(Model.Content.Id))

This is happening inside a bootstrap modal, so the form pops up dynamically when the ‘submit review’ button is pressed. product-review-form

Inside the _ProductReviewForm partial, I’m inheriting from UmbracoViewPage so I have access to all the properties of that view model.

The form tag has a knockout data binding on the submit event. This event passes the form element to that function.

<form data-bind="submit: submitReview" method="post" id="productReviewForm">
<div class="form-group">
    @Html.LabelFor(x => x.Name)
    @Html.ValidationMessageFor(x => x.Name)
    @Html.TextBoxFor(x => x.Name, new { @class = "form-control", data_bind = "value: Name" })
</div>
@* ... *@

Because the page is inheriting from a c# view model, I can tie into all the data annotations set up on that view model.

//ProductReviewViewModel.cs
[Display(Name = "Name")]
[Required(ErrorMessage = "Name is required")]
public string Name { get; set; }

A side note

One really weird thing I found that resulting in no small amount of hair pulling and shouting was data annotations on properties of type ‘int.’ If I added display names and error message data annotations, when it came to submitting the form using knockout, it would always come back with a 500 error code. When I removed the data annotations on int properties, it worked. Very weird.

Another side note

You may notice in the image above a star rating field. This is modified from the excellent example by james barnett. Thanks a lot! The razor markup for this is as follows

@Html.LabelFor(x => x.Rating, new { @style = "display:block" })
@Html.ValidationMessageFor(x => x.Rating)
<fieldset class="rating cf">
  @for (int i = 5; i > 0; i--)
  {
    @Html.RadioButtonFor(x => x.Rating, i, new { @id = "star" + i, value = i, data_bind = "checked: Rating" })
    <label for="star@(i)" class="full" title="@i star(s)"></label>
  }
</fieldset>

Where ‘Rating’ is an int property of the view model. Notice the knockout ‘checked’ binding that is used to update the knockout view model value.

The final bit of the form partial is the knockoutjs binding code, as follows…

@{
string data = new JavaScriptSerializer().Serialize(Model);
<script type="text/javascript">
    var productReviewFormViewModel = new ProductReviewFormViewModel(@Html.Raw(data));
    ko.applyBindings(productReviewFormViewModel, document.getElementById('productReviewForm'));
</script>
}

This takes the view model data, creates a javascript view model from it and then applies the knockout bindings. One point to note here is that since I will be applying knockout bindings later on in the page to other elements, I need to supply a context for this binding. That’s where the second argument of the ‘applyBindings’ function comes in.

The Product Review Form JS side

From here, its over to js land to handle the bindings and form submissions. Firstly, the data that comes into the javascript ‘constructor’ function is bound using the knockout mapping plugin…

ProductReviewFormViewModel = function (data) {
  var self = this;
  ko.mapping.fromJS(data, {}, self);
  //...

This says map the data to this object and don’t bother about any extra mapping parameters. Knockoutjs then creates observables from each of the properties that are mapped. The only other bit of this stage is to handle the form submission. This, again, initially caused a lot of ranting and sighing, mainly because this was quite new to me. In the end, this is how I got it working…

self.submitReview = function (formElement) {
var form = $(formElement);
if (form.valid()) {  // 1
  $.ajax({
    url: "/umbraco/api/ProductReview/SubmitReview/",
    type: "POST",
    data: ko.mapping.toJSON(this), // 2
    contentType: "application/json; charset=utf-8",
    dataType: 'json',
    success: function (data) {
      //hide the form show success message
      var msg = $("<div class='alert alert-success' role='alert'>%c</div>".replace('%c', data.Data)).hide();
      form.hide(400, function () {
        form.before(msg);
        form.remove();
        msg.fadeIn(400);
      });
    }
  });
}
return false;

};

I’ll take each bit here…

  1. When the form comes in to the function, I transform it to a jquery object. I can then call the ‘valid()’ function on it (part of jquery validation). If its valid, the data is posted.
  2. This bit is crucial. You can’t call ko.toJSON(this). That won’t work, believe me, I tried. Also, ko.mapping.toJSON(self) won’t work, neither will ko.mapping.toJSON(form). It has to be ‘this.’ The rest is pretty standard ajax stuff.

Saving the data

This bits fairly straight-forward. The data comes into an Umbraco api controller and I just used Umbraco’s ContentService, created the content, set the values then saved it. It’s pretty trivial but here’s the code…

var content = cs.CreateContent(DateTime.Today.ToString("yy/MM/dd"), model.ParentId, DocTypes.ProductReview, 0);
content.SetValue("title", model.Title);
//...rest of the values
content.Name = string.Format("{0} - {1}", model.Name, model.Title);
cs.Save(content);

Conclusion

This bit has talked about how to use c# viewmodels with data annotations and bind them to knockout js view models using knockout js mapping. You get all the benefits of data annotations, mvc forms and unobtrusive validation along with the benefits of knockout js, without having to manually create loads of validation rules or duplicate properties in the knockout viewmodels.

The next post is going to talk about how to get all submitted reviews, display them, and enable rating and commenting functionality on them, all using knockout js. This is going to include knockout templates, child observables, adding additional functions/observables to pre-mapped view models, extending knockout binding handlers, and implementing a ‘show more’ function to dynamically load another page of results. Stay tuned!

Leave a Reply