Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Javascript

SLGrid Knockout Component for CRUD operations in Single Page Application

4.40/5 (5 votes)
1 Dec 2015CPOL4 min read 17.5K  
Knockout Component SLGrid.

Introduction

SLGrid is Knockout Component with standard grid features: 

  • paging 
  • sorting
  • filtering 
  • inline editing 
  • each cell can contain another Knockout Component like DatePicker, Drop-Down, Calendar or any other reusable component
  • ...

I will present usage of SLGrid component in Single Page Application environment for CRUD operations.
These grid features are very useful for Admin part of any site with many different tables and relations.
Actually, this is attempt to finaly define set of actions needed for Create, Read, Update and Delete some entity.

Background

SLGrid is client Component grid, rather than grid for ASP.NET 5.1, MVC 4.2, PHP 7.8 ...

I decided to follow Steve’s Sanderson example, using 'yeoman' to scaffold out an SPA with Knockout, CrossroadsJS and RequireJS. CrossroadsJS implements hashtag routing, and RequireJS handles module loading and dependencies.

I was following the presentation of 
Steve Sanderson - Architecting large Single Page Applications with Knockout

Generate a starting point for a Knockout application

Getting the Knockout project going was surprisingly easy.
Here are the commands to install 'Yeoman' and scaffold the project:

   npm install -g yo
   npm install -g generator-ko
   yo ko

That gives us a simple project structure with routing and two initial views (Home and About pages).
Adding additional views and components can be done manually, but Yeoman also provides a shortcut command:

   yo ko:component <name>

The key technologies used in this demo are:

  • Knockout Components, clean way of organizing UI code into self-contained, reusable chunks
  • Reguirejs, for Asynchronous Module Definition (AMD)
  • Knockout Validation
  • gulpjs, The streaming build system 
  • babel, JavaScript ES2015 transpiler
  • JavaScript inheritance pattern
  • jQuery for managing Ajax calls to a services
  • Bootstrap for stylization of HTML markup

Try It

Using the code

For CRUD operations of an Entity we need to define 3  JavaScript classes, acutally 3 AMD modules. In example for entity 'People' we define:

  • Person, inherits SLEntity, definition of properties with default value and validation rules.
  • PersonDB, inherits SLEntityDB, database adapter, different for different types of databases, performs ajax calls to server
  • PersonGrid, uses SLGrid and SLPager, keeping list of entites, performing grid operations like paging, sorting, inline editing ...   It defines properties of Person Entity which will be presented in tabular view.   It generates Row and RowEdit templates based on definition of columns.

Actually we can generate these classes using 'yeoman', reading from database definition and respecting all relations from database like: one to many, many to many ...

JavaScript
// Person
class Person extends SLEntity {
    constructor(data) {
        super(data);

        this.PersonId = ko.observable(data.PersonId || 0)
        this.Name = ko.observable(data.Name || "").extend({ required: true, minLength: 2 })
        this.IsOnTwitter = ko.observable(data.IsOnTwitter === undefined ? false : data.IsOnTwitter)
        this.TwitterName = ko.observable(data.TwitterName || "")
        this.City = new CityAsForeign(data.City || {})
        this.NumOfPosts = ko.observable(data.NumOfPosts || 0)
        this.LastLogin = ko.observable(data.LastLogin ? new Date(data.LastLogin) : new Date())
    }
}

Person source code

JavaScript
// PersonDB

class PersonDB extends SLEntityDB {
    constructor(data) {
        super(data);
    }
}

PersonDB.prototype.Url = {
    "get" :     "api/people",
    "getById":  "api/people/getById",
    "add":      "api/people/post",
    "update":   "api/people/put"
}

PersonDB source code

JavaScript
// PersonGrid

class PersonGrid {

  constructor(params) {
    var self = this;

    this.Person = Person;
    this.pageSize = ko.observable(params.pageSize);

    this.pager = ko.observable();
    this.grid = ko.observable();
    this.grid.subscribe(function (grid) {
        // now we have the grid component instantiated
    })

    this.columns = [
        { fieldName: 'PersonId', header: 'Id', width: '50px', align: 'right' },
        { fieldName: 'Name', header: 'Name', width: 'auto' }, 
        { fieldName: 'IsOnTwitter', header: 'Twitter', width: '70px', align: 'center', presentation: 'bindingHandlerCheckbox' },
        { fieldName: 'City', header: 'City', width: '140px', 
                    markup: "<bs-select params='entityAsForeign:City, isViewMode:isViewMode' />" },
        { fieldName: 'NumOfPosts', header: '#Posts', width: '70px', align: 'center' },
        { fieldName: 'LastLogin', header: 'Last Login', width: '180px', align: 'center', 
                    markup: "<sl-date-picker params='date:LastLogin, isViewMode:isViewMode'/>" },
        { fieldName: '', header: 'Inline', width: '80px', presentation: 'bindingHandlerEditInline', sortable: false, align: 'center' },
        { fieldName: '', header: 'Edit', width: '60px', presentation: 'bindingHandlerEdit', sortable: false, align: 'center' }
    ];

    this.filter = new Person({});
    this.filter.City.CityId.subscribe(function (newValue) {
        self.grid()
            .Filter({ CityId: newValue == 'CAPTION' ? 0 : newValue })
            .getItems();
    });

    this.pageSize.subscribe(function (newValue) {
        self.grid().getItems(1 /*page*/, newValue /*pageSize*/);
    });

    // Add
    this.addNew = function () {
        location.hash = '#person-add';
    }

    // Edit
    this.editEntity = function (vm, e) {
        var personDB = Person.prototype.entityDB;
        personDB.getById(vm.PersonId(), function (data) {
            window.personData = data;
            location.hash = '#person-edit/' + vm.PersonId();
        })
    }

  }
}

PersonGrid source code

The Base javascript classes do the job:
  SLEntity source code
  SLEntityDB source code

What about $(document).ready

In this SPA environment we have dynamic loading of pages, on demand,  and we have components everywhere: page is Kncokout component, grid is Knockout component, DropDown is Knockout component ... We could use requirejs/domReady plugin, but thanks to Knockout we have the better solution. We want to run code over the component's element during its binding process,  so the custom binding is the way to go.

We put element into the template:

HTML
<div data-bind="MyComponentHandler:true">

and we set ViewModel like this:

JavaScript
// Binding will be applied when the element is introduced to the document.

    ko.bindingHandlers.MyComponentHandler = {
        init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
            viewModel.myElem = $(element);
            // bind any plugin here
            viewModel.myElem.selectpicker({ size: 10 });                
        }
    }

    class MyComponent {
        constructor(params) {
            this.myElem = null;
            // ...
        }
    }

How child Knockout Components communicate

Components are a powerful, clean way of organizing your UI code into self-contained, reusable chunks.
Knockout Components communicate through 'params' object that will be passed on to the component. 
Typically this is a key-value object containing multiple parameters, and is typically received by the component’s viewmodel constructor.

This picture presents the way Filter (drop-down) and SLGrid communicate:
Image 1

How child Knockout components SLGrid and SLPager get connected

Mocker

The mockjax plugin is a development and testing tool for intercepting and simulating ajax requests made with jQuery with a minimal impact on changes to production code. For the purposes of this sample app, server is actually a mocked server,  which enables us to have live demo, without hosting an actual server anywhere.

Bundling and Minifying

Dynamic loading of components. We set components we need for People into the same bundle

JavaScript
bundles: {
     //...
     'people': [ 'components/person-grid/person-grid',
                 'pages/people/people',
                 'pages/people/add/person-add',
                 'pages/people/edit/person-edit']
 }

That way we get dynamic loading of 'people.js', only when user clicks on 'People' in top bar menu.

People on demand

Yeoman Generator for SLGrid

The plan is to make generator  for SLGrid using 
http://yeoman.io/authoring/

Generator would read any Database definition:

  • tables
  • columns
  • relations

Generating pages and components needed for CRUD operations for some table (Entity).
That way we could generate complete starting point of some Admin site with many tables. 
With well designed components and JavaScript classes, it would be much more easier for developers to customize templates and implement additional logic.

For example reading definition of table City we could generate City class 

JavaScript
class City extends SLentity 
{
    constructor(data) {
        super(data);

        this.CityId = ko.observable(data.CityId)
                           .extend({ defaultValue: 102, required: true, minLength: 2 })
        this.Name = ko.observable(data.Name)
        
        this.CountryId = ko.observable(data.CountryId).extend({ foreignKey: Country })
    }
}

Also 'yeoman' generates others file needed for CRUD operations for City.

  •  CityController at server for operations:
     - getItems
     - getItemById
     - Add
     - Update
     - Remove
  •  CityDto - data transfer object between client and server  

components/
   city-grid/
       city-grid.js
       city-grid.html

models/
   city/
      city.js
      city-db.js

pages/    
    cities/
       add/
           city-add.js
           city-add.html

        edit/
           city-edit.js
           city-edit.html

       cities.js
       cities.html

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)