Mixing projected with generated content

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:

Mixing projected with generated content

An illustration of the navigation component tree and interactions

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.

Note

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:

  • Firstly, we create a content projection point that selects all the elements from the host element that match the name 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.
  • Secondly, we can use the 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:

Mixing projected with generated content

A screenshot of the newly-created project navigation

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset