我正在分享用于设计层次结构的工作代码。
注意:编译hierarchy.component.scss需要node-sass
安装 node-sass : npm install node-sass
层次结构.component.ts
import { Component, OnInit, Input, OnChanges, SimpleChanges, SimpleChange, Output, EventEmitter } from '@angular/core';
import { Node } from './hierarchy.model';
@Component({
selector: 'app-hierarchy',
templateUrl: './hierarchy.component.html',
styleUrls: ['./hierarchy.component.scss']
})
export class HierarchyComponent implements OnInit, OnChanges {
@Input() name: string;
@Input() data: Array<Node>;
@Input() selectedNodeIds: Array<string> = [];
@Output() selectedIds: EventEmitter<Array<string>> = new EventEmitter<Array<string>>();
public hierarchyData: Array<Node>;
public showHiearchy: boolean;
private actingNode: string;
constructor() {
this.name = 'Hierarchy';
this.showHiearchy = false;
this.hierarchyData = new Array<Node>();
}
ngOnInit(): void {}
// go to parent and set full or partial select. Their method.
private setParentFullOrPartialSelected(isChecked: boolean, node: Node, parent: Node) {
if (isChecked) {
if (parent) {
if (this.checkAllChildOfNodeSelected(parent)) {
parent.allSelected = true;
this.setParentFullOrPartialSelected(isChecked, parent, parent.parentRef);
} else {
this.removeAllSelected(parent);
}
}
} else {
this.removeAllSelected(node);
this.setParentPartialSelected(node);
}
}
private checkAllChildOfNodeSelected(parentNode: Node): boolean {
const nodes: Array<Node> = parentNode.children;
let condition = true;
(Array.isArray(nodes)) ? condition = nodes.every((node: Node) => this.isAllSelected(node)) : null;
return condition;
}
public isAllSelected(node: Node): boolean {
const condition = node.allSelected ? node.allSelected : true;
return condition && (node.allSelected);
}
private removeAllSelected(node: Node) {
node.allSelected ? node.allSelected = false : null;
}
private setParentPartialSelected(node: Node) {
if (node.parentRef) {
node.parentRef.allSelected ? node.parentRef.allSelected = false : null;
this.setParentPartialSelected(node.parentRef);
}
}
// Update parents and their method
private updateParents(isChecked: boolean, node: Node) {
const parent = node ? node.parentRef : null;
if (parent) {
if (isChecked) {
parent.checked = isChecked;
this.updateParents(isChecked, parent);
} else {
if (!this.isSomeChildOfNodeSelected(parent)) {
parent.checked = isChecked;
if (this.isSelected(parent)) {
this.removeSelectedNodeId(parent);
}
this.updateParents(isChecked, parent);
}
}
}
}
private isSomeChildOfNodeSelected(parentNode: Node): boolean {
const nodes: Array<Node> = parentNode.children;
let condition = false;
(Array.isArray(nodes)) ? condition = nodes.some((node: Node) => node.checked) : null;
return condition;
}
public checkboxChanged(isChecked: boolean, node: Node, parent: Node = null) {
node.checked = isChecked;
node.parentRef = parent;
this.actingNode = node.id;
node.allSelected = isChecked;
// go to parent and set full or partial select.
this.setParentFullOrPartialSelected(isChecked, node, parent);
// go to parent and make them checked or unchecked.
this.updateParents(isChecked, node);
// go to child and select them or de-select.
this.updateChildrens(isChecked, node);
this.updateSelectedNodeIds(isChecked, node);
this.selectedIds.emit(this.selectedNodeIds);
}
private updateChildrens(isChecked: boolean, node: Node) {
const childNodes: Array<Node> = node.children;
if (childNodes) {
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < childNodes.length; i++) {
childNodes[i].checked = isChecked;
childNodes[i].allSelected = isChecked;
if (isChecked) {
this.updateSelectedNodeIds(isChecked, childNodes[i]);
} else {
this.removeSelectedNodeId(childNodes[i]);
}
this.updateChildrens(isChecked, childNodes[i]); // use recursion to update childrens within children.
}
}
}
private updateSelectedNodeIds(isChecked: boolean, node: Node) {
if (isChecked) {
if (!this.isSelected(node)) {
this.selectedNodeIds.push(node.id);
}
} else {
this.removeSelectedNodeId(node);
}
}
private removeSelectedNodeId(node: Node) {
if (this.isSelected(node)) {
const index = this.selectedNodeIds.indexOf(node.id);
this.selectedNodeIds.splice(index, 1);
}
}
public isPartiallySelected(node: Node) {
return node.checked && !node.allSelected;
}
public isSelected(node: Node): boolean {
if (this.selectedNodeIds) {
return this.selectedNodeIds.indexOf(node.id) > -1;
}
return false;
}
public toggleHierarchy() {
this.showHiearchy = !this.showHiearchy;
}
public expandNode(node: Node) {
node.expanded = !node.expanded;
}
/**
* For each node it will set referance to parent node and also ensure to initilize the variables.
* @param nodes List of node.
* @param parentNode parent of node.
*/
public setNodeDefault(nodes: Array<Node>, parentNode: Node = null) {
nodes.forEach( (node: Node) => {
node.allSelected = false;
node.checked = false;
node.expanded = true;
node.parentRef = parentNode;
if (node.children) {
this.setNodeDefault(node.children, node);
}
});
}
ngOnChanges(changes: SimpleChanges) {
const dataChange: SimpleChange = changes['data'];
if (dataChange && dataChange.currentValue) {
this.hierarchyData = new Array<Node>( new Node('all', 'Select All', this.data));
// If select all is not required, then comment above line and use below line code
// this.hierarchyData = this.data;
this.setNodeDefault(this.hierarchyData);
}
}
}
层次结构.component.html
<div class="flex-inline-col dropdown">
<div class="dropdown__btn" (click)=toggleHierarchy()>{{name}}</div>
<div class="dropdown__content" [class.toggle-drop-down]="showHiearchy">
<div class="dropdown__search">
<input type="text" class="dropdown__search" autofocus>
</div>
<ul class="dropdown__hierarchy">
<li class="dropdown__hierarchy__list">
<ul class="hierarchy">
<ng-container *ngTemplateOutlet="hierarchyNode; context: {$implicit: hierarchyData, level: 'first'}"></ng-container>
</ul>
</li>
</ul>
</div>
</div>
<ng-template #hierarchyNode let-hierarchyData let-level="level" let-parent="parent">
<li *ngFor="let node of hierarchyData" [class.last-children]="!node.children"
class="flex-col hierarchy__item">
<div>
<div *ngIf="node.children" class="flex-inline hierarchy__item__icon" (click)="expandNode(node)">
<svg *ngIf="!node.expanded" version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>circle-right</title>
<path d="M16 0c-8.837 0-16 7.163-16 16s7.163 16 16 16 16-7.163 16-16-7.163-16-16-16zM16 29c-7.18 0-13-5.82-13-13s5.82-13 13-13 13 5.82 13 13-5.82 13-13 13z"></path>
<path d="M11.086 22.086l2.829 2.829 8.914-8.914-8.914-8.914-2.828 2.828 6.086 6.086z"></path>
</svg>
<svg *ngIf="node.expanded" version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>circle-down</title>
<path d="M32 16c0-8.837-7.163-16-16-16s-16 7.163-16 16 7.163 16 16 16 16-7.163 16-16zM3 16c0-7.18 5.82-13 13-13s13 5.82 13 13-5.82 13-13 13-13-5.82-13-13z"></path>
<path d="M9.914 11.086l-2.829 2.829 8.914 8.914 8.914-8.914-2.828-2.828-6.086 6.086z"></path>
</svg>
</div>
<input type="checkbox" id="check_{{node.id}}"
class="flex-inline hierarchy__item__checkbox"
[checked]="node.checked || isSelected(node)"
(change)="checkboxChanged($event.target.checked, node, parent)">
<label for="check_{{node.id}}" class="flex-inline hierarchy__item__label">
<span class="hierarchy__item__icon hierarchy__item__circle" *ngIf="!node.checked">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>radio-unchecked</title>
<path d="M16 0c-8.837 0-16 7.163-16 16s7.163 16 16 16 16-7.163 16-16-7.163-16-16-16zM16 28c-6.627 0-12-5.373-12-12s5.373-12 12-12c6.627 0 12 5.373 12 12s-5.373 12-12 12z"></path>
</svg>
</span>
<span class="hierarchy__item__icon hierarchy__item__circle-checked" *ngIf="node.checked && node.allSelected">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>checkmark</title>
<path d="M21.82 13.030l-1.002-1.002c-0.185-0.185-0.484-0.185-0.668 0l-6.014 6.013-2.859-2.882c-0.186-0.185-0.484-0.185-0.67 0l-1.002 1.003c-0.185 0.185-0.185 0.484 0 0.668l4.193 4.223c0.185 0.184 0.484 0.184 0.668 0l7.354-7.354c0.186-0.185 0.186-0.484 0-0.669zM16 3c-7.18 0-13 5.82-13 13s5.82 13 13 13 13-5.82 13-13-5.82-13-13-13zM16 26c-5.522 0-10-4.478-10-10 0-5.523 4.478-10 10-10 5.523 0 10 4.477 10 10 0 5.522-4.477 10-10 10z"></path>
</svg>
</span>
<span class="hierarchy__item__icon hierarchy__item__circle-minus" *ngIf="node.checked && !node.allSelected">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="28" viewBox="0 0 24 28">
<title>minus-circle</title>
<path d="M19 15v-2c0-0.547-0.453-1-1-1h-12c-0.547 0-1 0.453-1 1v2c0 0.547 0.453 1 1 1h12c0.547 0 1-0.453 1-1zM24 14c0 6.625-5.375 12-12 12s-12-5.375-12-12 5.375-12 12-12 12 5.375 12 12z"></path>
</svg>
</span>
{{node.name}}
</label>
</div>
<ul *ngIf="node.children" class="hierarchy" [style.display]="!node.expanded ? 'none' : 'block'">
<ng-container *ngTemplateOutlet="hierarchyNode; context: {$implicit: node.children, level: 'second', parent: node}"></ng-container>
</ul>
</li>
</ng-template>
层次结构.component.scss
* {
&,
&:before,
&:after {
box-sizing: border-box;
margin: 0px;
padding: 0px;
}
}
.flex {
display: flex;
}
.flex-col {
display: flex;
flex-direction: column;
}
.flex-inline {
display: inline-flex;
}
.flex-inline-col {
display: inline-flex;
flex-direction: column;
}
.absCenter {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.dropdown {
width: 300px;
position: relative;
&__btn {
background-color: #fff;
color: #000;
box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4);
height: 35px;
padding: 10px;
font-size: 14px;
text-align: center;
text-transform: UPPERCASE;
cursor: pointer;
&:after {
content: "";
width: 0; height: 0; position: absolute; right: 5px; top:45%;
border-top: 5px solid #000;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
}
&:active {
transform: translateY(2px);
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2)
}
}
&__content {
display: none;
position: absolute;
top: 38px;
left: 0px;
width: 100%;
z-index: 99;
background: #fff;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.4);
max-height: 300px;
overflow: hidden;
}
&__search {
width: 100%;
height: 30px;
border-bottom: 1px solid #e0e0e0;
}
&__hierarchy {
list-style: none;
max-height: 270px;
overflow: auto;
&__list {
margin-top: 5px;
}
}
}
.toggle-drop-down {
display: block;
}
svg {
width: 20px !important;
height: 20px !important;
}
.hierarchy {
margin-left: 10px;
&__item {
padding: 5px 0px;
&__icon {
width: 20px;
margin-right: 10px;
}
&__label {
padding-left: 10px;
}
&__checkbox {
align-self: center;
display: none;
}
}
}
.last-children {
margin-left: 35px;
}
层次结构.model.ts
mockData 用于测试目的。应用程序正常运行后,删除此 mockData。
export class Node {
constructor(
public id: string,
public name: string,
public children?: Array<Node>,
public allSelected?: boolean,
public expanded?: boolean,
public checked?: boolean,
public parentRef?: Node,
) { }
}
export const mockData: Array<Node> = new Array<Node>(
new Node('1000', 'First level 1', [
new Node('1100', 'Second level 11', [
new Node('1110', 'Third level 111', [
new Node('1111', 'Fourth level 1111'),
new Node('1112', 'Fourth level 1112'),
new Node('1113', 'Fourth level 1113'),
new Node('1114', 'Fourth level 1114')
]),
new Node('1120', 'Third level 112', [
new Node('1121', 'Fourth level 1121'),
new Node('1122', 'Fourth level 1122'),
new Node('1123', 'Fourth level 1123'),
new Node('1124', 'Fourth level 1124')
])
]),
new Node('1200', 'Second level 12', [
new Node('1210', 'Third level 121', [
new Node('1211', 'Fourth level 1211'),
new Node('1212', 'Fourth level 1212'),
new Node('1213', 'Fourth level 1213'),
new Node('1214', 'Fourth level 1214')
]),
new Node('1220', 'Third level 122', [
new Node('1221', 'Fourth level 1221'),
new Node('1222', 'Fourth level 1222'),
new Node('1223', 'Fourth level 1223'),
new Node('1224', 'Fourth level 1224')
])
])
]),
new Node('2000', 'First level 2', [
new Node('2100', 'Second level 21', [
new Node('2110', 'Third level 211', [
new Node('2111', 'Fourth level 2111'),
new Node('2112', 'Fourth level 2112'),
new Node('2113', 'Fourth level 2113'),
new Node('2114', 'Fourth level 2114')
]),
new Node('2120', 'Third level 212', [
new Node('2121', 'Fourth level 2121'),
new Node('2122', 'Fourth level 2122'),
new Node('2123', 'Fourth level 2123'),
new Node('2124', 'Fourth level 2124')
])
]),
new Node('2200', 'Second level 22', [
new Node('2210', 'Third level 221', [
new Node('2211', 'Fourth level 2211'),
new Node('2212', 'Fourth level 2212'),
new Node('2213', 'Fourth level 2213'),
new Node('2214', 'Fourth level 2214')
]),
new Node('2220', 'Third level 222', [
new Node('2221', 'Fourth level 2221'),
new Node('2222', 'Fourth level 2222'),
new Node('2223', 'Fourth level 2223'),
new Node('2224', 'Fourth level 2224')
])
])
])
);
app.component.ts
import { Component } from '@angular/core';
import { Node, mockData } from './hierarchy/hierarchy.model';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
public hierarchyData: Array<Node>;
public selectedNodeIds: Array<string> = [];
public selectedIds: Array<string> = [];
constructor() {
this.hierarchyData = mockData;
}
public changedSelectedIds(ids: Array<string>) {
this.selectedIds = ids;
}
}
app.component.html
<div class="container">
<app-hierarchy name="Hierarchy"
[data]="hierarchyData"
[selectedNodeIds]="selectedNodeIds"
(selectedIds)="changedSelectedIds($event)">
</app-hierarchy>
</div>
输出