Data structure

In a real-world application, data for websites would be stored and retrieved from a database of some sort. To use the data within a JavaScript web page, these data structures would be serialized to JSON format. Marionette uses standard Backbone models and collections for loading and serializing data structures. For the purpose of this sample application, our data structure will look like this:

Data structure

Class diagram of ManufacturerCollection and related Backbone models

The source of our data is the ManufacturerCollection, which will have a url property to load data from our site. This ManufacturerCollection holds a collection of ManufacturerModels, that are available via the models property. The ManufacturerCollection also implements two interfaces: IManufacturerCollection and IFilterProvider. We will discuss these two interfaces later on.

The properties of the ManufacturerModel will be used to render a single manufacturer's name and logo to the DOM. Each ManufacturerModel also has an array named boards, which holds an array of BoardModels.

Each BoardModel has properties that are necessary for rendering, as well as an array named board_types, which holds an array of BoardType classes. A BoardType is a simple string, and will hold a value of either "Wave", "Freestyle", or "Slalom".

Each BoardModel will also have an array of sizes, holding a BoardSize class, containing detailed information on the available sizes.

As an example, the JSON data structure that is used to serialize the preceding object structure, would be as follows:

{
"manufacturer": "JP Australia",
"manufacturer_logo": "jp_australia_logo.png",
"logo_class" : "",
"boards": [
    {
        "name": "Radical Quad",
        "board_types": [ { "board_type": "Wave" } ],

        "description": "Radical Wave Board",
        "image": "jp_windsurf_radicalquad_ov.png",
        "long_description": "long desc goes here",
        "sizes": [
            { "volume": 68, "length": 227, 
              "width": 53, "sail_min": "< 5.0", "sail_max": "< 5.2" }
        ]
    }]
}

In our sample application, a full JSON dataset can be found at /tscode/tests/boards.json.

Data interfaces

In order to use this JSON data structure within TypeScript, we will need to define a set of interfaces to describe the above data structure, as follows:

export interface IBoardType {
    board_type: string;
}
export interface IBoardSize {
    volume: number;
    length: number;
    width: number;
    sail_min: string;
    sail_max: string;
}
export interface IBoardModel {
    name: string;
    board_types: IBoardType[];
    description: string;
    image: string;
    long_description: string;
    sizes: IBoardSize[];
}
export interface IManufacturerModel {
    manufacturer: string;
    manufacturer_logo: string;
    logo_class: string;
    boards: IBoardModel[];
}

These interfaces simply match the model properties in the previous diagram, and we can then build the corresponding Backbone.Model classes that implement these interfaces. Note that for brevity, we have not listed each individual property of each model here, so be sure to refer to the accompanying source code for a full listing. Our Backbone models are as follows:

export class BoardType extends Backbone.Model
    implements IBoardType {
    get board_type() { return this.get('board_type'), }
    set board_type(val: string) { this.set('board_type', val); }
}
export class BoardSize extends Backbone.Model 
    implements IBoardSize {
    get volume() { return this.get('volume'),}
    set volume(val: number) { this.set('volume', val); }
    // more properties
}
export class BoardModel extends Backbone.Model implements IBoardModel {
    get name() { return this.get('name'), }
    set name(val: string) { this.set('name', val); }
    // more properties
    get sizes() { return this.get('sizes'), }
    set sizes(val: IBoardSize[]) { this.set('sizes', val); }
}
export class ManufacturerModel extends Backbone.Model implements IManufacturerModel {
    get manufacturer() { return this.get('manufacturer'), }
    set manufacturer(val: string) { this.set('manufacturer', val); }
    // more properties
    get boards() { return this.get('boards'), }
    set boards(val: IBoardModel[]) { this.set('boards', val); }
}

Each class extends Backbone.Model, and implements one of the interfaces that we have defined earlier. There is not much to these classes, except for defining a get and set method for each property, and using the correct property type.

At this stage, our models are in place, and we can write a few unit tests, just to make sure that we can create our models correctly:

it("should build a BoardType", () => {
    var boardType = new bm.BoardType(
        { board_type: "testBoardType" });
    expect(boardType.board_type).toBe("testBoardType");
});

We start with a simple test that creates a BoardType model, and then test that the board_type property has been set correctly. Similarly, we can create a test for the BoardSize model:

describe("BoardSize tests", () => {
    var boardSize: bm.IBoardSize;
    beforeAll(() => {
        boardSize = new bm.BoardSize(
          { "volume": 74, "length": 227,
            "width": 55, "sail_min": "4.0", "sail_max": "5.2" });
    });
    it("should build a board size object",() => {
        expect(boardSize.volume).toBe(74);
    });
});

This test is also just creating an instance of the BoardSize model, but it is using the beforeAll Jasmine method. For brevity, we are only showing one test, which checks the volume property, but in a real-world application we would test each of the BoardSize properties. Finally, we can write a test of the BoardModel as follows:

describe("BoardModel tests",() => {
    var board: bm.IBoardModel;
    beforeAll(() => {
        board = new bm.BoardModel({
            "name": "Thruster Quad",
            "board_types": [{ "board_type": "Wave" }],
            "description": "Allround Wave Board",
            "image": "windsurf_thrusterquad_ov.png",
            "long_description": 
                "Shaper Werner Gnigler and pro riders Robby Swift",
            "sizes": [
                { "volume": 73, "length": 228, "width": 55.5,
                     "sail_min": "4.0", "sail_max": "5.2" }
            ]
        });
    });

    it("should find name property",() => {
        expect(board.name).toBe("Thruster Quad");
    });
    it("should find sizes[0].volume property",() => {
        expect(board.sizes[0].volume).toBe(73);
    });
    it("should find sizes[0].sail_max property",() => {
        expect(board.sizes[0].sail_max).toBe("5.2");
    });
    it("should find board_types[0].sail_max property",() => {
        expect(board.board_types[0].board_type).toBe("Wave");
    });
});

Again, we are creating a BoardModel instance in our beforeAll function, and then testing that the properties are set correctly. Note the tests near the bottom of this code snippet: we are checking whether the sizes property and board_types properties have been built correctly, and that they are in fact arrays that can be referenced with [] array notation.

In the accompanying source code, you will find further tests for these models, as well as tests for the ManufacturerModel.

Note

Note how each model is constructed with a simple cut-and-paste of sections of the original JSON sample. When Backbone models are hydrated through RESTful services, these services are simply returning JSON—and our tests are, therefore, matching what Backbone itself would be doing.

Integration tests

At this stage, you may wonder why we are writing these sort of tests, as they might seem trivial, and are just checking whether certain properties have been constructed correctly. In real-world applications, models change quite frequently, especially in the beginning stages of a project. It is quite common to have one developer, or a portion of the team, who are responsible for the backend databases and server-side code that deliver JSON to the frontend. Another another team may be responsible for working on the frontend JavaScript code. By writing tests like these, you are clearly defining what your data structures should look like, and what properties you are expecting in your models. If a change is made server side that modifies a data structure, your team will be able to quickly identify where the cause of the problem lies.

Another reason to write property-based tests is that Backbone, Marionette, and just about any other JavaScript library will use these property names to render HTML to the frontend. If you have a template that is expecting a property called manufacturer_logo, and you change this property name to logo_image, then your rendering code will break. These errors are quite often difficult to track down at runtime. Following the Test Driven Development mantra of "fail early, and fail loudly", our model property tests will quickly highlight these potential errors, should they occur.

Once a series of property-based tests are in place, we can now focus on an integration test that will actually call the server-side code. This will ensure that our RESTful services are working correctly, and that the JSON data structure that our site is generating matches the JSON data structure that our Backbone models expect. Again, if two separate teams are responsible for client-side and server-side code, this sort of integration test will ensure that the data exchange is consistent.

We will be loading our data for this application through a Backbone.Collection class, and this collection will need to load multiple manufacturers. To this end, we can now build a ManufacturerCollection class as follows:

export class ManufacturerCollection 
    extends Backbone.Collection<ManufacturerModel>
{
    model = ManufacturerModel;
    url = "/tscode/boards.json";
}

This is a very simple Backbone.Collection class, which just sets the model property to our ManufacturerModel, and the url property to /tscode/boards.json. As our sample application does not have a backend database or REST services, so we will just load our JSON from disk at this stage. Note that even though we are using a static JSON file in this test, Backbone will still issue an HTTP request back to our server in order to load this file, meaning that any test of this ManufacturerCollection is, in fact, an integration test. We can now write some integration tests to ensure that this model can be loaded correctly from the url property, as follows:

describe("ManufacturerCollection tests", () => {
    var manufacturers: bm.ManufacturerCollection;

    beforeAll(() => {
        manufacturers = new bm.ManufacturerCollection();
        manufacturers.fetch({ async: false });
    });

    it("should load 3 manufacturers", () => {
        expect(manufacturers.length).toBe(3);
    });

    it("should find manufacturers.at(2)",() => {
        expect(manufacturers.at(2).manufacturer)
           .toBe("Starboard");
    });
}

We are again using the Jasmine beforeAll syntax to set up our ManufacturerCollection instance, and then calling fetch({ async: false }) to wait for the collection to be loaded. We then have two tests, one to check that we are loading three manufacturers into our collection, and another to check the Manufacturer model at index 2.

Traversing a collection

Now that we have a full ManufacturerCollection loaded, we can turn our attention to processing the data that it contains. We will need to search this collection to find two things: a list of manufacturers, and a list of board types. These two lists will be used by our filtering panel on the left-hand side panel. In a real-world application, these two lists may be provided by server-side code, returning simple JSON data structures to represent these two lists. In our sample application, however, we will show how to traverse the main manufacturer Backbone collection that we have already loaded. The filtering data structure is as follows:

Traversing a collection

FilterCollection class diagram with related Backbone models

Rather than listing the full implementation of Backbone models shown in the preceding diagram, we will take a look at the TypeScript interfaces instead. Our interfaces for these filtering models are as follows:

export enum FilterType {
    Manufacturer,
    BoardType,
    None
}
export interface IFilterValue {
    filterValue: string;
}
export interface IFilterModel {
    filterType: FilterType;
    filterName: string;
    filterValues: IFilterValue[];
}

We start with a FilterType enum, which we will use to define each of the types of filters we have available. We can filter our board list by either manufacturer name, board type, or clear all filters by using the None filter type.

The IFilterValue interface simply holds a string value that will be used for filtering. When we are filtering by board type, this string value would be one of "Wave", "Freestyle", or "Slalom", and when we are filtering by manufacturer, this string value will be the name of the manufacturer.

The IFilterModel interface will hold the FilterType, a name for the filter, and array of filterValues.

We will create a Backbone model for each of these interfaces, meaning that we will end up with two Backbone models, named FilterValue (which implements the IFilterValue interface), and FilterModel (which implements the IFilterModel interface). To house a collection of FilterModel instances, we will also create a Backbone collection named FilterCollection. This collection has a single method named buildFilterCollection, which will use an IFilterProvider interface to build its internal array of FilterModels. This IFilterProvider interface is as follows:

export interface IFilterProvider {
    findManufacturerNames(): bm.IManufacturerName[];
    findBoardTypes(): string[]
}

Our IFilterProvider interface has two functions. The findManufacturerNames function will return a list of manufacturer names (and their associated logos), and the findBoardTypes function will return a list of strings of all board types. This information is all that is needed to build up our FilterCollection internal data structures.

All of the values needed to populate this FilterCollection will come from data that is already contained within our ManufacturerCollection. The ManufacturerCollection will, therefore, need to implement this IFilterProvider interface.

Finding manufacturer names

Let's continue working within our test suite to flesh out the functionality of the findManufacturerNames function that the ManufacturerCollection will need to implement, as part of the IFilterProvider interface. This function returns an array of type IManufacturerName, which is defined as follows:

export interface IManufacturerName {
    manufacturer: string;
    manufacturer_logo: string;
}

We can now build a test using this interface:

it("should return manufacturer names ",() => {
    var results: bm.IManufacturerName[] = 
        manufacturers.findManufacturerNames();
    expect(results.length).toBe(3);
    expect(results[0].manufacturer).toBe("JP Australia");
});

This test is reusing the manufacturers variable that we set up in our previous test suite. It then calls the findManufacturerNames function, and expects the results to be an array of three manufacturer names, i.e. "JP Australia", "RRD", and "Starboard".

Now, we can update the actual ManufacturerCollection class, in order to provide an implementation of the findManufacturerNames function:

public findManufacturerNames(): IManufacturerName[] {
    var items = _(this.models).map((iterator) => {
        return {
            'manufacturer': iterator.manufacturer,
            'manufacturer_logo': iterator.manufacturer_logo
        };
    });
    return items;
}

In this function, we are using the Underscore utility function named map to loop through our collection. Each Backbone collection class has an internal array named models. The map function will loop through this models property, and call the anonymous function for each item in the collection, passing the current model into our anonymous function via the iterator argument. Our code then builds a JSON object with the required properties of the IManufacturer interface.

Note

The TypeScript compiler will generate errors if the returned object does not conform to the IManufacturer name interface.

Finding board types

We can now focus on the second function of the IFilterProvider interface, named findBoardTypes that the ManufacturerCollection will need to implement. Here is the unit test:

it("should find board types ",() => {
    var results: string[] = manufacturers.findBoardTypes();
    expect(results.length).toBe(3);
    expect(results).toContain("Wave");
    expect(results).toContain("Freestyle");
    expect(results).toContain("Slalom");
});

This test calls the findBoardTypes function, which will return an array of strings. We are expecting the returned array to contain three strings: "Wave", "Freestyle", and "Slalom".

The corresponding function in our ManufacturerCollection class is then implemented as follows:

public findBoardTypes(): string[] {
    var boardTypes = new Array<string>();
    _(this.models).each((manufacturer) => {
        _(manufacturer.boards).each((board) => {
            _(board.board_types).each((boardType) => {
                if (! _.contains(
                    boardTypes, boardType.board_type)) {
                        boardTypes.push(boardType.board_type);
                }
            });
        });
    });
    return boardTypes;
}

The implementation of the findBoardTypes function starts by creating a new string array named boardTypes, which will hold our results. We then use the Underscore each function to loop through each manufacturer. The Underscore each function is similar to the map function, and will iterate through each item in our collection. We then loop through each board in the manufacturer's arsenal, and through each board type listed per board. Finally, we are testing to see whether the board type collection contains an item already, using the underscore _.contains function. If it does not already have the board type in the array, we push the board_type string into our boardTypes array.

Note

The Underscore library has numerous utility functions available for searching, manipulating, and modifying arrays and collections—so be sure to consult the documentation to find suitable functions for use in your code. These functions are not limited to Backbone collections only, and can be used on any type of array.

This completes our work on the IFilterProvider interface, and its implementation within the ManufacturerCollection class.

Filtering a Collection

When a user clicks on a filter option on the left-hand side panel, we will need to apply the selected filter to the data contained within our manufacturer collection. In order to do this, we will need to implement two functions, named filterByManufacturer, and filterByBoardType within the ManufacturerCollection class. Let's start with a test to filter our collection by manufacturer name:

it("should filter by manufacturer name ",() => {
    var results = manufacturers.filterByManufacturer("RRD");
    expect(results.length).toBe(1);
});

This test calls the filterByManufacturer function, expecting only a single manufacturer to be returned. With this test in place, we can create the real filterByManufacturer function on the ManufacturerCollection as follows:

public filterByManufacturer(manufacturer_name: string) {
    return _(this.models).filter((item) => {
        return item.manufacturer === manufacturer_name;
    });
}

Here, we are using the Underscore function named filter to apply a filter to our collection.

The second filtering function is by board type, and is a little more complicated. We will need to loop through each manufacturer in our collection, then through each board, and then through each board type. If we find a match for the board type, we will flag this board to be included in the result set. Before we tackle the filterByBoardType function, let's write a test:

it("should only return Slalom boards ",() => {
    var results = manufacturers.filterByBoardType("Slalom");
    expect(results.length).toBe(2);
    _(results).each((manufacturer) => {
        _(manufacturer.boards).each((board) => {
            expect(_(board.board_types).some((boardType) => {
                return boardType.board_type == 'Slalom';
            })).toBeTruthy(); 

        });
    });
});

Our test calls the filterByBoardType function, using the string "Slalom" as a filter. Remember that this function will return a collection of ManufacturerModel objects at the top level, with the boards array within each of these objects filtered by board type. Our test then loops through each manufacturer, and each board in the result set, and then uses the Underscore function called some to test whether the board_types array has the correct board type.

Our code to implement this function on the ManufacturerCollection is also a little tricky, as follows:

public filterByBoardType(board_type: string) {
    var manufWithBoard = new Array();
    _(this.models).each((manuf) => { 
        var hasBoardtype = false;
        var boardMatches = new Array();
        _(manuf.boards).each((board) => {
            var match = _(board.board_types).some((item) => {
                return item.board_type == board_type;
            });
            if (match) {
                boardMatches.push(new BoardModel(board));
                hasBoardtype = true;
            }
        });

        if (hasBoardtype) {
            var manufFiltered = new ManufacturerModel(manuf);
            manufFiltered.set('boards', boardMatches);
            manufWithBoard.push(manufFiltered);
        }
    });
    return manufWithBoard;
}

Our ManufacturerCollection class instance holds the entire collection that was loaded via the JSON file from the site. In order to keep this data for repeated filters, we will need to construct a new ManufacturerModel array to return from this function – so that we do not to modify the underlying "global" data. Once we have constructed this new array, we can then loop through each manufacturer. If we find a board matching the required filter, we will set a flag named hasBoardType to true, to indicate that this manufacturer must be added to our filtered array.

Each manufacturer in this filtered array will also need to list only the board types that match our filter criteria, so we will need another array—called boardMatches—to hold these matching boards. Our code will then loop through each board, and check whether it has the required board_type. If so, we will add it to the boardMatches array and set the hasBoardType flag to true.

Once we have looped through each board for a manufacturer, we can check the hasBoardType flag. If our manufacturer has this board type, we will construct a new ManufacturerModel, and then set the boards property on this model to our in-memory array of the matching boards.

Our work with the underlying Backbone collections and models is now complete. We have also written a set of unit and integration tests to ensure that we can load our collection from the site, build our filtering lists from this collection, and then apply a particular filter to this data.

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

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