Binding the user controls

Now that we have converted our application to use AMD modules, let's gradually increase the complexity of the application. Currently, we search the DOM for individual elements and attach event handlers to them. This isn't necessarily a bad thing; however, it does leave our code vulnerable to a number of possible issues. The DOM may not have finished rendering the objects we are attempting to attach to, or the object could have been removed by another segment of code. Another pitfall of this approach is that if we wanted to add another drawing shape type to our application, we would have to add another element to the HTML and then more code to add the event handler. This is inefficient and could open up the possibility of making a mistake somewhere. Fortunately, there are a number of libraries available that allow us to implement binding patterns such as Model View Controller (MVC) and Model View ViewModel (MVVM). Knockout is one of these libraries, and it brings MVVM to JavaScript and TypeScript development.

Reusable controls

In the previous chapter, we learned about Knockout and its foreach binding that allows us to create a template in HTML that each object in an array will be bound to. We will apply the same concept to each of the options we have for our drawing application. Some of these objects are very similar in type and functionality, such as the type selection controls. However, the color picker has a very different interface to implement. This is where the Knockout template binding will come in handy.

The Knockout template binding allows us to provide an HTML template for a specific object type. This gives us the ability to create a separation between our HTML and the JavaScript that is running. In this case, we will have a list of user controls that will be displayed in the space above our canvas object. Before we create the template that we want to bind to, we will need to define the abstraction they will be bound to. The top layer of this abstraction will be the IUserControl type, which will contain two properties, id and templateName, and will be the minimum requirement for binding our DOM elements to:

export interface IUserControl {
    id: string;
    templateName: string;
}

We will also need interfaces to represent the tool selection buttons and the color picker. These interfaces are more specific to their direct functionality, but as long as they provide a template name that matches it, all of our bindings will flow seamlessly. These interfaces will be defined in a file called ControlsTypes.ts in the Controls directory and can be seen in the following code:

export interface IUpdateObservable {
    observable: KnockoutObservable<any>;
}
export interface IToolSelectionControl extends IUserControl, IUpdateObservable {
    toolType: DrawingTypes.DrawingToolType;
    click(viewModel: any, event: JQueryEventObject);
    observable: KnockoutObservable<DrawingTypes.DrawingToolType>;
    buttonText: KnockoutObservable<string>;
}
export interface IColorSelectionControl extends IUserControl, IUpdateObservable {
    change(viewModel: any, event: JQueryEventObject);
    observable: KnockoutObservable<string>;
}

As you can see, the IToolSelectionControl interface has a toolType property that will store the value that this control will be responsible for. There is also a click function that will serve as an event handler in the HTML bindings we will create shortly. The remaining two objects are Knockout observables, one of which will be used to inform the calling application that a new tool has been selected and the other will define the text that is displayed by the button. The IColorSelectionControl interface extends IUserControl in the same way that the IToolSelectionControl interface does, but the additional properties here are very different. This interface defines a change method that will be called when the color input change event fires. The final property of this interface is very similar to the observable property on the IToolSelectionControl interface. The primary difference between the two is that they store different types; in this case, we store the string value that represents the current color. Due to the close relationship between these two interfaces, we abstracted the IUpdateObservable interface out and then provided a more specific type definition experience within the more finely-grained interfaces.

Now that we have our interfaces defined, we can build our HTML templates to represent the different controls we wish to create. The first template will contain only a button element that has bindings of its own applied to it. The second template is the color picker with a different set of bindings that is unique to this element type. The final thing we must do is modify the area of our HTML that currently holds our controls. As you can see, the resulting HTML has no direct controls defined, but as controls are added to an observable list called userControls, they will appear in our application:

Reusable controls

As you can see, we use the foreach binding to create a new control for each object in the UserControls list that our HTML will be bound to. Inside this, there is a span element that is bound to the IUserControl interface. Our canvas object will remain unchanged for now, but there are some new elements at the bottom of the page. These new script tags will contain our HTML templates that the user controls will bind to. The button template creates its bindings around the tool selection control interface, binding the text to the ButtonText observable and the click event to the Click function. The color picker only needs to bind the change event to the Change method.

The final piece of this is to implement each of our interfaces and bind our view model to the DOM. Each of these types will be placed in separate files in the Controls directory. All of the files can be seen in the following screenshot:

Reusable controls

We will start with the base user control object, which will be in the UserControlBase.ts file that the remainder of our types will extend:

import ControlsTypes = require('Scripts/Controls/ControlsTypes'),
class UserControlBase implements ControlsTypes.IUserControl {
    public templateName: string = "TemplateNotProvided";
    constructor(public id: string) {
    }
}
export = UserControlBase;

The id property comes in as a required property, and therefore will always have a value. The templateName string is set to a bad value by default, and this ensures that any type extending the UserControl type will be forced to update this property. If an invalid template name is provided, Knockout will generate an error at runtime. However, if an empty string is provided, execution will continue as normal. Now let's provide the implementation for our tool selection buttons:

import ControlsTypes = require('Scripts/Controls/ControlsTypes'),
import UserControlBase = require('Scripts/Controls/UserControlBase'),
import DrawingTypes = require('Scripts/Drawing/DrawingTypes'),
class ToolSelectionControl extends UserControlBase
    implements ControlsTypes.IToolSelectionControl {
    public buttonText: KnockoutObservable<string> = ko.observable("");
    constructor(id: string, public toolType: DrawingTypes.DrawingToolType,
        public observable: KnockoutObservable<DrawingTypes.DrawingToolType>) {
        super(id);
        this.templateName = "ButtonTemplate";
        this.buttonText(DrawingTypes.DrawingToolType[this.toolType]);
    }
    public click(viewModel: any, event: JQueryEventObject) {
        this.observable(this.toolType);
    }
}
export = ToolSelectionControl;

The overall code here isn't very flashy, but it allows us to add new tool buttons with significantly less effort than what was previously required. The constructor of this type takes in the object's ID to pass to the base type constructor, the DrawingToolType value that the object instance will represent, and finally a Knockout observable that will be updated when the button is clicked. Since this is a concrete implementation, we must provide a valid name for our HTML template, and finally, we use a bit of enumeration magic to provide the value for our button's text. The click event does nothing more than assign the stored tool type to the observable that was provided when the object was created. The color selection control has an even simpler implementation:

import ControlsTypes = require('Scripts/Controls/ControlsTypes'),
import UserControlBase = require('Scripts/Controls/UserControlBase'),
class ColorSelectionControl extends UserControlBase
    implements ControlsTypes.IColorSelectionControl {
    constructor(id: string, public observable: KnockoutObservable<string>) {
        super(id);
        this.templateName = "ColorPickerTemplate";
    }
    public change(viewModel: any, event: JQueryEventObject) {
        this.observable((<any>event.currentTarget).value);
    }
}
export = ColorSelectionControl;

Creating a ViewModel

Now that we have our reusable controls set up, we need to create the view model that our application will be bound to and make some modifications to our existing types to ensure execution runs smoothly. To maintain the single responsibility pattern, we should create a new model that the UI will be bound to rather than using the DrawingModel class, which is responsible for maintaining the state of the shapes in our application. This new model will be bound to the DOM using Knockout and will be responsible for creating and maintaining the state of the application. It will reside in a file called DrawingModel.ts inside of the Drawing directory. Before we can create this model, we will need to modify the existing drawing model so that our application view model will be able to update the drawing model when the user interacts with the controls. As you can see, in the new interface definition provided, we have made the drawing tool and drawing color properties public members:

export interface IDrawingModel {
    selection: IDrawingShape;
    shapes: IDrawingShape[];
    addShape(shape: IDrawingShape);
    getNewShape(location: ShapeTypes.IPoint): IDrawingShape;
    drawingTool: DrawingToolType;
    drawingColor: string;
}

The getDrawingTool method is no longer necessary because we are providing public access to the underlying instance member. If we were dealing with released software, we would not want to modify this interface directly, but instead extend it and provide a new implementation for the DrawingModel type. However, since this is only a sample application, we can modify it to improve our overall design despite the breaking changes this will cause. Only the internal implementation of this class and a single reference inside of the canvas engine will need to be modified to return our application to a working state. Once these fixes have been made, we can effectively design the application view model that will reside in a file called DrawingApplicationModel.ts in the Scripts directory:

