IMPORTANT:
When you are not working on the main course project (first-angular-pro-udemy, found in your github folder), then you are working with basics-assignment-2-start project (found in your WORK programming folder -> angular learning folder).
Just Intro
We are starting here with a simple project that allows us to enter a server name and a server description, and it creates that server and displays it on screen. [we can create a server, which will have a red description, or we can add a server blueprint, which will have blue text].
That's the whole project.
Here is a picture: https://imgur.com/8pCJTHt
So far, all this functionality is in the same place (in the app component, no additional components).
But that is not ideal. Ideally, we would want to split things up. So we will create different components and reorganize the project.
//following with the course code
Essentially, we have the app component with an empty array called serverElements
, a cockpit
component with a form that allows us to input information to create a new server, and a server-element
component that will display the server (name and description).
Now, we need to make them communicate: when a new server is added from cockpit
, it needs to be inputted into the serverElements
array inside app
.
Also, from within app
, we are calling the server-element
component for each item of the serverElements
array, with an ngFor
as such:
<app-server-element *ngFor="let serverElement of serverElements"></app-server-element>
We still need to make cockpit communicated properly with the serverElements
array in app
, and make app-server-element
become aware the variables are being passed to it. We will establish the missing connections in the next section(s).
In short, we can use custom properties and events inside our own created components (not necessarily only for native html elements) to pass data between components. We will see this next.
Remember, our server-element
component is supposed to receive a bundle of server related information and display it:
server-element.componenet.html
<div
class="panel panel-default">
<div class="panel-heading">{{ element.name }}</div>
<div class="panel-body">
<p>
<strong *ngIf="element.type === 'server'" style="color: red">{{ element.content }}</strong>
<em *ngIf="element.type === 'blueprint'">{{ element.content }}</em>
</p>
</div>
</div>
Here is how it is being called from app:
<app-server-element *ngFor="let serverElement of serverElements"></app-server-element>
However, server-element
still can't reach the serverElement
passed to it (because it is not aware of it. I need to make it aware of it).
To make it aware of it, we will start by going to the server-element ts file and declaring a variable of type object:
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-server-element',
templateUrl: './server-element.component.html',
styleUrls: ['./server-element.component.css']
})
export class ServerElementComponent implements OnInit {
//here
element: {};
constructor() { }
ngOnInit(): void {
}
}
Note: this element hasn't been assigned a value yet, so its value so far is undefined. However, it is of type object. By putting
: {}
we are using typescript to say that this variable will be strictly holding an object.
But we also want to define the types of the properties inside the object. So we can further add types as such:
element: {type: string, name: string, content: string};
Here is a thing. Since element
is a variable inside the server-element
component, we can bind to it when we call the server-element
component from within app
:
<app-server-element
*ngFor="let serverElement of serverElements"
[element] = "serverElement"
></app-server-element>
With this, the connection between app
and server-element
has been almost done (app passes down a variable when calling server-element, and server-element receives it inside element).
Now, there are currently no values inside the serverElement array (the one we are passing elements from) - so we will add a random value there just for testing:
export class AppComponent {
//array with one element
serverElements =
[{type: 'server',
name: 'Testserver',
content: 'Just a test!'}];
}
In theory, we should be able to receive this value in the child component. However, we will still get an error.
There is a nice explanation on the video here that essentially boils down to saying that the properties of a component are only accessible from within the component.
And to be able to pass down values from parent to child, the parent should be able to see the variable of the child.
When we declared the element
variable above, it was only accessible inside the server-element
component. [This is generally a good thing].
But it is undesirable in our case, because we would like to be able to access the element
variable from within app
, in order to be able to bind to it as such: [element] = "serverElement"
.
Well, we can do it. We can allow parent components (components that will call a component) to access the properties of their children.
@Input()
DecoratorTo do that, when we declare a property inside a child, we should add a special decorator to it, which is: @Input()
. To use it, you need to import it (it got imported by default when you added it. Nicy):
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-server-element',
templateUrl: './server-element.component.html',
styleUrls: ['./server-element.component.css']
})
export class ServerElementComponent implements OnInit {
@Input() element: {type: string, name: string, content: string};
constructor() { }
ngOnInit(): void {
}
}
Now, we any parent component has access to element
. So the property binding will work.
To be able to call a component like this:
<app-server-element *ngFor="let serverElement of serverElements">
</app-server-element>
While passing down items from serverElements
, you need to create a special property inside the child component [we will call it element
in this case] and use for property binding in the html call. This special property is created by using @Input()
, as such:
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-server-element',
templateUrl: './server-element.component.html',
styleUrls: ['./server-element.component.css']
})
export class ServerElementComponent implements OnInit {
//here
@Input() element: {type: string, name: string, content: string};
constructor() { }
ngOnInit(): void {
}
}
Don't miss the import of
Input
And now we can call the child component with property binding, as such:
<app-server-element
*ngFor="let serverElement of serverElements"
[element] = "serverElement"
></app-server-element>
Just a nice feature to have
When binding to elements of components (last section), we can actually use a different name in the parent component than the one inside the child component.
To do that, we assign an alias by using the @Input()
method:
@Input('svrElement') element: {type: string, name: string, content: string};
Now, element
will no longer work when called by parent. I need to update it to receive the new name:
<app-server-element
*ngFor="let serverElement of serverElements"
[svrElement] = "serverElement"
></app-server-element>
So far, we've learned how to pass data from a parent component to a child component which it calls.
But what if something inside a child component changes, and I want to inform the parent component about that change?
Note: this feature might not be important in the cases where were observing the database. If we have a db observer, we can simply create a new element, save that element in the db through a services, and then the
serverElement
array will be automatically updated, with no need of inter component communication.
So in our current simple app, cockpit
has a form that allows the creation of a new server. When a new server is created, I need a way of telling app
(the parent component calling cockpit
) that a new server has been added (so that it gets added to the serverElements
array).
We do this through custom Event Binding
So we are going to add methods to the app.ts:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
serverElements = [{type: 'server', name: 'Testserver', content: 'Just a test!'}];
onServerAdded(serverData: {serverName: string, serverContent: string}) {
this.serverElements.push({
type: 'server',
name: serverData.serverName,
content: serverData.serverContent
});
}
onBlueprintAdded(blueprintData: {serverName: string, serverContent: string}) {
this.serverElements.push({
type: 'blueprint',
name: blueprintData.serverName,
content: blueprintData.serverContent
});
}
}
And we are going to event bind to these methods as we call cockpit from inside app.html:
<app-cockpit
(serverCreated)= "onServerAdded($event)"
(bluePrintCreated) = "onBlueprintAdded($event)"
></app-cockpit>
Question: how is this different from passing down a method through property binding, as such:
<app-left-menu [setActivePage]="setActivePage"></app-left-menu>
-? Because so far, it seems that both work the same.
Answer: I think it has to do with receiving an event from the Child (so in this section, we are not passing a method from parent to child and then calling it in child) - the child actually has its own methods, and when they fire, we want to send the output to the parent. Seems like it could have been done through simple property binding, but whatever.
You were trying to prove the above answer to yourself but you got weird errors in one project and didn't get them in another, so bye.
Now we go to cockpit.ts, and create two new properties which correspond to the bindings above. However, we want to declare them as events.
To indicate that a property is an event, we use eventEmitter in addition to Output
, as follows:
import { Component, OnInit, EventEmitter, Output } from '@angular/core';
@Component({
selector: 'app-cockpit',
templateUrl: './cockpit.component.html',
styleUrls: ['./cockpit.component.css']
})
export class CockpitComponent implements OnInit {
//@ts-ignore
@Output() serverCreated = new EventEmitter<{serverName: string, serverContent: string}>();
//@ts-ignore
@Output() blueprintCreated = new EventEmitter<{serverName: string, serverContent: string}>();
newServerName = '';
newServerContent = '';
constructor() { }
ngOnInit(): void {
}
onAddServer() {
this.serverCreated.emit({
serverName: this.newServerName,
serverContent: this.newServerContent
})
}
onAddBlueprint() {
this.blueprintCreated.emit({
serverName: this.newServerName,
serverContent: this.newServerContent
})
}
}
In the above, we created serverCreated
and blueprintCreated
as properties. But we want to use them in events, so we have to use the EventEmitter method [don't forget to import it].
In essence, we are now creating a new event type.
The EventEmiiter method will have to take the type definition for the event it is going to 'create'.
EventEmitter is used when we are creating our own custom events [hence using it as a constructor].
Then don't forget to import and add the Output
part.
Now that we created our own custom emittable events [of types serverCreated and blueprintCreated], we can emit events of the these types.
We do this in this part of the code:
onAddServer() {
this.serverCreated.emit({
serverName: this.newServerName,
serverContent: this.newServerContent
})
}
onAddBlueprint() {
this.blueprintCreated.emit({
serverName: this.newServerName,
serverContent: this.newServerContent
})
}
Self note: angular sucks
Okay so here is what is happening here:
cockpit
has a form. Once the form is submitted, one of two functions might be called. Assume onAddServer()
got called (keep in mind that a function is called from within cockpit
, but then it sends its output up to app
).
onAddServer()
will emit a custom event that we defined earlier in the same file. The custom event/method will also be present in the parent component.
So when an event is emitted, it will essentially be passed up to the parent component.
Just like the property binding, you can also assign an alias:
passing:
<app-cockpit
(bpCreated) = "onBlueprintAdded($event)"
></app-cockpit>
listenting:
@Output('bpCreated') blueprintCreated = new EventEmitter<{serverName: string, serverContent: string}>();
In summary, what we learn so far is how to pass variables/methods/events from one compoenent to another. This is similar to props in react.
However, just like when in react, we have very far away components, we would like something similar to state management (like redux) to use in anguar.
This would be the services. The services are somehow equivalent to state management in react, and we will learn about them later in the course.
For the CSS: In the normal case, a browser creates a whole css sheet from all the indivual sheets used within a website.
So in theory, if I put some css rule in one of my components stylesheets, it should be applied globally once all the sheets are combined (ex: p{color: red}
.
However, angular does not allow this to happen. Angular works in a way that forces the css applied to a component to be specific to only that component (only applied to it). This is called style encapsulation.
And that's why you have a shitton of additional attributes applied to each and every element when you inspect it - so that angular can specifically select it. It gives the same unique attribute to all elements within a component.
Of course, if we want css to be global, we just add it to the outer globally applied css sheet (the one autside app).
You can override the style encapsulation thingy that angular does. You don't really care so I won't write in this section.
In the cockpit app, when we entered the name of a new server, we used two way databinding in order to change the value of the variable (let's say, the newServerName variable) in realtime.
However, we are not printing that variable anywhere or anything: we just need it once we fire the onAddServer()
function. So all the changes before this point are not needed for me.
Having a two way databind is kind of an overkill in this scenario. I still need the value of the input - I can get it by simply using a local reference (no variable needed).
As such:
<div class="row">
<div class="col-xs-12">
<p>Add new Servers or blueprints!</p>
<label>Server Name</label>
<input type="text" class="form-control" #serverNameInput> <!--instead of [(ngModel)] like below-->
<label>Server Content</label>
<input type="text" class="form-control" [(ngModel)]="newServerContent">
<br>
<button
class="btn btn-primary btn-margin"
(click)="onAddServer(serverNameInput)">Add Server</button>
<button
class="btn btn-primary"
(click)="onAddBlueprint()">Add Server Blueprint</button>
</div>
</div>
Since we are now passing a variable, we should receive it at the method:
onAddServer(nameInput: HTMLInputElement) {
console.log(nameInput); //this will print the whole html element
console.log(nameInput.value)//this will print the input value
this.serverCreated.emit({
serverName: nameInput.value,
serverContent: this.newServerContent
})
}
Note: local references hold the whole 'element' - not just the input value (assuming we are dealing with an input element). I guess you can use it with any kind of element.
Note2: Once defined within an html template, the local variable (hashtag thingy) can be used all throughout that template (not the ts, just the html). When using it, you have to remove the hashtag before it, as we did in the example above.
If you plan to access an element inside ngOnInit()
, you should not just write it like this:
//bad
@ViewChild('serverContentInput') serverContentInput: ElementRef;
But you should add another parameter to it:
//good
@ViewChild('serverContentInput', {static: true}) serverContentInput: ElementRef;
However, if you don't plan on accessing the selected element inside of ngOnInit()
, then you should set it to false (or not add it all because it is false by default)
//accepted
@ViewChild('serverContentInput') serverContentInput: ElementRef;
//accepted
@ViewChild('serverContentInput', {static: false}) serverContentInput: ElementRef;
The same goes for
@ContentChild()
So we said earlier that we can only access local refrerences inside the html templated.
However, there is a way to access an element from within the ts code.