KnockoutJS: Editable Grids with TypeScript

Screen Shot 2015-12-09 at 1.21.26 PMWith the abundance of JavaScript libraries and frameworks available today, it’s hard to decide what is going to work best for a certain requirement. Add in the fact that there are many server-side tools that can also accomplish the task and you could spend hours just narrowing down options to test before deciding on the path you’ll take in the end. This was a recent conundrum for me when approached to incorporate child data management in the parent forms on a SharePoint 2010 project. My experience with JavaScript has been limited over my career because I’ve been so focused on the backend of SharePoint during the majority of that time. My current client has need for a better user experience, so I’ve been trying to fill that hole in my skills.  This project offered an opportunity to do just that.

While it’s possible to put an ASP GridView control in an Update Panel, a client-side approach seemed cleaner and a way to expand my JavaScript skills. I looked at many options like JQuery Datatables, koGrid, and a few others, but they didn’t give me the look, price (free), and/or TypeScript definitions for me to easily take off with implementing it.

I decided to build my own solution since it would be a relatively simple design and it would let me dig into KnockoutJS. In addition, it would be easier to use TypeScript to build an easier-to-maintain solution since it incorporates many of the ECMAScript 6 features like classes and modules, among others.

I’m not going to go into detail about the ins and outs of Knockout or TypeScript. There are tons of great resources online from Pluralsight, the TypeScript official site, and the interactive Knockout tutorials. I will show what my solution looked like and cover the key pieces. There are three main files we’ll look at: the grid View, the data object model (ContractOption) and the ViewModel.

View

Contract list form with an Option in edit mode.

Figure 1 – This is the Contract list form with an Option in edit mode.

I created an ASP.Net Web Control that mimics a SharePoint List Form for the Contracts list and then added the markup for the grid of Contract Options after the Contract fields. The data-bind HTML attribute is a cornerstone of Knockout views. As you can see in the code below, I use it to:

  1. Loop through each Player Option
    I simply want to loop through each Active Player Option.  My ‘data’ parameter is a Knockout Computed array in the ViewModel.  While it’s not as necessary in this example, I prefer to use the ‘as’ parameter for Knockout for each bindings because it is easier to follow which object you are referencing inside the loop. This context variable would be extremely useful in instances like nested loops.
  2. Determine which action links will be displayed for each item, if any
    One of the business rules from the original project was to only show actions for the last item in the collection. While my example of sports contracts probably doesn’t fit exactly within this rule, I did keep this in place. So, the user can use player options until a team option is added. The action links will only be displayed on the most recent option based on the end date. The ‘if’ binding will only show the action links if the player option number (another requirement from the original project) matches the length of the collection and if there are no team options. Also, if the list form is in display mode, they shouldn’t be allowed to modify any of the options. In addition, each action link has conditions on when it is visible. The edit and delete links are only available when the option is not being edited, while the save and cancel links will be visible when it is in edit mode.
  3. Determine which control to show if the Option is in Display Mode or Edit Mode
    This is pretty similar to the action links visibility. For the original project, the start date of an option is to be determined automatically, so only the end date needs to be editable. There are two controls in the end date cell. One is the label for read-only and a textbox for edit mode. NOTE: The original project has been upgraded to use templates instead of inline controls, but I haven’t updated this project to do that.  It’s definitely the cleaner approach, especially for more complex solutions.
  4. Bind events to buttons and links
    The action links and “add player option” button need to have click events set to functions in the ViewModel. The button is pretty straight-forward as it binds to the function “addPlayerOption” in the root-scoped object, the ViewModel. However, you’ll notice that the action links are formatted differently. I use the “bind” function to pass in the “$root.” This is to maintain the scope of “this.”
  5. Display field data
    Binding the actual data of the option to the controls is quite simple. The labels’ text property is bound to the Model parameter for that particular cell. The end date textbox, though, actually binds to the “option.EndDate.editValue.” The “editValue” property is a custom property I created to store the changed value of the field. We’ll cover it in more detail when we look at the Model, but this allows for cancelling the changes as well as being able to evaluate both the original value and the changed value at save time.
<table class="optionGrid">
    &amp;amp;amp;amp;lt;thead&amp;amp;amp;amp;gt;
        &amp;amp;amp;amp;lt;tr&amp;amp;amp;amp;gt;
            &amp;amp;amp;amp;lt;th&amp;amp;amp;amp;gt;
                Action
            &amp;amp;amp;amp;lt;/th&amp;amp;amp;amp;gt;
            &amp;amp;amp;amp;lt;th&amp;amp;amp;amp;gt;
                Option #
            &amp;amp;amp;amp;lt;/th&amp;amp;amp;amp;gt;
            &amp;amp;amp;amp;lt;th&amp;amp;amp;amp;gt;
                Start Date
            &amp;amp;amp;amp;lt;/th&amp;amp;amp;amp;gt;
            &amp;amp;amp;amp;lt;th&amp;amp;amp;amp;gt;
                End Date
            &amp;amp;amp;amp;lt;/th&amp;amp;amp;amp;gt;
        &amp;amp;amp;amp;lt;/tr&amp;amp;amp;amp;gt;
    &amp;amp;amp;amp;lt;/thead&amp;amp;amp;amp;gt;
    &amp;amp;amp;amp;lt;tbody data-bind=&amp;amp;amp;amp;quot;foreach: { data: activePlayerOptions, as: 'option' }&amp;amp;amp;amp;quot;&amp;amp;amp;amp;gt;
        &amp;amp;amp;amp;lt;tr&amp;amp;amp;amp;gt;
            &amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;
                &amp;amp;amp;amp;lt;div data-bind=&amp;amp;amp;amp;quot;if: ((option.OptionNumber == $root.activePlayerOptions().length) &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; $root.activeTeamOptions().length == 0 &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; ($root.formMode != 'Display'))&amp;amp;amp;amp;quot;&amp;amp;amp;amp;gt;
                    &amp;amp;amp;amp;lt;a href=&amp;amp;amp;amp;quot;#&amp;amp;amp;amp;quot; data-bind=&amp;amp;amp;amp;quot;click: $root.editContractOption.bind($root), visible: !$root.isPlayerOptionEditing(option)&amp;amp;amp;amp;quot;&amp;amp;amp;amp;gt;Edit&amp;amp;amp;amp;lt;/a&amp;amp;amp;amp;gt;
                    &amp;amp;amp;amp;lt;a href=&amp;amp;amp;amp;quot;#&amp;amp;amp;amp;quot; data-bind=&amp;amp;amp;amp;quot;click: $root.deleteContractOption.bind($root), visible: !$root.isPlayerOptionEditing(option)&amp;amp;amp;amp;quot;&amp;amp;amp;amp;gt;Delete&amp;amp;amp;amp;lt;/a&amp;amp;amp;amp;gt;
                    &amp;amp;amp;amp;lt;a href=&amp;amp;amp;amp;quot;#&amp;amp;amp;amp;quot; data-bind=&amp;amp;amp;amp;quot;click: $root.saveContractOption.bind($root), visible: $root.isPlayerOptionEditing(option)&amp;amp;amp;amp;quot;&amp;amp;amp;amp;gt;Update&amp;amp;amp;amp;lt;/a&amp;amp;amp;amp;gt;
                    &amp;amp;amp;amp;lt;a href=&amp;amp;amp;amp;quot;#&amp;amp;amp;amp;quot; data-bind=&amp;amp;amp;amp;quot;click: $root.cancelUpdateContractOption.bind($root), visible: $root.isPlayerOptionEditing(option)&amp;amp;amp;amp;quot;&amp;amp;amp;amp;gt;Cancel&amp;amp;amp;amp;lt;/a&amp;amp;amp;amp;gt;
                &amp;amp;amp;amp;lt;/div&amp;amp;amp;amp;gt;
            &amp;amp;amp;amp;lt;/td&amp;amp;amp;amp;gt;
            &amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;
                &amp;amp;amp;amp;lt;label data-bind=&amp;amp;amp;amp;quot;text: option.OptionNumber&amp;amp;amp;amp;quot; /&amp;amp;amp;amp;gt;
            &amp;amp;amp;amp;lt;/td&amp;amp;amp;amp;gt;
            &amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;
                &amp;amp;amp;amp;lt;label data-bind=&amp;amp;amp;amp;quot;text: option.StartDate&amp;amp;amp;amp;quot; /&amp;amp;amp;amp;gt;
            &amp;amp;amp;amp;lt;/td&amp;amp;amp;amp;gt;
            &amp;amp;amp;amp;lt;td&amp;amp;amp;amp;gt;
                &amp;amp;amp;amp;lt;input type=&amp;amp;amp;amp;quot;text&amp;amp;amp;amp;quot; class=&amp;amp;amp;amp;quot;editOptionEndDate&amp;amp;amp;amp;quot; data-bind=&amp;amp;amp;amp;quot;value: option.EndDate.editValue, visible: $root.isPlayerOptionEditing(option)&amp;amp;amp;amp;quot; /&amp;amp;amp;amp;gt;
                &amp;amp;amp;amp;lt;label data-bind=&amp;amp;amp;amp;quot;text: option.EndDate, visible: !$root.isPlayerOptionEditing(option)&amp;amp;amp;amp;quot; /&amp;amp;amp;amp;gt;
            &amp;amp;amp;amp;lt;/td&amp;amp;amp;amp;gt;
        &amp;amp;amp;amp;lt;/tr&amp;amp;amp;amp;gt;
    &amp;amp;amp;amp;lt;/tbody&amp;amp;amp;amp;gt;
&amp;amp;amp;amp;lt;/table&amp;amp;amp;amp;gt;
&amp;amp;amp;amp;lt;button data-bind=&amp;amp;amp;amp;quot;click: $root.addPlayerOption, visible: ($root.activeTeamOptions().length == 0 &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; $root.formMode != 'Display')&amp;amp;amp;amp;quot;&amp;amp;amp;amp;gt;Add Player Option&amp;amp;amp;amp;lt;/button&amp;amp;amp;amp;gt;

Model

The data model for ContractOption contains the various properties, most of which match up to SharePoint list fields. To enable inline editing for the grid, though, the “beginEdit” function is added to a Knockout Observable. This associates the functions for rollback and commit.

export class ContractOption {
    public Id: number;
    public OptionNumber: number;
    public StartDate: KnockoutObservable&amp;amp;amp;amp;lt;string&amp;amp;amp;amp;gt;;
    public EndDate: KnockoutObservable&amp;amp;amp;amp;lt;string&amp;amp;amp;amp;gt;;
    public ActiveStatus: KnockoutObservable&amp;amp;amp;amp;lt;string&amp;amp;amp;amp;gt;;
    public OptionType: string;
    public IsDirty: boolean;

    beginEdit(transaction) {
        this.EndDate.beginEdit(transaction);
    }

    toJSON() {
        var copy = ko.toJS(this);
        return copy;
    }

    constructor(data) {
        this.Id = data.Id;
        this.OptionNumber = data.OptionNumber;
        this.StartDate = ko.observable&amp;amp;amp;amp;lt;string&amp;amp;amp;amp;gt;(data.StartDate).extend({ editable: true });;
        this.EndDate = ko.observable&amp;amp;amp;amp;lt;string&amp;amp;amp;amp;gt;(data.EndDate).extend({ editable: true });
        this.ActiveStatus = ko.observable&amp;amp;amp;amp;lt;string&amp;amp;amp;amp;gt;(data.ActiveStatus);
        this.IsDirty = data.IsDirty;
        this.OptionType = data.OptionType;
    }
}

In the constructor, I extend the observables for EndDate and StartDate (although this isn’t necessary for this solution) to add an editable function. This function will populate the editValue property reference we saw in the view with the current EndDate.

ko.extenders.editable = function (target, option) {
    if ($.isArray(target()))
        target.editValue = ko.observableArray(target().slice());
    else
        target.editValue = ko.observable(target());
};

ko.observable.fn.beginEdit = function (transaction) {
    var self = this;
    var commitSubscription,
        rollbackSubscription;

    self.dispose = function () {        
        // kill this subscriptions
        commitSubscription.dispose();
        rollbackSubscription.dispose();
    };

    self.commit = function () {
        // EDIT NOTE: Removed date validation logic for brevity
            self(self.editValue());
            self.dispose();
    };

    self.rollback = function () {
        // rollback the edit value
        self.editValue(self());
        
        // dispose the subscriptions
        self.dispose();
    };
    
    //  subscribe to the transation commit and reject calls
    commitSubscription = transaction.subscribe(self.commit,
        self,
        &amp;amp;amp;amp;quot;commit&amp;amp;amp;amp;quot;);

    rollbackSubscription = transaction.subscribe(self.rollback,
        self,
        &amp;amp;amp;amp;quot;rollback&amp;amp;amp;amp;quot;);

    return self;
}

ViewModel

The ViewModel is where everything comes together. As mentioned before, the grid actually uses the activePlayerOption computed array. As you can see, there’s an observable array for all contract options. This is the full collection of all items in the SharePoint contract options list that are related to this contract. There’s a computed array for playerOptions which only contains the options that are flagged as “Player Option.”  Finally, the activePlayerOption computed array takes playerOptions and selects only the active options.  The original project required historical records of the options so, instead of actually deleting the list item, an active status field is set to “deleted.”  When a record for that option number is re-added, it will change that field back to “active.”

There are two other class-level variables that are needed. The editTransaction variable is a Knockout Subscribable. This creates the subscription to the beginEdit function in the Model for the commit and rollback functionality. The editingItem Observable is set when an item enters edit mode. It is used in the “isPlayerOptionEditing” function to determine if the current item is being edited so the appropriate action links and EndDate control.

