• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

atinc / ngx-tethys / #102

26 May 2026 08:11AM UTC coverage: 91.111% (+0.7%) from 90.407%
#102

push

web-flow
build: bump docgeni to 2.8.0-next.5 (#3809)

4571 of 5491 branches covered (83.25%)

Branch coverage included in aggregate %.

13141 of 13949 relevant lines covered (94.21%)

966.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.1
/src/tree/tree-node.component.ts
1
import {
2
    Component,
3
    ElementRef,
4
    TemplateRef,
5
    ViewEncapsulation,
6
    numberAttribute,
7
    inject,
8
    input,
9
    computed,
10
    contentChild,
11
    viewChild,
12
    output
13
} from '@angular/core';
14

15
import { THY_TREE_ABSTRACT_TOKEN } from './tree-abstract';
16
import { ThyTreeNode, ThyTreeEmitEvent, ThyTreeNodeCheckState, ThyClickBehavior } from './tree.class';
17
import { ThyTreeService } from './tree.service';
18
import { ThyLoading } from 'ngx-tethys/loading';
19
import { ThyIcon } from 'ngx-tethys/icon';
20
import { NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
21
import { coerceBooleanProperty } from 'ngx-tethys/util';
22

23
/**
24
 * 树形控件的节点组件
25
 * @private
26
 * @name thy-tree-node
27
 */
28
@Component({
29
    selector: 'thy-tree-node',
30
    standalone: true,
31
    templateUrl: './tree-node.component.html',
32
    encapsulation: ViewEncapsulation.None,
33
    imports: [ThyIcon, NgClass, NgStyle, NgTemplateOutlet, ThyLoading],
34
    host: {
35
        '[class.thy-tree-node]': 'true',
36
        '[class]': 'itemClass()'
37
    }
38
})
39
export class ThyTreeNodeComponent {
1✔
40
    root = inject(THY_TREE_ABSTRACT_TOKEN);
655✔
41
    thyTreeService = inject(ThyTreeService);
655✔
42

43
    /**
44
     * node 节点展现所需的数据
45
     */
46
    readonly node = input.required<ThyTreeNode>();
655✔
47

48
    /**
49
     * 设置 TreeNode 是否支持异步加载
50
     */
51
    readonly thyAsync = input(false, { transform: coerceBooleanProperty });
655✔
52

53
    /**
54
     * 设置 TreeNode 是否支持多选节点
55
     */
56
    readonly thyMultiple = input(false, { transform: coerceBooleanProperty });
655✔
57

58
    /**
59
     * 设置 TreeNode 是否支持拖拽排序
60
     */
61
    readonly thyDraggable = input(false, { transform: coerceBooleanProperty });
655✔
62

63
    /**
64
     * 设置 TreeNode 是否支持 Checkbox 选择
65
     */
66
    readonly thyCheckable = input(false, { transform: coerceBooleanProperty });
655✔
67

68
    /**
69
     * 点击节点的行为,`default` 为选中当前节点,`selectCheckbox` 为选中节点的 Checkbox, `thyCheckable` 为 true 时生效。
70
     * @default default
71
     */
72
    readonly thyClickBehavior = input<ThyClickBehavior>();
655✔
73

74
    /**
75
     * 设置节点名称是否支持超出截取
76
     * @default false
77
     */
78
    readonly thyTitleTruncate = input(false, { transform: coerceBooleanProperty });
655✔
79

80
    /**
81
     * 设置 TreeNode 的渲染模板
82
     */
83
    readonly templateRef = input<TemplateRef<any>>();
655✔
84

85
    /**
86
     * 设置子的空数据渲染模板
87
     */
88
    readonly emptyChildrenTemplateRef = input<TemplateRef<any>>();
655✔
89

90
    /**
91
     * 设置 node 点击事件
92
     */
93
    readonly thyOnClick = output<ThyTreeEmitEvent>();
655✔
94

95
    /**
96
     * 双击 node 事件
97
     */
98
    readonly thyDblClick = output<ThyTreeEmitEvent>();
655✔
99

100
    /**
101
     * 点击展开触发事件
102
     */
103
    readonly thyOnExpandChange = output<ThyTreeEmitEvent>();
655✔
104

105
    /**
106
     * 设置 check 选择事件
107
     */
108
    readonly thyOnCheckboxChange = output<ThyTreeEmitEvent>();
655✔
109

110
    /**
111
     * 设置 childrenTree 的渲染模板
112
     */
113
    readonly childrenTreeTemplateRef = contentChild<TemplateRef<any>>('childrenTree');
655✔
114

115
    /** The native `<div class="thy-tree-node-wrapper thy-sortable-item"></div>` element. */
116
    readonly treeNodeWrapper = viewChild<ElementRef<HTMLElement>>('treeNodeWrapper');
655✔
117

118
    /**
119
     * 开启虚拟滚动时,单行节点的高度,当`thySize`为`default`时,该参数才生效
120
     */
121
    readonly thyItemSize = input(44, { transform: numberAttribute });
655✔
122

123
    /**
124
     * 设置节点缩进距离,缩进距离 = thyIndent * node.level
125
     */
126
    readonly thyIndent = input(25, { transform: numberAttribute });
655✔
127

128
    readonly nodeIcon = computed(() => {
655✔
129
        return this.node().origin.icon;
117✔
130
    });
131

132
    readonly nodeIconStyle = computed(() => {
655✔
133
        return this.node().origin.iconStyle;
×
134
    });
135

136
    protected readonly itemClass = computed(() => {
655✔
137
        return this.node()?.itemClass?.join(' ');
715✔
138
    });
139

140
    checkState = ThyTreeNodeCheckState;
655✔
141

142
    public clickNode(event: Event) {
143
        const node = this.node();
7✔
144
        if (node.isDisabled) {
7✔
145
            this.expandNode(event);
2✔
146
        } else {
147
            if (this.thyCheckable() && this.thyClickBehavior() === 'selectCheckbox') {
5✔
148
                this.clickNodeCheck(event);
2✔
149
            } else {
150
                if (this.root.thyMultiple()) {
3✔
151
                    this.root.toggleTreeNode(node);
1✔
152
                } else {
153
                    this.root.selectTreeNode(node);
2✔
154
                }
155
            }
156
        }
157
        this.thyOnClick.emit({
7✔
158
            eventName: 'click',
159
            event: event,
160
            node: node
161
        });
162
    }
163

164
    public dbClickNode(event: Event) {
165
        this.thyDblClick.emit({
1✔
166
            eventName: 'dbclick',
167
            event: event,
168
            node: this.node()
169
        });
170
    }
171

172
    public clickNodeCheck(event: Event) {
173
        event.stopPropagation();
9✔
174
        const node = this.node();
9✔
175
        if (node.isChecked === ThyTreeNodeCheckState.unchecked) {
9✔
176
            node.setChecked(true);
5✔
177
        } else if (node.isChecked === ThyTreeNodeCheckState.checked) {
4✔
178
            node.setChecked(false);
1✔
179
        } else if (node.isChecked === ThyTreeNodeCheckState.indeterminate) {
3✔
180
            if (node.children?.length) {
3!
181
                const activeChildren = node.children.filter(item => !item.isDisabled);
16✔
182
                const isAllActiveChildrenChecked = activeChildren.every(item => item.isChecked);
5✔
183
                node.setChecked(!isAllActiveChildrenChecked);
3✔
184
            } else {
185
                node.setChecked(true);
×
186
            }
187
        }
188
        this.thyOnCheckboxChange.emit({
9✔
189
            eventName: 'checkboxChange',
190
            event: event,
191
            node: node
192
        });
193
    }
194

195
    public expandNode(event: Event) {
196
        event.stopPropagation();
6✔
197
        const node = this.node();
6✔
198
        this.node().setExpanded(!node.isExpanded);
6✔
199
        if (this.root.thyShowExpand()) {
6✔
200
            this.thyOnExpandChange.emit({
6✔
201
                eventName: 'expand',
202
                event: event,
203
                node: node
204
            });
205
            if (this.thyAsync() && node.children.length === 0) {
6✔
206
                node.setLoading(true);
1✔
207
            }
208
        }
209
    }
210

211
    public isShowExpand(node: ThyTreeNode) {
212
        return this.root.isShowExpand(node);
1,134✔
213
    }
214
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc