Categories
Angular JavaScript TypeScript

How to do Localization and Translation in Angular

With the modular structure of Angular apps, adding localization and translation features to it is easy. The most popular Angular add-on is ngx-translate . Angular 2+ is supported by ngx-translate .

Given an existing Angular app, we can install ngx-translate by running npm i ngx-translate .

After installing the package, make sure you have the following in your Angular module file. For example, in app.module.ts , we have:

export function HttpLoaderFactory(http: HttpClient) {  
  return new TranslateHttpLoader(http, './assets/i18n/', '.json');  
}

This allow us to load our JSON translation files from the ./assets/i18n/ . The JSON file should be named according to standard language codes listed in the ‘639–1’ column in the table of https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes. ngx-translate will load it into the Angular app, with its HTTP loader via a GET request.

app.module.ts should look something this, given all of these components and libraries are included in the app:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { RouterModule, Routes } from '@angular/router';
import { PaginationModule } from 'ngx-bootstrap/pagination';
import { AppComponent } from './app.component';
import { AccountPageComponent } from './account-page/account-page.component';
import { LoginPageComponent } from './login-page/login-page.component';
import { RegisterPageComponent } from './register-page/register-page.component';
import { MenuPageComponent } from './menu-page/menu-page.component';
import { OrdersPageComponent } from './orders-page/orders-page.component';
import { CategoryPageComponent } from './category-page/category-page.component';
import { AboutPageComponent } from './about-page/about-page.component';
import { HomePageComponent } from './home-page/home-page.component';
import { OrderService } from './order.service';
import { MenuService } from './menu.service';
import { UserService } from './user.service';
import { LocationService } from './location.service';
import { CategoryService } from './category.service';
import { RestaurantService } from './restaurant.service';
import { RestaurantsService } from './restaurants.service';
import { IsLoggedInGuard } from './is-logged-in.guard';
import { BottomBarComponent } from './bottom-bar/bottom-bar.component';
import { CartPageComponent } from './cart-page/cart-page.component';
import { VerifyAccountPageComponent } from './verify-account-page/verify-account-page.component';
import { SearchPageComponent } from './search-page/search-page.component';
import { RestaurantPageComponent } from './restaurant-page/restaurant-page.component';
import { ItemPageComponent } from './item-page/item-page.component';
import { ForgotPasswordPageComponent } from './forgot-password-page/forgot-password-page.component';
import { ResetPasswordPageComponent } from './reset-password-page/reset-password-page.component';
import { HttpClientModule, HttpClient } from '@angular/common/http';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
export function HttpLoaderFactory(http: HttpClient) {
  return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
const appRoutes: Routes = [
  { path: 'category/:category', component: CategoryPageComponent },
  { path: 'login', component: LoginPageComponent },
  { path: 'verifyaccount/:userId/:verifyToken', component: VerifyAccountPageComponent },
  { path: 'register', component: RegisterPageComponent, },
  { path: 'resetpassword/:userId/:resetToken', component: ResetPasswordPageComponent, },
  { path: 'cart', component: CartPageComponent, },
  { path: 'forgotpassword', component: ForgotPasswordPageComponent, },
  { path: 'restaurant/:id', component: RestaurantPageComponent },
  { path: 'item/:restaurantId/:menuId/:itemId', component: ItemPageComponent },
  { path: 'menu', component: MenuPageComponent, canActivate: [IsLoggedInGuard] },
  { path: 'orders', component: OrdersPageComponent, canActivate: [IsLoggedInGuard] },
  { path: 'about', component: AboutPageComponent, },
  { path: 'search/:address', component: SearchPageComponent, },
  { path: 'account', component: AccountPageComponent, canActivate: [IsLoggedInGuard] },
  { path: '**', component: HomePageComponent }
];
@NgModule({
  declarations: [
    AppComponent,
    AccountPageComponent,
    LoginPageComponent,
    RegisterPageComponent,
    MenuPageComponent,
    OrdersPageComponent,
    CategoryPageComponent,
    AboutPageComponent,
    HomePageComponent,
    BottomBarComponent,
    CartPageComponent,
    VerifyAccountPageComponent,
    SearchPageComponent,
    RestaurantPageComponent,
    ItemPageComponent,
    ForgotPasswordPageComponent,
    ResetPasswordPageComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    RouterModule.forRoot(
      appRoutes, { useHash: true }
    ),
    PaginationModule.forRoot(),
    HttpClientModule,
    TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useFactory: HttpLoaderFactory,
        deps: [HttpClient]
      }
    })
  ],
  providers: [
    RestaurantService,
    RestaurantsService,
    OrderService,
    MenuService,
    UserService,
    LocationService,
    CategoryService,
    IsLoggedInGuard
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

The JSON should be something like this (en.json):

{  
    "Chiyoda": "Chiyoda",  
    "Chūō": "Chūō",  
    "Minato": "Minato",  
    "Shinjuku": "Shinjuku",  
    "Bunkyō": "Bunkyō",  
    "Taitō": "Taitō",  
    "Sumida": "Sumida",  
    "Kōtō": "Kōtō",  
    "Shinagawa": "Shinagawa",  
    "Meguro": "Meguro",  
    "Ōta": "Ōta",  
    "Setagaya": "Setagaya",  
    "Shibuya": "Shibuya",  
    "Nakano": "Nakano",  
    "Suginami": "Suginami",  
    "Toshima": "Toshima",  
    "Kita": "Kita",  
    "Arakawa": "Arakawa",  
    "Itabashi": "Itabashi",  
    "Nerima": "Nerima",  
    "Adachi": "Adachi",  
    "Katsushika": "Katsushika",  
    "Edogawa": "Edogawa",  
    "Select a Location": "Select a Location",  
    "City": "City",  
    "Ward": "Ward",  
    "Submit": "Submit",  
    "Buffet": "Buffet",  
    "Office Packages": "Office Packages",  
    "Family Event": "Family Event",  
    "Seminar": "Seminar",  
    "High tea and dessert table": "High tea and dessert table",  
    "Christmas": "Christmas",  
    "Kids Party": "Kids Party",  
    "Wedding": "Wedding",  
    "Become a partner": "Become a partner",  
    "About": "About"  
}

and foreign language file (jp.json ):

{  
    "Chiyoda": "千代田区",  
    "Chūō": "中央区",  
    "Minato": "港区",  
    "Shinjuku": "新宿区",  
    "Bunkyō": "文京区",  
    "Taitō": "台東区",  
    "Sumida": "墨田区",  
    "Kōtō": "江東区",  
    "Shinagawa": "品川区",  
    "Meguro": "目黒区",  
    "Ōta": "大田区",  
    "Setagaya": "世田谷区",  
    "Shibuya": "渋谷区",  
    "Nakano": "中野区",  
    "Suginami": "杉並区",  
    "Toshima": "豊島区",  
    "Kita": "北区",  
    "Arakawa": "荒川区",  
    "Itabashi": "板橋区",  
    "Nerima": "練馬区",  
    "Adachi": "足立区",  
    "Katsushika": "葛飾区",  
    "Edogawa": "江戸川区",  
    "Select a Location": "地域を選択",  
    "City": "都市",  
    "Ward": "市区町村",  
    "Submit": "検索",  
    "Buffet": "ビュッフェ",  
    "Office Packages": "お弁当",  
    "Family Event": "家族行事",  
    "Seminar": "セミナー",  
    "High tea and dessert table": "デザート",  
    "Christmas": "クリスマス",  
    "Kids Party": "子供向けパーティー",  
    "Wedding": "ウェディング",  
    "Become a partner": "パートナー提携",  
    "About": "私たちについて"  
}

You can also nest your translations in a lower level in your JSON file, like so:

{  
  "text":{      
    "Chiyoda": "千代田区",  
    "Chūō": "中央区",  
    "Minato": "港区",  
    "Shinjuku": "新宿区",  
    "Bunkyō": "文京区",  
    "Taitō": "台東区",  
    "Sumida": "墨田区",  
    "Kōtō": "江東区",  
    "Shinagawa": "品川区",  
    "Meguro": "目黒区",  
    "Ōta": "大田区",  
    "Setagaya": "世田谷区",  
    "Shibuya": "渋谷区",  
    "Nakano": "中野区",  
    "Suginami": "杉並区",  
    "Toshima": "豊島区",  
    "Kita": "北区",  
    "Arakawa": "荒川区",  
    "Itabashi": "板橋区",  
    "Nerima": "練馬区",  
    "Adachi": "足立区",  
    "Katsushika": "葛飾区",  
    "Edogawa": "江戸川区",  
    "Select a Location": "地域を選択",  
    "City": "都市",  
    "Ward": "市区町村",  
    "Submit": "検索",  
    "Buffet": "ビュッフェ",  
    "Office Packages": "お弁当",  
    "Family Event": "家族行事",  
    "Seminar": "セミナー",  
    "High tea and dessert table": "デザート",  
    "Christmas": "クリスマス",  
    "Kids Party": "子供向けパーティー",  
    "Wedding": "ウェディング",  
    "Become a partner": "パートナー提携",  
    "About": "私たちについて"  
  }  
}

Then in your template, you use the keys with the translate filter in to get translated text:

{{'Select a Location' | translate}}

If the translations in JSON is nested in another property, you can display them like so:

{{'text.About' | translate}}

Note that you cannot have spaces in your keys if you use the dot notation.

With this, the Select a Location key is looked up in the JSON, and depending on the language you set, it’ll get the right text from the value gotten from the key. If the value does not existing for the given key, then it will display the translate key.

Language can be changed in the code by injecting the TranslateService into your code. For example, in your component code, you do the following:

constructor(private translate: TranslateService)

Then you can do:

this.translate.use('jp');

to set the app to display Japanese.

You can also display the default language like so:

this.translate.setDefaultLang('jp');

Translations in Angular is straightforward. It is just a matter of installing a new package, adding the translation, setting the language, and make sure that it displays in the template.

Categories
JavaScript TypeScript

Introduction to TypeScript Functions 

Functions are small blocks of code that takes in some inputs and may return some output or have side effects. A side effect is when a function modifies some variable outside the function. We need functions to organize code into small blocks that are reusable. Without functions, if we want to re-run a piece of code, we have to copy it in different places. Functions are critical to any TypeScript program. In this article, we look at how to define TypeScript functions, how to add types to functions, and passing in arguments in different ways.

Defining Functions

To define a function in TypeScript, we can do the following:

function add(a: number, b: number){  
  return a + b;  
}

The function above adds 2 numbers together and returns the sum. a and b are parameters, which lets us pass in arguments into the function to compute and combine them. add is the name of the function, which is optional in JavaScript. The return statement lets the result of the function be sent to another variable or as an argument of another function. This is not the only way to define a function. Another way is to write it as an arrow function:

const add = (a: number, b: number) => a + b;

The 2 are equivalent. However, if we have to manipulate this in the function, then the 2 aren’t the same since the arrow function will not change the value of this inside the function, like the function defined with the function keyword does. We can assign the function to a variable since functions are objects in TypeScript. Note that the function above only works if it’s one line since the return is implicitly done if the arrow function is one line. If it’s more than one line, then we write it with brackets like so:

const add = (a: number, b: number) => {  
  return a + b;  
}

This way we have to write the return statement explicitly. Otherwise, it won’t return a value.

In both examples above, we have the parameter and then the type following each parameter. This is because in TypeScript, if a parameter isn’t followed by a type, then the type will be inferred as having the any type, which will be rejected if we have the noImplicitAny flag set when we compile the code.

Function Types

To add more checks for the types of the parameters that are passed in and also the return type of the function, we can add a type designation to the function explicitly. We can do this by writing the signature and then adding the fat right arrow, and then appending the return type of the function after that. For example, we can write the following to designate the type of our add function:

const add: (a: number, b: number) => number =  
  (a: number, b: number) => a + b;

The type designation of the add function is:

(a: number, b: number) => number

in the code above. If we put different types for the parameter than the ones designated by the type we wrote, then the TypeScript compiler will raise an error and refuse to compile the code. For example, if we write:

const add: (a: number, b: number) => number =  
  (a: number, b: string) => a + b;

Then we get the error:

Type '(a: number, b: string) => string' is not assignable to type '(a: number, b: number) => number'.Types of parameters 'b' and 'b' are incompatible.Type 'number' is not assignable to type 'string'.(2322)

from the TypeScript compiler. If we don’t write out the type explicitly, TypeScript is still smart enough to infer the type from what’s given on the right side of the assignment operator, so we don’t have to write it out explicitly. It saves us effort from having to type everything to use TypeScript.

Calling a Function

If we are referencing and using a function, then we are calling a function.

To call the function, we write:

add(1, 2) // 3

Since our function returns a value, if we console log the return value of the add function:

console.log(add(1, 2)) // logs 3

We can also assign the return value to a variable:

const sum = add(1, 2);  
console.log(sum) // logs 3

Part of a Function

All functions have some or all of the following parts:

  • function keyword, which is optional
  • the function name, which is optional
  • parentheses — this is required for any function.
  • the parameter inside the parentheses, which is optional
  • opening and closing curly brackets — required for all functions except for single line arrow functions
  • return statement, which is optional. If a function doesn’t have a return statement, it returns undefined . A return statement which has nothing following it will end the executing of the function, so it’s handy for controlling the flow of your function.

Using Arguments

As we can see, many functions have arguments. Arguments are data that is passed into a function for computation, so if we have a function call add(1, 2), then 1 and 2 are the arguments.

On the other hand, parameters are what we write in the parentheses when we define a function to clarify what we can pass in as arguments.

We do not have to define a function parameters to pass in arguments since we have the arguments object in each function. However, it is not recommended since it’s unclear what you want to pass in. We can use the arguments for optional things that we want to pass in, however.

For example, if we go back to the add function we defined above:

function add(a: number, b: number){  
  return a + b;  
}

a and b are parameters. When we call it by writing add(1, 2). 1 and 2 are arguments.

We can specify up to 255 parameters when we define a function. However, we shouldn’t do define more than 5 usually since it becomes hard to read.

Pass in Arguments by Value

There are 2 ways to pass in arguments in TypeScript. One way is to pass in arguments by value. Passing by value means that the arguments passed in are completely separate from the variables you pass in.

The content of the variable is copied into the argument and is completely separate from the original variable if we pass in a variable to a function.

The original variable won’t change even if we change the argument variables that we passed in.

Primitive data types like string, number, boolean, undefined and the null object is passed in by value in TypeScript.

For instance, if we have the following code:

const a = 1;  
const addOne = (num: number) => num + 1  
const b = addOne(a);  
console.log(b);

a would still be 1 even after we call addOne on a since we make a copy of a and set it to num when we are passing in a as the argument for the call to addOne .

Pass in Arguments by Reference

Non-primitive are passed into a function by reference, which means that the reference to the object passed in as the argument is passed into the function. The copy of the content is not made for the argument and the passed in object is modified directly.

For example, if we have the following code:

let changeObj = (obj: { foo: string }) => obj.foo = 'bar'  
const obj = {  
  foo: 'baz'  
}  
changeObj(obj);  
console.log(obj); // logs {foo: "bar"}

The original obj object is defined as { foo: 'baz' }. However, when we pass obj into the changeObj function, where the passed in obj argument is changed in place. The original obj object that we passed in is changed. obj.foo becomes 'bar' instead of 'baz' as it’s originally defined.

Missing Arguments

You do not need to pass in all the arguments into a function in TypeScript. Whatever argument that’s not passed in will have undefined set in its place. So if you don’t pass in all the arguments, then you have to check for undefined in the arguments so that you don’t get unexpected results. For example, with the add function that we had:

function add(a, b){  
  return a + b;  
}

If we call add without the second argument by writing add(1), then we get NaN since 1 + undefined is not a number.

Default Function Parameter Values

We can set default function parameters for optional parameters to avoid unexpected results. With the add function, if we want to make b optional then we can set a default value to b. The best way to do it is:

function add(a: number, b: number = 1){  
  return a + b;  
}

In the code above, we set b to 1 so that we if we don’t pass in anything for b, we automatically get 1 by default for the b parameter. So if we run add(1) we get 2 instead of NaN .

An older, alternative way to do this is to check if the type of the parameter is undefined like so:

function add(a: number, b: number){  
  if (typeof b === 'undefined'){  
    b = 1;  
  }  
  return a + b;  
}

This achieves the same purpose as the first function, but the syntax is clumsier.

TypeScript functions allow us to organize code into small parts that can be reused. There’re many ways to define a function, but sticking to the commonly recommended ways like using arrow functions and not using arguments too much is recommended. We will continue to look at TypeScript functions in the next part of this series.

Categories
JavaScript TypeScript

Introduction to TypeScript Functions: Anonymous Functions and More

Functions are small blocks of code that take in some inputs and may return some output or have side effects. A side effect means that it modifies some variables outside the function.

We need functions to organize code into small blocks that are reusable.

Without functions, if we want to re-run a piece of code, we have to copy it in different places. Functions are critical to any TypeScript program.

In this article, we continue to look at different parts of TypeScript functions, including passing in a variable amount of arguments, recursion, function nesting, and defining functions in objects.

Calling Functions with More Arguments that Parameters

In TypeScript, we can call a function with more arguments than parameters. If we just pass them in without accessing them from the argument object, they’ll be ignored. You can get the extra arguments that aren’t in the parameters with the argument object and use them. The argument object has the parameters with numerical keys just like the indexes of an array. Another way to access extra arguments is through the rest parameter.

For example, if we call the add function with extra parameters:

function add(a: number, b: number, ...rest: any){  
  console.log(arguments);  
  return a + b;  
}  
add(1, 2, 3);

The ...rest part of the signature captures the parameters that we don’t expect to be passed in. We used the rest operator, which is indicated by the 3 periods before the word rest to indicate that there might be more parameters at the end after b. We need this in TypeScript so that we don’t get the mismatch between the number of parameters and the number of arguments passed in. In vanilla JavaScript, ...rest is optional.

In the console.log call, we should get:

0: 1  
1: 2  
2: 3

Variable Scope in Functions

Functions inside shouldn’t be accessible outside of functions unless they are global variables. We should avoid defining global variables as much as we can to prevent bugs and hard to trace errors since they can be accessed anywhere in the program. To prevent defining global variables, we should use let to define variables and const to define constants. For example, we should define functions like so:

function add(a: number, b: number){  
  let sum = a + b;  
  return sum;  
}

In this case, we have sum which is only accessible within the function since it’s defined with the let keyword.

Anonymous Functions

Anonymous are functions with no names. Since they have no name, they cannot be referenced anywhere. They are often passed into other functions as callback functions, which is called when the function is passed into an argument. However, you can assign anonymous functions into a variable so it becomes a named function.

They can also be self executing. This is means that you can define the function and make it run immediately. For example, if we write:

const sum = (function(a: number, b: number){  
  return a + b;  
})(1, 2);
console.log(sum) // log 3

We log 3 because we defined a function to add 2 numbers, and then passed in 1 and 2 as the arguments immediately after by wrapping the function in parenthesis and then passed the arguments to it.

Recursion

You can call the same function from within itself in TypeScript. This is called recursion. All recursive functions must have an end condition, which is called the base case, so that it knows when it stops executing. Otherwise, you can get a function that’s called an infinite number of times, which will crash the browser.

To write a recursive function, we can write:

function sumOfSquares(num: number): number {  
  let sum: number = Math.pow(num, 2);  
  if (num == 1) {  
    return 1  
  } else {  
    return sum + sumOfSquares(num - 1)  
  }    
}

In this example, we wrote a function to compute the sum of squares for a given number. We compute the square of num and then if we have num equal to 1 then we return 1. Otherwise, we return the sum of sum plus the result of call sumOfSquares on num — 1 . We keep reducing num so that we can reach our base case of 1, adding up the results while doing so.

Nesting Functions

Functions can be nested within each other. This means that we can define a function inside another function. For example, we can write:

function convertToChicken(name: string){  
  function getChickenName(name: string){  
    return `Chicken ${name}`;  
  }  
  return getChickenName(name)  
}

In this case, we called getChickeName inside the convertToChicken call. So if we write convertToChicken('chicken') , then we get 'Chicken chicken' since we called get getChickeName and returned the result. The scope of variables is the name. let and const are block-scoped so they cannot be accessed outside of the original function that’s defined, but they are available in the nested function, so if we have:

function convertToChicken(name: string) {  
  let originalName = name;  function getChickenName(newName: string) {  
    console.log(originalName)  
    return `Chicken ${newName}`;  
  }  
  return getChickenName(name)  
}

Then originalName will still be defined in the console.log.

Defining Function in an Object

We can define a function in an object in a few ways. We can use the function keyword or arrow function as usual, but we can also write it with a shorthand for the function keyword. For example, if we have a bird object and we want to define the chirp function, we can write:

const bird = {  
 chirp: function(){  
   console.log('chirp', this)  
  }  
}

or use the following shorthand:

const bird = {  
 chirp(){  
   console.log('chirp', this)  
  }  
}

The 2 are the same since the chirp function will have the bird object as the value of this.

On the other hand, if you use an arrow function:

const bird = {  
 chirp: () => {  
   console.log('chirp', this)  
  }  
}

We’ll get an error from the Typescript compiler because the value of this is the globalThis value, which the TypeScript compiler doesn’t allow. We get the error “The containing arrow function captures the global value of ‘this’.(7041)” when we try to compile the code above.

TypeScript functions allow us to organize code into small parts that can be reused. There’re many ways to define a function, but sticking to the commonly recommended ways like using arrow functions and not using arguments too much is recommended.

Categories
JavaScript TypeScript

Introduction to TypeScript Data Types — Tuple, Enum, and Any

JavaScript, like any other programming language, has its own data structures and types. JavaScript has a few data types that we have to know about in order to build programs with it. Different pieces of data can be put together to build more complex data structures.

JavaScript is a loosely typed, or dynamically typed, language. This means that a variable that’s declared with one type can be converted to another type without explicitly converting the data to another type.

Variables can also contain any type at any time, depending on what’s assigned. With dynamically typed languages, it’s hard to determine the type that a variable has without logging it and we might assign data that we don’t want in the variable.

TypeScript rectifies these issues by letting us set fixed types for variables so that we’re sure of the types. In this article, we’ll look at the TypeScript data types that are exclusive to TypeScript. In this article, we will look at the tuple, enum, and any data types.

Tuple

A tuple is a comma-separated list of objects. We can have as many comma-separated items as we want. However, in reality, we probably shouldn’t have more than 10 comma-separated items in a type. In TypeScript, we can declare a variable with the type by using brackets, with the type names separated by commas inside. This means that each entry in a tuple will have the type that’s set when we declared the tuple variable. For example, we can write:

let x: [string, number, boolean] = ['hello', 1, true];  
console.log(x);

Then we get:

["hello", 1, true]

A tuple is just an array that has fixed types of each entry. If we put the type that’s different than what we have specified in the position when we declared it, then we get an error. For example, if we have:

let x: [string, number, boolean] = [2, 1, true];  
console.log(x);

Then we get the “Type ‘number’ is not assignable to type ‘string’.” error, and the program won’t run. We can access an entry in tuple like we do with arrays since they’re just arrays with fixed types for each entry. For example, we can write the following code:

let x: [string, number, boolean] = ['hello', 1, true];  
console.log(x);  
console.log(x[0]);  
console.log(x[1]);  
console.log(x[2]);

Then we get:

hello  
1  
true

Likewise, the destructuring assignment operator also works as expected like any other arrays. For example, we can write:

const x: [string, number, boolean] = ['hello', 1, true];  
const [str, num, bool] = x;  
console.log(x);  
console.log(str);  
console.log(num);  
console.log(bool);

Then we get the same output as before. We can also put non-primitive objects inside tuple objects. For example, we can have an instance of a class that we created like in the following code:

class Person{  
  name: string;  
  constructor(name: string){  
    this.name = name;  
  }  
}  
const x: [string, number, boolean, Person] = ['hello', 1, true, new Person('Jane')];  
const [str, num, bool, person] = x;  
console.log(x);  
console.log(str);  
console.log(num);  
console.log(bool);  
console.log(person);

Then we get the following:

hello  
1  
true  
Person {name: "Jane"}

from the console.log output.

Enum

TypeScript has an enum type that’s not available in JavaScript. An enum type is a data type that has a set named values called elements, members, enumeral or enumerator of the type. They’re identifiers that act like constants in the language. In TypeScript, an enum has a corresponding index associated with it. The members start with the index 0 by default, but it can be changed to start at any index we like and the subsequent members will have indexes that increment from that starting number instead. For example, we can write the following code to define a simple enum in TypeScript:

enum Fruit { Orange, Apple, Grape };

Then we can access a member of an enum like with the following code:

enum Fruit { Orange, Apple, Grape };  
console.log(Fruit.Orange);

Then console.log from the code above should get us 0 since we didn’t specify a starting index for the enum. We can specify the starting index of an enum with something like in the following code:

enum Fruit { Orange = 1, Apple, Grape };  
console.log(Fruit.Orange);  
console.log(Fruit.Apple);  
console.log(Fruit.Grape);

Then we get the following logged from each console.log statement in order:

1  
2  
3

We can specify the same index for each member, but it wouldn’t be very useful:

enum Fruit { Orange = 1, Apple = 1, Grape };  
console.log(Fruit.Orange);  
console.log(Fruit.Apple);  
console.log(Fruit.Grape);

Then we get:

1  
1  
2

from the console.log. As we can see, we specify the index pretty much however we want to change it. We can even have negative indexes:

enum Fruit { Orange = -1, Apple, Grape };  
console.log(Fruit.Orange);  
console.log(Fruit.Apple);  
console.log(Fruit.Grape);

Then we get:

-1  
0  
1

from the console.log . To get an enum member by its index, we can just use the bracket notation like we access array entries by its index. For example, we can write the following code:

enum Fruit { Orange, Apple, Grape };  
console.log(Fruit[0]);  
console.log(Fruit[1]);  
console.log(Fruit[2]);

Then we get:

Orange  
Apple  
Grape

Any

In TypeScript, the any type means that we can assign anything to the variable that’s been declared with the type any. It’s no different from assigning anything as we do with variables in JavaScript. This lets us adopt JavaScript slowly to TypeScript, and also allows us to use dynamic objects like dictionaries. It also lets us use variables that we don’t know the type of like members from third-party library modules. We can assign anything to a variable with the any type and not get any errors. For example, we can declare and use a variable with the any type like in the code below:

let x: any = 1;  
console.log(x);  
x = 'string';  
console.log(x);

If we run the code above, then we get the following console.log value:

1  
string

The any type if also handy for declaring other data types like arrays. If we declare an array with the any type, then we can put data of any type as entries in our declared array. We can declare an array with any type like in the following code:

let anyList: any[] = [1, true, "abc"];  
console.log(anyList);

Then we get:

[1, true, "abc"]

from the console.log. TypeScript has an Object type which corresponds to the Object object in JavaScript. Therefore, it can’t be used like the any type. The Object type has its own methods like toString, hasOwnProperty, etc., and it’s nothing like the any type, which actually means that the variable can be assigned anything. For example, if we write the following:

let x: Object = 2;  
x.toFixed();

We would get the error “Property ‘toFixed’ does not exist on type ‘Object’.”. However, we can write the following code:

let x: Object = 2;  
console.log(x.hasOwnProperty('foo'));

As we can see, the Object type has a hasOwnProperty property method, which is what the Object object in JavaScript has.

A tuple is a comma-separated list of objects. We can have as many comma-separated items as we want. It’s just a JavaScript array with fixed types for each entry. TypeScript has an enum type that’s not available in JavaScript. An enum type is a data type that has a set named values called elements, members, enumeral or enumerator of the type.

They’re identifiers that act like constants in the language. Each enum member has an index, which can be set arbitrarily. We can also get the member name from its index with the bracket notation like how we get an array entry by its index.

The any type lets us assign anything to the variable that’s been declared with the type any . It’s no different from assigning anything as we do with variables in JavaScript.

Categories
Angular JavaScript TypeScript

Build a Native Desktop App with Angular and Electron

Electron is a JavaScript framework that allows you to build cross-platform apps by converting a web app into a native app for the supported platforms. This provides a convenient way for web developers to write software for the platform that they are not familiar with by using web technologies. By using Node.js, it supports some native functionality like file manipulation and hardware interaction. Electron has been getting more popular over time, and now it is used for companies like Slack and Microsoft to develop their popular apps.

Angular is a popular framework for building web apps, and it has a powerful CLI that makes the building process seamless. However, it does not support building an app into an Electron without some changes. There is an Angular Electron boilerplate repository that combines the 2, making it easy for you to get started.

In this story, we will build an Electron app based on Angular which gets news headlines from the New York Times API, located at https://developer.nytimes.com/. To use it, you have to register for a free API key.

The API supports CORS, so front-end apps from domains outside of nytimes.com can access their APIs. This means that we can build an Angular app with it. To build our app, you have to go to the website and register for an API key.

To start building the app, we install the Angular CLI by running npm i -g @angular/cli. After that, instead of running ng new to create the project, we check out the angular-electron repo located at https://github.com/maximegris/angular-electron.git and use the latest version on the master branch.

Next, copy the code into your own project folder. Now we can start building the app. To start, we rename the app name to new-york-times. The renaming has to be done in multiple files: electron-builer.json, package.json, angular.json.

In electron-builder.json, we replace the existing code with the following:

{  
  "productName": "new-york-times",  
  "directories": {  
    "output": "release/"  
  },  
    "files": [  
        "**/*",  
        "!**/*.ts",  
        "!*.code-workspace",  
        "!LICENSE.md",  
        "!package.json",  
        "!package-lock.json",  
        "!src/",  
        "!e2e/",  
        "!hooks/",  
        "!angular.json",  
        "!_config.yml",  
        "!karma.conf.js",  
        "!tsconfig.json",  
        "!tslint.json"  
    ],  
  "win": {  
    "icon": "dist",  
    "target": [  
      "portable"  
    ]  
  },  
  "mac": {  
    "icon": "dist",  
    "target": [  
      "dmg"  
    ]  
  },  
  "linux": {  
    "icon": "dist",  
    "target": [  
      "AppImage"  
    ]  
  }  
}

Note the productName’s value is different from the original.

Then in package.json, we replace the existing code with:

{
  "name": "new-york-times",
  "version": "1.0.0",
  "description": "Angular 8 with Electron (Typescript + SASS + Hot Reload)",
  "homepage": "https://github.com/maximegris/angular-electron",
  "author": {
    "name": "Maxime GRIS",
    "email": "maxime.gris@gmail.com"
  },
  "keywords": [
    "angular",
    "angular 8",
    "electron",
    "typescript",
    "sass"
  ],
  "main": "main.js",
  "private": true,
  "scripts": {
    "postinstall": "npm run postinstall:electron && electron-builder install-app-deps",
    "postinstall:web": "node postinstall-web",
    "postinstall:electron": "node postinstall",
    "ng": "ng",
    "start": "npm run postinstall:electron && npm-run-all -p ng:serve electron:serve",
    "build": "npm run postinstall:electron && npm run electron:serve-tsc && ng build --base-href ./",
    "build:dev": "npm run build -- -c dev",
    "build:prod": "npm run build -- -c production",
    "ng:serve": "ng serve",
    "ng:serve:web": "npm run postinstall:web && ng serve -o",
    "electron:serve-tsc": "tsc -p tsconfig-serve.json",
    "electron:serve": "wait-on http-get://localhost:4200/ && npm run electron:serve-tsc && electron . --serve",
    "electron:local": "npm run build:prod && electron .",
    "electron:linux": "npm run build:prod && electron-builder build --linux",
    "electron:windows": "npm run build:prod && electron-builder build --windows",
    "electron:mac": "npm run build:prod && electron-builder build --mac",
    "test": "npm run postinstall:web && ng test",
    "e2e": "npm run build:prod && mocha --timeout 300000 --require ts-node/register e2e/**/*.spec.ts",
    "version": "conventional-changelog -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md",
    "lint": "ng lint"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "0.802.2",
    "@angular/cli": "8.2.2",
    "@angular/common": "8.2.2",
    "@angular/compiler": "8.2.2",
    "@angular/compiler-cli": "8.2.2",
    "@angular/core": "8.2.2",
    "@angular/forms": "8.2.2",
    "@angular/language-service": "8.2.2",
    "@angular/platform-browser": "8.2.2",
    "@angular/platform-browser-dynamic": "8.2.2",
    "@angular/router": "8.2.2",
    "@ngx-translate/core": "11.0.1",
    "@ngx-translate/http-loader": "4.0.0",
    "@types/jasmine": "3.3.16",
    "@types/jasminewd2": "2.0.6",
    "@types/mocha": "5.2.7",
    "@types/node": "12.6.8",
    "chai": "4.2.0",
    "codelyzer": "5.1.0",
    "conventional-changelog-cli": "2.0.21",
    "core-js": "3.1.4",
    "electron": "6.0.2",
    "electron-builder": "21.2.0",
    "electron-reload": "1.5.0",
    "jasmine-core": "3.4.0",
    "jasmine-spec-reporter": "4.2.1",
    "karma": "4.2.0",
    "karma-chrome-launcher": "3.0.0",
    "karma-coverage-istanbul-reporter": "2.1.0",
    "karma-jasmine": "2.0.1",
    "karma-jasmine-html-reporter": "1.4.2",
    "mocha": "6.2.0",
    "npm-run-all": "4.1.5",
    "rxjs": "6.5.2",
    "spectron": "8.0.0",
    "ts-node": "8.3.0",
    "tslint": "5.18.0",
    "typescript": "3.5.3",
    "wait-on": "3.3.0",
    "webdriver-manager": "12.1.5",
    "zone.js": "0.9.1"
  },
  "engines": {
    "node": ">=10.9.0"
  },
  "dependencies": {
    "@angular/animations": "^8.2.3",
    "@angular/cdk": "^8.1.3",
    "@angular/material": "^8.1.3",
    "@angular/material-moment-adapter": "^8.1.3",
    "@ngrx/store": "^8.2.0",
    "moment": "^2.24.0"
  }
}

Note that in the build script, we have --base-href ./ at the end. This is important because the styles and script will have the wrong path if we didn’t add it. The base-href is needed to specify that we tell the Electron web view to find the scripts and style files by their relative path instead of the absolute path.

We also included all the libraries we need like Angular Material, Moment.js, NGRX store, and the scripts for building and running our app. We changed the first key value pair to “name”: “new-york-times”.

After that, we do a similar renaming in angular.json by replacing what is there with the following:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "new-york-times": {
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist",
            "index": "src/index.html",
            "main": "src/main.ts",
            "tsConfig": "src/tsconfig.app.json",
            "polyfills": "src/polyfills.ts",
            "assets": [
              "src/assets",
              "src/favicon.ico",
              "src/favicon.png",
              "src/favicon.icns",
              "src/favicon.256x256.png",
              "src/favicon.512x512.png"
            ],
            "styles": [
              "src/styles.scss"
            ],
            "scripts": []
          },
          "configurations": {
            "dev": {
              "optimization": false,
              "outputHashing": "all",
              "sourceMap": true,
              "extractCss": true,
              "namedChunks": false,
              "aot": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": false,
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.dev.ts"
                }
              ]
            },
            "production": {
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "aot": true,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ]
            }
          }
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "new-york-times:build"
          },
          "configurations": {
            "dev": {
              "browserTarget": "new-york-times:build:dev"
            },
            "production": {
              "browserTarget": "new-york-times:build:production"
            }
          }
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n",
          "options": {
            "browserTarget": "new-york-times:build"
          }
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "main": "src/test.ts",
            "polyfills": "src/polyfills-test.ts",
            "tsConfig": "src/tsconfig.spec.json",
            "karmaConfig": "src/karma.conf.js",
            "scripts": [],
            "styles": [
              "src/styles.scss"
            ],
            "assets": [
              "src/assets",
              "src/favicon.ico",
              "src/favicon.png",
              "src/favicon.icns",
              "src/favicon.256x256.png",
              "src/favicon.512x512.png"
            ]
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": [
              "src/tsconfig.app.json",
              "src/tsconfig.spec.json"
            ],
            "exclude": [
              "**/node_modules/**"
            ]
          }
        }
      }
    },
    "new-york-times-e2e": {
      "root": "e2e",
      "projectType": "application",
      "architect": {
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": [
              "e2e/tsconfig.e2e.json"
            ],
            "exclude": [
              "**/node_modules/**"
            ]
          }
        }
      }
    }
  },
  "defaultProject": "new-york-times",
  "schematics": {
    "@schematics/angular:component": {
      "prefix": "app",
      "styleext": "scss"
    },
    "@schematics/angular:directive": {
      "prefix": "app"
    }
  }
}

We renamed all instances of angular-electron with new-york-times in this file.

Next, in environment.ts, environment.prod.ts, and environment.dev.ts, we put

apikey: 'new york times api key',  
apiUrl: 'https://api.nytimes.com/svc'

into the AppConfig object. Replace the New York Times API key with your own.

In index.html, we change it to:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>New York Times</title>
  <base href="/">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
</body>
</html>

The text between the <title> tags will be the title that will be displayed in our app.

Also, we need to install the packages listed in package.json. We do this by running npm i. Next we run ng add @ngrx/store to add the boilerplate of @ngrx/store flux store into our app.

With that completed, we can begin building the app. To start, we run the following commands:

$ ng g component homePage  
$ ng g component articleSearchPage  
$ ng g component articleSearchResults  
$ ng g component toolBar  
$ ng g class menuReducer  
$ ng g class searchResultsReducer  
$ ng g service nyt

These commands will create the files that we need for our app. Now we can include the library modules into our main app module. To do this, we put the following into app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomePageComponent } from './home-page/home-page.component';
import { ArticleSearchPageComponent } from './article-search-page/article-search-page.component';
import { ArticleSearchResultsComponent } from './article-search-results/article-search-results.component';
import { StoreModule } from '@ngrx/store';
import { reducers } from './reducers';
import { NytService } from './nyt.service';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { ToolBarComponent } from './tool-bar/tool-bar.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatButtonModule } from '@angular/material/button';
import { MatMomentDateModule } from '@angular/material-moment-adapter';
import { HttpClientModule } from '@angular/common/http';
import { MatSelectModule } from '@angular/material/select';
import { MatCardModule } from '@angular/material/card';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { MatIconModule } from '@angular/material/icon';
import { MatGridListModule } from '@angular/material/grid-list';
@NgModule({
  declarations: [
    AppComponent,
    HomePageComponent,
    ArticleSearchPageComponent,
    ArticleSearchResultsComponent,
    ToolBarComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    StoreModule.forRoot(reducers),
    FormsModule,
    MatSidenavModule,
    MatToolbarModule,
    MatInputModule,
    MatFormFieldModule,
    MatDatepickerModule,
    BrowserAnimationsModule,
    MatButtonModule,
    MatMomentDateModule,
    HttpClientModule,
    MatSelectModule,
    MatCardModule,
    MatListModule,
    MatMenuModule,
    MatIconModule,
    MatGridListModule
  ],
  providers: [
    NytService
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Most of the modules in the import array are Angular Material modules. We will use them throughout the app.

Now we have to create the part of the app where we get and store data. To do this, run:

$ ng g service nyt

This is where we make our HTTP calls to the New York Times API. Now we should have a file called nyt.service.ts. In there, we put:

import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from 'src/environments/environment';
@Injectable({
  providedIn: 'root'
})
export class NytService {
  constructor(
    private http: HttpClient
  ) { }

  search(data) {
    let params: HttpParams = new HttpParams();
    params = params.set('api-key', environment.apikey);
    if (data.q !== undefined) {
      params = params.set('q', data.q);
    }
    if (data.begin_date !== undefined) {
      params = params.set('begin_date', data.begin_date);
    }
    if (data.end_date !== undefined) {
      params = params.set('end_date', data.end_date);
    }
    if (data.sort !== undefined) {
      params = params.set('sort', data.sort);
    }
    return this.http.get(`${environment.apiUrl}/search/v2/articlesearch.json`, { params });
  }

  getArticles(section: string = 'home') {
    let params: HttpParams = new HttpParams();
    params = params.set('api-key', environment.apikey);
    return this.http.get(`${environment.apiUrl}/topstories/v2/${section}.json`, { params });
  }
}

The search function takes the data we will pass in, and if it is defined, then it will be included in the GET request’s query string. The second parameter in the this.http.get function takes a variety of options, including headers and query parameters. HttpParams objects are converted to query strings when the code is executed. getArticles does similar things as the search function except with a different URL.

Then in environment.ts, we put:

export const environment = {  
  production: false,  
  apikey: 'your api key',  
  apiUrl: 'https://api.nytimes.com/svc' 
};

This makes the URL and API key referenced in the service file available.

Next, we need to add a Flux data store to persist our menu state and search results. First we have to run:

$ ng add @ngrx/store

This adds the boilerplate code to the Flux store. Then, we run:

$ ng g class menuReducer  
$ ng g class searchResultsReducer

We execute this in the src\app\reducers folder, which was created after running ng add @ngrx/store to make the files for our reducers.

Then in menu-reducer.ts, we put:

export const SET_MENU_STATE = 'SET_MENU_STATE';

export function MenuReducer(state: boolean, action) {  
    switch (action.type) {  
        case SET_MENU_STATE:  
            return action.payload;default:  
            return state;  
    }  
}

And in search-result-reducer.ts, we put:

export const SET_SEARCH_RESULT = 'SET_SEARCH_RESULT';

export function SearchResultReducer(state, action) {  
    switch (action.type) {  
        case SET_SEARCH_RESULT:  
            return action.payload;default:  
            return state;  
    }  
}

These two pieces of code will allow the menu and search results to be stored in memory and be propagated to components that subscribe to the data.

Next in src\app\reducers\index.ts, we put:

import { SearchResultReducer } from './search-results-reducer';  
import { MenuReducer } from './menu-reducer';

export const reducers = {  
  searchResults: SearchResultReducer,  
  menuState: MenuReducer  
};

This will allow our module to access our reducers since we have StoreModule.forRoot(reducers) in app.module.ts.

Now we’ll work on our app’s toolbar. To make the toolbar, we put the following in tool-bar.component.ts:

import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { SET_MENU_STATE } from '../reducers/menu-reducer';
@Component({
  selector: 'app-tool-bar',
  templateUrl: './tool-bar.component.html',
  styleUrls: ['./tool-bar.component.scss']
})
export class ToolBarComponent implements OnInit {
  menuOpen: boolean;
  constructor(
    private store: Store<any>
  ) {
    store.pipe(select('menuState'))
      .subscribe(menuOpen => {
        this.menuOpen = menuOpen;
      })
  }

  ngOnInit() {
  }

  toggleMenu() {
    this.store.dispatch({ type: SET_MENU_STATE, payload: !this.menuOpen });
  }
}
this.store.dispatch({ type: SET_MENU_STATE, payload: !this.menuOpen });

This sends the state of the menu to the rest of the app.

store.pipe(select('menuState'))  
  .subscribe(menuOpen => {  
    this.menuOpen = menuOpen;  
  })

The above gets the state of the menu and is used for displaying and toggling the menu state.

In the corresponding template, tool-bar.component.html, we put:

<mat-toolbar>  
    <a (click)='toggleMenu()' class="menu-button">  
        <i class="material-icons">  
            menu  
        </i>  
    </a>  
    New York Times App  
</mat-toolbar>

And in tool-bar.component.scss, we put:

.menu-button {  
  margin-top: 6px;  
  margin-right: 10px;  
  cursor: pointer;  
}.mat-toolbar {  
  background: #009688;  
  color: white;  
}

In app.component.scss, we put:

#content {  
  padding: 20px;  
  min-height: 100vh;  
}

ul {  
  list-style-type: none;  
  margin: 0;  
  li {  
    padding: 20px 5px;  
  }  
}

This changes the color of the toolbar.

Then in app.component.ts, we put:

import { Component, HostListener } from '@angular/core';
import { SET_MENU_STATE } from './reducers/menu-reducer';
import { Store, select } from '@ngrx/store';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  menuOpen: boolean;
  constructor(
    private store: Store<any>,
  ) {
    store.pipe(select('menuState'))
      .subscribe(menuOpen => {
        this.menuOpen = menuOpen;
      })
  }

  @HostListener('document:click', ['$event'])
  public onClick(event) {
    const isOutside = !event.target.className.includes("menu-button") &&
      !event.target.className.includes("material-icons") &&
      !event.target.className.includes("mat-drawer-inner-container")
    if (isOutside) {
      this.menuOpen = false;
      this.store.dispatch({ type: SET_MENU_STATE, payload: this.menuOpen });
    }
  }
}

This makes it so when we click outside of the left side menu, it’ll be closed.

In app.component.html, we put:

<mat-sidenav-container class="example-container">  
    <mat-sidenav mode="side" [opened]='menuOpen'>  
        <ul>  
            <li>  
                <b>  
                    New York Times  
                </b>  
            </li>  
            <li>  
                <a routerLink='/'>Home</a>  
            </li>  
            <li>  
                <a routerLink='/search'>Search</a>  
            </li>  
        </ul></mat-sidenav>  
    <mat-sidenav-content>  
        <app-tool-bar></app-tool-bar>  
        <div id='content'>  
            <router-outlet></router-outlet>  
        </div>  
    </mat-sidenav-content>  
</mat-sidenav-container>

This displays our left side menu and routes.

In style.scss, we put:

/* You can add global styles to this file, and also import other style files */  
@import "~@angular/material/prebuilt-themes/indigo-pink.css";

body {  
  font-family: "Roboto", sans-serif;  
  margin: 0;  
}

form {  
  mat-form-field {  
    width: 95vw;  
    margin: 0 auto;  
  }  
}

.center {  
  text-align: center;  
}

This imports the Material Design styles and sets the width of our forms.

Then in app-routing.module.ts, we put the following so we can see the pages we made when we go the specified URLs:

import { NgModule } from '@angular/core';  
import { Routes, RouterModule } from '@angular/router';  
import { HomePageComponent } from './home-page/home-page.component';  
import { ArticleSearchPageComponent } from './article-search-page/article-search-page.component';

const routes: Routes = [  
  { path: '', component: HomePageComponent },  
  { path: 'search', component: ArticleSearchPageComponent }  
];

@NgModule({  
  imports: [RouterModule.forRoot(routes)],  
  exports: [RouterModule]  
})  
export class AppRoutingModule { }

In index.html, we put:

<!doctype html>  
<html lang="en"><head>  
  <meta charset="utf-8">  
  <title>New York Time</title>  
  <base href="/">  
  <link href="[https://fonts.googleapis.com/icon?family=Material+Icons](https://fonts.googleapis.com/icon?family=Material+Icons)" rel="stylesheet">  
  <link href="[https://fonts.googleapis.com/css?family=Roboto&display=swap](https://fonts.googleapis.com/css?family=Roboto&display=swap)" rel="stylesheet">  
  <meta name="viewport" content="width=device-width, initial-scale=1">  
  <link rel="icon" type="image/x-icon" href="favicon.ico">  
</head><body>  
  <app-root></app-root>  
</body></html>

This includes the Roboto font commonly used with Material Design and Material Icons.

Now we build the logic for our two pages. First we start with our home page. In home-page.component.html, we put:

import { Component, OnInit } from '@angular/core';  
import { NytService } from '../nyt.service';

@Component({  
  selector: 'app-home-page',  
  templateUrl: './home-page.component.html',  
  styleUrls: ['./home-page.component.scss']  
})  
export class HomePageComponent implements OnInit {  
  sections: string[] =  
    `arts, automobiles, books, business, fashion, food, health,  
    home, insider, magazine, movies, national, nyregion, obituaries,  
    opinion, politics, realestate, science, sports, sundayreview,  
    technology, theater, tmagazine, travel, upshot, world`  
      .replace(/ /g, '')  
      .split(',');  
  results: any[] = [];  
  selectedSection: string = 'home';

  constructor(  
    private nytService: NytService  
  ) { }

  ngOnInit() {  
    this.getArticles();  
  }

  getArticles() {  
    this.nytService.getArticles(this.selectedSection)  
      .subscribe(res => {  
        this.results = (res as any).results;  
      })  
  }  
}

We get the articles on the first load with the ngOnInit function, and then once the page is loaded, we can choose which section to load.

In home-page.component.html, we put:

<div class="center">  
    <h1>{{selectedSection | titlecase }}</h1>  
    <mat-menu #appMenu="matMenu">  
        <button mat-menu-item *ngFor='let s of sections' (click)='selectedSection = s; getArticles()'>{{s | titlecase }}  
        </button>  
    </mat-menu><button mat-raised-button [matMenuTriggerFor]="appMenu">  
        Sections  
    </button>  
</div>  
<br>
<mat-card *ngFor='let r of results'>  
    <mat-list role="list">  
        <mat-list-item>  
            <mat-card-title>  
                {{r.title}}  
            </mat-card-title>  
        </mat-list-item>  
    </mat-list>  
    <mat-card-subtitle>  
        <mat-list role="list">  
            <mat-list-item>Published Date: {{r.published_date | date: 'full' }}</mat-list-item>  
            <mat-list-item><a href='{{r.url}}'>Link</a></mat-list-item>  
            <mat-list-item *ngIf='r.byline'>{{r.byline}}</mat-list-item>  
        </mat-list>  
    </mat-card-subtitle>  
    <mat-card-content>  
        <mat-list role="list">  
            <mat-list-item>{{r.abstract}}</mat-list-item>  
        </mat-list>  
        <img *ngIf='r.multimedia[r.multimedia.length - 1]?.url' [src]='r.multimedia[r.multimedia.length - 1]?.url'  
            [alt]='r.multimedia[r.multimedia.length - 1]?.caption' class="image">  
    </mat-card-content>  
</mat-card>

This is where we display the results from the New York Times API, including headline titles, pictures, publication date, and other data. | titlecase is called a pipe. It maps the object to the left of the pipe symbol by calling the function on the right.

The titlecase pipe for example, converts a string to a title-case string. Pipes can also take arguments like date: ‘full’ to set options that can used with the pipe.

The code on the top of the file is where we enable users to select the section they want to load by letting them choose the section they want to load. It’s here:

<div class="center">  
    <h1>{{selectedSection | titlecase }}</h1>  
    <mat-menu #appMenu="matMenu">  
        <button mat-menu-item *ngFor='let s of sections' (click)='selectedSection = s; getArticles()'>{{s | titlecase }}  
        </button>  
    </mat-menu><button mat-raised-button [matMenuTriggerFor]="appMenu">  
        Sections  
    </button>  
</div>  
<br>

This block splits the string into an array of strings with those names and without the spaces:

sections: string[] =  
    `arts, automobiles, books, business, fashion, food, health,  
    home, insider, magazine, movies, national, nyregion, obituaries,  
    opinion, politics, realestate, science, sports, sundayreview,  
    technology, theater, tmagazine, travel, upshot, world`  
      .replace(/ /g, '')  
      .split(',');

Then in home-page.component.scss, we put:

.image {  
  width: 100%;  
  margin-top: 30px;  
}

This styles the pictures displayed.

Next we build the page to search for articles. It has a form and a space to display the results.

In article-search.component.ts, we put:

import { Component, OnInit } from '@angular/core';  
import { SearchData } from '../search-data';  
import { NgForm } from '@angular/forms';  
import { NytService } from '../nyt.service';  
import * as moment from 'moment';  
import { Store } from '@ngrx/store';  
import { SET_SEARCH_RESULT } from '../reducers/search-results-reducer';

@Component({  
  selector: 'app-article-search-page',  
  templateUrl: './article-search-page.component.html',  
  styleUrls: ['./article-search-page.component.scss']  
})  
export class ArticleSearchPageComponent implements OnInit {  
  searchData: SearchData = <SearchData>{  
    sort: 'newest'  
  };  
  today: Date = new Date();constructor(  
    private nytService: NytService,  
    private store: Store<any>  
  ) {}

  ngOnInit() {  
  }

  search(searchForm: NgForm) {  
    if (searchForm.invalid) {  
      return;  
    }  
    const data: any = {  
      begin_date: moment(this.searchData.begin_date).format('YYYYMMDD'),  
      end_date: moment(this.searchData.end_date).format('YYYYMMDD'),  
      q: this.searchData.q  
    }  
    this.nytService.search(data)  
      .subscribe(res => {  
        this.store.dispatch({ type: SET_SEARCH_RESULT, payload: (res as any).response.docs });  
      })  
  }}

This gets the data when we click search and propagates the results to the Flux store, which will be used to display the data at the end.

In article-search.component.html, we put:

<div class="center">
    <h1>Search</h1>
</div>
<br>
<form #searchForm='ngForm' (ngSubmit)='search(searchForm)'>
    <mat-form-field>
        <input matInput placeholder="Keyword" required #keyword='ngModel' name='keyword' [(ngModel)]='searchData.q'>
        <mat-error *ngIf="keyword.invalid && (keyword.dirty || keyword.touched)">
            <div *ngIf="keyword.errors.required">
                Keyword is required.
            </div>
        </mat-error>
    </mat-form-field>
    <br>
    <mat-form-field>
        <input matInput [matDatepicker]="startDatePicker" placeholder="Start Date" [max]="today" #startDate='ngModel'
            name='startDate' [(ngModel)]='searchData.begin_date'>
        <mat-datepicker-toggle matSuffix [for]="startDatePicker"></mat-datepicker-toggle>
        <mat-datepicker #startDatePicker></mat-datepicker>
    </mat-form-field>
    <br>
    <mat-form-field>
        <input matInput [matDatepicker]="endDatePicker" placeholder="End Date" [max]="today" #endDate='ngModel'
            name='endDate' [(ngModel)]='searchData.end_date'>
        <mat-datepicker-toggle matSuffix [for]="endDatePicker"></mat-datepicker-toggle>
        <mat-datepicker #endDatePicker></mat-datepicker>
    </mat-form-field>
    <br>
    <mat-form-field>
        <mat-label>Sort By</mat-label>
        <mat-select required [(value)]="searchData.sort">
            <mat-option value="newest">Newest</mat-option>
            <mat-option value="oldest">Oldest</mat-option>
            <mat-option value="relevance">Relevance</mat-option>
        </mat-select>
    </mat-form-field>
    <br>
    <button mat-raised-button type='submit'>Search</button>
</form>
<br>
<app-article-search-results></app-article-search-results>

This is the search form for the articles. It includes a keyword field, start and end date datepickers, and a drop-down to select the way to sort. These are all Angular Material components. <app-article-search-results></app-article-search-results> is the article search result component which we generated but have not built yet.

Note that the [( in [(ngModel)] denotes two-way data binding between the component or directive, and the current component and [ denote one-way binding from the current component to the directive or component.

Next in article-search.results.ts, we put:

import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
@Component({
  selector: 'app-article-search-results',
  templateUrl: './article-search-results.component.html',
  styleUrls: ['./article-search-results.component.scss']
})
export class ArticleSearchResultsComponent implements OnInit {
  searchResults: any[] = [];
  constructor(
    private store: Store<any>
  ) {
    store.pipe(select('searchResults'))
      .subscribe(searchResults => {
        this.searchResults = searchResults;
      })
  }

  ngOnInit() {
  }
}

This block gets the article search results stored in our Flux store and sends it to our template for displaying:

store.pipe(select('searchResults'))  
      .subscribe(searchResults => {  
        this.searchResults = searchResults;  
      })

In article-search.results.html, we put:

<mat-card *ngFor='let s of searchResults'>
    <mat-list role="list">
        <mat-list-item>
            <mat-card-title>
                {{s.headline.main}}
            </mat-card-title>
        </mat-list-item>
    </mat-list>
    <mat-card-subtitle>
        <mat-list role="list">
            <mat-list-item>Date: {{s.pub_date | date: 'full' }}</mat-list-item>
            <mat-list-item><a href='{{s.web_url}}'>Link</a></mat-list-item>
            <mat-list-item *ngIf='s.byline.original'>{{s.byline.original}}</mat-list-item>
        </mat-list>
    </mat-card-subtitle>
    <mat-card-content>
        <div class="content">
            <p>{{s.lead_paragraph}}</p>
            <p>{{s.snippet}}</p>
        </div>
    </mat-card-content>
</mat-card>

This just displays the results from the store.

In article-search-results.component.scss, we add:

.content {  
  padding: 0px 15px;  
}

This adds some padding to the paragraphs.

With the logic done, we just need to replace the code in core.module.ts, with the following:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AppModule } from '../app.module';
@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    AppModule
  ]
})
export class CoreModule { }

And in shared.module.ts, we put:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { WebviewDirective } from './directives/';
import { PageNotFoundComponent } from './components';
@NgModule({
  declarations: [ WebviewDirective, PageNotFoundComponent],
  imports: [CommonModule, TranslateModule],
  exports: [TranslateModule, WebviewDirective]
})
export class SharedModule {}

to include the PageNotFoundComponent in the module so that the build will proceed.

After adding all the code, we can run npm run ng:serve:web to preview the app in a browser, and to preview it as an Electron, we run npm run electron:local .

Finally, to build the app into a Windows executable, run npm run electron:windows . The executable should be located in the root of the release folder in the project folder.