Parent-Child Component Communication with Angular and Vanilla JS
Tags: angular, javascript, typescript, webdev
Published: 2022-05-25 22:21:32

> 
<small> This post uses the code picked up from [my last article](https://dev.to/riapacheco/custom-slide-out-menu-component-with-angular-animations-and-scss-453l) (that created a custom slide-out menu with dynamic data), the code of which you can [fork from this branch](https://github.com/riapacheco/menu-demo/tree/part-1). As an aside: this is a long article because it's the one I needed when I started coding with Angular.
</small>

---

## Core Concepts
* Defining Types with Interfaces (and why)
* Component-to-Component Communication with `@Input()` and `@Output()`
* Two-way Data Binding through Child Selectors
* One-Way Binding of Events with `EventEmitter`
* Labelling DOM elements with Dynamic Parent Data

---

### Skip Ahead
- [Brief **Refresher**: Data and Event](#brief-refresher-component-data-and-click-event)
- [Why Define Types with **Interfaces**?](#why-define-types-with-interfaces)
 - [**Creating** the Interface](#creating-the-interface)
 - [**Adding** the Interface to the Component's Data](#adding-the-interface-to-the-components-data)
- [**Parent Component** Placement and Preparation](#parent-component-placement-and-preparation)
 - [Making the Parent the **Main Data Reader**](#making-the-parent-the-main-data-reader)
- [Binding **Data Across Components**](#binding-data-across-components)
- [**Event Binding** for Dynamic Functions](#event-binding-for-dynamic-functions)
 - [The **Goal**: Scrolling to a "Section"](#the-goal-scrolling-to-a-section)
 - [The **Challenge**: Anchoring Elements with Dynamic Data](#the-challenge-anchoring-elements-with-dynamic-data)
 - [Binding the **Event**](#binding-the-event)
- [Scrolling to **Dynamically Anchored** Elements](#scrolling-to-dynamically-anchored-elements)
 - [**Dynamic** IDs](#dynamic-ids)
 - [Vanilla JS and **scrollIntoView**](#vanilla-js-and-scrollintoview)
- [**End** Result](#end-result)


---

# Brief Refresher: Component Data and Click Event

In the last article, you might remember that we added data directly to a component (`.ts` file) so that we could bind it dynamically to the template (`.html` file). 

<small> If you missed it, [fork this branch](https://github.com/riapacheco/menu-demo/tree/part-1/src/app/components/sidebar) from my repo</small>

> FYI: I took the example `strings` from a Systems Engineering textbook that was sitting next to me (yes, I read textbooks for fun)
```typescript
export class SidebarComponent implements OnInit {
  showsSidebar = true;
  sections = [
    {
      sectionHeading: 'The Evolving State of SE',
      sectionTarget: 'theEvolvingStateOfSe',
      sectionContents: [
        {
          title: 'Definitions and Terms',
          target: 'definitionsAndTerms',
        },
        {
          title: 'Root Cause Analysis',
          target: 'rootCauseAnalysis',
        },
        {
          title: 'Industry and Government',
          target: 'industryAndGovernment',
        },
        {
          title: 'Engineering Education',
          target: 'engineeringEducation',
        },
        {
          title: 'Chapter Exercises',
          target: 'chapterExercises'
        }
      ]
    },
    { 
      // ... another section's data
    }
  ];

  // more component code
}
```

### Click Event

We also created a click event that would pass the `target` data (called `sectionTarget` at the first level and `target` within the `sectionContents` array) through to the console.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v58z6y5ujjjk9zd4d9cx.gif)

---

# Why Define Types with Interfaces?

* Adding the wrong data type by accident (e.g. a property type that has an `s` added to the end which isn't in the original format, like "content<strong style="color: red;">s</strong>" versus "content") is extremely difficult to debug
* Writing lots of code (without defined data types), will not notify us of any mismatched data between a component (or service's) function and the data source until runtime
* We want _some_ order when it comes to the wild wild west of JavaScript

## Creating the Interface
Let's create an interface that defines the data we revisited in the [previous section](#brief-refresher-component-data-and-click-event). Create the file by running `$ ng g interface interfaces/system-concepts --type=interface`. 
* The `--type=interface` flag simply adds `-.interface.ts` extension to the file name so that it's neat and pretty.

First we'll add the main shape of the data. Notice that I added an `I` to the beginning of the interface name to differentiate it from classes and to take advantage of VSCode's IntelliSense plugins for TS:
```typescript
// system-concepts.interface.ts

export interface ISystemConcepts {
  sectionHeading?: string;
  sectionTarget?: string;
  sectionContents?: [];
}
```
* Adding question marks makes the property _optional_ so that no errors occur if we don't mention this property when mentioning the object.

Since the `sectionContents` property contains an array of objects which have their own defined properties (for `title` and `target`), we'll add another interface before this one so we can refer to it with this array:
```typescript
// system-concepts.interface.ts

export interface ISectionContents {
  title?: string;
  target?: string;
}
export interface ISystemConcepts {
  sectionHeading?: string;
  sectionTarget?: string;
  sectionContents?: ISectionContents[]; // We refer to ISectionContents here
}
```

## Adding the Interface to the Component's Data
Now back inside the component we can import the interface and add it to the `sections` array. Make sure to follow it with `[]` since it's defining an array instead of a single object (section):
```typescript
// sidebar.component.ts

import { ISystemConcepts } from './../../interfaces/system-concepts.interface';

export class SidebarComponent implements OnInit {
  showsSidebar = true;

  // Here is where we add the imported interface
  sections: ISystemConcepts[] = [
    {
      sectionHeading: 'The Evolving State of SE',
      sectionTarget: 'theEvolvingStateOfSe',
      sectionContents: [
        {
          title: 'Definitions and Terms',
          target: 'definitionsAndTerms',
  
  // More component code
}
```
* If you run `$ ng serve`, you'll see that there's no errors
* While the app is still served locally, try changing the interface to see what errors pop up

---

# Parent Component Placement and Preparation
* Create a parent component called _Article_ by running `$ ng g c views/article` in your terminal.
* In the `app.component.html` file, remove the `<app-sidebar>` selector and replace it with the new `<app-article></app-article` selector
* If you run `$ ng serve`, you should see "article works!" in your browser.
```html
<!-- app.component.html -->

<app-article></app-article>
```
* Now remove the content from the Article component and replace it with the child `<app-sidebar></app-sidebar>` selector

You'll see it behave exactly the same as before
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h975uvom91yc2knqbmov.gif)

## Making the Parent the Main Data Reader
Copy the data from the child component's `sections` array and paste it into a new `sections` property created in the parent component.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3vmuxfpuupzqvfsvf5vf.gif)

If you erase the data from the child component, leaving behind an empty `sections: ISystemConcepts[] = [];` property, the locally served app will still show the actual sidebar itself, but none of the data will render.

---

# Binding Data Across Components
To render the data that's now defined in the Parent `article.component.ts` file, we have to create a door that translates one property as another (since they wouldn't need the same names). To do this, we import the `@Input()` decorator (the "door") from Angular's core package into the child `sidebar.component.ts` file, and prefix that child's `sections` property with it like this:
```typescript
// sidebar.component.ts

// We import `Input` from angular core ⤵️
import { Component, Input, OnInit } from '@angular/core';

// ... other code 

export class SidebarComponent implements OnInit {
  showsSidebar = true;
  @Input() sections: ISystemConcepts[] = [];

  // ... more code
}
```

This "door" we created allows us to now bind the data to the `<app-sidebar></app-sidebar>` selector that's inside the parent component. In Angular, anything with `[]` indicates two-way data binding, while `()` indicates events (comes up later).

```html
<!--article.component.html-->

<app-sidebar [sections]>
</app-sidebar>
```

However, this will shoot out an **error** since we haven't defined any parent properties for the `[sections]` property to _translate_ into. Since the parent component's data property is _also_ called `sections` we can add it here:

```html
<!--article.component.html-->
<!--This means [childDataProperty]="parentDataProperty"-->
<app-sidebar [sections]="sections">
</app-sidebar>
```
Now, if you serve the app, you'll see that the data is back.

Before moving on, remember to add the `ISystemConcepts[]` interface to the parent's `sections` property like we had in the child component and to create a `showsTableOfContents = false;` property in the parent that the child should access the same way as above:
```typescript
// article.component.ts

export class ArticleComponent implements OnInit {
  showsTableOfContents = false;
}
```
```typescript
// sidebar.component.ts

export class SidebarComponent implements OnInit {
  @Input() showsSidebar = true;
}
```
```html
<!--article.component.html-->
<app-sidebar
  [showsSidebar]="showsTableOfContents"
  [sections]="sections">
</app-sidebar>
```

---

# Event Binding for Dynamic Functions
Now that we've reviewed component-to-component binding (using `[]` in the selector element), we can similarly bind events using `()`. 

# The Goal: Scrolling to a "Section"
Generally you can use Angular's `@ViewChild` decorator to identify an element in the DOM and perform actions on it. As an example, if your template has an element with an anchored ID like this:
```html
<div #thisDiv></div>
```
You can identify it in the component like this:
```typescript
@ViewChild('thisDiv') divElement!: ElementRef;
```
Which you can then perform functions on, like a pretty `scrollIntoView()`:
```typescript
onSomeEvent(event) {
  if (event) { 
    this.divElement.nativeElement.scrollIntoView({ behavior: 'smooth'}); 
  }
}
```

This is our desired use-case:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yknrfqqecb6xb5c9zfwd.gif)

---

# The Challenge: Anchoring Elements with Dynamic Data
Since we want the child component to remain "dumb", and for any instantiated data to come from the parent, our creating static IDs for elements we'd want to scroll into wouldn't match any data fed from the child's emitted event. Ideally, we want the app to define anchor IDs with dynamic parent data; and to scroll to those exact anchors if they match the parent data coming through the click event.

## Binding the Event
First, we want the child (sidebar) component to send an event to the parent. We can do this using Angular's `EventEmitter` from the core package.
* Import `Output` and `EventEmitter` from the core package into the sidebar component
* Prefix a label [I’m calling it “onSelection”] with `@Output()` and have it instantiate an event emitter to the type of `string`
```typescript
// sidebar.component.ts

// Import from core package like this ⤵️
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';

// ... more code

export class SidebarComponent implements OnInit {
  @Input() showsSidebar = true;
  @Input() sections: ISystemConcepts[] = [];

  // Here's where we label a new EventEmitter with `onSelection` ⤵️
  @Output() onSelection = new EventEmitter<string>();
```

Now, instead of the child's `onTargetContentClick()` function simply sending target strings to your console, we can use the emitter to _emit_ the string to the parent (where another parallel method will catch it):
```typescript
// sidebar.component.ts

onTargetContentClick(targetString: string | undefined, event: Event) {
  if (event) {
    this.onSelection.emit(targetString);
    this.closeSidebar();
  }
}
```

Similar to how we bounded data to the selector to populate the template, we can bind events with `()` instead of `[]` to match up with events in the component:
```html
<!--article.component.html-->

<app-sidebar
  (onSelection)=""
  [showsSidebar]="showsTableOfContents"
  [sections]="sections">
</app-sidebar>
```

And just like before, it will return an error unless we create a parallel event for it to pass the data to:
```typescript
// article.component.ts

onSelect(event: Event) {
  console.log(event);
}
```

So now our selector is complete:
```html
<!--article.component.html-->

<app-sidebar
  (onSelection)="onSelect($event)"
  [showsSidebar]="showsTableOfContents"
  [sections]="sections">
</app-sidebar>
```

---

# Scrolling to Dynamically Anchored Elements
In the `article.component.html` file, I created some structure beneath the `<app-sidebar>` selector for populating article content.
> Note: the `mt-5` and `mb-3` classes come from my [@riapacheco/yutes](https://www.npmjs.com/package/@riapacheco/yutes) package that uses SCSS arguments for margin- and padding-manipulating classes (e.g. `mt-5` translates to `margin-top: 5rem`)
```html
<!--article.component.html -->

<!--Child Component-->
<app-sidebar
  [showsSidebar]="showsTableOfContents"
  [sections]="sections"
  (onSelection)="onSelect($event)">
</app-sidebar>

<div class="article-view">

  <!--Section Block -->
  <div
    *ngFor="let section of sections"
    class="section mt-5 mb-3">
    <!--Section Title -->
    <h1>
      {{ section.sectionHeading }}
    </h1>
    
    <!--Content Block-->
    <div
      *ngFor="let content of section.sectionContents"
      class="section-items mt-3">
      <h3>
        {{ content.title }}
      </h3>

      <!--Filler text that would have otherwise come from the data source but I was lazy -->
      <p>
        Lorem ipsum dolor sit, amet consectetur adipisicing elit. Maxime odio molestiae doloremque nulla facilis. Pariatur officiis repudiandae ab vero ratione? Sint illum rem error aliquam consectetur incidunt quo laborum. Neque!
        Lorem ipsum dolor sit amet consectetur adipisicing elit. Odit repellendus fugiat commodi impedit incidunt mollitia sed non dicta qui velit earum quos eligendi, eveniet porro labore dolore, fugit inventore alias.
      </p>
    </div>
    
  </div>
</div>
```

With a little SCSS:
```scss
:host {
  width: 100%;
}

.article-view {
  width: 65%;
  margin: auto;
}
```


## Dynamic IDs
Ideally, we want the anchor IDs to match the _target_ strings fed through the click event. So, for the section heading itself (that we want to scroll to), we add the following ID (which matches the data passed through). We do the same for the content block too!:
```html
<div class="article-view">

  <!--Section Block -->
  <div
    *ngFor="let section of sections"
    class="section mt-5 mb-3">

    <!--**ADDED THIS ID⤵️** -->
    <h1 id="{{section.sectionTarget}}">
      {{ section.sectionHeading }}
    </h1>
    
    <!--Content Block-->
    <div
      *ngFor="let content of section.sectionContents"
      class="section-items mt-3">
      <!--**ADDED THIS ID ⤵️ **-->
      <h3 id="{{content.target}}">
        {{ content.title }}
      </h3>
      <p>
        Lorem ipsum dolor sit, amet consectetur adipisicing elit. Maxime odio molestiae doloremque nulla facilis. Pariatur officiis repudiandae ab vero ratione? Sint illum rem error aliquam consectetur incidunt quo laborum. Neque!
        Lorem ipsum dolor sit amet consectetur adipisicing elit. Odit repellendus fugiat commodi impedit incidunt mollitia sed non dicta qui velit earum quos eligendi, eveniet porro labore dolore, fugit inventore alias.
      </p>
    </div>
```

---

# Vanilla JS and scrollIntoView
In the parent component we can now use some Vanilla JS to grab that ID and attach it to a const; which will act as our anchor for scrolling:
```typescript
onSelect(targetString: string) {
  const targetLocation = document.getElementById(targetString);
  targetLocation?.scrollIntoView({ behavior: 'smooth' });
}
```

---

# End Result
And there you have it: a reusable sidebar component that can scroll to dynamic IDs you don't need to statically populate every time. Now you can create menus, documentation views, and whatever else you’d like with a reusable sidebar component you made from scratch!

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rqpfqnyrbk4urkalfxef.gif)

Ri