© Adam Freeman 2019
A. FreemanEssential TypeScripthttps://doi.org/10.1007/978-1-4842-4979-6_21

21. Creating a Vue.js App, Part 1

Adam Freeman1 
(1)
London, UK
 
In this chapter, I start the process of building the example web application using Vue.js. Of the three frameworks that I have demonstrated in this part of the book, it is Vue.js that changes the most when TypeScript is used, providing not only type checking but also a completely different way of working with the framework’s core building blocks. For quick reference, Table 21-1 lists the TypeScript compiler options used in this chapter.
Table 21-1.

The TypeScript Compiler Options Used in This Chapter

Name

Description

allowSyntheticDefaultImports

This option allows imports from modules that do not declare a default export. This option is used to increase code compatibility.

baseUrl

This option specifies the root location used to resolve module dependencies.

esModuleInterop

This option adds helper code for importing from modules that do not declare a default export and is used in conjunction with the allowSyntheticDefaultImports option.

experimentalDecorators

This option determines whether decorators are enabled.

importHelpers

This option determines whether helper code is added to the JavaScript to reduce the amount of code that is produced overall.

jsx

This option specifies how HTML elements in TSX files are processed.

lib

This option selects the type declaration files the compiler uses.

module

This option determines the style of module that is used.

moduleResolution

This option specifies the style of module resolution that should be used to resolve dependencies.

paths

This option specifies the locations used to resolve module dependencies.

sourceMap

This option determines whether the compiler generates source maps for debugging.

strict

This option enables stricter checking of TypeScript code.

target

This option specifies the version of the JavaScript language that the compiler will target in its output.

types

This option specifies a list of declaration files to include in the compilation process.

Preparing for This Chapter

Vue.js projects are most easily created using the Vue Cli package, which has built-in support for creating Vue.js projects that include TypeScript support. Open a command prompt and run the command shown in Listing 21-1 to install the Vue Cli package.

Tip

You can download the example project for this chapter—and for all the other chapters in this book—from https://github.com/Apress/essential-typescript .

npm install --global @vue/[email protected]
Listing 21-1.

Installing the Project Creation Package

The first @ character is part of the package name, @vue-cli. The second @ character is the separator between the package name and the version that is required, 3.8.0.

Once the package has been installed, navigate to a convenient location and run the command shown in Listing 21-2 to create a new Vue.js project.
vue create vueapp
Listing 21-2.

Creating a New Project

The project setup process is interactive. Select the answers to each question shown in Table 21-2.
Table 21-2.

The Project Setup Questions and Answers

Question

Answers

Please pick a preset

Manually select features

Check the features needed for your project

Babel, TypeScript, Router, Vuex

Use class-style component syntax?

Y

Use Babel alongside TypeScript?

Y

Use history mode for router?

Y

Where do you prefer placing config for Babel, PostCSS, ESLint, etc.?

In dedicated config files

Once you have answered the questions, the project will be created, and the packages it requires will be installed.

Configuring the Web Service

Run the commands shown in Listing 21-3 to navigate to the project folder and add the packages that will provide the web service and allow multiple packages to be started with a single command.
cd vueapp
npm install --save-dev [email protected]
npm install --save-dev [email protected]
npm install [email protected]
Listing 21-3.

Adding Packages to the Project

To provide the data for the web service, add a file called data.js to the vueapp folder with the content shown in Listing 21-4.
module.exports = function () {
    return {
        products: [
            { id: 1, name: "Kayak", category: "Watersports",
                description: "A boat for one person", price: 275 },
            { id: 2, name: "Lifejacket", category: "Watersports",
                description: "Protective and fashionable", price: 48.95 },
            { id: 3, name: "Soccer Ball", category: "Soccer",
                description: "FIFA-approved size and weight", price: 19.50 },
            { id: 4, name: "Corner Flags", category: "Soccer",
                description: "Give your playing field a professional touch",
                price: 34.95 },
            { id: 5, name: "Stadium", category: "Soccer",
                description: "Flat-packed 35,000-seat stadium", price: 79500 },
            { id: 6, name: "Thinking Cap", category: "Chess",
                description: "Improve brain efficiency by 75%", price: 16 },
            { id: 7, name: "Unsteady Chair", category: "Chess",
                description: "Secretly give your opponent a disadvantage",
                price: 29.95 },
            { id: 8, name: "Human Chess Board", category: "Chess",
                description: "A fun game for the family", price: 75 },
            { id: 9, name: "Bling Bling King", category: "Chess",
                description: "Gold-plated, diamond-studded King", price: 1200 }
        ],
        orders: []
    }
}
Listing 21-4.

The Contents of the data.js File in the vueapp Folder

Update the scripts section of the package.json file to configure the development tools so that the toolchain and the web service are started at the same time, as shown in Listing 21-5.
...
"scripts": {
    "start": "npm-run-all -p serve json",
    "json": "json-server data.js -p 4600",
        "serve": "vue-cli-service serve",
        "build": "vue-cli-service build"
},
...
Listing 21-5.

Configuring Tools in the package.json File in the vueapp Folder

These entries allow both the web service that will provide the data and the Vue.js development tools to be started with a single command.

Configuring the Bootstrap CSS Package

Use the command prompt to run the command shown in Listing 21-6 in the vueapp folder to add the Bootstrap CSS framework to the project.
npm install [email protected]
Listing 21-6.

Adding the CSS Package

The Vue.js development tools require a configuration change to incorporate the Bootstrap CSS stylesheet in the application. Open the main.ts file src folder and add the statement shown in Listing 21-7.
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import "bootstrap/dist/css/bootstrap.min.css";
Vue.config.productionTip = false
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')
Listing 21-7.

Adding a Stylesheet in the main.ts File in the src Folder

Starting the Example Application

Use the command prompt to run the command shown in Listing 21-8 in the vueapp folder.
npm start
Listing 21-8.

Starting the Development Tools

The Vue.js development tools take a moment to start and perform the initial compilation, producing output like this:
...
DONE  Compiled successfully in 3497ms
Version: typescript 3.5.1
Time: 3043ms
  App running at:
  - Local:   http://localhost:8080/
  - Network: http://10.0.75.1:8080/
  Note that the development build is not optimized.
  To create a production build, run npm run build.
...
Once the initial compilation has been completed, open a browser window and navigate to http://localhost:8080 to see the placeholder content created by the command in Listing 21-2 and which is shown in Figure 21-1.
../images/481342_1_En_21_Chapter/481342_1_En_21_Fig1_HTML.jpg
Figure 21-1.

Running the example application

Understanding TypeScript in Vue.js Development

TypeScript isn’t required for Vue.js development, but it has become such a popular choice that the main Vue.js packages contain complete type declaration files, and the Vue Cli package can create projects ready-configured for TypeScript.

Vue.js files don’t have a different file extension when they use TypeScript features and are defined in files with the vue extension that can contain template, style, and script elements, known as single-file components. When using TypeScript, you can choose to define Vue.js components using classes that are annotated with decorators, which I described in Chapter 15. You can see an example of how classes and decorators are used in Vue.js in the Home.vue file in the src/views folder, which contains template and script elements.
<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
  </div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src
@Component({
  components: {
    HelloWorld,
  },
})
export default class Home extends Vue {}
</script>
The language used for the script element is specified by the lang attribute, like this:
...
<script lang="ts">
...

This value specifies TypeScript and ensures that the code will be processed by the TypeScript compiler. Some features, such as the class syntax for defining components, are available only when using TypeScript. The alternative is to use the object literal syntax to define components in non-TypeScript projects. Both approaches can be used in the same project, but I have used the class syntax throughout this chapter because it makes the best use of the TypeScript strengths.

Understanding the TypeScript Vue.js Toolchain

