loading...

Passing data between nested components with Angular

chiangs profile image Stephen E. Chiang Originally published at chiangs.ninja on ・6 min read

One of the biggest changes from AngularJS to Angular (2+) is the step away from two-way data binding. The problem with two-way data binding is the potential for unexpected cascading effects and can be hard to reason about the bigger the project is. With the change to one-way data binding in Angular, data is passed down through components and if a changed is required as a result of some action, it is emitted back up to the top where the change is actually made because Angular applications are made up of a hierarchy of components.

This is achieved through a combination of defining the input and output properties of the child-element in the parent component html and the @Input and @Output of the child component.

Input and Output as an API for your component

In other words, a component can receive data from its parent as long as the receiving component has been specifically defined (or exposed) ways to receive that data, just like how an API functions. Similarly, components can send data to their parents by triggering an event the parent listens for.

Here’s a graphic of how it works between Parent and Child:

Parent-Child Component Data Comm

Just remember that in the ParentComponent.html as you define the ChildComponent, the child properties are on the left, and the parent are on the right, sort of like when you declare a variable and assign it a value where the variable is on the left side of the = and the value is on the right.

ParentComponent.html input/output

It is important to note that doing this for more than 1 or 2 layers of nested components or across to same level components can get complicated and pretty confusing. A better way to centralize this data is through the use of services, which I’ll write about in the next article.

Here, I’m going to create a ParentComponent and ChildComponent, instantiate a new object of a Stephen class(model) in the ParentComponent and then pass the ChildComponent a specific array of properties of the Stephen object. After that, I’ll define an output where when one of the properties is clicked, the ParentComponent acknowledges the event and changes the way I’m addressed: Mr. Stephen | Chiang | Stephen E. Chiang.

Create a new Angular project using the CLI, if you don’t have one set up yet: $ ng new parent-child

  • Create a parent and child component that is nested in the parent.

-Create a simple class, in this case, I’m just creating a model of myself a stephen.model.ts

$ ng g c parent
$ ng g c parent/child
$ ng g class stephen

In the model, I’ll add the following attributes:

export class Stephen {
    firstName: string = 'Stephen';
    lastName: string = 'Chiang';
    fullName: string = 'Stephen E. Chiang';
}

In the app.component.html file, we’re going to erase the default filler and add the ParentComponent element: <app-parent></app-parent>

In the parent.component.ts file, we’re going to make an instance of the object:

  • Import the Stephen class.
  • Declare the object and instantiate a new instance in the constructor.
  • Declare the first name property as the default to display upon construction of the ParentComponent to show that you’ve properly instantiated the object.
  • Define the ChildComponent input name [stephen] to accept the ParentComponent’s stephen: Stephen object.
  • Define the ParentComponent output named (onNameSelected) and assign that to an operation in the ParentComponent that updates the selectedName by calling the ParentComponent’s updateName function.
  • Define the updateName function to set the new name based on the string that will be emitted by the ChildComponent via the Output.
  • Because of how simple this example is, there’s no need to separate the parent.component.html and parent.component.ts code so we’re going to do it inline.
  • Also of note, if updates to the selectedName were to occur from within the ParentComponent, one would need to subscribe to the property to update changes, but in this case change notice will be sent from outside (ChildComponent), so the ParentComponent is already listening for changes.
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { Stephen } from '../stephen.model';

@Component({
    selector: 'app-parent',
    template: `

        Hello, Mr. (or Ms.): {{ selectedName }}

`,
styleUrls: ['./parent.component.css'],
    encapsulation: ViewEncapsulation.None
})

export class ParentComponent implements OnInit {
    stephen: Stephen;
    selectedName: string;

    constructor() {
        this.stephen = new Stephen();
        this.selectedName = this.stephen.firstName;
    }

    ngOnInit() {
    }

    updateName(selectedName: string): void {
    console.log('in parent');
    this.selectedName = selectedName;
    }

}

 

If you were to run $ ng serve right now, all you’d see is:

child works!

In the ChildComponent:

  • Import Input, Output, and EventEmitter as well as Stephen or whatever you named your model class.
  • Make a simple unordered list and for each list item, string interpolate each attribute.
  • Each list item has a (click) event that calls the clicked function and passes the attribute.
  • Declare the @Input() named asstephen` to match how its defined in its element of the ParentComponent html.
  • Declare the @Output() named asonNameSelected` to match and set it as an EventEmitter type that emits a string.
  • Instantiate a new EventEmitter in the constructor and set it to the EventEmitter declared as the @Output().
  • Define the clicked function that takes a string and calls the output EventEmitter to emit the string back to the parent.
  • There are more efficient ways to display the information, for example, making the name attributes a string[] and then use *ngFor to iterate and create a `

` element for each item in the array, which would reduce repeated code, but for this quick and simple example, it works just fine.

– Here you see we did not instantiate a new Stephen object, and yet we were able to access the attributes. You could have also passed in specific attributes of the object instead of the entire thing if you wanted.

 

import { Component, OnInit, ViewEncapsulation, Input, Output, EventEmitter } from '@angular/core';
import { Stephen } from '../../stephen.model';

@Component({
    selector: 'app-child',
    template: `

        {{ stephen.firstName }}
        {{ stephen.lastName }}
        {{ stephen.fullName }}
        `,
    styleUrls: ['./child.component.css'],
    encapsulation: ViewEncapsulation.None
})

export class ChildComponent implements OnInit {
    @Input() stephen: Stephen;
    @Output() onNameSelected: EventEmitter;

    constructor() {
        this.onNameSelected = new EventEmitter();
    }

    ngOnInit() {
    }

    clicked(name: string): void {
        this.onNameSelected.emit(name);
    }
}

At this point, your app should work, when you click on one of the names, it will update the parent:

list to update parent

Here’s a chart that shows both Parent and Child side by side to further help illustrate how it all ties together with details removed.

The ChildComponent emits back up to the ParentComponent and lets the parent decide what to do with the event. This helps keep logic in fewer and more logical places than complex logic in every single component in the entire app.

parent-child

Using this method to pass data cross-component or in heavily nested components (more than 3 layers) can get confusing. If you find yourself needing to pass a specific piece of data around a lot, it might be time to think about centralizing it and using a service , which I will discuss using the same example in the next article, which will be in the next few days because we’re expecting a little baby girl any day now and I better get this next article out now, otherwise it could be weeks or months!

Please don’t hesitate to send your questions, comments, criticisms and follow me here or any of my social media accounts.

Thanks!

Stephen

Posted on by:

chiangs profile

Stephen E. Chiang

@chiangs

Pursuing a perpetual state of flow. Learn, Build, Eat, Sleep, Improve. Apply ☕️ & 🍺 liberally. 🇺🇸 🇩🇰 🇸🇯

Discussion

markdown guide
 

Hi ! (having this problem)

export class imageUploadComponent {
@Input() imagen:string;

console.log(this.imagen) = undefined

how can I pass an image as a parameter ?

 

Hi Daniel without seeing all of your code I'm not exactly sure what you're trying to do. But I'm assuming you are trying to pass the path to the image as a string? Be sure to pass an absolute path rather than relative.

If it's undefined perhaps the variable is not yet defined or the input is not yet provided at the time when the console log is called.

 

Hi Stephe !

In my form i Hava this:





I use the just to see that the info is there.
then I call <upload-image...., with the parameter that you see.
But when I get into :

@Component({
selector: 'upload-imagen',
templateUrl: './imagen.component.html',
styleUrls: ['./imagen.component.scss'],

})

export class imageUploadComponent {
@Input() imagen: any;
....
and do a console.log(this.imagen) can see :undefined
So I guess Im not passing the parameter ok or Im not using (@Input() imagen: any;) the correct way.

Hope you undertand me and thx for your time

 

Hi Stephe !

In my form i Hava this:
(use this img temp so just to check my image is there)

I use the just to see that the info is there.
then I call <upload-image...., with the parameter that you see.
But when I get into :

@Component({
selector: 'upload-imagen',
templateUrl: './imagen.component.html',
styleUrls: ['./imagen.component.scss'],

})

export class imageUploadComponent {
@Input() imagen: any;
....
and do a console.log(this.imagen) can see :undefined
So I guess Im not passing the parameter ok or Im not using (@Input() imagen: any;) the correct way.

Hope you undertand me and thx for your time

Hi I can't really debug for you from here... But have you imported input?

@Component({
selector: 'upload-imagen', ....

and ...
export class imageUploadComponent {
@Input() imagen: any;

Could it be that passing parameter has a size limit ?

Thx

 

Hi

No the path, the image I have in a json file

So if you've imported input like this:

import { component, input} from `@angular/core`

What does your HTML markup look like?

Since I cant find the solution, Im trying to assign the Image to a global variable and then assign it to a local var

Im trying something differet like this

@Injectable()
export class Globals {
public imagenUno: any;
}

then:
this.globals.imagenUno=data.Imagen;
console.log('Foto:'+this.globals.imagenUno) (I see the image in conole.log)

then I have image.componet.ts
import { Globals } from 'src/app/globals';

constructor(private globals: Globals ) {
console.log('Foto:'+this.globals.imagenUno) (get Foto:undefined)
}

Well I guess Im missing something with angular 7

 

I created a dev.to account just to say thank you. Clear and concise to the point - just what I needed!

 

Thanks for this great article, but do you have a link to next one of the series? Sharing data between components using a service? Much appreciated, thanks.

 

Hi Beto, sadly I never got to it...but essentially if the service is provided in root then it is a singleton, meaning you have just one instance of it during it's lifetime.

That means any value you hold in it essentially is available to any component no matter how nested by injecting the service so then you won't find yourself having to continuously pass down the value through a whole tree of components.

I don't recommend exposing or accessing that variable in the service directly. Instead, write a method that accesses it.

Here's the documentation on this: Singleton Services.