private contractOptions: KnockoutObservableArray&amp;amp;amp;amp;lt;ContractOption&amp;amp;amp;amp;gt; = ko.observableArray([]);

//All Option Years, TeamOptions
public playerOptions = ko.computed(() =&amp;amp;amp;amp;gt; {
    return ko.utils.arrayFilter(this.contractOptions(), function (item: ContractOption) {
        return item.OptionType === EditableGrid.ContractOptionType.Player;
    });
});
public teamOptions = ko.computed(() =&amp;amp;amp;amp;gt; {
    return ko.utils.arrayFilter(this.contractOptions(), function (item: ContractOption) {
        return item.OptionType === EditableGrid.ContractOptionType.Team;
    });
});

//Active Option Years, TeamOptions
public activePlayerOptions = ko.computed(() =&amp;amp;amp;amp;gt; {
    return ko.utils.arrayFilter(this.playerOptions(), function (item: ContractOption) {
        return item.ActiveStatus() == 'Active';
    });
});
public activeTeamOptions = ko.computed(() =&amp;amp;amp;amp;gt; {
    return ko.utils.arrayFilter(this.teamOptions(), function (item: ContractOption) {
        return item.ActiveStatus() == 'Active';
    });
});

public editingItem: KnockoutObservable&amp;amp;amp;amp;lt;ContractOption&amp;amp;amp;amp;gt; = ko.observable&amp;amp;amp;amp;lt;ContractOption&amp;amp;amp;amp;gt;();

public editTransaction = new ko.subscribable();

The final pieces to look at are the action link functions. However, I’m not going to go into detail on the new Option method as that doesn’t have much to do with the grid.

Edit
This is where we get the editTransaction initialized and set the editingItem.

editContractOption(currentContractOption: ContractOption) {
    if (this.editingItem() == null) {
        currentContractOption.beginEdit(this.editTransaction);
        $(&amp;amp;amp;amp;quot;input[id='startDateForEdit']&amp;amp;amp;amp;quot;).val(currentContractOption.StartDate());
        this.editingItem(currentContractOption);
    }
}

Save
This function calls the commit through editTransaction which sets the Option EndDate to the new value then updates the list item asynchronously. The “onItemUpdated” function sets the editingItem to null which will put the item back into read-only mode.The hasSuccess flag is set in the commit function if the EndDate value is valid and updated.  This was a requirement for the original project and was kept because it just made sense to validate the value.

saveContractOption(currentContractOption: ContractOption) {
    this.editTransaction.notifySubscribers(null, &amp;amp;amp;amp;quot;commit&amp;amp;amp;amp;quot;);

    var hasSuccess = ($(&amp;amp;amp;amp;quot;input[id='editIsSuccessful']&amp;amp;amp;amp;quot;).val() === 'true');

    var that = this;

    if (hasSuccess) {
        if (this.formMode === &amp;amp;amp;amp;quot;New&amp;amp;amp;amp;quot;)
            this.onItemUpdated();
        else {
            var tempOption = currentContractOption;
            this.updateListItem(tempOption, false).then(
                function (item) {
                    that.onItemUpdated();
                },
                function (sender, args) {
                    that.onQueryFailed(sender, args);
                }
            );
        }
    }
}

Cancel
This fires the rollback, setting the EndDate value back to the original then sets the editingItem back to null.

cancelUpdateContractOption(currentContractOption: ContractOption) {
    this.editTransaction.notifySubscribers(null, &amp;amp;amp;amp;quot;rollback&amp;amp;amp;amp;quot;);

    this.editingItem(null);
}

Delete
Since the record isn’t actually deleted from the list, it updates that ActiveStatus field and updates the List Item asynchronously.

deleteContractOption(currentContractOption: ContractOption) {
    currentContractOption.ActiveStatus('Deleted');

    var that = this;

    if (this.formMode === &amp;amp;amp;amp;quot;New&amp;amp;amp;amp;quot;)
        this.onItemDeleted();
    else {
        var tempOption = currentContractOption;
        this.deleteListItem(tempOption).then(
            function (item) {
                that.onItemUpdated();
            },
            function (sender, args) {
                that.onQueryFailed(sender, args);
            }
            );
    }
}

Conclusion

This project provided me a great first experience with TypeScript and Knockout. It probably wouldn’t be hard to allow multiple items, if that was something you wanted.  There are some things that could use some refactoring, but it provides a good example on how to accomplish inline editing grids.  I’ve made this project available on GitHub.

About Sam Larko

Sam Larko has been working with SharePoint for six years and across the last three versions. While his experience is primarily in development, Sam has become more involved in administration and architecture over the last three years. In addition to SharePoint, he has a strong desire to become involved in mobile development, especially Android and Windows Phone.