The Vue.js development tools rely on webpack and the Webpack Development Server packages, which I used in Chapter 15 and which are also used by the Angular and React development tools. When a project is created to use TypeScript, a tsconfig.json file is created to configure the compiler with the following settings:
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "strict": true,
    "jsx": "preserve",
    "importHelpers": true,
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "sourceMap": true,
    "baseUrl": ".",
    "types": ["webpack-env"],
    "paths": {
      "@/*": ["src/*"]
    },
    "lib": ["esnext", "dom", "dom.iterable", "scripthost"]
  },
  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue",
              "tests/**/*.ts", "tests/**/*.tsx"],
  "exclude": ["node_modules"]
}

The configuration enables decorators and allows the use of all JavaScript features, including those that are not yet part of the specification.

The Vue.js development tools deal with vue files by converting the contents of the template element into code statements and using the TypeScript compiler to process the contents of the script element. The compiled code is passed to the Babel package, which is used to target a specific version of the JavaScript language. Regular TypeScript files and TypeScript JSX files are also supported, and the results are bundled into files that are served to the browser through the Webpack Development Server, as shown in Figure 21-2.
../images/481342_1_En_21_Chapter/481342_1_En_21_Fig2_HTML.jpg
Figure 21-2.

The Vue.js toolchain

Supporting TypeScript in a Vue.js project requires some adaptations, which is why you will see the shims-tsx.d.ts and shims-vue.d.ts files in the project folder. These files provide type declarations that allow the TypeScript compiler to resolve dependencies on TypeScript JSX and Vue single-file components.

Creating the Entity Classes

To define the data types that the application will manage, create the src/data folder and add to it a file called entities.ts with the code shown in Listing 21-9.
export class Product  {
    constructor(
        public id: number,
        public name: string,
        public description: string,
        public category: string,
        public price: number) {}
};
export class OrderLine {
    constructor(public product: Product, public quantity: number) {
        // no statements required
    }
    get total(): number {
        return this.product.price * this.quantity;
    }
}
export class Order {
    private lines: OrderLine[] = [];
    constructor(initialLines?: OrderLine[]) {
        if (initialLines) {
            this.lines.push(...initialLines);
        }
    }
    public addProduct(prod: Product, quantity: number) {
        let index = this.lines.findIndex(ol => ol.product.id === prod.id)
        if (index > -1) {
            if (quantity === 0) {
                this.removeProduct(prod.id);
            } else {
                this.lines[index].quantity += quantity;
            }
        } else {
            this.lines.push(new OrderLine(prod, quantity));
        }
    }
    public removeProduct(id: number) {
        this.lines = this.lines.filter(ol => ol.product.id !== id);
    }
    get orderLines(): OrderLine[] {
        return this.lines;
    }
    get productCount(): number {
        return this.lines.reduce((total, ol) => total += ol.quantity, 0);
    }
    get total(): number {
        return this.lines.reduce((total, ol) => total += ol.total, 0);
    }
}
Listing 21-9.

The Contents of the entities.ts File in the src/data Folder

These types describe products and orders and the relationship between them. Unlike the other chapters in this part of the book, Product is defined as a class and not a type alias, because the Vue.js development tools rely on concrete types. The Vue.js change detection system doesn’t work well with the JavaScript Map, so the Order class for this chapter is written using an array for storage.

Displaying a Filtered List of Products

Vue.js supports a number of different ways of defining components, which are the key building block for displaying content to the user. For this book, I am going to use the most popular, which is the single-file component format that combines HTML and its supporting code in one file. (These files can also contain CSS, but I won’t be using that feature since I am relying on the Bootstrap package configured in Listing 21-6).

The convention is to store individual components in the src/components folder and compose them together for display to the user using the src/views folder. To display the details of a single product, add a file named ProductItem.vue to the src/components folder and add the content shown in Listing 21-10.
<template>
    <div class="card m-1 p-1 bg-light">
        <h4>
            {{ product.name }}
            <span class="badge badge-pill badge-primary float-right">
                ${{ product.price.toFixed(2) }}
            </span>
        </h4>
        <div class="card-text bg-white p-1">
            {{ product.description }}
            <button class="btn btn-success btn-sm float-right"
                    @click="handleAddToCart">
                Add To Cart
            </button>
            <select class="form-control-inline float-right m-1"
                    v-model.number="quantity">
                <option>1</option>
                <option>2</option>
                <option>3</option>
            </select>
        </div>
    </div>
</template>
<script lang="ts">
import { Product } from "../data/entities";
import { Component, Prop, Vue } from "vue-property-decorator";
@Component
export default class ProductItem extends Vue {
    @Prop() private product!: Product;
     quantity: number = 1;
     handleAddToCart() {
         this.$emit("addToCart", { product: this.product, quantity: this.quantity });
     }
 }
</script>
Listing 21-10.

The Contents of the ProductItem.vue File in the src/component Folder

A Vue.js component’s template element uses data bindings, denoted by double curly brackets ({{ and }}), to display data values and uses event handling attributes, prefixed by the @ character, to handle events. The expressions specified by the bindings and the event attributes are evaluated using the featured defined by the class in the script element.

This component in Listing 21-10 displays the details of a Product object and emits an event when the user clicks the Add To Cart button.

The combination of TypeScript and the features provided by the decorators change the way that Vue.js components are defined, allowing the use of components that are defined as classes that extend Vue, to which the Component decorator is applied.
...
@Component
export default class ProductItem extends Vue {
...
The @Prop decorator denotes a property whose value will be supplied by the parent component, like this:
...
@Prop() private product!: Product;
...

Notice that the prop property requires the definite assignment assertion, described in Chapter 7, which indicates that a value will be available, even though it is not visible to the TypeScript compiler.

Displaying a List of Categories and the Header

To display the category buttons, add a file called CategoryList.vue to the src/components folder and add the content shown in Listing 21-11.
<template>
    <div>
        <button v-for="c in categories"
            v-bind:key="c"
            v-bind:class="getButtonClasses(c)"
            @click="selectCategory(c)">
                {{ c }}
        </button>
    </div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Emit } from "vue-property-decorator";
@Component
export default class CategoryList extends Vue {
    @Prop()
    private categories!: string[];
    @Prop()
    private selected: string = this.categories[0];
    selectCategory(category: string) {
        this.$emit("selectCategory", category);
    }
    getButtonClasses(category: string) : string {
        let btnClass = this.selected === category
            ? "btn-primary": "btn-secondary";
        return `btn btn-block ${btnClass}`;
    }
 }
</script>
Listing 21-11.

The Contents of the CategoryList.vue File in the src/components Folder

This component displays a list of buttons and highlights the one that corresponds to the selected category. The element attributes in the template section are evaluated as string literal values unless they are prefixed with v-bind, which tells Vue.js to create a data binding between the code in the script element and the value assigned to the attribute. This is an example of a Vue.js directive, and it allows the result of methods defined by the component class to be inserted into the HTML in the template section, for example:
...
v-bind:class="getButtonClasses(c)"
...
This fragment tells Vue.js that the value of the class attribute should be the result of calling the getButtonClasses method. The argument for the method is obtained from another directive, v-for, which repeats an element for each object in a sequence.
...
<button v-for="c in categories" v-bind:key="c" v-bind:class="getButtonClasses(c)"
    @click="selectCategory(c)">
        {{ c }}
</button>
...

This v-for directive tells Vue.js to create a button element for each value returned in the sequence returned by the categories property. To perform efficient updates, Vue.js requires a key attribute to be assigned to each element, which is why v-for and v-bind:key are used together.

The result is a series of button elements for each category. Clicking the button invokes the selectCategory method, which triggers a custom event and allows a component to signal the user’s category selection to another part of the application.

