Getting Started
Greetings! I am a novice in the world of Angular2, Typescript, and StackOverflow.com. I am facing an issue that I hope you can assist me with. I have successfully created a collapse animation for a button using ngOnChanges() when the button is clicked. Now, my goal is to make the menu collapse and hide whenever the user clicks anywhere on the page outside the displayed menu or away from the button managing the menu.
To achieve this, I want my collapse.animation.ts file to detect changes in the collapse property from my header.component.ts using ngDoCheck() when triggered by the clickOutsideEvent().
I attempted to implement events like (blur)="onBlur()"
or (blur)="expression"
, but they did not function as intended due to focus on a single HTML element. Perhaps I failed to implement them correctly...
The exception message I am encountering is: angular2.dev.js:23730 EXCEPTION: Error trying to diff 'true' in [isCollapsed in HeaderComponent@16:59]
Apologies if my post is lengthy or unclear. Any assistance provided would be highly valued.
The Code in Progress
collapse.animation.ts: This is where I handle the collapse animation and attempt to detect changes in the collapse state to show/hide accordingly.
import {Directive, DoCheck, KeyValueDiffers , OnChanges, ElementRef, Input } from 'angular2/core';
import {AnimationBuilder} from 'angular2/src/animate/animation_builder';
import {CssAnimationBuilder} from 'angular2/src/animate/css_animation_builder';
@Directive({
selector: '[collapse]',
host: {
'[attr.aria-expanded]': '!collapse',
'[attr.aria-hidden]': 'collapse'
}
})
export class Collapse implements OnChanges, DoCheck {
@Input() duration: number = 200; // Animation speed in ms (750 = 0.75 sec)
@Input() collapse: any; // Boolean defining collapse state
private _animation: CssAnimationBuilder; // CSS Animation
differ: any; // Property/attribute tracker.
constructor(private _animationBuilder: AnimationBuilder, private _element: ElementRef, private differs: KeyValueDiffers) {
// Initialize the CSS animation.
this._animation = _animationBuilder.css();
// Initialize the tracker.
this.differ = differs.find({}).create(null);
}
// Attempting to track changes...
ngDoCheck() {
var changes = this.differ.diff(this.collapse);
if (changes) {
changes.forEachChangedItem((elt) => {
if (elt.key === 'isCollapsed') {
this.show();
}
else {
this.hide();
}
});
}
}
// Manage the collapse property
// Works when clicking the button
ngOnChanges(changes) {
if (changes.collapse) {
if (this.collapse) {
this.hide();
} else {
this.show();
}
}
}
header.component.ts: My header component housing the menu
import { Component, Input } from 'angular2/core';
import { RouteConfig, RouterOutlet, ROUTER_DIRECTIVES, ROUTER_PROVIDERS} from "angular2/router";
import { Collapse } from './../../css/animations/collapse.animation';
import { ClickOutside } from './../directives/clickOutside.directive';
@Component({
selector: 'headerComponent',
templateUrl: 'app/master/header.component.html',
styleUrls: ['app/master/header.component.css', 'app/master/menuNavigation.component.css', 'css/animations/collapse.animation.css'],
directives: [
ROUTER_DIRECTIVES,
Collapse,
ClickOutside
],
providers: [
ROUTER_PROVIDERS
],
})
export class HeaderComponent {
@Input() collapse: boolean = false;
private headerMenuClass: string = "header_menu";
// Handle clickOutside method
handleClickOutside(className) {
// Detect "click" on the button linked to the menu with class "header_menu". No need to collapse the menu when clicking the button.
let FirstClassName: string = className.split(" ")[0];
if (FirstClassName != this.headerMenuClass) {
console.log(this.collapse);
// Trying to make my collapse.animation.ts detect this !!
this.collapse = !this.collapse;
}
}
}
header.component.html: HTML template for my component
<div id="top_bar" class="top_bar">
<!-- Button to display the menu -->
<button type="button" class="header_menu nav_icon root_navitem" id="btn_menu_switch" (click)="isCollapsed = !isCollapsed">
<img class="header_menu" src="css/App_Themes/VAL/48x48/m_bars.png" alt="" />
</button>
<button type="button" class="nav_icon" id="btn_home" [routerLink]="['Accueil']">
<img src="css/App_Themes/VAL/48x48/m_home.png" alt="" />
</button>
</div>
<!-- Menu to be collapsed at will -->
<div id="left_bar" tabindex="-1" class="left_bar collapse" [collapse]="isCollapsed"
(clickOutside)="handleClickOutside( $event.target.className )">
<div id="left_bar_menu" class="left_bar_menu">
<button type="button" class="left_bar_icon" id="btn_left_histo">
<img src="css/App_Themes/VAL/48x48/m_history.png" alt="" />
</button>
<hr class="sep" />
<button type="button" class="left_bar_icon" id="btn_left_board" [routerLink]="['Dashboard']">
<img src="css/App_Themes/VAL/48x48/m_board.png" alt="" />
</button>
<hr class="sep" />
</div>
</div>
clickOutside.directive.ts: Allows detection of clicks outside a component and retrieves information. While it may not be the most elegant solution, improvements will be made in the future.
import {Directive, EventEmitter, Input, Output } from 'angular2/core';
// Global variable within the class
var localEvent: any = null;
@Directive({
selector: '[clickOutside]',
host: {
"(click)": "trackEvent( $event )",
"(document: click)": "compareEvent( $event )"
}
})
export class ClickOutside {
@Output() clickOutside;
constructor() {
this.clickOutside = new EventEmitter();
}
// If the event at the document root matches the event target, it means the event started inside the target and bubbled up. If the document root event DOES NOT MATCH the last known target event, it originated from outside the target.
compareEvent(event) {
if (event !== localEvent) {
this.clickOutside.emit(event);
}
localEvent = null;
}
// Track click event on bound target.
trackEvent(event) {
// Start tracking the event as it bubbles up the DOM tree when the user clicks inside the bound target. This allows determining if the event originated within the target.
localEvent = event;
}
}