File

src/treeview/tree-node.component.ts

Implements

AfterContentChecked OnInit OnDestroy

Metadata

Index

Properties
Methods
Inputs
Outputs
Accessors

Constructor

constructor(treeViewService: TreeViewService)
Parameters :
Name Type Optional
treeViewService TreeViewService No

Inputs

active
Type : boolean
Default value : false
children
Type : Node[]
Default value : []
depth
Type : number
Default value : 0

Determines the depth of the node Calculated by default when passing Node array to TreeViewComponent, manual entry required otherwise

disabled
Type : boolean
Default value : false
expanded
Type : boolean
Default value : false
gap
Type : number
Default value : 0
icon
Type : string | TemplateRef<any>
iconContext
Type : any
id
Type : string
Default value : `tree-node-${TreeNodeComponent.treeNodeCount++}`
label
Type : string | TemplateRef<any>
labelContext
Type : any
node
Type : Node

Simple way to set all attributes of Node component via node object Would simplify setting component attributes when dynamically rendering node.

selectable
Type : boolean
Default value : true
selected
Type : boolean
Default value : false
value
Type : any

Outputs

nodeBlur
Type : EventEmitter
nodeFocus
Type : EventEmitter
nodeSelect
Type : EventEmitter
nodetoggle
Type : EventEmitter

Methods

calculateOffset
calculateOffset()

Calculate the node offset

Returns : number

Number

emitBlurEvent
emitBlurEvent(event)
Parameters :
Name Optional
event No
Returns : void
emitFocusEvent
emitFocusEvent(event)
Parameters :
Name Optional
event No
Returns : void
Public isProjected
isProjected()
Returns : any
Public isTemplate
isTemplate(value)
Parameters :
Name Optional
value No
Returns : boolean
navigateTree
navigateTree(event: KeyboardEvent)

Manages the keyboard accessibility for children expansion & selection

Parameters :
Name Type Optional
event KeyboardEvent No
Returns : void
ngAfterContentChecked
ngAfterContentChecked()

Caclulate offset for margin/padding

Returns : void
ngOnDestroy
ngOnDestroy()

Unsubscribe from subscriptions

Returns : void
ngOnInit
ngOnInit()

Highlight the node

Returns : void
nodeClick
nodeClick(event)

Selects the node and emits the event from the tree view component

Parameters :
Name Optional
event No
Returns : void
toggleExpanded
toggleExpanded(event)

Expand children if not disabled

Parameters :
Name Optional Description
event No

: Event

Returns : void

Properties

Private _node
offset
Private subscription
Type : Subscription
Static treeNodeCount
Type : number
Default value : 0

Accessors

node
getnode()
setnode(node: Node)

Simple way to set all attributes of Node component via node object Would simplify setting component attributes when dynamically rendering node.

Parameters :
Name Type Optional
node Node No
Returns : void
import {
	Component,
	Input,
	Output,
	EventEmitter,
	OnInit,
	OnDestroy,
	AfterContentInit,
	TemplateRef,
	AfterContentChecked
} from "@angular/core";
import { Subscription } from "rxjs";
import { TreeViewService } from "./treeview.service";
import { EventOnNode, Node } from "./tree-node.types";

@Component({
	selector: "cds-tree-node",
	template: `
		<div
			[id]="id"
			class="cds--tree-node"
			[ngClass]="{
				'cds--tree-node--active': active,
				'cds--tree-node--disabled': disabled,
				'cds--tree-node--selected': selected,
				'cds--tree-leaf-node': !children.length,
				'cds--tree-parent-node': children.length,
				'cds--tree-node--with-icon': icon
			}"
			[attr.aria-expanded]="expanded || null"
			[attr.aria-current]="active || null"
			[attr.aria-selected]="disabled ? null : selected"
			[attr.aria-disabled]="disabled"
			role="treeitem"
			[attr.tabindex]="selected ? 0 : -1"
			(focus)="emitFocusEvent($event)"
			(blur)="emitBlurEvent($event)"
			(keydown)="navigateTree($event)">
			<div
				*ngIf="!children.length"
				class="cds--tree-node__label"
				[style.padding-inline-start.rem]="offset"
				[style.margin-inline-start.rem]="-offset"
				(click)="nodeClick($event)">
				<!-- Icon -->
				<ng-container *ngIf="icon && !isTemplate(icon)">
					<svg
						class="cds--tree-node__icon"
						[cdsIcon]="icon"
						size="16">
					</svg>
				</ng-container>
				<ng-template *ngIf="isTemplate(icon)" [ngTemplateOutlet]="icon"></ng-template>
				<ng-container *ngIf="!isTemplate(label)">{{label}}</ng-container>
				<ng-template
					*ngIf="isTemplate(label)"
					[ngTemplateOutlet]="label"
					[ngTemplateOutletContext]="{ $implicit: labelContext }">
				</ng-template>
			</div>
			<div
				*ngIf="children.length"
				class="cds--tree-node__label"
				[style.padding-inline-start.rem]="offset"
				[style.margin-inline-start.rem]="-offset"
				role="group"
				(click)="nodeClick($event)">
				<span
					class="cds--tree-parent-node__toggle"
					[attr.disabled]="disabled || null"
					(click)="toggleExpanded($event)">
					<svg
						class="cds--tree-parent-node__toggle-icon"
						[ngClass]="{'cds--tree-parent-node__toggle-icon--expanded' : expanded}"
						ibmIcon="caret--down"
						size="16">
					</svg>
				</span>
				<span class="cds--tree-node__label__details">
					<!-- Icon -->
					<ng-container *ngIf="icon && !isTemplate(icon)">
						<svg
							class="cds--tree-node__icon"
							[cdsIcon]="icon"
							size="16">
						</svg>
					</ng-container>
					<ng-template
						*ngIf="isTemplate(icon)"
						[ngTemplateOutlet]="icon"
						[ngTemplateOutletContext]="{ $implicit: iconContext }">
					</ng-template>
					<ng-container *ngIf="!isTemplate(label)">{{label}}</ng-container>
					<ng-template
						*ngIf="isTemplate(label)"
						[ngTemplateOutlet]="label"
						[ngTemplateOutletContext]="{ $implicit: labelContext }">
					</ng-template>
				</span>
			</div>
			<div
				*ngIf="expanded"
				role="group"
				class="cds--tree-node__children">
				<ng-container *ngIf="isProjected(); else notProjected">
					<ng-content></ng-content>
				</ng-container>
				<ng-template #notProjected>
					<cds-tree-node
						*ngFor="let childNode of children"
						[node]="childNode"
						[depth]="depth + 1"
						[disabled]="disabled"
						(nodetoggle)="nodetoggle.emit($event)">
					</cds-tree-node>
				</ng-template>
			</div>
		</div>
	`
})
export class TreeNodeComponent implements AfterContentChecked, OnInit, OnDestroy {
	static treeNodeCount = 0;
	@Input() id = `tree-node-${TreeNodeComponent.treeNodeCount++}`;
	@Input() active = false;
	@Input() disabled = false;
	@Input() selectable = true;
	@Input() expanded = false;
	@Input() label: string | TemplateRef<any>;
	@Input() labelContext: any;
	@Input() selected = false;
	@Input() value;
	@Input() icon: string | TemplateRef<any>;
	@Input() iconContext: any;
	@Input() gap = 0;
	@Input() children: Node[] = [];

	/**
	 * Determines the depth of the node
	 * Calculated by default when passing `Node` array to `TreeViewComponent`, manual entry required otherwise
	 */
	@Input() depth = 0;

	/**
	 * Simple way to set all attributes of Node component via node object
	 * Would simplify setting component attributes when dynamically rendering node.
	 */
	@Input() set node(node: Node) {
		this._node = node;

		this.id = node.id ?? this.id;
		this.active = node.active ?? this.active;
		this.disabled = node.disabled ?? this.disabled;
		this.selectable = node.selectable ?? this.selectable;
		this.expanded = node.expanded ?? this.expanded;
		this.label = node.label ?? this.label;
		this.labelContext = node.labelContext ?? this.labelContext;
		this.value = node.value ?? this.value;
		this.icon = node.icon ?? this.icon;
		this.selected = node.selected ?? this.selected;
		this.depth = node.depth ?? this.depth;
		this.gap = node.gap ?? this.gap;
		this.children = node.children ?? this.children;
		this.iconContext = node.iconText ?? this.iconContext;
	}

	get node() {
		return this._node;
	}

	@Output() nodeFocus = new EventEmitter<EventOnNode>();
	@Output() nodeBlur = new EventEmitter<EventOnNode>();
	@Output() nodeSelect = new EventEmitter<Node>();
	@Output() nodetoggle = new EventEmitter<EventOnNode>();

	offset;
	private _node;
	private subscription: Subscription;

	constructor(private treeViewService: TreeViewService) {}

	/**
	 * Caclulate offset for margin/padding
	 */
	ngAfterContentChecked(): void {
		this.offset = this.calculateOffset();
	}

	/**
	 * Highlight the node
	 */
	ngOnInit(): void {
		// Highlight the node
		this.subscription = this.treeViewService.selectionObservable.subscribe((value: Map<string, Node>) => {
			this.selected = this.selectable && value.has(this.id);
			this.active = this.selectable && this.selected;
		});
	}

	/**
	 * Unsubscribe from subscriptions
	 */
	ngOnDestroy(): void {
		this.subscription?.unsubscribe();
	}

	/**
	 * Selects the node and emits the event from the tree view component
	 * @param event
	 */
	nodeClick(event) {
		if (!this.disabled) {
			event.target.parentElement.focus();
			if (this.selectable || this.children.length === 0) {
				this.selected = true;
				this.active = true;
				const node = { id: this.id, label: this.label, value: this.value };
				// Passes event to all nodes to update highlighting & parent to emit
				this.treeViewService.selectNode(node);
				this.nodeSelect.emit(node);
			} else {
				this.toggleExpanded(event);
			}
		}
	}

	/**
	 * Calculate the node offset
	 * @returns Number
	 */
	calculateOffset() {
		// Parent node with icon
		if (this.children.length && this.icon) {
			return this.depth + 1 + this.depth * 0.5;
		}

		// parent node without icon
		if (this.children.length) {
			return this.depth + 1;
		}

		// leaf node with icon
		if (this.icon) {
			return this.depth + 2 + this.depth * 0.5;
		}

		return this.depth + this.gap + 2.5;
	}

	emitFocusEvent(event) {
		const node = { id: this.id, label: this.label, value: this.value };
		this.nodeFocus.emit({ node, event });
		this.treeViewService.focusNode(node);
	}

	emitBlurEvent(event) {
		this.nodeBlur.emit({ node: { id: this.id, label: this.label, value: this.value }, event });
	}

	/**
	 * Expand children if not disabled
	 * @param event: Event
	 */
	toggleExpanded(event) {
		if (!this.disabled) {
			this.nodetoggle.emit({ node: { id: this.id, label: this.label, value: this.value }, event });
			this.expanded = !this.expanded;
			// Prevent selection of the node
			event.stopPropagation();
		}
	}

	/**
	 * Manages the keyboard accessibility for children expansion & selection
	 */
	navigateTree(event: KeyboardEvent) {
		if (event.key === "ArrowLeft" || event.key === "ArrowRight" || event.key === "Enter") {
			event.stopPropagation();
		}
		// Unexpand
		if (event.key === "ArrowLeft") {
			if (this.expanded && this.children) {
				this.toggleExpanded(event);
			}
		}

		if (event.key === "ArrowRight") {
			if (!this.expanded && this.children) {
				this.toggleExpanded(event);
			}
		}

		if (event.key === "Enter") {
			event.preventDefault();
			this.nodeClick(event);
		}
	}

	public isTemplate(value) {
		return value instanceof TemplateRef;
	}

	public isProjected() {
		return this.treeViewService.contentProjected;
	}
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""