Our task management application supports the listing of multiple projects where a user can manage tasks. We need to provide a navigation that enables a user to browse through the existing projects. As projects come from our data store, the navigation will need to be generated dynamically. However, we also would like to have the possibility of specifying some navigation items within our navigation, as static content with pure templating.
In this section, we will create a simple navigation component, which will use content projection, so that we can add static navigation items. At the same time, navigation items can be generated from data and mixed with the static content-based navigation items.
Let's first take a look at an illustration of the architectural design and composition that we're going to use to implement our navigation:
We'll use an intermediate component between the Navigation
and NavigationItem
components. The NavigationSection
component is responsible for the division of multiple items into a section. The navigation sections also have a title that will be displayed on top of the item list.
The illustration shows two NavigationSection
components, where the left one uses pure content projection to create items, as we have learned in the previous section. The right NavigationSection
component generates items using an input data structure, which is a list of navigation item models.
As we have intermediate components between the Navigation
and NavigationItems
components (we can only have one selected navigation), we also establish a direct communication path between them. We will achieve this using ancestor component injection.
The architectural approach for this navigation is just one of many possible approaches. We choose this approach in order to show you how we can easily mix content projection and generated content. In this example, we don't use the Angular router to provide navigation state and route mapping. This will be part of a later chapter.
Let's start bottom up with the NavigationItem
component and create a new navigation-item.js
file in a newly-created navigation/navigation-section/navigation-item
path:
// We rely on the navigation component to know if we are active import {Navigation} from '../../navigation'; @Component({ selector: 'ngc-navigation-item' }) export class NavigationItem { @Input() title; @Input() link; constructor(@Inject(Navigation) navigation) { this.navigation = navigation; } // Here, we are delegating to the navigation component to see if // we are active or not isActive() { return this.navigation.isItemActive(this); } // If this link is activated we need to tell the navigation component onActivate() { this.navigation.activateLink(this.link); } }
From the NavigationItem
component code, we can see that we're directly communicating with the Navigation
ancestor component. We can simply inject the NavigationComponent
, as this is a child of the component. As the Navigation
items will never exist without a Navigation
component, we should be fine with this direct dependency.
Let's move on to the NavigationSection
component that is the intermediate component between the Navigation
component and the items and is responsible for the grouping of items together.
We will create a file called navigation-section.js
in the navigation/navigation-section
path:
@Component({ selector: 'ngc-navigation-section', directives: [NavigationItem] }) export class NavigationSection { @Input() title; @Input() items; }
Hold on! That's all that this needs? Didn't we say that we want our NavigationSection
component to also be responsible for not only providing a way to insert content, but also accepting data in order to generate items? Well, this is true. However, this is actually pure templating logic, and it can be done solely within the template file of the component. All that we need is an optional input with item data that we will use to generate the NavigationItem
components.
Let's create the view template for this component in a file named navigation-section.html
:
<h2 class="navigation-section__title">{{title}}</h2> <ul class="navigation-section__list"> <ng-content select="ngc-navigation-item"></ng-content> <ngc-navigation-item *ngFor="let item of items" [title]="item.title" [link]="item.link"></ngc-navigation-item> </ul>
Well, this wasn't rocket science, was it? However, this shows the great flexibility that we have in Angular component templates:
ngc-navigation-item
. This means that the NavigationItem
components can be placed outside the component in a very static fashion to create, for example, static links. As the model properties of navigation items are directly exposed as bindable attributes on the NavigationItem
element, we can also place them statically into a pure HTML template with regular DOM attributes.NgFor
directive to generate the NavigationItem
components inside the component. Here, we just iterate over the list of navigation item models that acts as an optional input to our component. We use bindings in the items model so that we can even propagate change into our navigation item components.As a final step, we create the Navigation
component itself that uses content projection points so that we can manage the NavigationSection
component from outside. We create a file called navigation.js
to write the code of the Navigation
component:
import {NavigationSection} from './navigation-section/navigation-section'; @Component({ selector: 'ngc-navigation', directives: [NavigationSection] }) export class Navigation { @Input() activeLink; // Checks if a given navigation item is currently active by its // link. This function will be called by navigation item child // components. isItemActive(item) { return item.link === this.activeLink; } // If a link wants to be activated within the navigation, this // function needs to be called. This way child navigation item // components can activate themselves. activateLink(link) { this.activeLink = link; this.activeLinkChange.next(this.activeLink); } }
In the Navigation
component, we store the state of which navigation item is activated. This is also provided as input to the component so that we can set the activated link with an input binding from outside. The isItemActive
and activateLink
functions are there to monitor and change the state of the active item within the navigation. These functions are directly used within the NavigationItem
components, which inject the navigation using ancestor component injection.
Now, the only bit that is missing is to include our navigation in the main application. For this, we will edit the app.html
template of the component:
<div class="app"> <div class="app__l-side"> <ngc-navigation [activeLink]="getSelectedProjectLink()" (activeLinkChange)="selectProjectByLink($event)"> <ngc-navigation-section title="Projects" [items]="getProjectNavigationItems()"> </ngc-navigation-section> </ngc-na vigation> </div> <div class="app__l-main"> … </div> </div>
Here, we only use the generative approach to write a NavigationSection
component where we actually pass a list of navigation item models into the navigation component. This list is generated by the getProjectNavigationItems
function on our main application component using the available projects from our observable data structure: