Skip to content

More on Components

Adarsh Kumar Maurya edited this page Dec 10, 2018 · 1 revision

Introduction

When building clean components, we want to ensure everything is strongly typed, our styles are encapsulated, we respond to appropriate lifecycle events, and we transform data to user-friendly values as needed. Welcome back to Angular: Getting Started, from Pluralsight. My name is Deborah Kurata, and in this chapter we learn several ways to improve upon our components. Components are one of the key building blocks of our application. The cleaner, stronger, and more durable we make these blocks, the better our application. So, how can we make our components better? Strong typing helps minimize errors through better syntax checking and improved tooling, but what if there is no predefined type for a property? To strongly type a property that has no predefined type, we define the type ourselves using an interface. If a component needs special styles, we can encapsulate those styles within the component to ensure they don't leak out to any other component in the application. A component has a lifecycle managed by Angular. Angular provides a set of lifecycle hooks we can use to tap into key points in that lifecycle, adding flexibility and responsiveness to our application. As we saw in the last chapter, pipes provide a convenient way to transform bound data before displaying it in the view. We may have other application- unique data transformation requirements. Luckily, we can build our own custom pipes. Any time we build and test a component once and nest it in several places in the application, we have minimized development time and improved the overall quality of the application. In this chapter, we explain interfaces and demonstrate how to use them to strongly type our objects. We look at how to encapsulate component styles, we introduce the component lifecycle and how to hook into its events, and we detail how to build a custom pipe. We cover building nested components in the next chapter. Looking at our application architecture, in this chapter we'll add features to improve the product list component. Let's get started.

Defining Interfaces

One of the benefits of using TypeScript is its strong typing. Every property has a type, every method has a return type, and every method parameter has a type. The strong typing helps minimize errors through better syntax checking and tooling. In some cases, however, we have a property or method that does not have a predefined type, such as our products array here. We defined our products array as any, which negates the benefits of strong typing. To specify custom types, we can define an interface. An interface is a specification identifying a related set of properties and methods. A class commits to supporting the specification by implementing the interface. That means the class includes code for each property and method identified in the interface. We can then use the interface as a data type. ES5 and ES2015 do not support interfaces, but TypeScript does, so interfaces are transpiled out and are not found in the resulting JavaScript. This means that interfaces are development time only. Their purpose is to provide strong typing and better tooling support as we build, debug, and maintain our code. Here is an example of a TypeScript interface. We define an interface using the interface keyword, followed by the interface name, which is often the name of the business object that the interface describes. By many naming conventions, the interface is prefixed with an I for interface, though some TypeScript developers leave off this prefix. The export keyword here at the front exports this interface, thereby making it available for use anywhere in the application. The body of the interface defines the set of properties and methods appropriate for this business object. For each property, the interface includes the property name, a colon, and the property data type. For each method, the interface includes the method name, the list of parameters along with their datatypes, a colon, and the method return type. To use the interface as a data type, we import the interface, then use the interface name as the data type. Looks easy enough. Let's try it out. We are back in the sample application looking at the product-list.component. Here we see that we defined our products array as any, so let's create an interface that defines what a product is. We'll put the interface into its own file in the products folder. We'll name that file product.ts after the business object it defines. We first type the export keyword to ensure the other parts of the application can use this interface. That is, after all, why we are creating it. Next, we type in the interface keyword, then the name of the interface. I like to use the I prefix to distinguish the interface from the business object, and we are defining a product, so IProduct. Inside the interface we define the properties and methods appropriate for the business object. For our sample application, we only need properties. For each property, we define the property name, a colon, and the type of the property. Notice that we are typing the releaseDate as a string. If we later work with this value as a date object, we could change this type to a date. That's it, that's all we have to do to define an interface. Now we can use this interface as our data type in the product list component. Before we do though, let's try something. Let's introduce a typographical error into our products array. No error is detected, we won't even know we made a mistake until we see the application in the browser, and we notice that no image is displayed for one of the products. As you can imagine, these kinds of errors could cause hard to find bugs. Now let's replace the any with IProduct. We get a syntax error. What did we miss? Yep, we need an import. And, bang, now that our array of products is strongly typed, we are notified that we made an error in our product array property. We don't even have to view it in the browser to see that something is amiss. So this is a good demonstration of one of the benefits of strong typing. Let's fix that error. Notice that we get IntelliSense now for these properties, another great benefit of strong typing. Let's check this out in the browser. Now everything works as it did. In our interface file, we could define a class for the product business object here as well, something like this. In general, we only create a business object class if that class provides some functionality that we want to use throughout our application, such as this calculateDiscount method, and if we think we might want to create a business object at some point in the future, then we definitely want to add the I prefix to our interface, but for our sample application, we don't need any product methods, so we don't really need a product class. We will use this interface throughout the application to strongly type our products. Next, let's look at encapsulating component styles.

Encapsulating Component Styles

When we built a template for a component, we sometimes need styles unique to that template. For example, if we build a sidebar navigation component, we may want special li or div element styles. When we nest a component that requires special styles within a container component, we need a way to bring in those unique styles. One option is to define those styles directly in the template's HTML, but that makes it harder to see, reuse, and maintain those styles. Another option is to define the styles in an external style sheet that makes them easier to maintain, but that puts the burden on the container component to ensure the external style sheet is linked in the index.html. That makes our nested component somewhat more difficult to reuse, but there is a better way. To help us out with this issue, the Component decorator has properties to encapsulate styles as part of the component definition. These properties are styles and styleUrls. We add unique styles directly to the component using the styles property. This property is an array, so we can add multiple styles separated by commas. A better solution is to create one or more external style sheets and identify them with the styleUrls property. This property is an array, so we can add multiple style sheets separated by commas. By encapsulating the styles within the component, any defined selectors or style classes are only applicable to the component's template and won't leak out into any other part of the application. Let's try this out. Before we change any code, let's look again at our Product List view in the browser. Notice the table headers. They could use a little color, so let's build an external style sheet for our product list component. We'll add a new file in the products folder, and since this file only contains the styles for our product list component, we'll call it product-list.component.css. In this style sheet, we add a table header style. We can modify the thead element styles directly because the style sheet is encapsulated in this component and the styles defined here won't affect any other component in the application. We could add any other styles as needed to jazz up our product list component. To use this new style sheet, we modify the product list component. In the Component decorator, we specify our unique style sheet. We add the styleUrls property and pass it an array. In the first element of the array we specify the path to our style sheet. Since we defined the CSS file in the same folder as the component, we can use the ./ relative path syntax. We could add more style sheets here, separated with commas. Let's review the result in the browser, and we see that the table header is now a nice blue color. We can use the styles or styleUrls property of the Component decorator any time we want to encapsulate unique styles for our component. Next up, let's dive into lifecycle hooks.

Using Lifecycle Hooks

A component has a lifecycle managed by Angular. Angular creates the component, renders it, creates and renders its children, processes changes when its data bound properties change, and then destroys it before removing its template from the DOM. Angular provides a set of lifecycle hooks we can use to tap into this lifecycle and perform operations as needed. Since this is a getting started tutorial, we'll limit our focus to the three most commonly used lifecycle hooks. Use the OnInit lifecycle hook to perform any component initialization after Angular has initialized the data bound properties. This is a good place to retrieve the data for the template from a back-end service, as we'll see later in this tutorial. Use the OnChanges lifecycle hook to perform any action after Angular sets data bound input properties. We have not yet covered input properties; we'll see those in the next chapter. Use the OnDestroy lifecycle hook to perform any cleanup before Angular destroys the component. To use a lifecycle hook, we implement the lifecycle hook interface. We talked about interfaces earlier in this chapter. Since Angular itself is written in TypeScript, it provides several interfaces we can implement, including one interface for each lifecycle hook. For example, the interface for the OnInit lifecycle hook is OnInit. Notice that it is not prefixed with an I. We are using the OnInit interface from Angular, so any guesses as to our next step? Yep, we need to import the lifecycle hook interface. We can then write the hook method. Each lifecycle hook interface defines one method whose name is the interface name prefixed with ng, for Angular. For example, the OnInit interface hook method is named ngOnInit. Our first step here of implementing the interface is actually optional. Recall from our discussion of interfaces earlier in this chapter that neither ES5 nor ES2015 support interfaces. They are features of TypeScript. That means that the interfaces are transpiled out of the resulting JavaScript, so we don't really have to implement the interface to use lifecycle hooks, we can simply write code for the hook method. However, it is good practice and provides better tooling when we implement the interface. At this point in our application, we don't need to implement any lifecycle hooks, but we'll use them in later chapters, so let's try them out now. We are looking at the product list component. We'll add the OnInit lifecycle hook to this component. First, we implement the interface by adding it to the class signature. Type implements and the name of the interface, OnInit. The interface name is showing an error, and we know why, we don't have the import. Let's add that now. Since OnInit is also imported from angular/core, we simply add it to our first import statement. Now we have another syntax error here. Class ProductListComponent incorrectly implements interface OnInit, as the message states. Now that we've implemented the interface, we must write code for every property and method in that interface. The OnInit interface only defines one method, ngOnInit, so we need to write the code for the ngOnInit method. We'll add it down here by the other methods. Since we don't really need to do anything with this at this point, we'll just use console.log to log a message to the console. We can view the application in the browser and use the F12 Developer Tools to open the console and view the logged message. We can see our message here. We'll use ngOnInit later in this tutorial. Up next, we'll build a custom pipe.

Building Custom Pipes

As we saw in the last chapter, we use pipes for transforming bound properties before displaying them in a view. There are built-in pipes that transform a single value or an iterable list of data. We covered these in the last chapter. In this chapter, we want to build our own custom pipe. For our sample application, the product code is stored with a dash, and displayed that way here, but the users would prefer to see the product code with a space instead. We could build a custom pipe to replace the dashes with spaces, but let's build a more generalized custom pipe that transforms any specified character in a string to a space. The code required to build a custom pipe may look somewhat familiar at this point. It uses patterns similar to other code we've created in this tutorial. Here is the class. We add a pipe decorator to the class to define it as a pipe. Similar to the other decorators we've used, this is a function, so we add parentheses. We pass an object to the function, specifying the name of the pipe. This is the name for the pipe used in the template, as we'll see shortly. We implement a PipeTransform interface, which has one method, transform. We write code in the transform method to transform a value and return it. The first parameter of the transform method is the value we are transforming. In this example, we transform a string. Any additional parameters define arguments we can use to perform the transformation. In our case, we want to pass in the character that we want to replace with spaces. The method return type is also defined as a string because we are returning the transformed string, and of course we have our import to import what we need. Notice how similar this looks to the components we've created in this tutorial? Angular provides a very consistent coding experience. To use a custom pipe in a template, simply add a pipe and the pipe name. Include any arguments required by the transformation, separated by colons. The value being converted, our productCode here, is passed in as the first argument to the transform method. This is our pipe name. The colon identifies the pipe parameter, so our dash is passed in as the second argument to the transform method. The passed-in value is then transformed as defined by the logic within this method, and the transformed string is returned and displayed here. But of course, that is not enough. We also need to tell Angular where to find this pipe. We add the pipe to an Angular chapter. How do we know which Angular chapter? Well at this point that's easy because we only have one, AppModule, but if we had multiple chapters we'd add it to the chapter that declares the component that needs the pipe. In our example, the product list component's template needs the pipe, so we add the declaration to the same Angular chapter that declares the product list component. We define the pipe in the declarations array of the NgModule decorator. Now let's build our custom pipe. Recall that our requirement is to transform a string so that any dashes on the string are instead displayed as spaces, but we are going to build a more generic pipe that can replace any specified character with a space. Since our custom pipe is somewhat general, we'll add it to the shared folder. We'll create a new file and call it convert-to-spaces.pipe.ts, following our naming conventions. First, let's write the code for the class, export so we can import this pipe where we need it, class, and the class name. We'll call it ConvertToSpacesPipe. We decorate the class with the Pipe decorator and import pipe from angular/core. We set the name property of the object passed into the Pipe decorator, defining the pipe's name. That's the name we'll use when we reference the pipe in the HTML. Next, we'll implement the PipeTransform interface. This syntax error is because we don't have the import statement, so let's add that next. We still have a syntax error because when we implement the PipeTransform interface, we are required to implement every property and method defined in that interface. For the PipeTransform interface, there is only one method, transform. We'll define the string value to transform as the first parameter, and the character string to use in the transformation as the second parameter, and we can add a return statement to get rid of this last syntax error. Now, what do we want the transform method to do? Our goal is to replace any of the specified characters in a string with spaces. We'll use the JavaScript string replace method to replace the specified character with a space. That's it. Now we are ready to use our pipe. In the product list template, we'll add our pipe to the product code, but the product code already has a pipe. That's okay, we can add any number of pipes. First we specify the pipe name, and then any pipe parameters. In this case, we want to replace a dash with a space, so we pass in a dash here as the parameter. Let's give this a try. Our page did not display, and using the F12 tools, we can see why. The pipe convertToSpaces could not be found. What did we forget? Recall from the slides that we need to tell Angular about our new pipe. We do that by declaring the pipe in an Angular chapter. Our product list components template wants to use the convertToSpaces pipe, so we open the Angular chapter that declares the product list component, which in our example is the AppModule. We then add ConvertToSpacesPipe to the declarations and add the needed import. Now any component declared in AppModule can use the convertToSpaces pipe. Are we ready to try again? Success! Our product code now appears with spaces instead of dashes. Build a custom pipe anytime you need to perform application- unique data transformations. Notice, however, that our page interactivity is still not complete. The product list is not yet filtering based on the user-entered filter criteria. Let's look at that next.

Filtering a List

We have an input box here for the user to enter a filtered string. The list of products should then be filtered on the entered string. How do we do that? One way we can filter our list of products is to use a pipe. However, Angular doesn't come with a built-in pipe to provide filtering or sorting. As of the time of this recording, the Angular documentation at angular.io stated, "Angular doesn't offer such pipes because they perform poorly and prevent aggressive minification." If we have a small amount of data that we are filtering or sorting, we could build a custom pipe anyway, but is there a better way? Again from the Angular documentation, "The Angular team and many experienced Angular developers strongly recommend moving, filtering, and sorting logic into the component itself." Okay, let's do that, but how? Here is our product list component. Let's think through what we need to do. First, we need a filtered list of products that we can bind to. We can define a property for that here. Why don't we just filter our products array? Because once we filter the products array, we lose our original data and can't get it back without re-getting the data from its source. Next, we need a way to know when the user changes the filter criteria. We could use event binding and watch for key presses or value changes, but an easier way is to change our listFilter property into a getter and setter, like this. The property getter and setter work just like the simple property. When the data binding needs the value, it will call the getter and get the value. Every time the user modifies the value, the data binding calls the setter, passing in that changed value. If we want to perform some logic every time the value has changed, we can add it here in the setter. We want to set our filtered products array to the filtered list of products, like this. Here we are using the JavaScript conditional operator to handle the possibility that the listFilter string is empty, null or undefined. If there is a listFilter value, this code filters on that value. If the listFilter is not set, the filtered products property is assigned to the entire set of products, and that makes sense. If there is no filter, we should display all of the products, but we skipped the messy part. What about this performFilter method? I'll paste the code and we can talk through it. This code starts by converting the filter criteria to lowercase. Why? So we can compare apples to apples when we filter the product list. We want a case insensitive comparison. Then we return the filtered list of products. Let's look closer at the filter method call. We are using the array filter method to create a new array with elements that pass the test defined in the provided function. We use the ES2015 arrow syntax to define that filter function. For each product in the list, the product name is converted to lowercase and indexOf is used to determine if the filter text is found in the product name. If so, the element is added to the filtered list. See the MDN entry for filter at this link for more information on the array filter function. There is one more bit of code we need here. We want to set default values for both the filteredProducts and the listFilter properties. The best place to set default values for more complex properties is in the class constructor. The class constructor is a function that is executed when the component is first initialized. We want to set the filteredProducts to the full list of products, and the default listFilter to cart, like we had earlier. Our last step then is to change our template to bind to our filteredProducts property instead of the products property. Now let's give it a try. Our default filter is cart, so now we only see the Garden Cart. Change the filter, and we see different entries. It's working! Not too shabby, not too shabby at all. Let's finish up this chapter with some checklists we can use as we work more with components.

Checklists and Summary

We've covered several discrete topics in this chapter, so we have a checklist for each one. First, interfaces. We use interfaces to specify custom types, such as product in our sample application. Interfaces are a great way to promote strong typing in our applications. When creating an interface, use the interface keyword. In the body of the interface, define the appropriate properties and methods along with their types, and don't forget to export the interface so it can be used anywhere in our application. We implement an interface, including built-in Angular interfaces, to ensure that our class defines every property and method identified in that interface. Add the implements keyword and the interface name to the class signature, then be sure to write code for every property and method in the interface. Otherwise, TypeScript displays a syntax error. We can encapsulate the styles for our component in the component itself, that way the styles required for the component are associated with the component alone and don't leak into any other parts of the application. Use the styles property of the Component decorator to specify the template styles as an array of strings. Use the styleUrls property of the Component decorator to identify an array of external style sheet paths. The specified styles are then encapsulated in the component. Lifecycle hooks allow us to tap into a component's lifecycle to perform operations. The steps for using a lifecycle hook are: import the lifecycle hook interface; implement the lifecycle hook interface in the component class, the step is not required, To build a custom pipe, import Pipe and PipeTransform. Create a class that implements the PipeTransform interface. This interface has one method, transform. Be sure to export the class so the pipe can be imported from other components. Write code in the transform method to perform the needed transformation and decorate the class with the Pipe decorator. We can use a custom pipe in any template anywhere we can specify a pipe. In an Angular chapter, import the pipe. In the metadata, declare the pipe in the declarations array, then any template associated with a component declared in that Angular chapter can use that pipe. In a template, immediately after the property to transform, type a pipe character, specify the pipe name, and enter the pipe arguments, if any, separated by colons. This chapter provided a set of techniques for improving our components. We walked through how to use an interface to strongly type our custom objects. We saw how to encapsulate styles within a component. We were introduced to the component lifecycle and how to tap into that lifecycle with lifecycle hooks. And we discovered how to build a custom pipe that we can use in any component template. So in this chapter, we've completed the product list component. Yay! Next up, we'll see how to build nested components and build this star component.