Getting Started with Angular: Components

Getting Started with Angular: Components

In the last post, we covered the elements of routing in Angular. This post is going to cover what we’re routing to, Components. The starting code can be found here:

To demonstrate this we’re going to add a page for a single product. We’ll demonstrate:

  • The configuration available in the @Component decorator

  • How to reference additional components on your page

  • Creating new components

  • Creating and using standalone components

Let’s get started, shall we?

What is a Component?

Components are the main elements of Angular applications. Each component can be configured with a variety of options, which we’ll get into soon. However, there are 2 main parts to a component; the template and the code.

The template holds all of the HTML for your component. This is the part that will be rendered when our component is loaded up in a browser. The template can be made up of standard HTML elements, component selectors (which will be discussed soon), and built-in Angular elements. A template can either be specified within the component configuration or as a separate file. Built-in elements will be discussed in a future post alongside some of the built-in directives.

The code is a Typescript class marked with the @Component decorator. When we’re adding logic to our components this is where we’ll be putting the changes. It’s best practice to avoid putting logic directly in the template and handle it using Typescript instead.

What can be configured?

We’ll start by creating a new module and component for our product page. We’ll do this by running:

ng g module Product --module app --route product

As well as creating our module and component it will also configure the declarations and routing for it to work. Now we can go to http://localhost:4200/product and see our product works! message.

Now we’ve got our component we can take a look at the configuration options. If we open product.component.ts we can see the default options added to the Component decorator:

@Component({
  selector: 'app-product',
  templateUrl: './product.component.html',
  styleUrls: ['./product.component.scss']
})

Let’s start with covering the defaults:

selector

Specifies the element we use to include this component in other components. We’ll cover how to make use of this soon.

templateUrl

The path to an HTML file containing the content to be displayed for this component. We define either the templateUrl or template, not both.

styleUrls

An array of paths to files containing the styles to be imported for this component.

There are a whole host of other options we have for configuration. Some of these are quite niche, so I won’t cover them here (looking at you interpolation). Here are some of the more widely applicable ones:

template

Like templateUrl, this defines the content to be displayed for this component. Rather than defining it in a separate file, we can define it in line with the component configuration. We define either the templateUrl or template, not both.

styles

Gives us a way to specify styles within the component configuration rather than a separate file. Unlike templateUrl/template we can specify both the styleUrls and styles values, they will be combined in the built version.

changeDetection

The strategy to use for responding to change detection in this component. Change detection and the Angular lifecycle are important topics for working effectively with Angular, so we’ll cover those in detail in a separate post.

encapsulation

Allows us to specify how we want styles to be scoped. This is another one that deserves its own post, but I’ll summarize the three options we have:

  • Emulated - Styles are only applied to elements within the current component. This is the default.

  • None - Styles are applied globally and may affect elements outside of the current component. Only use this if you’re really, really sure there’s no other way.

  • ShadowDom - Like Emulated, styles are only applied to elements within the current component. Also prevents global styles from being applied to this component or any of its children.

standalone

This is a boolean value indicating whether the component should be treated as standalone. We will discuss standalone components shortly.

That was a lot of information without actually getting us anywhere. We’ll move on to something more interesting, building our component.

Populating the template

Let's start by getting some content on the page. We'll start by entering the following in product.component.html:

<div class="product-image">
    <img src="../../assets/images/taco.png" />
</div>

<div class="buttons">
    <button mat-button color="primary">Add to Basket</button>
</div>

<mat-divider></mat-divider>

<p class="product-description">
    <span>
        <p>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
            dolore magna aliqua. Arcu dictum varius duis at consectetur lorem. Aenean vel elit scelerisque mauris
            pellentesque
            pulvinar pellentesque habitant. Congue eu consequat ac felis donec et odio pellentesque. Consectetur lorem
            donec massa sapien faucibus et. Mi bibendum neque egestas congue. Morbi blandit cursus risus at ultrices mi
            tempus
            imperdiet. Ut porttitor leo a diam sollicitudin tempor. Lacus vel facilisis volutpat est velit egestas dui
            id. Nam at lectus urna duis convallis convallis. Neque aliquam vestibulum morbi blandit cursus risus. Semper
            eget duis at tellus at urna condimentum mattis. Eu volutpat odio facilisis mauris. Lectus arcu bibendum at
            varius
            vel pharetra vel. Nec feugiat nisl pretium fusce id velit ut. Etiam erat velit scelerisque in. Sit amet
            purus
            gravida quis blandit turpis cursus in hac.
        </p>
    </span>
</p>

<mat-divider></mat-divider>

<div class="product-reviews">
    <div class="review-entry">
        <textarea matInput></textarea>
    </div>

    <div class="review-buttons">
        <button mat-button color="primary">Post Review</button>
    </div>

    <div class="reviews-list">
        <p>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
            dolore magna aliqua. Arcu dictum varius duis at consectetur lorem. Aenean vel elit scelerisque mauris
            pellentesque
            pulvinar pellentesque habitant.
        </p>
    </div>
