Categories
TypeScript

Introduction to TypeScript Interfaces — Indexable Types

The big advantage of TypeScript over plain JavaScript is that it extends the features of JavaScript by adding functionality that ensures the type safety of our program’s objects. It does this by checking the shape of the values that objects take on.

Checking the shape is called duck typing or structural typing. Interfaces are one way to fill the role naming data types in TypeScript. It’s very useful for defining contracts within our code in TypeScript programs. In the last article, we looked at how to define a TypeScript interface and adding required and optional properties to it. In this article, we’ll continue to look at other properties of TypeScript interfaces like indexable types.

Indexable Types

We can define indexable types for data like arrays. Any object that uses bracket notation like arrays and dynamic object types can be designated with indexable types. Indexable types have an index signature that describes the types that we can use as an index for our object, alongside the return type for the corresponding index. It’s very handy for designating the types for dynamic objects. For example, we can design an array that only accepts strings like in the following code:

interface NameArray {
    [index: number]: string;
}

let nameArray: NameArray = ["John", "Jane"];
const john = nameArray[0];
console.log(john);

In the code above, we defined the NameArray interface that takes in a index that is of type number as the index signature, and the return type of the corresponding index signature is a string. Then when we designate a variable with the NameArray type then we can use the index to get the entries of the array. However, with this code, the array methods and operators aren’t available since we only have the [index: number] index signature and nothing, so the TypeScript compiler isn’t aware that it’s an array even though it looks like one to the human eye.

Index signatures support 2 types. They can either be strings or numbers. It’s possible to support both types of indexes, but the type returned from a numeric indexer must be a subtype of the one returned by the string indexes. This is because JavaScript will convert numeric indexes to strings when it’s trying to accessing entries or properties with numeric properties. This ensures that it’s possible to get different results returned for the same index.

For example, the following code would give us an error from the TypeScript compiler:

class Animal {
  name: string = '';
}

class Cat extends Animal {
  breed: string = '';
}

interface Zoo {
    [x: number]: Animal;
    [x: string]: Cat;
}

If we try to compile the code above, we would get “Numeric index type ‘Animal’ is not assignable to string index type ‘Cat’.(2413)”. This is because we have Cat as a return type of the string index, which is a subtype of Animal. We can’t have this since if we have 2 index signatures with different types, then the supertype must be the return type of the index signature with the string type, and the index signature with the number type must have the subtype of the of returned by the one with the string index signature. This means that if we flip the return types around, then code will be compiled and run:

class Animal {
  name: string = '';
}

class Cat extends Animal {
  breed: string = '';
}

interface Zoo {
    [x: number]: Cat;
    [x: string]: Animal;
}

Since Animal is a supertype of Cat, we must have Animal as the return type of the string index signature, and the Cat type as the return type of the number index signature.

Photo by Nathalie SPEHNER on Unsplash

Index signatures enforce that all normal property matches their return type in addition to the ones that are accessed by the bracket notation since in JavaScript obj.prop and obj['prop'] are the same. This means that if we have the following code:

interface Dictionary {
  [x: string]: string;
}

let dict: Dictionary = {};
dict.prop = 1;

Then we would get the error “Type ‘1’ is not assignable to type ‘string’.(2322)” since we specified that all properties are strings in the variable that has the Dictionary type. If we want to accept other types in the properties of our objects, we have to use union types. For example, we can write the following interface to let the properties of the object with the given type accept both string and numbers as values:

interface Dictionary {
  [x: string]: string | number;
  num: number;
}

let dict: Dictionary = { num: 0 };

In the example above, we accept both string and number as both types of our values. So we add a property with a number type without the TypeScript compiler rejecting the code with an error. Therefore, in the last line of the code above, we can add a num property to the object with the value 0.

We can also make an index signature readonly so that we can prevent assignment to their indices. For example, we can mark an index signature as read only with the following code:

interface Dictionary {
  readonly [x: string]: string;
}

let dict: Dictionary = {'foo': 'foo'};

Then when we try to assign another value to dict['foo'] like in the code below, the TypeScript compiler will reject the code and won’t compile it:

interface Dictionary {
  readonly [x: string]: string;
}

let dict: Dictionary = {'foo': 'foo'};
dict['foo'] = 'foo';

If we try to compile the code above, we’ll get the error “Index signature in type ‘Dictionary’ only permits reading.(2542)”. This means that we can only set the properties and values of a read only property when the object is being initialized, but subsequent assignments will fail.

Conclusion

Indexable types are very handy for defining the return values of the properties of dynamic objects. It takes advantage of the fact that we can access JavaScript properties by using the bracket notation. This is handy for properties that have invalid names if defined without the bracket notation or anything that we want to be able to be accessed by the bracket notation and we want type checking on those properties or entries. With indexable types, we make sure that properties that are assigned and set by the bracket notation have the designated types.

Also, this also works for regular properties since bracket notation is the same as the dot notation for accessing properties. Also, we can designate index signatures as readonly so that they can be written to when the object with a type with indexable types is initialized but not after. If we have both number and string index signatures, then the string indexable signature must have the return type that’s the super-type of the one with the number index signature so that we get consistent types for objects when we access properties.

Categories
TypeScript

Great New Features Released with TypeScript 3.5

TypeScript is improving every day. We keep getting new features with every release. In this article, we’ll look at the new stuff that was released with TypeScript 3.5.

New features include speed improvements to incremental builds, new Omit helper type, better excess property checks in union types, and type inference for the composition of constructors.

Speed Improvements

With the --incremental build mode, subsequent builds are faster because of the caching of references, file locations, and other build related data.

Omit Helper Type

The Omit helper type was introduced in TypeScript 3.5 to let us create a new type from existing types by excluding some properties from the original.

For example, given the Person type defined in the following code:

type Person = {
    name: string;
    age: number;
    address: string;
};

We can create a new type without the address property by using Omit:

type NewPerson = Omit<Person, "address">;

Which is the same as:

type NewPerson = {
    name: string;
    age: number;
}

Better Excess Property Checks in Union Types

Before TypeScript 3.5, excess property checks didn’t catch properties in some cases. If we have a union type, then TypeScript versions before 3.5 allows a property with the same name as the type of a union type but with a different type than what’s specified in the type definition.

For example, if we have:

type Person = {
    name: string;
    age: number;
};

type Address = {
    address: string;
}

const person: Person | Address = {
    name: 'Joe',
    age: 1,
    address: true
};

We can set address to something that’s not a string, which isn’t something that should be allowed.

This has been fixed in TypeScript 3.5. Now address has to be a string since it’s specified to be a string.

The --allowUmdGlobalAccess Flag

UMD global declarations files can now be referenced in TypeScript 3.5 using the new --allowUmdGlobalAccess flag.

It adds more flexibility for mixing and matching 3rd party libraries. Now the globals that libraries declare can be consumed, even from within modules.

Smarter Union Type Checking

We would get an error with the following union type definition and variable assignment before TypeScript 3.5:

type Foo = { done: boolean, value: string }
type Bar =
    | { done: false, value: string }
    | { done: true, value: string };

declare let source: Foo;
declare let target: Bar;

target = source;

Before 3.5, done would be recognized as having a literal type with the value instead of the boolean type.

Now it recognizes the type for the done field as being boolean. This now works boolean can only be true or false .

Higher-Order Type Inference From Generic Constructors

When we compose generic constructors as we do in the following function:

function composeConstructors<T, U, V>(
    F: new (x: T) => U, G: new (y: U) => V): (x: T) => V {
    return x => new G(new F(x))
}

TypeScript 3.5 can infer the type T , U , and V by inferring the chain of types that are formed from the composition.

If we have the following code:

class Foo<T> {
    value: T;
    constructor(value: T) {
        this.value = value;
    }
}

class Bar<U> {
    value: U;
    constructor(value: U) {
        this.value = value;
    }
}

let f = composeConstructors(Foo, Bar);
let a = f('foo');

Now we’ll get that a has the type Bar<Foo<string>> . Versions before 3.5 has the type Bar<{}> for a .

TypeScript 3.5 is smarter now. It can infer types formed by the composition of constructors.

With TypeScript 3.5, it’s smarter and faster. It can infer types formed by the composition of constructors by going through the chain of composition.

Excess property checks are checked for union types, which didn’t happen in earlier versions.

Also, we have the -- allowUmdGlobalAccess flag to run access global variables from UMD modules.

Finally, we have the Omit type for creating a new type from existing types with some properties removed.

Categories
TypeScript

Introduction to JavaScript Inheritance

JavaScript is an object-oriented language. However, it’s different from many other OO languages in that it uses prototype-based inheritance instead of class-based inheritance.

Prototype-based inheritance means that objects inherit items from its prototype. A prototype is just another object, which can be inherited by other objects.

This is different from class-based inheritance in that classes are templates for creating new objects. Classes can inherit from other classes to reuse code from the class it’s inheriting from.

Old Syntax of Inheritance

Constructor Functions

Before ES6, we only have constructor functions to serve as templates to create new objects which are instances of the constructor.

For example, we can define a constructor function as follows:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Then we can create a new instance of Person by writing:

let person = new Person('Joe', 10);

To inherit items from other constructor functions in a constructor function, we have to call the parent constructor function that we want to inherit from with the call method, and then set our constructor’s prototype’s constructor property to the parent constructor function that we want to inherit from.

For example, if we want a Employee constructor function to inherit the properties of the Person constructor, we can write:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

function Employee(name, age, title) {
  this.title = title;
  Person.call(this, name, age);
  this.__proto__.constructor = Person;
}

let employee = new Employee('Joe', 20, 'waiter');
console.log(employee);

The call method takes the value of this we want to set, and the rest are arguments we pass into the function that the call method is called on.

If we look at the __proto__ property of the employee object, which has the prototype for it, we should get that __proto__.constructor of it should be the Person constructor like we set it to.

The properties and the values of the employee object should be what we passed into the Employee constructor when we called it.

Object.create()

The Object.create() method is another way to inherit from a prototype when we create an object.

The argument that it takes is the prototype object that we want the object returned from it to inherit from.

For example, we can use Object.create to create an object with a prototype as follows:

const person = {
  name: 'Joe',
  age: 20
}

let employee = Object.create(person);
employee.title = 'waiter';

console.log(employee);

If we look at the employee object, we’ll see that the __proto__ property will have the age and name properties set with values.

Setting the proto Property Directly

Setting the __proto__ property directly has been officially supported since ES6 and it’s an undocumented way to set the prototype of an object in various browsers before it like Firefox.

We can set an object to the __proto__ property directly, by writing something like:

const person = {
  name: 'Joe',
  age: 20
}

let employee = {
  title: 'waiter'
};

employee.__proto__ = person;
console.log(employee);

We should get the exact structure of the properties and values as we did when we created an object with the Object.create() method.

One thing we have to be careful about is that we don’t want to accidentally set it if we don’t want to change an object’s prototype. This may happen if we use JavaScript objects as maps. With ES6, we can use the Map object for this purpose.

Object.defineProperty

We can also use the defineProperty method to set the prototype of an object. For example, we can write:

const person = {
  name: 'Joe',
  age: 20
}

let employee = {
  title: 'waiter'
};

Object.defineProperty(employee, '__proto__', {
  value: person
});
console.log(employee.__proto__);

When we log the value of employee.__proto__ , we get back the person object.

Note that the prototype is in the value property of the 3rd argument of the defineProperty method call.

Photo by Chiara Daneluzzi on Unsplash

New Class Syntax

With the release of ES6, the new class syntax is introduced. On the surface, it looks like we have class-based inheritance, but underneath the surface, it’s exactly the same as before.

The class syntax is the same as constructor functions. For example,

function Person(name, age) {
  this.name = name;
  this.age = age;
}

is the same as:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

We can instantiate both by writing:

const person = new Person('Joe', 10);

And we get the same object when we inspect its properties.

The class syntax also creates a clear and convenient way to do inheritance that looks like a traditional class-based inheritance.

We can create a super-class and a child class can inherit from it with the extends keyword. For example, we can write:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

class Employee extends Person {
  constructor(name, age, title) {
    super(name, age);
    this.title = title;
  }
}

const employee = new Employee('Joe', 20, 'waiter');

In the code above, we have the extends keyword to indicate which class Employee inherits from. We can only inherit from one class.

The super method is called to call the parent constructor and set its’ properties. In this case, calling super will call the constructor method in the Person class.

this refers to the class that it’s inside in each class.

This is exactly the same as what we did before:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

function Employee(name, age, title) {
  this.title = title;
  Person.call(this, name, age);
  this.__proto__.constructor = Person;
}

let employee = new Employee('Joe', 20, 'waiter');
console.log(employee);

The only thing is that when we inspect the employee object, we get that the __proto__.constructor property shows class instead of function .

The class syntax makes inheritance much more clear than before. It’s much needed syntactic sugar for the prototypical inheritance model that’s in JavaScript since the beginning.

Also, with the class syntax, we don’t have to call the call method on the parent constructor object and set this.__proto__.constructor anymore.

It’s better than using the Object.create() or setting the __proto__ property directly. Setting the __proto__ property has its problems like accidentally setting the wrong prototype.

Categories
TypeScript

Introduction to TypeScript Interfaces — Object Literals and Function Types

The big advantage of TypeScript over plain JavaScript is that it extends the features of JavaScript by adding functionality that ensures the type safety of our program’s objects. It does this by checking the shape of the values that objects take on.

Checking the shape is called duck typing or structural typing. Interfaces are one way to fill the role naming data types in TypeScript. It’s very useful for defining contracts within our code in TypeScript programs. In the last article, we looked at how to define a TypeScript interface and adding required and optional properties to it. In this article, we’ll continue from the previous article and look at other properties of TypeScript interfaces.

Excess Property Checks

Object properties get extra checks when they’re being assigned to a variable with the type designated by an interface. This also applies to object literals that we pass into functions as arguments. For example, the following code wouldn’t be compiled by the TypeScript compiler and give us an error:

interface Person{
  name: string
}

const greet = (person: Person) => {
  console.log(`Hello, ${person.name}`);
}

greet({ name: 'Joe', foo: 'abc' });

The excess property check done by the TypeScript compiler will reject the code since we have an extra foo property that isn’t defined in the Person interface, so add it in the object in the parameter would fail because of TypeScript’s excess property checks for object literals. Assigning the same object literal to a variable will also fail. For example, if we have the following code:

interface Person{
  name: string
}
const greet = (person: Person) => {
  console.log(`Hello, ${person.name}`);
}
const person: Person = { name: 'Joe', foo: 'abc' };
greet(person);

We would get the error “Type ‘{ name: string; foo: string; }’ is not assignable to type ‘Person’. Object literal may only specify known properties, and ‘foo’” if we try to compile the code with the TypsScript compiler or look at the code at a text editor that supports TypeScript. However, we can use the type assertion operator as to designate the type of the object literal as we like it. So if we’re sure that the object literal if of the type Person even though it has a foo property in it, we can write the following code:

interface Person{
  name: string
}
const greet = (person: Person) => {
  console.log(`Hello, ${person.name}`);
}
const person: Person = { name: 'Joe', foo: 'abc' } as Person;
greet(person);

With the code above, the TypeScript compiler won’t complain of any issues. It’ll just assumes that the object literal is of type Person even though it has a foo property. However, we do have some properties that are dynamic or may only sometimes appear, we can also add a dynamic property to our TypeScript interfaces like in the following code:

interface Person{
  name: string,
  [prop: string]: any
}

const greet = (person: Person) => {
  console.log(`Hello, ${person.name}. ${person.age ? `You're ${person.age} years old.` : ''}`);
}

const person: Person = { name: 'Jane', age: 20 };
greet(person);

In the code above, we added:

[prop: string]: any

to our Person interface. The line above means that the type Person can have any other property other than name . The property name is a string, which is the case for the dynamic property names in JavaScript, and these dynamic properties can take on any value since specified the any type for the dynamic property. As we can see, we have the following line:

const person: Person = { name: 'Jane', age: 20 };

where our object literal has the age property but it’s not explicitly defined in our interface definition. This is because we have the dynamic property after the name property. The [prop: string] is called the index signature.

We can also get around the excess property check for object literals by assigning a variable to another variable. For example, if we have the following code:

interface Person{
  name: string
}

const greet = (person: Person) => {
  console.log(`Hello, ${person.name}. ${person.age ? `You're ${person.age} years old.` : ''}`);
}

const person: Person = { name: 'Jane', age: 20 };
greet(person);

which wouldn’t compile and run because of the excess property check, we can get around it by assigning the person constant to a new variable or constant that doesn’t have a type designated to it like we do below:

interface Person{
  name: string
}

const greet = (person: Person) => {
  console.log(`Hello, ${person.name}`);
}

const person = { name: 'Jane', age: 20 };
greet(person);

The person constant doesn’t have a type designated to it so the excess property check for object literals won’t be run.

The excess property check is recommended to be enforced for simple objects like the ones we have above. For more complex, dynamic objects, we can use the ways we outline above to get around the checks to get the code running. However, do be aware that most excess property errors are actually typos in our code, so they’re legitimate bugs that should be fixed.

Photo by Max Baskakov on Unsplash

Function Types

With TypeScript interfaces, we can also define the signature of functions by designating the data type for each parameter and the return type of the function. This prevents us from passing in parameters that have the wrong data type or forgetting to pass in arguments into our function calls, and also ensures that our function always have the same return type and we won’t be returning things that we don’t expect in our code.

We can define a interface for designating the parameter and return data types of our function, and the function signature like we do in the code below:

interface GreetFn{
  (name: string, age: number): string
}

const greet: GreetFn = (name: string, age: number) => {
  return `Hello, ${name}. You're ${age} years old`;
}

console.log(greet('Jane', 20));

The code above has the function greet that follows the function signature defined on the left side of the colon in the GreetFn interface and the return data type on the right side of the interface, so the code will run and produce output from the console.log statement in the last line. We should get ‘Hello, Jane. You’re 20 years old’. If we designate our greet function with the type GreetFn but our function signature or return type stray away from the ones designated in the GreetFn interface then we’ll get errors. For example, if we have:

interface GreetFn{
  (name: string, age: number): string
}
const greet: GreetFn = (name: string, age: number, foo: any) => {
  return `Hello, ${name}. You're ${age} years old`;
}
console.log(greet('Jane', 20));

Then we’ll get the error message “Type ‘(name: string, age: number, foo: any) => string’ is not assignable to type ‘GreetFn’.(2322)“ since our parameter list doesn’t match the signature listed in the interface. Likewise, if our function’s return type doesn’t match the one we defined in the interface, we’ll also get an error. For example if we have the following code:

interface GreetFn{
  (name: string, age: number): string
}
const greet: GreetFn = (name: string, age: number) => {
  return 0;
}
console.log(greet('Jane', 20));

We’ll get the error “Type ‘(name: string, age: number) => number’ is not assignable to type ‘GreetFn’. Type ‘number’ is not assignable to type ‘string’.” This means that the greet function must return a string since we specified that the type of the greet function is GreetFn .

Function parameters are checked one at a time, so the TypeScript compiler infers the position of the type of a parameter by its position even though no types are designated by us when we define our function. For example, the following will still work even though we didn’t specified the type of our parameters explicitly:

interface GreetFn{
  (name: string, age: number): string
}
const greet: GreetFn = (name, age) => {
  return `Hello, ${name}. You're ${age} years old`;
}
console.log(greet('Jane', 20));

If we pass in something with the wrong data type according to the interface we defined like in the code below, we’ll get an error:

interface GreetFn{
  (name: string, age: number): string
}
const greet: GreetFn = (name, age) => {
  return `Hello, ${name}. You're ${age} years old`;
}
console.log(greet('Jane', ''));

When we try to compile the code above, we’ll get the error “Argument of type ‘“”’ is not assignable to parameter of type ‘number’.(2345)“. This means that TypeScript is smart enough to infer the type by its position. Type inference is also done for the return type, so if we write the following code:

interface GreetFn{
  (name: string, age: number): string
}
const greet: GreetFn = (name, age) => {
  return 0;
}
console.log(greet('Jane', 20));

Then we’ll get the error “Type ‘(name: string, age: number) => number’ is not assignable to type ‘GreetFn’. Type ‘number’ is not assignable to type ‘string’.(2322)” so the code won’t compile.

Excess property checks for object literals are useful since it’s much harder for us to add wrong properties or typos into our code when we’re assigning object literals or passing them in as arguments of functions. We can get around it with type assertion or assigning to a variable with different types or no types. We can also define interfaces for functions to define the expected parameters for a function and also the expected return type for them.

Categories
Angular JavaScript TypeScript

Angular Animation Callbacks and Key Frames

Angular is a popular front-end framework made by Google. Like other popular front-end frameworks, it uses a component-based architecture to structure apps.

In this article, we look at animation callback and keyframes.

Animation Callbacks

The animation trigger emits callbacks when it starts and when it finishes.

For example, we can log the value of the event by writing the following code:

app.component.ts :

import { Component, HostBinding } from "@angular/core";  
import {  
  trigger,  
  transition,  
  style,  
  animate,  
  state  
} from "@angular/animations";

@Component({  
  selector: "app-root",  
  templateUrl: "./app.component.html",  
  styleUrls: ["./app.component.css"],  
  animations: [  
    trigger("openClose", [  
      state(  
        "true",  
        style({ height: "200px", opacity: 1, backgroundColor: "yellow" })  
      ),  
      state(  
        "false",  
        style({ height: "100px", opacity: 0.5, backgroundColor: "green" })  
      ),  
      transition("false <=> true", animate(500))  
    ])  
  ]  
})  
export class AppComponent {  
  onAnimationEvent(event: AnimationEvent) {  
    console.log(event);  
  }  
}

app.component.html :

<button (click)="show = !show">Toggle</button>  
<div  
  [@openClose]="show ? true: false"  
  (@openClose.start)="onAnimationEvent($event)"  
  (@openClose.done)="onAnimationEvent($event)"  
>  
  {{show ? 'foo' : ''}}  
</div>

In the code above, we have:

(@openClose.start)="onAnimationEvent($event)"  
(@openClose.done)="onAnimationEvent($event)"

to call the onAnimationEvent callback when the animation begins and ends respectively.

Then in our onAnimationEvent callback, we log the content of the event parameter.

It’s useful for debugging since it provides information about the states and elements of the animation.

Keyframes

We can add keyframes to our animation to create animations that are more complex than 2 stage animations.

For example, we can write the following:

app.component.ts :

import { Component } from "@angular/core";  
import {  
  trigger,  
  transition,  
  style,  
  animate,    
  keyframes  
} from "@angular/animations";

@Component({  
  selector: "app-root",  
  templateUrl: "./app.component.html",  
  styleUrls: ["./app.component.css"],  
  animations: [  
    trigger("openClose", [  
      transition('true <=> false', [  
        animate('2s', keyframes([  
          style({ backgroundColor: 'blue' }),  
          style({ backgroundColor: 'red' }),  
          style({ backgroundColor: 'orange' })  
        ]))  
    ])  
  ]  
})  
export class AppComponent {  
  onAnimationEvent(event: AnimationEvent) {  
    console.log(event);  
  }  
}

app.component.html :

<button (click)="show = !show">Toggle</button>  
<div [@openClose]="show ? true: false">  
  {{show ? 'foo' : 'bar'}}  
</div>

In the code above, we add keyframes with different styles in AppComponent .

They’ll run in the order that they’re listed for the forward state transition and reverse for the reverse state transition.

Then when we click Toggle, we’ll see the color changes as the text changes.

Offset

Keyframes include an offset that defines the point in the animation where each style change occurs.

Offsets are relative measures from zero to one. They mark the beginning and end of the animation.

These are optional. Offsets are automatically assigned when they’re omitted.

For example, we can assign offsets as follows:

app.component.ts :

import { Component } from "@angular/core";  
import {  
  trigger,  
  transition,  
  style,  
  animate,    
  keyframes  
} from "@angular/animations";

@Component({  
  selector: "app-root",  
  templateUrl: "./app.component.html",  
  styleUrls: ["./app.component.css"],  
  animations: [  
    trigger("openClose", [  
      transition('true <=> false', [  
        animate('2s', keyframes([  
          style({ backgroundColor: 'blue', offset: 0 }),  
          style({ backgroundColor: 'red', offset: 0.6 }),  
          style({ backgroundColor: 'orange', offset: 1 })  
        ]))  
    ])  
  ]  
})  
export class AppComponent {  
  onAnimationEvent(event: AnimationEvent) {  
    console.log(event);  
  }  
}

app.component.html :

<button (click)="show = !show">Toggle</button>  
<div [@openClose]="show ? true: false">  
  {{show ? 'foo' : 'bar'}}  
</div>

In the code above, we added offset properties to our style argument objects to change the timing of the color changes.

The color changes should shift slightly in timing compared to before.

Keyframes with a Pulsation

We can use keyframes to create a pulse effect by defining styles at a specific offset throughout the animation.

To add them, we can change the opacity of the keyframes as follows:

app.component.ts :

import { Component } from "@angular/core";  
import {  
  trigger,  
  transition,  
  style,  
  animate,    
  keyframes  
} from "@angular/animations";

@Component({  
  selector: "app-root",  
  templateUrl: "./app.component.html",  
  styleUrls: ["./app.component.css"],  
  animations: [  
    trigger("openClose", [  
      transition('true <=> false', [  
        animate('1s', keyframes ( [  
          style({ opacity: 0.1, offset: 0.1 }),  
          style({ opacity: 0.6, offset: 0.2 }),  
          style({ opacity: 1,   offset: 0.5 }),  
          style({ opacity: 0.2, offset: 0.7 })  
        ]))  
    ])  
  ]  
})  
export class AppComponent {  
  onAnimationEvent(event: AnimationEvent) {  
    console.log(event);  
  }  
}

app.component.html :

<button (click)="show = !show">Toggle</button>  
<div [@openClose]="show ? true: false">  
  {{show ? 'foo' : 'bar'}}  
</div>

In the code above, we have the style argument objects that have the opacity and offset properties.

The opacity difference will create a pulsating effect.

The offset will change the timing of the opacity changes.

Then when we click Toggle, we should see the pulsating effect.

Automatic Property Calculation with Wildcards

We can set CSS style properties to a wildcard to do automatic calculations.

For example, we can use wildcards as follows:

app.component.ts :

import { Component } from "@angular/core";  
import {  
  trigger,  
  transition,  
  style,  
  animate,  
  state  
} from "@angular/animations";

@Component({  
  selector: "app-root",  
  templateUrl: "./app.component.html",  
  styleUrls: ["./app.component.css"],  
  animations: [  
    trigger("openClose", [  
      state("in", style({ height: "*" })),  
      transition("true => false", [  
        style({ height: "*", backgroundColor: "pink" }),  
        animate(250, style({ height: 0 }))  
      ]),  
      transition("false => true", [  
        style({ height: "*", backgroundColor: "yellow" }),  
        animate(250, style({ height: 0 }))  
      ])  
    ])  
  ]  
})  
export class AppComponent {  
  onAnimationEvent(event: AnimationEvent) {  
    console.log(event);  
  }  
}

app.component.html :

<button (click)="show = !show">Toggle</button>  
<div [@openClose]="show ? true: false">  
  {{show ? 'foo' : 'bar'}}  
</div>

In the code above, we set the height of the styles to a wildcard because we don’t want to set the height to a fixed height.

Then when we click Toggle, we see the color box grow and shrink as the animation runs.

Conclusion

We can add callbacks to our animation to debug our animations since we can log the values there.

To make more complex animations, we can use keyframes.

Offsets can be used to change the timing of the keyframes of the animation.

We can use wildcards to automatically set CSS style values.