To create the component that displays the header, add a file named Header.vue to the src/components folder with the content shown in Listing 21-12.
<template>
    <div class="p-1 bg-secondary text-white text-right">
        {{ displayText }}
        <button class="btn btn-sm btn-primary m-1">
            Submit Order
        </button>
    </div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { Order } from "../data/entities";
@Component
export default class Header extends Vue {
    @Prop() order!: Order
    get displayText() : string {
        let count = this.order.productCount;
        return count === 0 ? "(No Selection)"
            : `${ count } product(s), $${ this.order.total.toFixed(2)}`
    }
}
</script>
Listing 21-12.

The Contents of the Header.vue File in the src/components Folder

The Header component displays a summary of the current order, following the pattern of defining a class that extends Vue and using decorators to denote a component and its props. The button element displayed by the Header component is a placeholder that I replace once the outline of the application is complete.

Composing and Testing the Components

To create the component that will display the header, the list of products, and the category buttons, add a file named ProductList.vue to the src/views folder and add the code shown in Listing 21-13. The location of this file denotes that it presents a view by composing other components, which is a common convention, albeit one that you don’t have to follow in your own projects.
<template>
    <div>
        <Header v-bind:order="order" />
        <div class="container-fluid">
            <div class="row">
                <div class="col-3 p-2">
                    <CategoryList v-bind:categories="categories"
                        v-bind:selected="selectedCategory"
                        @selectCategory="handleSelectCategory" />
                </div>
                <div class="col-9 p-2">
                    <ProductItem v-for="p in filteredProducts" v-bind:key="p.id"
                        v-bind:product="p" @addToCart="handleAddToCart" />
                </div>
            </div>
        </div>
    </div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { Product, Order } from "../data/entities";
import ProductItem from "../components/ProductItem.vue";
import CategoryList from "../components/CategoryList.vue";
import Header from "../components/Header.vue";
@Component({
    components: {
        ProductItem, CategoryList, Header
    }
})
export default class ProductList extends Vue {
    products: Product[] = []
    order: Order = new Order();
    selectedCategory: string = "All";
    constructor() {
        super();
        [1, 2, 3, 4, 5].map(num =>
            this.products.push(new Product(num, `Prod${num}`, `Product ${num}`,
                `Cat${num % 2}`, 100)));
    }
    get categories() : string[] {
        return ["All", ...new Set(this.products.map(p => p.category))];
    }
    get filteredProducts() : Product[] {
        return this.products.filter(p =>
            this.selectedCategory == "All" || this.selectedCategory === p.category);
    }
    handleSelectCategory(category: string) {
        this.selectedCategory = category;
    }
    handleAddToCart(data: {product: Product, quantity: number}) {
        this.order.addProduct(data.product, data.quantity);
    }
}
</script>
Listing 21-13.

The Contents of the ProductList.vue File in the src/views Folder

The ProductList component combines the ProductItem, CategoryList, and Header components to present content to the user. Using other components is a multistep process. First, the component must be imported using an import statement.
...
import Header from "../components/Header.vue";
...

Notice that curly brackets are not used in the import statement and that the file extension is included. Even though the class has been exported by name in the Header.vue file, the default keyword is also required so that the compiled component can be imported elsewhere in the application.

Next, the Component decorator receives a configuration object, and the list of components is specified using the components property.
...
@Component({
    components: {
        ProductItem, CategoryList, Header
    }
})
...
The final step is to add elements to the template section of the file to apply the components and provide the values for the props, like this:
...
<Header v-bind:order="order" />
...

The Header element applies the Header component. Vue.js uses the v-bind directive to create a data binding that sets the Header component’s order prop to the order property defined by the ProductList class, allowing one component to provide data values to another.

To make sure that the components can display content to the user, replace the contents of the App.Vue file with those shown in Listing 21-14.
<template>
    <ProductList />
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import ProductList from "./views/ProductList.vue";
@Component({
    components: {
        ProductList
    }
})
export default class App extends Vue {
    // no statements required
}
</script>
Listing 21-14.

Replacing the Contents of the App.vue File in the src Folder

The App component has been updated to display a ProductList, replacing the placeholder content added to the project when it was set up. When the changes to the App component are saved, the browser will be updated with the content shown in Figure 21-3, displaying test data. I’ll add support for the web service shortly, but the test data allows the basic features to be tested.
../images/481342_1_En_21_Chapter/481342_1_En_21_Fig3_HTML.jpg
Figure 21-3.

Testing the product list components

Creating the Data Store

Data in most Vue.js projects is managed using the Vuex package, which provides data store features that are integrated into the Vue.js API. The answers used during project setup added Vuex to the package and set up a placeholder data store, which can be seen in the store.ts file in the src folder, as shown here:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
  state: {
  },
  mutations: {
  },
  actions: {
  }
})
Vuex data stores are set up with three properties: state, mutations, and actions. The state property is used to set up the state data managed by the data store, the mutations property is used to define functions that modify the state data, and the actions property is used to define asynchronous tasks that use mutations to update the store. Data stores can also define a getters property, which is used to compute data values from the data held in the store. Listing 21-15 adds the basic state data, mutations, and getters required for the example application using test data to get the store started.
import Vue from 'vue'
import Vuex from 'vuex'
import { Product, Order } from './data/entities';
Vue.use(Vuex)
export interface StoreState {
    products: Product[],
    order: Order,
    selectedCategory: string
}
type ProductSelection = {
    product: Product,
    quantity: number
}
export default new Vuex.Store<StoreState>({
    state: {
        products: [1, 2, 3, 4, 5].map(num => new Product(num, `Store Prod${num}`,
                `Product ${num}`, `Cat${num % 2}`, 450)),
        order: new Order(),
        selectedCategory: "All"
    },
    mutations: {
        selectCategory(currentState: StoreState, category: string) {
            currentState.selectedCategory = category;
        },
        addToOrder(currentState: StoreState, selection: ProductSelection) {
            currentState.order.addProduct(selection.product, selection.quantity);
        }
    },
    getters: {
        categories(state): string[] {
            return ["All", ...new Set(state.products.map(p => p.category))];
        },
        filteredProducts(state): Product[] {
            return state.products.filter(p => state.selectedCategory === "All"
                || state.selectedCategory === p.category);
        }
    },
    actions: {
    }
})
Listing 21-15.

Setting Up the Data Store in the store.ts File in the src Folder

The project has been configured with declaration files for Vuex, which allows a data store to be created with a generic type argument that describes the types of the state data, which TypeScript can then use to perform type checking. In the listing, I define a StoreState interface that describes the types of the product, order, and selectedCategory values the data store will manage, and I use the interface as the type argument to create the store.
...
export default new Vuex.Store<StoreState>({
...

The types in the StoreState interface are applied to the state section of the data store, as well as to the functions in the mutations and getters sections, ensuring that only the properties specified by the interface are used and that they are assigned only the expected types.

Creating Data Store Decorators

Vuex provides helper functions that are used to map data store features to components, but these are awkward to use with the class-based component syntax. Fortunately, with a little extra effort, it is easy to write decorators that do the same job and fit into the class-based approach. Add a file named storeDecorators.ts in the src/data folder and add the code shown in Listing 21-16.
import store, { StoreState } from "../store";
export function state<T extends keyof StoreState>(name: T) {
    return function(target: any, propKey: string): any {
        return {
            get: function() {
                return store.state[name];
            }
        }
    }
}
export function getter(name?: string) {
    return function(target: any, propKey: string): any {
        return {
            get: function() {
                return store.getters[name || propKey];
            }
        }
    }
}
export function mutation(name?: string) {
    return function(target: any, propKey: string, descriptor: PropertyDescriptor) {
        descriptor.value = function(...args: any) {
            store.commit(name || propKey, ...args);
        }
    }
}
export function action(name?: string) {
    return function(target: any, propKey: string, descriptor: PropertyDescriptor) {
        descriptor.value = function(...args: any) {
            store.dispatch(name || propKey, ...args);
        }
    }
}
Listing 21-16.

The Contents of the storeDecorators.ts File in the src/data Folder

The decorators in Listing 21-16 connect a class member to the data store. The state and getter decorators are applied to properties and transform them so they return the result from a state property or getter function defined in the data store. They work by replacing the definition of the property with a getter accessor that invokes the data store feature. The mutation and action decorators replace the implementation of a method with a function that invokes a mutation or an action defined by the data store.

Connecting Components to the Data Store

The decorators defined in Listing 21-16 can be applied to class-based components to connect properties and methods to the data store, allowing access to shared state without the need to pass prop values around. Listing 21-17 connects the Header component to the data store.
<template>
    <div class="p-1 bg-secondary text-white text-right">
        {{ displayText }}
        <button class="btn btn-sm btn-primary m-1">
            Submit Order
        </button>
    </div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { Order } from "../data/entities";
import { state } from "../data/storeDecorators";
@Component
export default class Header extends Vue {
    @state("order")
    order!: Order
    get displayText() : string {
        let count = this.order.productCount;
        return count === 0 ? "(No Selection)"
            : `${ count } product(s), $${ this.order.total.toFixed(2)}`
    }
}
</script>
Listing 21-17.

Connecting to the Data Store in the Header.vue File in the src/components Folder

The state decorator connects the order property defined by the Header class to the order state data property in the data store. In Listing 21-18, I have applied the decorators to the ProductList component.
<template>
    <div>
    <Header />
        <div class="container-fluid">
            <div class="row">
                <div class="col-3 p-2">
                    <CategoryList v-bind:categories="categories"
                        v-bind:selected="selectedCategory"
                        @selectCategory="handleSelectCategory" />
                </div>
                <div class="col-9 p-2">
                    <ProductItem v-for="p in filteredProducts" v-bind:key="p.id"
                        v-bind:product="p" @addToCart="handleAddToCart" />
                </div>
            </div>
        </div>
    </div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { Product, Order } from "../data/entities";
import ProductItem from "../components/ProductItem.vue";
import CategoryList from "../components/CategoryList.vue";
import Header from "../components/Header.vue";
import { state, getter, mutation } from "../data/storeDecorators";
@Component({
    components: {
        ProductItem, CategoryList, Header
    }
})
export default class ProductList extends Vue {
    @state("selectedCategory")
    selectedCategory!: string;
    @getter()
    filteredProducts!: Product[];
    @getter()
    categories!: string[]
    @mutation("selectCategory")
    handleSelectCategory(category: string) {}
    @mutation("addToOrder")
    handleAddToCart(selection: { product: Product, quantity: number}) {}
}
</script>
Listing 21-18.

Applying Decorators in the ProductList.vue File in the src/views Folder

The Header element in the template section no longer requires an order prop because the Header component gets its data directly from the store. In the script section, the connection to the data store replaces the local state data defined by the component and effectively selects the combination of state data, getters, and mutations that are required to support the template. The decorators can be configured with an optional parameter when the name of the method or property doesn’t match the required data store feature. When the changes are saved, the data store will be used and show the test data, as shown in Figure 21-4.
../images/481342_1_En_21_Chapter/481342_1_En_21_Fig4_HTML.jpg
Figure 21-4.

Using a data store

Adding Support for the Web Service

To prepare the data store for working with the web service, I added the actions shown in Listing 21-19. Actions are asynchronous operations that can apply mutations to modify the data store.
import Vue from 'vue'
import Vuex from 'vuex'
import { Product, Order } from './data/entities';
Vue.use(Vuex)
export interface StoreState {
    products: Product[],
    order: Order,
    selectedCategory: string,
    storedId: number
}
type ProductSelection = {
    product: Product,
    quantity: number
}
export default new Vuex.Store<StoreState>({
    state: {
        products: [],
        order: new Order(),
        selectedCategory: "All",
        storedId: -1
    },
    mutations: {
        selectCategory(currentState: StoreState, category: string) {
            currentState.selectedCategory = category;
        },
        addToOrder(currentState: StoreState, selection: ProductSelection) {
            currentState.order.addProduct(selection.product, selection.quantity);
        },
        addProducts(currentState: StoreState, products: Product[]) {
            currentState.products = products;
        },
        setOrderId(currentState: StoreState, id: number) {
            currentState.storedId = id;
        },
        resetOrder(currentState: StoreState) {
            currentState.order = new Order();
        }
    },
    getters: {
        categories(state): string[] {
            return ["All", ...new Set(state.products.map(p => p.category))];
        },
        filteredProducts(state): Product[] {
            return state.products.filter(p => state.selectedCategory === "All"
                || state.selectedCategory === p.category);
        }
    },
    actions: {
        async loadProducts(context, task: () => Promise<Product[]>) {
            let data = await task();
            context.commit("addProducts", data);
        },
        async storeOrder(context, task: (order: Order) => Promise<number>) {
            context.commit("setOrderId", await task(context.state.order));
            context.commit("resetOrder");
        }
    }
})
Listing 21-19.

Adding Actions in the store.ts File in the src Folder

Actions are able to modify the data store only through mutations. The changes in Listing 21-19 define actions that allow products to be loaded and added to the store and that allow orders to be sent to the server.

Vue.js doesn’t include integrated support for HTTP requests. A popular choice for working with HTTP is the Axios package, which I have used throughout this part of the book and which was added to the example project in Listing 21-3. To define the HTTP operations that the example application requires, I added a file called httpHandler.ts to the src/data folder and added the code shown in Listing 21-20.
import Axios from "axios";
import { Product, Order}  from "./entities";
const protocol = "http";
const hostname = "localhost";
const port = 4600;
const urls = {
    products: `${protocol}://${hostname}:${port}/products`,
    orders: `${protocol}://${hostname}:${port}/orders`
};
export class HttpHandler {
    loadProducts() : Promise<Product[]> {
        return Axios.get<Product[]>(urls.products).then(response => response.data);
    }
    storeOrder(order: Order): Promise<number> {
        let orderData = {
            lines: [...order.orderLines.values()].map(ol => ({
                productId: ol.product.id,
                productName: ol.product.name,
                quantity: ol.quantity
            }))
        }
        return Axios.post<{id : number}>(urls.orders, orderData)
            .then(response => response.data.id);
    }
}
Listing 21-20.

The Contents of the httpHandler.ts File in the src/data Folder

The changes in Listing 21-21 to the App component use the action decorator defined in Listing 21-16 to load the products from the web service.
<template>
    <ProductList />
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import ProductList from "./views/ProductList.vue";
import { HttpHandler } from "./data/httpHandler";
import { action } from './data/storeDecorators';
import { Product } from './data/entities';
@Component({
    components: {
        ProductList
    }
})
export default class App extends Vue {
    private handler = new HttpHandler();
    constructor() {
        super();
        this.loadProducts(this.handler.loadProducts);
    }
    @action()
    loadProducts(task: () => Promise<Product[]>) {}
}
</script>
Listing 21-21.

Using the Web Service in the App.vue File in the src Folder

The constructor invokes the loadProducts method, which is mapped to the action of the same name in the data store. The result is that real product data is obtained from the data store, as shown in Figure 21-5.
../images/481342_1_En_21_Chapter/481342_1_En_21_Fig5_HTML.jpg
Figure 21-5.

Using the web service

Summary

In this chapter, I showed you how to create a Vue.js app that uses TypeScript. The project creation package provides integrated support for TypeScript, which allows Vue.js components to be defined as classes. I used this feature to create the basic structure of the application and defined decorators to connect components to Vuex data store features and load data from the web service. In the next chapter, I complete the Vue.js web project and prepare the application for deployment.

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

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