Angular2 - two way databinding on a component variable / component class property? - javascript

In Angular2 (Beta 6) I have a component for a main menu.
<mainmenu></mainmenu>
I want to bind a boolean for wide or narrow. So I made it into this:
<mainmenu [(menuvisible)]="true"></mainmenu>
But what I want (I think) is to bind to a javascript class property (as I may have other things to bind but want to be tidy by using a single class in the component).
I get an error
EXCEPTION: Template parse errors: Invalid property name
'menumodel.visible' ("
][(menumodel.visible)]="menumodel.visible">
If I try the same with a single variable instead of a class I get:
Template parse errors: Parser Error: Unexpected token '='
However this (one way binding?) does seem to work (but I might want to trigger the menu to go wide/narrow from another component so felt this should be a two-way data bound property):
<menu [vis]="true"></menu>
This is a bit of my menu component:
#Component({
selector: 'menu',
templateUrl: './app/menu.html',
providers: [HTTP_PROVIDERS, ApplicationService],
directives: [ROUTER_DIRECTIVES, FORM_DIRECTIVES, NgClass, NgForm]
})
export class MenuComponent implements OnInit {
mainmenu: MainMenuVM;
constructor(private _applicationService: ApplicationService) {
this.mainmenu = new MainMenuVM();
}
// ...ngOnInit, various functions
}
Here is my MainMenu View Model class
export class MainMenuVM {
public visible: boolean;
constructor(
) { this.visible = true; }
}
I'm trying to create a menu which has icons and text, but can go narrow to just show icons. I will emit this event upwards to a parent component to alter the position of the container next to the menu. Triggering a content container to maximised will trigger the menu to go narrow - I am not saying this is the best way, but I would like to resolve this particular question before going deeper.
Please note: I am not databinding to an input control here - just databinding to a component so I can then modify the UI.
This is from the Angular cheatsheet
<my-cmp [(title)]="name">
Sets up two-way data binding. Equivalent to: <my-cmp [title]="name" (titleChange)="name=$event">
Thanks in advance!
UPDATE
Integrating the code from the accepted answer and adapting for my particular use case here the final working code:
app.html
...header html content
// This is what I started with
<!--<menu [menuvisible]="true" (menuvisibleChange)="menuvisible=$event"></menu>-->
// This is two way data binding
// 1. Banana-in-a-box is the input parameter
// 2. Banana-in-a-box is also the output parameter name (Angular appends it's usage with Change in code - to follow shortly)
// 3. Banana-in-a-box is the short hand way to declare the commented out code
// 4. First parameter (BIAB) refers to the child component, the second refers the variable it will store the result into.
// 5. If you just need an input use the remmed out code with just the first attribute / value
<menu [(menuvisible)]="menuvisible"></menu>
.. div content start
<router-outlet></router-outlet>
.. div content end
app.component.ts (root)
export class AppComponent implements OnInit{
menuvisible: Boolean;
}
menu.component.ts (child of root)
export class MenuComponent implements OnInit {
// Parameters - notice the appending of "Change"
#Input() menuvisible: boolean;
#Output() menuvisibleChange: EventEmitter<boolean> = new EventEmitter<boolean>();
// Init
ngOnInit() {
// Populate menu - fetch application list
this.getApplications();
// Initially we want to show/hide the menu depending on the input parameter
(this.menuvisible === true) ? this.showMenu() : this.hideMenu();
}
//...more code
}
menu.html
<div id="menu" [ngClass]="menuStateClass" style="position: absolute; top:0px; left: 0px;z-index: 800; height: 100%; color: #fff; background-color: #282d32">
<div style="margin-top: 35px; padding: 5px 0px 5px 0px;">
<ul class="menuList" style="overflow-x: hidden;">
<li>IsMenuVisible:{{menuvisible}}</li>
<li style="border-bottom: 1px solid #3d4247"><a (click)="toggleMenu()"><i class="fa fa-bars menuIcon" style="color: white; font-size: 16px;"></i></a></li>
<li *ngFor="#app of applications">
<a [routerLink]="[app.routerLink]">
<i class="menuIcon" [ngClass]="app.icon" [style.color]="app.iconColour" style="color: white;"></i>
<span [hidden]="menuStateTextHidden">{{ app.name }}</span>
</a>
</li>
</ul>
</div>
</div>
Remember to import what you need e.g.
import {Component, EventEmitter, OnInit, Input, Output} from
'angular2/core';
Highly recommend this video on You Tube:
Angular 2 Tutorial (2016) - Inputs and Outputs

For two-way binding you need something like:
#Component({
selector: 'menu',
template: `
<button (click)="menuvisible = !menuvisible; menuvisibleChange.emit(menuvisible)">toggle</button>
<!-- or
<button (click)="toggleVisible()">toggle</button> -->
`,
// HTTP_PROVIDERS should now be imports: [HttpModule] in #NgModule()
providers: [/*HTTP_PROVIDERS*/, ApplicationService],
// This should now be added to declarations and imports in #NgModule()
// imports: [RouterModule, CommonModule, FormsModule]
directives: [/*ROUTER_DIRECTIVES, FORM_DIRECTIVES, NgClass, NgForm*/]
})
export class MenuComponent implements OnInit {
#Input() menuvisible:boolean;
#Output() menuvisibleChange:EventEmitter<boolean> = new EventEmitter<boolean>();
// toggleVisible() {
// this.menuvisible = !this.menuvisible;
// this.menuvisibleChange.emit(this.menuvisible);
// }
}
And use it like
#Component({
selector: 'some-component',
template: `
<menu [(menuvisible)]="menuVisibleInParent"></menu>
<div>visible: {{menuVisibleInParent}}</div>
`
directives: [MenuComponent]
})
class SomeComponent {
menuVisibleInParent: boolean;
}

I've created a short plunkr.
ngModel Like Two-Way-Databinding for components
You have at least two possibilities to to create a two way databinding for components
V1: With ngModel Like Syntax, there you have to create a #Output property with the same name line the #Input property + "Change" at the end of the #Output property name
#Input() name : string;
#Output() nameChange = new EventEmitter<string>();
with V1 you can now bind to the Child Component with the ngModel Syntax
[(name)]="firstname"
V2. Just create one #Input and #Output property with the naming you prefer
#Input() age : string;
#Output() ageChanged = new EventEmitter<string>();
with V2 you have to create two attributes to get the two way databinding
[age]="alter" (ageChanged)="alter = $event"
Parent Component
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
template: `<p>V1 Parentvalue Name: "{{firstname}}"<br/><input [(ngModel)]="firstname" > <br/><br/>
V2 Parentvalue Age: "{{alter}}" <br/><input [(ngModel)]="alter"> <br/><br/>
<my-child [(name)]="firstname" [age]="alter" (ageChanged)="alter = $event"></my-child></p>`
})
export class AppComponent {
firstname = 'Angular';
alter = "18";
}
Child Component
import { Component, Input, Output, EventEmitter } from '#angular/core';
#Component({
selector: 'my-child',
template: `<p>V1 Childvalue Name: "{{name}}"<br/><input [(ngModel)]="name" (keyup)="onNameChanged()"> <br/><br/>
<p>V2 Childvalue Age: "{{age}}"<br/><input [(ngModel)]="age" (keyup)="onAgeChanged()"> <br/></p>`
})
export class ChildComponent {
#Input() name : string;
#Output() nameChange = new EventEmitter<string>();
#Input() age : string;
#Output() ageChanged = new EventEmitter<string>();
public onNameChanged() {
this.nameChange.emit(this.name);
}
public onAgeChanged() {
this.ageChanged.emit(this.age);
}
}

Related

How to set a value of a property of a nested item in order to make it visible via *ngIf directive

I have created a component to reuse the mat-progress-spinner from angular material. I need this in order to avoid putting for every single page the same code. Here is the code that is working:
<div id="overlayProgressSpinner">
<div class="center">
<mat-progress-spinner
style="margin:0 auto;"
mode="indeterminate"
diameter="100"
*ngIf="loading">
</mat-progress-spinner>
</div>
</div>
It is simple. Only to set "loading" as true or false.
What did I do?
I put above code inside a custom component. Now it is like so:
<app-progress-spinner></app-progress-spinner>
its HTML code is the same and its TS code is as a follows:
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-progress-spinner',
templateUrl: './progress-spinner.component.html',
styleUrls: ['./progress-spinner.component.scss']
})
export class ProgressSpinnerComponent implements OnInit {
loading = false;
constructor() { }
ngOnInit() {
}
public isLoading(value: boolean) {
this.loading = value;
}
public changeSpinnerCSSClass() {
const htmlDivElement = (window.document.getElementById('overlayProgressSpinner') as HTMLDivElement);
if (this.loading) {
htmlDivElement.className = 'overlay';
} else {
htmlDivElement.className = '';
}
}
}
when the property "loading" belongs to the current component, I can show and hide the "mat-progress-spinner" component. Otherwise, when it belongs to "app-progress-spinner" it is set but it is not being displayed. The code that I am trying to make it visible is as follows:
this.progressSpinner.isLoading(false); // it is set, but it does not work.
this.progressSpinner.changeSpinnerCSSClass(); // it works
it appears that *ngIf="loading" cannot be set by using the approach the works if the logic behind belongs to the current component.
How to achieve this?
You need to create an input in your ProgressSpinnerComponent. To do that, add the #Input() decorator before the property loading:
import { Component, OnInit, Input } from '#angular/core';
#Component({
selector: 'app-progress-spinner',
templateUrl: './progress-spinner.component.html',
styleUrls: ['./progress-spinner.component.scss']
})
export class ProgressSpinnerComponent implements OnInit {
#Input() loading = false;
So anywhere you need to use the app-progress-spinner you do:
<app-progress-spinner [loading]="loading"></app-progress-spinner>
Note: The loading variable assigned to the input loading belongs to the component that contains theapp-progress-spinner.
This happens because every component have it own scope, meaning that it have no access to external world unless you create an input or output in order to receive or send data. There's also the ngModel that can be used for bi-diretional data, but not recommend in most cases.

How to make child component detects object (#Input()) from parent component has changed in Angular

I have an object from parent component also received in a child component similar to this:
{
attribute: 'aaaa',
attribute2: [
{
value
},
{
value
},
{
value
},
]
}
This object is an #Input from a parent component. When I make changes to the objects inside the attribute2 array, I would like the child component detect that changes were made and then gets updated. As this is an object, I could'nt make it work, so I clone the entire object (this.objet = _.cloneDeep(this.object) in the parent component so then the child component detects that changes happened.
Is there any other way of doing this that does not clone the entire object? Thanks in advance
EDIT:
Child Component
export class ChildComponent implements OnInit, OnChanges {
#Input() public object: any;
}
html
<div>
<span>{{object.attribute}}</span>
<div *ngFor="let items of object.attribute2">{{item.value}}</div>
</div>
Parent Component
export class ParentComponent implements OnInit {
public object: any;
updateObject() {
this.object.attribute2[1] = 'Changed value';
this.object = _.cloneDeep(this.object);
}
}
html
<div>
<child-component [object]="object"></child-component>
</div>
An efficient way is to use EventEmitter and service communication to
trigger changes in the child component.
On way as mentioned by #Tony is to use ngOnChanges(). It is a good shortcut for detecting bounded properties change but as you add more and more bindings, using this hook will affect you application in the long run because it will run every time any of the bound property changes whether or not you desire it all the calls.
So for Service based communication, I've created an example on
Stackblitz:
https://stackblitz.com/edit/angular-fgut7t
Gist: https://gist.github.com/stupidly-logical/a34e272156b498513505127967aec851
In this example, I am binding an Array to the child component using #Input() an on addition of new data, the array is updated by the parent and the latest value is passed on the service which then emits this value. The child component subscribes to this value and the relevant code is executed.
The Service:
import { Injectable, EventEmitter } from '#angular/core';
#Injectable({
providedIn: "root"
})
export class DataService {
dataUpdated:EventEmitter<any> = new EventEmitter();
constructor() { }
setLatestData(data) {
this.dataUpdated.emit(data);
}
}
Child Component TS
import { Component, OnInit, Input } from '#angular/core';
import { DataService } from '../data-service.service';
#Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit {
#Input() allData: [];
latestData: any;
constructor(private dataService: DataService) { }
ngOnInit() {
this.dataService.dataUpdated.subscribe((data) => {
this.latestData = data;
});
}
}
Child Component HTML
<p>
Latest Data: {{ latestData }}
</p>
<h3>List:</h3>
<li *ngFor="let data of allData">
{{ data }}
</li>
Parent Component TS
import { Component } from '#angular/core';
import { DataService } from './data-service.service'
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
name = 'Angular';
dataArr = [];
constructor(private dataService: DataService){}
onAddTimestamp() {
let timestamp = new Date();
this.dataArr.push(timestamp);
this.dataService.setLatestData(timestamp);
}
}
Parent Component HTML
<hello name="{{ name }}"></hello>
<p>
Start editing to see some magic happen :)
</p>
<button
(click)="onAddTimestamp()"
>
Add Timestamp
</button>
<app-child
[allData] = "dataArr"
></app-child>
Use the ngOnChanges() lifecycle method in your component.
ngOnChanges is called right after the data-bound properties have been
checked and before view and content children are checked if at least
one of them has changed.
Some like this
#Input() object: string;
ngOnChanges(changes: SimpleChanges) {
console.log(changes.object.currentValue);
// You can also use object.previousValue and
// object.firstChange for comparing old and new values
}

Angular pass input variable to another component on button click

I come from a Python background, but I started trying to learn Angular and I'm really having trouble. Working between components is confusing to me and I can't figure it out. I made a good example that I think if someone helped me with it would go along way towards understanding Angular.
I just have two components: a "header" component and an app component. In the header component, I ask for the user's name and they click a button, and then it should show "Hello {{name}}" in the next component. I cannot get it to work to say the least and it's really frustrating. The Header part seems to work okay, but it's just not communicating with the other component at all. Neither the button part or the "name" part are working so I am clearly misunderstanding something I need to do when it comes to listening from the parent component.
Here is my Header HTML:
Name: <input type="text" id="userInput" value="Joe">
<button (click)=showName()>Show More</button>
Here is my Header TS:
import { Component, OnInit, Output, EventEmitter } from '#angular/core';
#Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.css']
})
export class HeaderComponent implements OnInit {
bodyDiv = false;
inputName = '';
#Output() buttonClicked = new EventEmitter();
constructor() { }
ngOnInit() {
}
showName() {
console.log('showName clicked.');
this.bodyDiv = true;
this.inputName = document.getElementById('userInput').value;
console.log(this.inputName);
console.log(this.bodyDiv);
this.buttonClicked.emit(this.bodyDiv);
this.buttonClicked.emit(this.inputName);
}
}
Here is the main Component's HTML:
<app-header (buttonClicked)='showNextComponent($event)'></app-header>
<p *ngIf="![hiddenDiv]" [inputName]="name">Hello {{ name }} </p>
Here is the main component's TS:
import { Component } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
hiddenComponent = true;
title = 'show-button';
showNextComponent() {
console.log('Button clicked.');
this.hiddenComponent = false;
console.log(this.hiddenComponent);
}
}
So who can show me what I'm doing wrong and help figure out Angular a little better? :) Thank you!
replace showName function with below code :
showName() {
console.log('showName clicked.');
this.bodyDiv = true;
this.inputName = document.getElementById('userInput').value;
console.log(this.inputName);
console.log(this.bodyDiv);
this.buttonClicked.emit(this.inputName);
}
replace below code in your main component.
name:string
showNextComponent(value:string) {
this.name = value;
}
replace below code in your html :
<app-header (buttonClicked)='showNextComponent($event)'></app-header>
<p *ngIf="name">Hello {{ name }} </p>
Please let me if you have any question and I would suggest try to use ngmodel or something else instead of directly communicating with the DOM.
Here is a slightly modified and working sample: https://stackblitz.com/edit/angular-jhhctr
The event emitter in the header component emits the name (string) which is the $event in showNextComponent($event). You have to capture this in the main component and assign it to a local variable to be able to use it in the main component's template as {{name}}
[inputName]="name" is incorrect. You can pass values like that to angular components not to actual HTML DOM elements.
There are couple of ways to communicate from one component to another in angular - Using #Input()in your child component will expects an input from parent component and #Output() from your child component will emit an event from the child component
So in your case if you want to pass a value from parent to child you need to use input property or decorator on your child property - I will provide you the code but just go through proper guidance from the link provided this will make you to create better angular applications https://angular.io/guide/component-interaction
First you need to swap your components your header component should be your parent and the child component will be your main component - if you want to work in the same way just move your codes vice versa
Header html
Name: <input type="text" id="userInput" name='userInput' [(ngModel)]='inputName' value="Joe">
<button (click)=showName()>Show More</button>
<div [hidden]='bodyDiv'>
<app-header [bindName]='inputName'></app-header>
</div>
Header Component
import { Component, OnInit, Output, EventEmitter } from '#angular/core';
#Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.css']
})
export class HeaderComponent implements OnInit {
bodyDiv = true;
inputName = '';
constructor() { }
ngOnInit() {
}
showName() {
bodyDiv = false;
}
}
Main Component Html
<p>Hello {{ bindName }} </p>
Main component ts
import { Component, Input } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
#Input()
bindName: string;
}
In your header component the inputName property will be binded using two way data binding where i used [(ngModel)]='inputName' so whatever you enter in the input text it will be updated in your inputName property
Now we need to do only one thing just to show your child component with any event - so when the button is clicked the div with [hidden] property will be false and it will be displayed and as we pass the inputName to the child Component it will be updated
And finally the child component will be displayed and the input written in the text will be updated in the child component - when the child component html displays the bindName will be updated and there will be result you expected
That's all I think this should work well - Try this and let me know - Thanks Happy coding !!
Don't forget to look into the link above where you can see many types of component interactions

Set some background color only for login page

I am trying to give my login page some background color(only for my login page)
For that, I have built a directive in my shared folder
import {
Directive,
ElementRef,
Renderer2,
Input,
AfterViewInit
} from "#angular/core";
#Directive({
selector: "[appBackground]"
})
export class BackgroundDirective implements AfterViewInit {
constructor(private renderer: Renderer2, private el: ElementRef) {}
#Input() backgroundColor: string;
ngAfterViewInit() {
this.renderer.setStyle(
this.el.nativeElement,
"background-color",
this.backgroundColor
);
}
}
and then in my login component, I am doing this
export class LoginComponent implements OnInit {
color: string;
emailFormControl = new FormControl("", [
Validators.required,
Validators.pattern(EMAIL_REGEX)
]);
passwordFormControl = new FormControl("", [Validators.required]);
constructor(private dir: BackgroundDirective) {}
ngOnInit() {
console.log(this.dir);
this.dir.backgroundColor = "lightblue";
}
}
and then in login.component.html
<div BackgroundDirective>
// my login page content
</div>
But I see no change in my login page background color, what I am missing?
There are multiple errors in your example. here is a stackblitz implementing your code
1/ Directive are meant to be used in an HTML declarative way. You don't have to inject it in your constructor (for simple cases)
2/ your selector is [appBackground] so you have to use your directive this way. You have to use the chosen selector, not the directive ClassName
<div appBackground>...</div>
3/ your added an Input on your directive (#Input() backgroundColor: string;)
so if you want to set the color you can use it this way
<div appBackground backgroundColor="red"></div>
4/ don't forget to add your directive in the declarations parts of your module.

Add a component dynamically to a child element using a directive

Trying to place a component dynamically to a child element, using a directive.
The component (as template):
#Component({
selector: 'ps-tooltip',
template: `
<div class="ps-tooltip">
<div class="ps-tooltip-content">
<span>{{content}}</span>
</div>
</div>
`
})
export class TooltipComponent {
#Input()
content: string;
}
the directive:
import { TooltipComponent } from './tooltip.component';
#Directive({
selector: '[ps-tooltip]',
})
export class TooltipDirective implements AfterViewInit {
#Input('ps-tooltip') content: string;
private tooltip: ComponentRef<TooltipComponent>;
constructor(
private viewContainerRef: ViewContainerRef,
private resolver: ComponentFactoryResolver,
private elRef: ElementRef,
private renderer: Renderer
) { }
ngAfterViewInit() {
// add trigger class to el
this.renderer.setElementClass(this.elRef.nativeElement, 'ps-tooltip-trigger', true); // ok
// factory comp resolver
let factory = this.resolver.resolveComponentFactory(TooltipComponent);
// create component
this.tooltip = this.viewContainerRef.createComponent(factory);
console.log(this.tooltip);
// set content of the component
this.tooltip.instance.content = this.content as string;
}
}
The problem is that this is creating a sibling and I want a child (see bellow)
result:
<a class="ps-btn ps-tooltip-trigger" ng-reflect-content="the tooltip">
<span>Button</span>
</a>
<ps-tooltip>...</ps-tooltip>
wanted result:
<a class="ps-btn ps-tooltip-trigger" ng-reflect-content="the tooltip">
<span>Button</span>
<ps-tooltip>...</ps-tooltip>
</a>
Thanks in advance for your help!
Even dynamic component is inserted as sibling element you can still move element to desired place by using:
this.elRef.nativeElement.appendChild(this.tooltip.location.nativeElement);
Plunker Example
A better approach would be to have a nested ng-template with template reference variable on it such that the component is added as a sibling to ng-template but is now child to ng-template's parent.
Your template should be
<div class="ps-tooltip">
<div class="ps-tooltip-content">
<span>{{content}}</span>
<ng-template #addHere></ng-template>
</div>
</div>
And in your component
#ViewChild('addHere') addHere: ViewContainerRef;
ngAfterViewInit() {
...
this.tooltip = addHere.createComponent(factory)
...
}

Categories

Resources