</div>

This is a good start, but if we load it up we'll see it doesn't look quite right. So, let's style it. In product.component.scss we'll enter:

.product-image {
    display: flex;
    justify-content: center;

    img {
        object-fit: scale-down;
        max-width: 500px;
    }
}

.buttons {
    width: 100%;
    display: flex;
    justify-content: center;
}

.product-description {
    flex: 1;
}

.product-reviews {
    width: 80%;
    display: block;
    margin-right: auto;
    margin-left: auto;
    margin-top: 20px;

    .review-entry textarea {
        width: 100%;
    }

    .review-buttons {
        display: flex;
        justify-content: flex-end;
    }
}

There, looking much better now.

We've got our page now, but we're not taking advantage of the component system. If we look at product.component.html we'll see 4 elements could be their own components:

  • Product image

  • Description

  • Add to basket button

  • Reviews

We'll create a component for each of these in our existing product module.

Creating new components

We’ll start with the product reviews. We can create a component under our existing product module by running:

ng g component  product/product-reviews --module product

We can now move the implementation from product.component.html and the related styles to product-reviews.component.html.

<div class="review-entry">
    <textarea matInput></textarea>
</div>

<div class="review-buttons">
    <button mat-button color="primary">Post Review</button>
</div>

<div class="reviews-list">
    <p>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
        dolore magna aliqua. Arcu dictum varius duis at consectetur lorem. Aenean vel elit scelerisque mauris
        pellentesque
        pulvinar pellentesque habitant.
    </p>
</div>
.review-entry textarea {
    width: 100%;
}

.review-buttons {
    display: flex;
    justify-content: flex-end;
}

Now we’ve moved this HTML into the new component we can go to product.component.html and replace it with <app-product-reviews />. If everything has gone to plan the page should look the same as it did before. This allows us to simplify our higher-level components while building up functionality by composing lower-level components.

This is only one advantage of using the component system though. If we compare our product component with our product list component we can see they both need the other components we’re going to create. We’re going to make them reusable between both by declaring them as standalone components.

Standalone components

Let’s start by defining what standalone components are. Standalone components work in the same way as standard components, but they don’t require a declaration in a module. This gives us a lot of flexibility in where and how we make use of our components.

Rather than importing a full module and dragging in everything that has been declared, we can import specific components at either a module level (for standard components) or component level (for use in other standalone components).

We’re going to use the CLI to generate our new components for us, but I’ll start by discussing how to convert existing components. Converting should be an easy process, consisting of three steps:

  • Add standalone: true to the component configuration

  • Move any declarations of the component to the imports section of the relevant modules/standalone components

  • Add imports to the configuration of the converted component to pull in any required elements

Depending on the component being converted and the size of the project this could be a fairly sizeable imports list. If you find this happening it might be that your component could do with some refactoring into smaller subcomponents.

Since we don’t have an existing component to convert, we can just run:

ng g component  shared/product-image --standalone

This is going to create a product-image.component in the shared folder. We can now use the template from product.component:

<img src="../../assets/images/taco.png" />

And for the styles, I’ve modified the styles to work a bit better as a shared component:

img {
    object-fit: scale-down;
    max-width: 100%;
}

img:hover {
    transition: ease 2s;
    transform: scaleY(-1);
}

You may notice I’ve added a hover style. This is just to demonstrate how easy it is to share additional functionality when reusing components. This particular function might be useless (what’s life without a little whimsy?), but we could just as easily implement something like opening a carousel of images on click.

Now we’ve got our standalone component in place we need to add it to the imports of both product.module.ts and product-list.module.ts. With that imported, we’re just going to replace our <img /> elements with <app-product-image />.

Reloading the page we can now see the image flip when we hover our mouse over. We’ll also see the images are much larger than they were before. The styling will need to be modified slightly to account for the new component. However, I won’t detail that here. Nor will I cover creating the other two components we need. Instead, I’m going to come to the end of this post. Go ahead and have a go at sorting the rest out yourself, and when you’re ready you can compare against the complete code for this post.

Detour: Getting to the product page

Something I haven’t covered is how to link to the product page. This is easy to do in Angular (at least with our simple structure) using routerLink. We’re going to go to product-list.component.html and replace our instances of <li> with <li [routerLink]="'/product'">. Now we will be taken to the product page when we click on one of our products.

Note that we’ve set the path to /product rather than product. If we omit the slash the router will attempt to append the string to the end of whatever path we’re already on. So instead of being taken to http://localhost:4200/product like we want, we’ll actually end up being taken to http://localhost:4200/products/product.

What’s next?

You may have noticed that something has gone wrong when we added our new product image component. We’ve now got the same image for everything!

We’re going to fix this in the next post. We’ll cover how to pass data in and out of our components using the @Input and @Output decorators. We’re also going to take some time to discuss the built-in elements and directives that can help us take more control of our template.

The final code for this post can be found here: