Dynamic Bootstrap Modal Content with Knockout

This is a demonstration of how to use Knockout.js with Twitter Bootstrap to edit a collection of items.

by Francesco Frustaci

HTML

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.0/css/bootstrap.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-modal/2.2.6/css/bootstrap-modal.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-modal/2.2.6/css/bootstrap-modal-bs3patch.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-migrate/1.4.0/jquery-migrate.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.0/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-modal/2.2.6/js/bootstrap-modalmanager.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-modal/2.2.6/js/bootstrap-modal.min.js"></script>

<div class="row">
  <div class="col-sm-6 col-sm-offset-3">
    <button type="button" class="btn btn-success btn-sm pull-right"
            data-bind="click: AddNewUser">
      <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
      Add New User
    </button>
    <h2 style="margin-top: 0px;">
      Users
      <span class="badge" data-bind="text: Users().length"></span>
    </h2>
  </div>
</div>

<div class="row">
  <div class="col-sm-6 col-sm-offset-3">
    <ul data-bind="foreach: Users" class="list-group hover">
      <li class="list-group-item">
        <div class="row">
          <div class="col-xs-6">
            <span data-bind="text: Name" style="word-wrap: break-word;"></span>
          </div>
          <div class="col-xs-2">
            <span data-bind="text: Age() + ' yrs old'" class="badge"></span>
          </div>
          <div class="col-xs-4 text-right">
            <a href="#" data-bind="click: $parent.EditUser">Edit</a> |
            <a href="#" data-bind="click:...

SCSS

/**** These styles are here to override the styles in 'bootstrap-modal-bs3patch.css' ****/

$screen-sm-min = 768px; //<== Standard Bootstrap width

.modal {
  left: 25%;
  width: 50% !important;
  
  @media screen and (-webkit-min-device-pixel-ratio:0) { 
    /* Safari and Chrome */
    left: 50%;
    width: 500px;
  }
  
  @media (max-width: $screen-sm-min) {
    left: 0;
    width: 100% !important;
  }
}

JavaScript

ko.bindingHandlers['modal'] = {
  init: function(element, valueAccessor, allBindingsAccessor) {
    var allBindings = allBindingsAccessor();
    var $element = $(element);
    $element.addClass('hide modal');
    
    if (allBindings.modalOptions && allBindings.modalOptions.beforeClose) {
      $element.on('hide', function() {
        var value = ko.utils.unwrapObservable(valueAccessor());
        return allBindings.modalOptions.beforeClose(value);
      });
    }
  },
  update: function(element, valueAccessor) {
    var value = ko.utils.unwrapObservable(valueAccessor());
    if (value) {
      $(element).removeClass('hide').modal('show');
    } else {
      $(element).modal('hide');
    }
  }
};

/* ViewModel for the individual records in our collection. */
var User = function(name, age) {
  var self = this;
  self.Name = ko.observable(ko.utils.unwrapObservable(name));
  self.Age = ko.observable(ko.utils.unwrapObservable(age));
}

/* The page's main ViewModel. */
var ViewModel = function() {
  var self = this;
  self.Users = ko.observableArray();
  
  self.ValidationErrors = ko.observableArray([]);
  
  // Logic to ensure that user being edited is in a valid state
  self.ValidateUser = function(user) {
    if (!user) {
      return false;
    }
    
    var currentUser = ko.utils.unwrapObservable(user);
    var currentName = ko.utils.unwrapObservable(currentUser.Name);
    var currentAge = ko.utils.unwrapObservable(currentUser.Age);
    
    self.ValidationErrors.removeAll(); // Clear out any previous errors
    
    if (!currentName)
      self.ValidationErrors.push("The user's name is required.");
    
    if (!currentAge) {
      self.ValidationErrors.push("Please enter the user's age.");
    } else { // Just some arbitrary checks here...
      if (Number(currentAge) == currentAge && currentAge % 1 === 0) { // is a whole number
        if (currentAge < 2) {
          self.ValidationErrors.push("The user's age must be 2 or greater.");
        } else if...