import ControlsTypes = require('Scripts/Controls/ControlsTypes'),
import DrawingModel = require('Scripts/Drawing/DrawingModel'),
import CanvasEngine = require('Scripts/Drawing/CanvasEngine'),
import Controls = require('Scripts/Controls/Controls'),
import DrawingTypes = require('Scripts/Drawing/DrawingTypes'),

class DrawingApplicationModel {
    public userControls: KnockoutObservableArray<ControlsTypes.IUserControl> =
                        ko.observableArray([]);
    public selectedToolType: KnockoutObservable<DrawingTypes.DrawingToolType> =
                            ko.observable(DrawingTypes.DrawingToolType.Select);
    public selectedColor: KnockoutObservable<string> = ko.observable("#000000");
    private _drawingModel: DrawingTypes.IDrawingModel;
    constructor(canvas: HTMLCanvasElement) {
        this._drawingModel = new DrawingModel();
        var engine = new CanvasEngine(canvas, this._drawingModel);
        this._buildControls();
        this._createSubscriptions();
    }    
    private _buildControls() {
        var selectionControl = new Controls.ToolSelectionControl("selectButton",
            DrawingTypes.DrawingToolType.Select, this.selectedToolType);
        var rectangleControl = new Controls.ToolSelectionControl("rectangleButton",
            DrawingTypes.DrawingToolType.Rectangle, this.selectedToolType);
        var lineControl = new Controls.ToolSelectionControl("lineButton",
            DrawingTypes.DrawingToolType.Line, this.selectedToolType);
        var colorControl = new Controls.ColorSelectionControl("colorPicker",
            this.selectedColor);
        this.userControls.push(selectionControl, rectangleControl,
            lineControl, colorControl);
    }
    private _createSubscriptions() {
        this.selectedToolType.subscribe((newValue) => {
            if (this._drawingModel) {
                this._drawingModel.drawingTool = newValue;
            }
        });
        this.selectedColor.subscribe((newValue) => {
            if (this._drawingModel) {
                this._drawingModel.drawingColor = newValue;
            }
        });
    }
}
export = DrawingApplicationModel;

We will need three Knockout observable types: one for the controls we wish to display, another to maintain the currently selected control, and a final one to track the current color. This view model will create and modify the drawing model and canvas engine objects as necessary as well. The constructor takes HTMLCanvasElement, which the engine will use as a parameter and the rest of the application is isolated to within this class. We have two private initialization methods. The first method builds the array of controls that the DOM is bound to. As you can see, we now use code to define these objects rather than manipulate the DOM directly. Each of the tool selection controls receives the observable that the application model uses to track the state of the observable object from the IUpdateObservable interface. The color picker, on the other hand, receives the SelectedColor observable. As you can see, it is now as easy as adding a few lines of code to create a whole new button:

var freehandControl = new Controls.ToolSelectionControl("freehandButton",
            DrawingTypes.DrawingToolType.Freehand, this.SelectedToolType);
        this.userControls.push(freehandControl);

Once all of these objects are instantiated, they are added to the controls array, causing the Knockout binding to fire and display all of our buttons. This does not get us all the way to the goal, however. The user will be able to interact with each of these controls and their values will be updated in the application model, but the drawing model will have no concept of these changes. The second private method in our application model will bind listeners to the observable values, and any time the value of an observable changes, we will be able to execute some code. In this case, any time the observable maintaining the selected tool changes, we will update the drawing model's DrawingTool property, and any time the selected color changes, we will modify the DrawingColor property.

The entry point into our application, app.ts, has now been simplified into just a couple of lines of code that creates the application model and binds it to the DOM:

import DrawingApplicationModel = require('Scripts/DrawingApplicationModel'),
$(document).ready(() => {
    var canvas: HTMLCanvasElement =
        <HTMLCanvasElement>window.document.getElementById("drawingCanvas");
    ko.applyBindings(new DrawingApplicationModel(canvas));
});

While the look of our application has not changed at all from the end user's perspective, we have accomplished quite a bit from a development standpoint. Our code has been separated into smaller logical chunks within a nested structure for folder-driven discovery. Adding new user controls requires no direct DOM interaction, and neither does our connection between the DOM and our application model. The unfortunate side effect of this is that it makes the loading of our page inefficient.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset