Chapter 7. Enhancing the Drawing Application

When we started the drawing application in Chapter 5, Creating a Simple Drawing Application, we grouped all of our code together by functionality. This allowed us to reduce the number of script tags required for our application to work. Using what we learned in the previous chapter, we will modify the drawing application to use AMD modules and RequireJS. We will also integrate Knockout and jQuery to improve the overall user experience. Finally, we will look at how we can use RequireJS to build all of our application code into a single minified file for deployment optimization. The enhancements we will make to our application include:

  • Converting to AMD modules
  • Binding user controls
  • Generating a single output file
  • Styling the application

Converting to AMD modules

Converting to AMD modules is a fairly simple process. However, this is a good time to look at the structure and maintainability of our project. Before getting started, we must install the RequireJS NuGet package Version 2.1.14 and the corresponding declaration file requirejs.TypeScript.DefinitelyTyped Version 0.2.0.

Converting to AMD modules

The TypeScript build settings must also be changed to use the AMD module system as shown in the previous chapter. Once these steps are completed, we can begin enhancing our application. To do this, we will want to add a folder structure to our application that will help us easily separate modules based on functionality. At this point, it is important to decide how you want to divide up the code for your application. Since we are using RequireJS modules, we will be using the export keyword for all objects that we want to use from other modules. There are three ways to use the export keyword, and each one has different ramifications on the resulting JavaScript. The following example shows the more common use of the keyword:

export class Freehand implements IFreehand {
    public points: Array<IPoint> = [];
    constructor() {
    }
    public AddPoint(point: IPoint) {
        this.points.push(point);
    }
}

In this example, the export keyword annotates the type declaration. This tells the TypeScript compiler to place this type on the exports object of the module being generated.

Tip

The export keyword is only available when the -- module flag is provided to the compiler. An attempt to use it without providing this option will result in a compiler error. Visual Studio provides this flag when you select a module system in the TypeScript Build section of the project properties.

The resulting JavaScript from this class definition will look very similar to our previous module definitions. However, there will be some special syntax added since we have chosen to use AMD modules:

define(["require", "exports"], function(require, exports) {
  var Freehand = (function () {
        function Freehand() {
            this.points = [];
        }
        Freehand.prototype.AddPoint = function (point) {
            this.points.push(point);
        };
        return Freehand;
    })();
    exports.Freehand = Freehand;
});

The differences between an AMD module and a standard JavaScript module are apparent from the very beginning of the definition. The AMD specification provides a function called define that is used to assist the module loader. While there are only two parameters shown for this function here, the function optionally takes a third parameter that precedes the two shown in the generated JavaScript code. This optional first parameter is an identifier for the module, and we will look at this parameter in more detail later in this chapter when we want to minify our final application code. The second parameter for the define function is a list of module dependencies. Every require statement that we use to reference another module in our project will be included in this list. The third and final parameter is the only mandatory parameter for us to define a module, and it is a function that will execute to instantiate an object or module. Each of the dependencies provided to the define function will be provided as parameters to the function for use inside of the module.

The export keyword can also be assigned to a type. In the following example, you can see that rather than decorating our class with the export keyword, we instead use the assignment operator to give it a new value:

class Circle implements ICircle {
    constructor(public radius: number) {
    }
    public resize(radius: number) {
        this.radius = radius;
    }
    public area(): number {
        return Math.PI * this.radius * this.radius;
    }
} 
export = Circle;

Using this method limits us to exporting a single type from a file; however, this type can be a module containing any number of exported objects. The resulting JavaScript looks like the following code:

define(["require", "exports"], function(require, exports) {
    var Circle = (function () {
        function Circle(radius) {
            this.radius = radius;
        }
        Circle.prototype.resize = function (radius) {
            this.radius = radius;
        };
        Circle.prototype.area = function () {
            return Math.PI * this.radius * this.radius;
        };
        return Circle;
    })();
    
    return Circle;
});

As you can see, the object being exported is no longer placed on the exports object, but is instead the return value of the function used by the module loader. Both of these uses are completely valid, but they have a significant effect on how we use the types that we have defined. When decorating the type definition with the export keyword, any other segment of code attempting to use that type must require the module and then access it through the module name, as shown in the following screenshot:

Converting to AMD modules

Note

In this example, the path used to reference modules is built relative to the project directory. The RequireJS API allows you to change this path using a configuration file. For more information about the RequireJS API and configuration files, please visit http://requirejs.org/docs/api.html.

However, when we use the assignment method to export a type, we have direct access to that object when we import it into another code block, as you can see in the following screenshot:

Converting to AMD modules

The third and final use of the export keyword is to attach an imported type to the exports object. When this is done, the imported object isn't just available through its own definition, but we will also be able to access it through the module that exports it. The following sample shows how this is done:

export import CanvasEngineAction = require('Scripts/Drawing/Enums/CanvasEngineAction'),
export import DrawingToolType = require('Scripts/Drawing/Enums/DrawingToolType'),
Converting to AMD modules

Hang on a minute; those aren't the values for our enumeration. What went wrong? Well, this has something to do with the way that the compiler interprets the return type of the CanvasEngineAction enum that was imported and then exported. Enumerations are a clever representation of a number, so the type system reads the return type as a number and determines that these must be our available options. Modules and classes do not have this problem because their type is unique. To work around this problem, we simply have to correctly type the action variable and our enumeration will behave as expected.

Converting to AMD modules

All of these methods are completely valid and fit different needs. As we refactor the drawing application, we will use a combination of these methods to break our code into logical groupings. Using the TypeScript compiler isn't the only way for us to segment out our code. Keeping code in a directory structure that groups similar objects together results in easy differentiation between code functionality. In the following screenshot, you can see that we have created a separate directory for our generic shape objects from any drawing-related logic. The drawing code is separated into more finely-grained subsections as well depending on the functionality each grouping will contain.

Converting to AMD modules

Now we can start breaking our code into separate files, each serving a different and specific purpose. All of our interface types for a specific area can be placed into a single TypeScript file called <Area>Types.ts, for example, ShapeTypes.ts. This will give us a place to reference interfaces and enumerations while keeping our classes and actual implementation code separate. Each interface should be decorated with the export keyword to ensure that we are able to interact with all of these types in a consistent manner. Next, we need to decide how to structure our implementation code. Each of the different shape objects could be broken into a separate code file and placed inside the Shapes directory that we created. However, the amount of code in these classes is minimal and relatively easy to manage, so we can easily place them all inside of a single module. If the implementations start to grow in size like with the drawing-related objects, we will want separate files for each type to keep the code clear and easy to maintain.

The complete contents of the Shapes.ts file is shown in the following code:

import ShapeTypes = require('Scripts/Shapes/ShapeTypes'),
export class Point implements ShapeTypes.IPoint {
    constructor(public X: number, public Y: number) {
    }
}
export class Line implements ShapeTypes.ILine {
    constructor(public p1: ShapeTypes.IPoint, public p2: ShapeTypes.IPoint) {
    }
    public Length(): number {
        var a2 = Math.pow(this.p2.X - this.p1.X, 2);
        var b2 = Math.pow(this.p2.Y - this.p1.Y, 2);
        return Math.sqrt(a2 + b2);
    }
} 
export class Rectangle implements ShapeTypes.IRectangle {
    constructor(public Height: number, public Width: number) {
    }
    public Resize(height: number, width: number) {
        this.Height = height;
        this.Width = width;
    } 
} 
export class Freehand implements ShapeTypes.IFreehand {
    public Points: Array<ShapeTypes.IPoint> = [];
    constructor() {
    }
    public AddPoint(point: ShapeTypes.IPoint) {
        this.Points.push(point);
    }
}
export class Circle implements ShapeTypes.ICircle {
    constructor(public Radius: number) {
    }
    public Resize(radius: number) {
        this.Radius = radius;
    }
    public Area(): number {
        return Math.PI * this.Radius * this.Radius;
    }
}

As you can see, we import the ShapeTypes module, which gives us access to all of the abstractions we created for the shapes. Normally, interfaces are available at the global level in the TypeScript type system. However, because we decorated them with the export keyword, they have been placed on a module type that has no associated code. We reference each of the interfaces through this module type. However, the resulting JavaScript contains no reference to it. Now, we should move into the Drawing area of the application and divide up those types accordingly. We will start with the interfaces and enumerations because they are the most basic types and the implementation code won't work without them.

Each enumeration can be placed in its own file, while all of the interfaces will be placed in a single DrawingTypes.ts file. Since some of our interfaces reference the enumerations, we will need to import them into the DrawingTypes file. This is a particularly good time to use the export and import keywords in conjunction to attach the enumerations to the DrawingTypes module. This will allow us to access each of these enumerations in the same way that we access the interfaces in our application. Next, let's break up the drawing shapes into separate files. Each one of these classes requires a significant amount of code, and having them separated into individual code files will make it easier for us to maintain them. The following screenshot shows each of the different files in the Drawing directory:

Converting to AMD modules

We will recombine them into a single module called DrawingShapes that we will use to directly access these types. The DrawingShapes module is nothing more than a list of export imports, as shown in the following code:

export import DrawingShapeBase = require('Scripts/Drawing/DrawingShapes/DrawingShapeBase'),
export import DrawingRectangle = require('Scripts/Drawing/DrawingShapes/DrawingRectangle'),
export import DrawingLine = require('Scripts/Drawing/DrawingShapes/DrawingLine'),

The DrawingModel and CanvasEngine files will import the modules they need and assign the classes they contain to the export object. With all of our code now in AMD modules, we can modify our HTML to load only the main entry point to our application, app.js, which resides in the root directory of the project, and the rest of the modules will be loaded as necessary through program execution.

Converting to AMD modules
..................Content has been hidden....................

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