(id: string): T;
18 | };
19 | };
20 |
21 | // First, initialize the Angular testing environment.
22 | getTestBed().initTestEnvironment(
23 | BrowserDynamicTestingModule,
24 | platformBrowserDynamicTesting()
25 | );
26 | // Then we find all the tests.
27 | const context = require.context('./', true, /\.spec\.ts$/);
28 | // And load the modules.
29 | context.keys().map(context);
30 |
--------------------------------------------------------------------------------
/projects/example-app/src/app/basictree/basictree.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-basictree',
5 | template: `
6 |
7 |
8 | Keys:
9 | down | up | left | right | space | enter
10 | `,
11 | styles: []
12 | })
13 | export class BasicTreeComponent {
14 | nodes = [
15 | {
16 | name: 'root1',
17 | children: [
18 | { name: 'child1' },
19 | { name: 'child2' }
20 | ]
21 | },
22 | {
23 | name: 'root2',
24 | children: [
25 | { name: 'child2.1', children: [] },
26 | { name: 'child2.2', children: [
27 | {name: 'grandchild2.2.1'}
28 | ] }
29 | ]
30 | },
31 | { name: 'root3' },
32 | { name: 'root4', children: [] },
33 | { name: 'root5', children: null }
34 | ];
35 | }
36 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/guides/update-guide/update-guide.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-update-guide',
5 | templateUrl: './update-guide.component.html',
6 | styleUrls: ['./update-guide.component.scss']
7 | })
8 | export class UpdateGuideComponent {
9 |
10 | adding =
11 | `
12 |
13 |
14 | class MyComponent {
15 | nodes = [{ name: 'node' }];
16 |
17 | @ViewChild(TreeComponent)
18 | private tree: TreeComponent;
19 |
20 | addNode() {
21 | this.nodes.push({ name: 'another node' });
22 | this.tree.treeModel.update();
23 | }
24 | }
25 | `;
26 |
27 | immutable =
28 | `
29 |
30 |
31 | nodes = [...]
32 |
33 | addNode(newNode) {
34 | // Just add node and replace nodes variable:
35 | this.nodes = [...this.nodes, newNode];
36 | }
37 | `;
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/guides/redux-guide/redux-guide.component.html:
--------------------------------------------------------------------------------
1 | Redux / Immutable Data
2 |
3 | Use ID
4 |
5 | Working with the tree using immutable data is possible.
6 | Make sure that:
7 |
8 | You provide a unique id property on each node
9 | If you have a different key property, then set the idField in the options
10 | You override drop action as stated below
11 |
12 |
13 | Override drop action
14 | Drag and drop by default mutates the children.
15 | If working with immutable data, you must override the action and supply your custom behaviour:
16 | {{ options }}
17 |
18 | Rebuilding the tree
19 | Every time the nodes array changes, the entire tree model is rebuilt.
20 | This might be costly if you have a huge amount of nodes that change very frequently.
21 |
--------------------------------------------------------------------------------
/projects/docs-app/.browserslistrc:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # For the full list of supported browsers by the Angular framework, please see:
6 | # https://angular.io/guide/browser-support
7 |
8 | # You can see what browsers were selected by your queries by running:
9 | # npx browserslist
10 |
11 | last 1 Chrome version
12 | last 1 Firefox version
13 | last 2 Edge major versions
14 | last 2 Safari major versions
15 | last 2 iOS major versions
16 | Firefox ESR
17 | not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line.
18 | IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
19 |
--------------------------------------------------------------------------------
/projects/example-app/src/app/fields/fields.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { ITreeOptions } from 'angular-tree-component';
3 |
4 | @Component({
5 | selector: 'app-fields',
6 | template: `
7 | Overriding displayField & nodeClass
8 |
9 | `,
10 | styles: [
11 | ]
12 | })
13 | export class FieldsComponent {
14 | nodes = [
15 | {
16 | _id: '1',
17 | title: 'root1',
18 | className: 'root1Class',
19 | nodes: [{_id: '3', title: 'child1', className: 'root1Class'}]
20 | },
21 | {
22 | _id: '2',
23 | title: 'root2',
24 | className: 'root2Class'
25 | }
26 | ];
27 |
28 | options: ITreeOptions = {
29 | displayField: 'title',
30 | idField: '_id',
31 | childrenField: 'nodes',
32 | nodeClass: (node) => node.data.className
33 | };
34 | }
35 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/examples/basic-usage/basic-usage.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-basic-usage',
5 | templateUrl: './basic-usage.component.html',
6 | styleUrls: ['./basic-usage.component.scss']
7 | })
8 | export class BasicUsageComponent implements OnInit {
9 |
10 | nodes = [
11 | {
12 | name: 'root1',
13 | children: [
14 | { name: 'child1' },
15 | { name: 'child2' }
16 | ]
17 | },
18 | {
19 | name: 'root2',
20 | children: [
21 | { name: 'child2.1', children: [] },
22 | { name: 'child2.2', children: [
23 | {name: 'grandchild2.2.1'}
24 | ] }
25 | ]
26 | },
27 | { name: 'root3' },
28 | { name: 'root4', children: [] },
29 | { name: 'root5', children: null }
30 | ];
31 |
32 | constructor() { }
33 |
34 | ngOnInit(): void {
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/fundamentals/issues/issues.component.html:
--------------------------------------------------------------------------------
1 | Common Issues
2 |
3 | Tree not rendered
4 | Case: when tree is hidden (for example inside tab or modal), it is not rendered when it becomes visible.
5 | Solution: after it becomes visible (preferably using setTimeout) - call tree.sizeChanged(), which recalculates the rendered nodes according to the actual viewport size.
6 |
7 | Tree state (expanded / selected nodes) gets lost
8 | Maybe you are not supplying unique IDs to the nodes.
9 | The tree maintains its state by using IDs, and if you don't supply ones the tree will generate random ones automatically. Which means that if you update the data - the state will be lost.
10 |
11 | Scroll Into View doesn't work
12 | See scrollContainer option in Options for more information.
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## I'm submitting a...
2 |
3 |
4 |
5 | [ ] Regression (a behavior that used to work and stopped working in a new release)
6 | [ ] Bug report
7 | [ ] Feature request
8 | [ ] Documentation issue or request
9 |
10 |
11 | ## What is the current behavior?
12 |
13 |
14 |
15 | ## Expected behavior:
16 |
17 |
18 |
19 | ## Minimal reproduction of the problem with instructions:
20 |
21 |
25 |
26 | ## Version of affected browser(s),operating system(s), npm, node and angular-tree-component:
27 |
28 | ## Other information:
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Report a bug or regression in functionality
4 | ---
5 |
6 |
7 |
8 | ## Minimal reproduction of the bug/regression with instructions:
9 |
10 |
11 |
12 |
13 |
14 | ## Expected behavior:
15 |
16 |
17 |
18 | ## Versions of Angular Tree Component, Angular, Node, affected browser(s) and operating system(s):
19 |
20 | ## Other information:
21 |
22 | ## I would be willing to submit a PR to fix this issue
23 |
24 | [ ] Yes (Assistance will be provided if you need help to submit a pull request)
25 | [ ] No
26 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/custom-elements/lazy-custom-element.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ElementRef, Input, OnInit } from '@angular/core';
2 | import { ElementsLoader } from './elements-loader';
3 | import { Logger } from '../shared/logger.service';
4 |
5 | @Component({
6 | selector: 'aio-lazy-ce',
7 | template: '',
8 | })
9 | export class LazyCustomElementComponent implements OnInit {
10 | @Input() selector = '';
11 |
12 | constructor(
13 | private elementRef: ElementRef,
14 | private elementsLoader: ElementsLoader,
15 | private logger: Logger,
16 | ) {}
17 |
18 | ngOnInit() {
19 | if (!this.selector || /[^\w-]/.test(this.selector)) {
20 | this.logger.error(new Error(`Invalid selector for 'aio-lazy-ce': ${this.selector}`));
21 | return;
22 | }
23 |
24 | this.elementRef.nativeElement.innerHTML = `<${this.selector}>${this.selector}>`;
25 | this.elementsLoader.loadCustomElement(this.selector);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/projects/angular-tree-component/src/lib/mobx-angular/mobx-proxy.ts:
--------------------------------------------------------------------------------
1 | import { action as mobxAction } from 'mobx';
2 | import { computed as mobxComputed } from 'mobx';
3 | import { observable as mobxObservable } from 'mobx';
4 |
5 | // Re-export mobx operators to be able to use inside components with AOT:
6 | export function actionInternal(...args) {
7 | return (mobxAction as any)(...args);
8 | }
9 | export const action: typeof mobxAction = Object.assign(
10 | actionInternal,
11 | mobxAction
12 | ) as any;
13 |
14 | function computedInternal(...args) {
15 | return (mobxComputed as any)(...args);
16 | }
17 | export const computed: typeof mobxComputed = Object.assign(
18 | computedInternal,
19 | mobxComputed
20 | ) as any;
21 |
22 | function observableInternal(...args) {
23 | return (mobxObservable as any)(...args);
24 | }
25 |
26 | export const observable: typeof mobxObservable = Object.assign(
27 | observableInternal,
28 | mobxObservable
29 | ) as any;
30 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/guides/expanding-guide/expanding-guide.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-expanding-guide',
5 | templateUrl: './expanding-guide.component.html',
6 | styleUrls: ['./expanding-guide.component.scss']
7 | })
8 | export class ExpandingGuideComponent {
9 |
10 | allNodes = `
11 |
12 |
13 | @Component {
14 | nodes = [...];
15 | @ViewChild('tree') tree;
16 |
17 | ngAfterViewInit() {
18 | this.tree.treeModel.expandAll();
19 | }
20 | }`;
21 |
22 | specific = `
23 |
24 |
25 | @Component {
26 | nodes = [...];
27 | @ViewChild('tree') tree;
28 |
29 | ngAfterViewInit() {
30 | const someNode = this.tree.treeModel.getNodeById('someId');
31 | someNode.expand();
32 |
33 | const firstRoot = this.tree.treeModel.roots[0];
34 | firstRoot.setActiveAndVisible();
35 | }
36 | }`;
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/projects/angular-tree-component/src/lib/components/tree-node-expander.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, ViewEncapsulation } from '@angular/core';
2 | import { TreeNode } from '../models/tree-node.model';
3 |
4 | @Component({
5 | selector: 'tree-node-expander',
6 | encapsulation: ViewEncapsulation.None,
7 | styles: [],
8 | template: `
9 |
10 |
17 |
18 |
19 |
20 |
21 |
22 | `
23 | })
24 | export class TreeNodeExpanderComponent {
25 | @Input() node: TreeNode;
26 | }
27 |
--------------------------------------------------------------------------------
/projects/angular-tree-component/src/lib/components/tree-node-drop-slot.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, ViewEncapsulation } from '@angular/core';
2 | import { TreeNode } from '../models/tree-node.model';
3 |
4 | @Component({
5 | selector: 'TreeNodeDropSlot, tree-node-drop-slot',
6 | encapsulation: ViewEncapsulation.None,
7 | styles: [],
8 | template: `
9 |
14 |
15 | `
16 | })
17 | export class TreeNodeDropSlot {
18 | @Input() node: TreeNode;
19 | @Input() dropIndex: number;
20 |
21 | onDrop($event) {
22 | this.node.mouseAction('drop', $event.event, {
23 | from: $event.element,
24 | to: { parent: this.node, index: this.dropIndex }
25 | });
26 | }
27 |
28 | allowDrop(element, $event) {
29 | return this.node.options.allowDrop(element, { parent: this.node, index: this.dropIndex }, $event);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/projects/docs-app/src/styles/_scrollbar.scss:
--------------------------------------------------------------------------------
1 | body::-webkit-scrollbar, mat-sidenav.sidenav::-webkit-scrollbar, .mat-sidenav-content::-webkit-scrollbar {
2 | height: 6px;
3 | width: 6px;
4 | }
5 |
6 | body::-webkit-scrollbar-track, mat-sidenav.sidenav::-webkit-scrollbar-track, .mat-sidenav-content::-webkit-scrollbar-track {
7 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
8 | }
9 |
10 | body::-webkit-scrollbar-thumb, mat-sidenav.sidenav::-webkit-scrollbar-thumb, .mat-sidenav-content::-webkit-scrollbar-thumb {
11 | background-color: $mediumgray;
12 | outline: 1px solid $darkgray;
13 | }
14 |
15 | .search-results::-webkit-scrollbar, .toc-container::-webkit-scrollbar {
16 | height: 4px;
17 | width: 4px;
18 | }
19 |
20 | .search-results::-webkit-scrollbar-track, .toc-container::-webkit-scrollbar-track {
21 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
22 | }
23 |
24 | .search-results::-webkit-scrollbar-thumb, .toc-container::-webkit-scrollbar-thumb {
25 | background-color: $mediumgray;
26 | outline: 1px solid slategrey;
27 | }
28 |
--------------------------------------------------------------------------------
/projects/example-app/src/app/virtualscroll/virtualscroll.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 | import { TreeNode, TreeModel, ITreeOptions } from 'angular-tree-component';
3 |
4 | @Component({
5 | selector: 'app-virtualscroll',
6 | styles: [
7 | ],
8 | template: `
9 |
10 |
11 |
17 |
18 |
19 | `
20 | })
21 | export class VirtualscrollComponent {
22 | nodes: any[];
23 |
24 | options: ITreeOptions = {
25 | nodeHeight: 23,
26 | useVirtualScroll: true
27 | };
28 |
29 | constructor() {
30 | this.nodes = new Array(1000).fill(null).map((item, i) => ({
31 | id: `${i}`,
32 | name: `rootDynamic${i}`,
33 | children: new Array(100).fill(null).map((item, n) => ({
34 | id: `${i}.${n}`,
35 | name: `rootChildDynamic${i}.${n}`
36 | }))
37 | }));
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/projects/example-app/src/app/scrollcontainer/scrollcontainer.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, OnInit } from '@angular/core';
2 | import { TreeNode, TreeModel, TREE_ACTIONS, KEYS, IActionMapping, ITreeOptions } from 'angular-tree-component';
3 |
4 | @Component({
5 | selector: 'app-scrollcontainer',
6 | styles: [
7 | ],
8 | template: `
9 | Padding
10 |
11 |
17 |
18 | `
19 | })
20 | export class ScrollContainerComponent implements OnInit {
21 | nodes: any[] = [];
22 | options: ITreeOptions = {
23 | scrollContainer: document.body.parentElement
24 | };
25 | constructor() {
26 | }
27 | ngOnInit() {
28 | for (let i = 0; i < 200; i++) {
29 | this.nodes.push({
30 | name: `rootDynamic${i}`,
31 | subTitle: `root created dynamically ${i}`
32 | });
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "module": "es2020",
5 | "moduleResolution": "node",
6 | "target": "es2015",
7 | "outDir": "/dist/out-tsc",
8 | "allowSyntheticDefaultImports": true,
9 | "emitDecoratorMetadata": true,
10 | "experimentalDecorators": true,
11 | "inlineSourceMap": true,
12 | "inlineSources": true,
13 | "declaration": true,
14 | "skipLibCheck": true,
15 | "stripInternal": true,
16 | "typeRoots": ["node_modules/@types"],
17 | "lib": [
18 | "es2017",
19 | "dom"
20 | ],
21 | "paths": {
22 | "angular-tree-component": [
23 | "dist/angular-tree-component",
24 | "dist/angular-tree-component"
25 | ],
26 | "angular-tree-component/*": [
27 | "dist/angular-tree-component/*",
28 | "dist/angular-tree-component/*"
29 | ]
30 | }
31 | },
32 | "compileOnSave": false,
33 | "buildOnSave": false,
34 | "angularCompilerOptions": {
35 | "skipTemplateCodegen": true,
36 | "enableIvy": false
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/custom-elements/custom-elements.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { ROUTES} from '@angular/router';
3 | import { ElementsLoader } from './elements-loader';
4 | import {
5 | ELEMENT_MODULE_LOAD_CALLBACKS,
6 | ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES,
7 | ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN
8 | } from './element-registry';
9 | import { LazyCustomElementComponent } from './lazy-custom-element.component';
10 |
11 | @NgModule({
12 | declarations: [ LazyCustomElementComponent ],
13 | exports: [ LazyCustomElementComponent ],
14 | providers: [
15 | ElementsLoader,
16 | { provide: ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN, useValue: ELEMENT_MODULE_LOAD_CALLBACKS },
17 |
18 | // Providing these routes as a signal to the build system that these modules should be
19 | // registered as lazy-loadable.
20 | // TODO(andrewjs): Provide first-class support for providing this.
21 | // { provide: ROUTES, useValue: ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES, multi: true },
22 | ],
23 | })
24 | export class CustomElementsModule { }
25 |
--------------------------------------------------------------------------------
/projects/docs-app/src/assets/github-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
14 |
15 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, waitForAsync } from '@angular/core/testing';
2 | import { AppComponent } from './app.component';
3 |
4 | describe('AppComponent', () => {
5 | beforeEach(waitForAsync(() => {
6 | TestBed.configureTestingModule({
7 | declarations: [AppComponent]
8 | }).compileComponents();
9 | }));
10 |
11 | it('should create the app', () => {
12 | const fixture = TestBed.createComponent(AppComponent);
13 | const app = fixture.componentInstance;
14 | expect(app).toBeTruthy();
15 | });
16 |
17 | it(`should have as title 'docs-app'`, () => {
18 | const fixture = TestBed.createComponent(AppComponent);
19 | const app = fixture.componentInstance;
20 | expect(app.title).toEqual('docs-app');
21 | });
22 |
23 | it('should render title', () => {
24 | const fixture = TestBed.createComponent(AppComponent);
25 | fixture.detectChanges();
26 | const compiled = fixture.nativeElement;
27 | expect(compiled.querySelector('.content span').textContent).toContain(
28 | 'docs-app app is running!'
29 | );
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/examples/crud-example/crud-example.component.html:
--------------------------------------------------------------------------------
1 | Tree with create, copy and delete options
2 |
3 | Working tree
4 | Source Code
5 |
8 |
9 | How to implement
10 |
11 |
12 | This example is based on Issue 813 .
13 | It relies on the treeNodeTemplate to customize the display of each node.
14 | The example uses simple buttons to handle basic functions for creating, copying and deleting nodes.
15 |
16 |
17 | The example does not show a edit function because of the endless possibilities on how to implement this.
18 | One option could be to show an input field instead of the span with the name of the node.
19 | This could be triggered via button, double click or how your app enters edit mode.
20 |
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Circlon Group
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/guides/async-guide/async-guide.component.html:
--------------------------------------------------------------------------------
1 | Async Data
2 |
3 |
4 | The tree allows to load children asynchronously using getChildren option, and hasChildren field on the node.
5 |
6 |
7 | Demo
8 |
9 | Source Code
10 |
11 |
14 |
15 | 'getChildren' option
16 |
17 | This options receives a function that has a TreeNode parameter, and returns a value or a promise that resolves to the node's children:
18 |
19 | (node:TreeNode) => TreeNode[] | Promise<TreeNode[]>
20 |
21 |
22 | The function will be called whenever a node is expanded, the hasChildren field is true, and the children field is empty.
23 | The result will be loaded into the node's children attribute.
24 |
25 |
26 | Example
27 |
28 | {{ javascript }}
29 |
--------------------------------------------------------------------------------
/projects/docs-app/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function(config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client: {
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | dir: require('path').join(__dirname, '../../coverage/docs-app'),
20 | reports: ['html', 'lcovonly', 'text-summary'],
21 | fixWebpackSourcePaths: true
22 | },
23 | reporters: ['progress', 'kjhtml'],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | autoWatch: true,
28 | browsers: ['Chrome'],
29 | singleRun: false,
30 | restartOnFileChange: true
31 | });
32 | };
33 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/fundamentals/templates/templates-demo/templates-demo.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { ITreeOptions } from 'angular-tree-component';
3 |
4 | @Component({
5 | selector: 'app-templates-demo',
6 | templateUrl: './templates-demo.component.html',
7 | styleUrls: ['./templates-demo.component.scss']
8 | })
9 | export class TemplatesDemoComponent {
10 |
11 | nodes1 = [
12 | {
13 | title: 'root1',
14 | className: 'root1Class'
15 | },
16 | {
17 | title: 'root2',
18 | className: 'root2Class',
19 | hasChildren: true
20 | }
21 | ];
22 |
23 | nodes2 = [
24 | {
25 | title: 'root1',
26 | className: 'root1Class'
27 | },
28 | {
29 | title: 'root2',
30 | className: 'root2Class',
31 | children: [
32 | { title: 'child1', className: 'child1Class' }
33 | ]
34 | }
35 | ];
36 |
37 | options1: ITreeOptions = {
38 | getChildren: () => new Promise((resolve, reject) => { })
39 | };
40 |
41 | options0: ITreeOptions = {
42 | displayField: 'title',
43 | nodeClass: (node) => `${node.data.title}Class`
44 | };
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # Compiled binary addons (http://nodejs.org/api/addons.html)
20 | build/Release
21 |
22 | # Users Environment Variables
23 | .lock-wscript
24 |
25 | # OS generated files #
26 | .DS_Store
27 | ehthumbs.db
28 | Icon?
29 | Thumbs.db
30 |
31 | # Node Files #
32 | /node_modules
33 | /bower_components
34 | /projects/**/node_modules
35 | npm-debug.log
36 |
37 | # Coverage #
38 | /coverage/
39 | /xunit/
40 |
41 | # Typing #
42 | /src/typings/tsd/
43 | /typings/
44 | /tsd_typings/
45 |
46 | # Dist #
47 | /public/__build__/
48 | /src/*/__build__/
49 | /__build__/**
50 | /public/dist/
51 | /src/*/dist/
52 | .webpack.json
53 | /projects/**/package-lock.json
54 |
55 | # Doc #
56 |
57 | # IDE #
58 | .vscode/*
59 | .idea/
60 | *.swp
61 |
62 | /dist
63 |
64 | compiled
65 |
66 | testScreenshots
67 | testResults
68 | e2eResults
69 |
70 | e2e/dist
71 |
--------------------------------------------------------------------------------
/projects/angular-tree-component/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client: {
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | dir: require('path').join(__dirname, '../../coverage/angular-tree-component'),
20 | reports: ['html', 'lcovonly', 'text-summary'],
21 | fixWebpackSourcePaths: true
22 | },
23 | reporters: ['progress', 'kjhtml'],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | autoWatch: true,
28 | browsers: ['Chrome'],
29 | singleRun: false,
30 | restartOnFileChange: true
31 | });
32 | };
33 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/guides/update-guide/update-guide.component.html:
--------------------------------------------------------------------------------
1 | Updating the tree
2 |
3 | Changing node attributes
4 | Changing any attributes on the node itself will be reflected immediately, since change detection will force the tree node components to re-render.
5 |
6 | Adding / Removing nodes
7 | After adding or removing nodes from the tree, one must call the update method on the treeModel for it to take affect.
8 | For example:
9 | {{ adding }}
10 | This is due to the fact that the treeModel builds its own model around the nodes data, to calculate node levels, paths, parent links etc. So it must be informed of any change to the nodes' structure.
11 | Calling update will have no effect on the tree status, i.e. current selected node, current focused node, current expanded nodes, etc.
12 |
13 | Working with immutable data
14 |
15 | Changing the reference to nodes will also trigger an update automatically.
16 | So if you work for example with immutable data and replace the nodes array instead of mutating it - there is no need to call any method.
17 |
18 | {{ immutable }}
19 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/guides/async-guide/async/async.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { ITreeOptions } from 'angular-tree-component';
3 |
4 | @Component({
5 | selector: 'app-async',
6 | templateUrl: './async.component.html',
7 | styleUrls: ['./async.component.scss']
8 | })
9 | export class AsyncComponent {
10 |
11 | options: ITreeOptions = {
12 | getChildren: this.getChildren.bind(this),
13 | useCheckbox: true
14 | };
15 |
16 | nodes: any[] = [];
17 |
18 | asyncChildren = [
19 | {
20 | name: 'child1',
21 | hasChildren: true
22 | }, {
23 | name: 'child2'
24 | }
25 | ];
26 |
27 | constructor() {
28 | this.nodes = [
29 | {
30 | name: 'root1',
31 | children: [
32 | { name: 'child1' }
33 | ]
34 | },
35 | {
36 | name: 'root2',
37 | hasChildren: true
38 | },
39 | {
40 | name: 'root3'
41 | }
42 | ];
43 | }
44 |
45 | getChildren(node: any) {
46 | const newNodes = this.asyncChildren.map((c) => Object.assign({}, c));
47 |
48 | return new Promise((resolve, reject) => {
49 | setTimeout(() => resolve(newNodes), 1000);
50 | });
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## PR Checklist
2 | Please check if your PR fulfills the following requirements:
3 |
4 | - [ ] The commit message follows our guidelines: https://github.com/CirclonGroup/angular-tree-component/blob/master/CONTRIBUTING.md#commit-message-guidelines
5 | - [ ] Tests for the changes have been added (for bug fixes / features)
6 |
7 | ## PR Type
8 | What kind of change does this PR introduce?
9 |
10 |
11 |
12 | ```
13 | [ ] Bugfix
14 | [ ] Feature
15 | [ ] Code style update (formatting, local variables)
16 | [ ] Refactoring (no functional changes, no api changes)
17 | [ ] Build related changes
18 | [ ] CI related changes
19 | [ ] Documentation content changes
20 | [ ] Other... Please describe:
21 | ```
22 |
23 | ## What is the current behavior?
24 |
25 |
26 | Closes #
27 |
28 | ## What is the new behavior?
29 |
30 | ## Does this PR introduce a breaking change?
31 |
32 | ```
33 | [ ] Yes
34 | [ ] No
35 | ```
36 |
37 |
38 |
39 | ## Other information
40 |
--------------------------------------------------------------------------------
/THIRD_PARTY_LICENSES:
--------------------------------------------------------------------------------
1 | * https://github.com/ayamflow/virtual-scroll/
2 | // The MIT License (MIT)
3 |
4 | // Copyright (c) 2014 Florian Morel
5 |
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 |
13 | // The above copyright notice and this permission notice shall be included in all
14 | // copies or substantial portions of the Software.
15 |
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | // SOFTWARE.
23 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/examples/examples.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { BasicUsageComponent } from './basic-usage/basic-usage.component';
4 | import { TreeModule } from 'angular-tree-component';
5 | import { BasicTreeComponent } from './basic-usage/basic-tree/basic-tree.component';
6 | import { RouterModule } from '@angular/router';
7 | import { ColumnsExampleComponent } from './columns-example/columns-example.component';
8 | import { ColumnsComponent } from './columns-example/columns/columns.component';
9 | import { CrudExampleComponent } from './crud-example/crud-example.component';
10 | import { CrudComponent } from './crud-example/crud/crud.component';
11 | import { LoadMoreExampleComponent } from './load-more-example/load-more-example.component';
12 | import { LoadMoreComponent } from './load-more-example/load-more/load-more.component';
13 |
14 | @NgModule({
15 | declarations: [BasicUsageComponent, BasicTreeComponent, ColumnsExampleComponent, ColumnsComponent, CrudExampleComponent, CrudComponent, LoadMoreExampleComponent, LoadMoreComponent],
16 | imports: [
17 | CommonModule,
18 | TreeModule,
19 | RouterModule,
20 | ]
21 | })
22 | export class ExamplesModule { }
23 |
--------------------------------------------------------------------------------
/projects/example-app/src/app/async/async.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { ITreeOptions, TreeNode} from 'angular-tree-component';
3 |
4 | @Component({
5 | selector: 'app-async',
6 | template: `
7 |
8 | `,
9 | styles: []
10 | })
11 | export class AsyncTreeComponent {
12 | options: ITreeOptions = {
13 | getChildren: this.getChildren.bind(this),
14 | useCheckbox: true
15 | };
16 |
17 | nodes: any[] = [];
18 |
19 | asyncChildren = [
20 | {
21 | name: 'child1',
22 | hasChildren: true
23 | }, {
24 | name: 'child2'
25 | }
26 | ];
27 |
28 | constructor() {
29 | this.nodes = [
30 | {
31 | name: 'root1',
32 | children: [
33 | { name: 'child1' }
34 | ]
35 | },
36 | {
37 | name: 'root2',
38 | hasChildren: true
39 | },
40 | {
41 | name: 'root3'
42 | }
43 | ];
44 | }
45 |
46 | getChildren(node: any) {
47 | const newNodes = this.asyncChildren.map((c) => Object.assign({}, c));
48 |
49 | return new Promise((resolve, reject) => {
50 | setTimeout(() => resolve(newNodes), 1000);
51 | });
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/guides/auto-scroll-guide/auto-scroll-guide.component.html:
--------------------------------------------------------------------------------
1 | Auto Scrolling
2 |
3 | When navigating with keys or clicking a node in the edges of the screen, the tree will automatically try to scroll the selected node into view.
4 | Also, you can call scrollIntoView on any node to make it visible.
5 |
6 | Inner Scroll
7 | Auto scrolling is built-in to the tree. Simply wrap the tree with a container that has a height and you're good.
8 | The tree has an inner element called viewport that is scrollable.
9 |
10 | External Scroll (Scrolling as part of the body / other container)
11 | Sometimes you don't want to wrap the tree with a scrollable container, and want the scrolling to be part of a different element.
12 | Use the scrollContainer option for this purpose as described in Options
13 |
14 | Demo of scrollContainer
15 | Source Code
16 |
19 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/layout/nav-item/nav-item.component.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
14 |
15 |
21 |
22 |
27 |
28 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Javascript Node CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details
4 | #
5 | version: 2
6 | jobs:
7 | build:
8 | docker:
9 | # specify the version you desire here
10 | - image: circleci/node:8.9-browsers
11 |
12 | # Specify service dependencies here if necessary
13 | # CircleCI maintains a library of pre-built images
14 | # documented at https://circleci.com/docs/2.0/circleci-images/
15 | # - image: circleci/mongo:3.4.4
16 |
17 | working_directory: ~/repo
18 |
19 | steps:
20 | - checkout
21 |
22 | # Download and cache dependencies
23 | - restore_cache:
24 | keys:
25 | - v1-dependencies-{{ checksum "package.json" }}
26 | # fallback to using the latest cache if no exact match is found
27 | - v1-dependencies-
28 |
29 | - run: npm install
30 | - run: npm run test:setup
31 |
32 | - save_cache:
33 | paths:
34 | - node_modules
35 | key: v1-dependencies-{{ checksum "package.json" }}
36 |
37 | # run tests!
38 | # - run: npm run test
39 | - run: npm run test:ci:local
40 |
41 | - store_test_results:
42 | path: /tmp/test-results
43 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/fundamentals/state-binding/state-binding-demo/state-binding-demo.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { ITreeState } from 'angular-tree-component';
3 |
4 | @Component({
5 | selector: 'app-state-binding-demo',
6 | templateUrl: './state-binding-demo.component.html',
7 | styleUrls: ['./state-binding-demo.component.scss']
8 | })
9 | export class StateBindingDemoComponent {
10 |
11 | get state(): ITreeState {
12 | return localStorage.treeState && JSON.parse(localStorage.treeState);
13 | }
14 | set state(state: ITreeState) {
15 | localStorage.treeState = JSON.stringify(state);
16 | }
17 |
18 | options = {
19 | getChildren: () => new Promise((resolve) => {
20 | setTimeout(() => resolve([
21 | { id: 5, name: 'child2.1', children: [] },
22 | { id: 6, name: 'child2.2', children: [
23 | { id: 7, name: 'grandchild2.2.1' }
24 | ] }
25 | ]), 2000);
26 | })
27 | };
28 |
29 | nodes = [
30 | {
31 | id: 1,
32 | name: 'root1',
33 | children: [
34 | { id: 2, name: 'child1' },
35 | { id: 3, name: 'child2' }
36 | ]
37 | },
38 | {
39 | id: 4,
40 | name: 'root2',
41 | hasChildren: true
42 | }
43 | ];
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/projects/angular-tree-component/src/lib/mobx-angular/tree-mobx-autorun.directive.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Directive,
3 | ViewContainerRef,
4 | TemplateRef,
5 | OnInit,
6 | OnDestroy,
7 | Input,
8 | EmbeddedViewRef
9 | } from '@angular/core';
10 | import { autorun } from 'mobx';
11 |
12 | @Directive({ selector: '[treeMobxAutorun]' })
13 | export class TreeMobxAutorunDirective implements OnInit, OnDestroy {
14 | protected templateBindings = {};
15 | protected dispose: any;
16 | protected view: EmbeddedViewRef;
17 | @Input() treeMobxAutorun;
18 |
19 | constructor(
20 | protected templateRef: TemplateRef,
21 | protected viewContainer: ViewContainerRef
22 | ) {}
23 |
24 | ngOnInit() {
25 | this.view = this.viewContainer.createEmbeddedView(this.templateRef);
26 |
27 | if (this.dispose) {
28 | this.dispose();
29 | }
30 |
31 | if (this.shouldDetach()) {
32 | this.view.detach();
33 | }
34 | this.autoDetect(this.view);
35 | }
36 |
37 | shouldDetach() {
38 | return this.treeMobxAutorun && this.treeMobxAutorun.detach;
39 | }
40 |
41 | autoDetect(view: EmbeddedViewRef) {
42 | this.dispose = autorun(() => view.detectChanges());
43 | }
44 |
45 | ngOnDestroy() {
46 | if (this.dispose) {
47 | this.dispose();
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/projects/docs-app/src/styles/_alert.scss:
--------------------------------------------------------------------------------
1 | .alert {
2 | padding: 16px;
3 | margin: 24px 0px;
4 | @include font-size(14);
5 | color: $darkgray;
6 | width: 100%;
7 | box-sizing: border-box;
8 | clear: both;
9 |
10 | h1, h2, h3, h4, h5, h6 {
11 | font-weight: 500;
12 | }
13 |
14 | &.is-critical {
15 | border-left: 8px solid $brightred;
16 | background-color: rgba($brightred, 0.05);
17 |
18 | h1, h2, h3, h4, h5, h6 {
19 | color: $brightred;
20 | }
21 | }
22 |
23 | &.is-important {
24 | border-left: 8px solid $orange;
25 | background-color: rgba($orange, 0.05);
26 |
27 | h1, h2, h3, h4, h5, h6 {
28 | color: $orange;
29 | }
30 | }
31 |
32 | &.is-helpful {
33 | border-left: 8px solid $blue;
34 | background-color: rgba($blue, 0.05);
35 |
36 | h1, h2, h3, h4, h5, h6 {
37 | color: $blue;
38 | }
39 | }
40 |
41 | &.archive-warning {
42 | background-color: $darkred;
43 | border-radius: 4px;
44 | margin-bottom: 1rem;
45 |
46 | * {
47 | color: $white;
48 | }
49 |
50 | a {
51 | color: $white;
52 | font-weight: bold;
53 | text-decoration: underline;
54 |
55 | &:hover {
56 | opacity: 0.9;
57 | }
58 | }
59 | }
60 |
61 | > * {
62 | margin: 8px 16px;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/guides/large-tree-guide/large-tree-guide.component.html:
--------------------------------------------------------------------------------
1 | Large Trees & Virtual Scroll
2 |
3 | When having a large amount of nodes and experiencing performance issues, it is recommended to use the virtual scroll option.
4 | To use this option, one must supply the height of the container, and the height of each node in the tree.
5 | You can also specify height for the dropSlot which is located between nodes.
6 | Example:
7 |
8 | {{ html }}
9 | {{ javascript }}
10 |
11 | Hidden trees
12 |
13 | If the tree is hidden (for example inside tab or modal), it will not be rendered when it becomes visible.
14 | After it becomes visible (preferably using setTimeout) - you need to call tree.sizeChanged(), which recalculates the rendered nodes according to the actual viewport size.
15 |
16 |
17 | Demo
18 | Initializing 100,000 nodes, please be patient...
19 | Source Code
20 |
23 |
--------------------------------------------------------------------------------
/e2e/drag.testcafe.js:
--------------------------------------------------------------------------------
1 | const { TreeDriver } = require('./helpers/tree.driver');
2 |
3 | fixture `Drag and Drop`
4 | .page `http://localhost:4200/#/drag`
5 | .beforeEach( async t => {
6 | t.ctx.tree = new TreeDriver('tree-root');
7 | t.ctx.root1 = t.ctx.tree.getNode('root1');
8 | t.ctx.child1 = t.ctx.root1.getNode('child1');
9 | t.ctx.root2 = t.ctx.tree.getNode('root2');
10 | t.ctx.child21 = t.ctx.root2.getNode('child2.1');
11 | });
12 |
13 | test('should show the tree', async t => {
14 | await t.expect(t.ctx.tree.isPresent()).ok();
15 | });
16 |
17 | test('should have expected children', async t => {
18 | await t.expect(t.ctx.root1.getNodes().count).eql(2)
19 | .expect(t.ctx.root2.getNodes().count).eql(2)
20 | .expect(t.ctx.child21.getNodes().count).eql(0);
21 | });
22 |
23 | test('should allow to drag leaf', async t => {
24 | await t.ctx.child1.dragToNode(t, t.ctx.child21);
25 | await t.ctx.child21.clickExpander(t)
26 | .expect(t.ctx.root1.getNodes().count).eql(1)
27 | .expect(t.ctx.child21.getNodes().count).eql(1);
28 | });
29 |
30 | // TODO: find out why fails on saucelabs
31 | test.skip('should allow to drag to drop slot', async t => {
32 | await t.ctx.child1.dragToDropSlot(t, t.ctx.child21)
33 | .expect(t.ctx.root1.getNodes().count).eql(1)
34 | .expect(t.ctx.root2.getNodes().count).eql(3);
35 | });
36 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/guides/drag-drop-guide/drag-drop/drag-drop.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { ITreeOptions, ITreeState } from 'angular-tree-component';
3 | import { v4 } from 'uuid';
4 |
5 | @Component({
6 | selector: 'app-drag-drop',
7 | templateUrl: './drag-drop.component.html',
8 | styleUrls: ['./drag-drop.component.scss']
9 | })
10 | export class DragDropComponent {
11 |
12 | state: ITreeState = {
13 | expandedNodeIds: {
14 | 1: true,
15 | 2: true
16 | },
17 | hiddenNodeIds: {},
18 | activeNodeIds: {}
19 | };
20 |
21 | options: ITreeOptions = {
22 | allowDrag: (node) => node.isLeaf,
23 | getNodeClone: (node) => ({
24 | ...node.data,
25 | id: v4(),
26 | name: `copy of ${node.data.name}`
27 | })
28 | };
29 |
30 | nodes = [
31 | {
32 | id: 1,
33 | name: 'root1',
34 | children: [
35 | { name: 'child1' },
36 | { name: 'child2' }
37 | ]
38 | },
39 | {
40 | name: 'root2',
41 | id: 2,
42 | children: [
43 | { name: 'child2.1', children: [] },
44 | { name: 'child2.2', children: [
45 | {name: 'grandchild2.2.1'}
46 | ] }
47 | ]
48 | },
49 | { name: 'root3' },
50 | { name: 'root4', children: [] },
51 | { name: 'root5', children: null }
52 | ];
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/e2e/dragover-styling.testcafe.js:
--------------------------------------------------------------------------------
1 | const { TreeDriver } = require('./helpers/tree.driver');
2 |
3 | fixture `Drag and Drop Styling`
4 | .page `http://localhost:4200/#/dragover-styling`
5 | .beforeEach( async t => {
6 | t.ctx.tree = new TreeDriver('tree-root');
7 | t.ctx.root1 = t.ctx.tree.getNode('root1');
8 | t.ctx.child1 = t.ctx.root1.getNode('child1');
9 | t.ctx.root2 = t.ctx.tree.getNode('root2');
10 | t.ctx.child21 = t.ctx.root2.getNode('child2.1');
11 | });
12 |
13 | test('should show the tree', async t => {
14 | await t.expect(t.ctx.tree.isPresent()).ok();
15 | });
16 |
17 | test('should have expected children', async t => {
18 | await t.expect(t.ctx.root1.getNodes().count).eql(2)
19 | .expect(t.ctx.root2.getNodes().count).eql(2)
20 | .expect(t.ctx.child21.getNodes().count).eql(0);
21 | });
22 |
23 | test('should allow to drag leaf', async t => {
24 | await t.ctx.child1.dragToNode(t, t.ctx.child21);
25 | await t.ctx.child21.clickExpander(t)
26 | .expect(t.ctx.root1.getNodes().count).eql(1)
27 | .expect(t.ctx.child21.getNodes().count).eql(1);
28 | });
29 |
30 | // TODO: find out why fails on saucelabs
31 | test.skip('should allow to drag to drop slot', async t => {
32 | await t.ctx.child1.dragToDropSlot(t, t.ctx.child21)
33 | .expect(t.ctx.root1.getNodes().count).eql(1)
34 | .expect(t.ctx.root2.getNodes().count).eql(3);
35 | });
36 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/guides/checkboxes-guide/checkboxes/checkboxes.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { ITreeOptions } from 'angular-tree-component';
3 |
4 | @Component({
5 | selector: 'app-checkboxes',
6 | templateUrl: './checkboxes.component.html',
7 | styleUrls: ['./checkboxes.component.scss']
8 | })
9 | export class CheckboxesComponent {
10 |
11 | nodes = [
12 | {
13 | name: 'root1',
14 | },
15 | {
16 | name: 'root2',
17 | children: [
18 | { name: 'child1' },
19 | { name: 'child2', children: [
20 | { name: 'grandchild1' },
21 | { name: 'grandchild2' }
22 | ] }
23 | ]
24 | },
25 | {
26 | name: 'asyncroot',
27 | hasChildren: true
28 | }
29 | ];
30 |
31 | options: ITreeOptions = {
32 | useCheckbox: true,
33 | getChildren: this.getChildren.bind(this)
34 | };
35 |
36 | optionsDisabled: ITreeOptions = {
37 | useCheckbox: true,
38 | getChildren: this.getChildren.bind(this),
39 | useTriState: false
40 | };
41 |
42 | getChildren(node: any) {
43 | const newNodes = [
44 | {
45 | name: 'child1'
46 | }, {
47 | name: 'child2'
48 | }
49 | ];
50 |
51 | return new Promise((resolve, reject) => {
52 | setTimeout(() => resolve(newNodes), 1000);
53 | });
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/projects/example-app/src/app/save-restore/save-restore.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { ITreeState } from 'angular-tree-component';
3 |
4 | const getChildren = () => new Promise((resolve) => {
5 | setTimeout(() => resolve([
6 | { id: 5, name: 'child2.1', children: [] },
7 | { id: 6, name: 'child2.2', children: [
8 | { id: 7, name: 'grandchild2.2.1' }
9 | ] }
10 | ]), 2000);
11 | });
12 |
13 | @Component({
14 | selector: 'app-saverestore',
15 | template: `
16 |
17 |
18 | `,
19 | styles: []
20 | })
21 | export class SaveRestoreComponent {
22 | get state(): ITreeState {
23 | return localStorage.treeState && JSON.parse(localStorage.treeState);
24 | }
25 | set state(state: ITreeState) {
26 | localStorage.treeState = JSON.stringify(state);
27 | }
28 |
29 | options = {
30 | getChildren
31 | };
32 |
33 | nodes = [
34 | {
35 | id: 1,
36 | name: 'root1',
37 | children: [
38 | { id: 2, name: 'child1' },
39 | { id: 3, name: 'child2' }
40 | ]
41 | },
42 | {
43 | id: 4,
44 | name: 'root2',
45 | hasChildren: true
46 | }
47 | ];
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | menu
4 |
5 |
6 | Angular Tree Component
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
30 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/fundamentals/api/api-demo/api-demo.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { ITreeOptions } from 'angular-tree-component';
3 |
4 | @Component({
5 | selector: 'app-api-demo',
6 | templateUrl: './api-demo.component.html',
7 | styleUrls: ['./api-demo.component.scss']
8 | })
9 | export class ApiDemoComponent {
10 |
11 | options: ITreeOptions = {
12 |
13 | };
14 | nodes = [
15 | {
16 | name: 'root1',
17 | children: [
18 | {
19 | name: 'child1'
20 | }, {
21 | name: 'child2'
22 | }
23 | ]
24 | },
25 | {
26 | name: 'root2',
27 | children: [
28 | {
29 | name: 'child2.1'
30 | }, {
31 | name: 'child2.2',
32 | children: [
33 | {
34 | id: 1001,
35 | name: 'subsub'
36 | }
37 | ]
38 | }
39 | ]
40 | }
41 | ];
42 |
43 | addNode(tree: any) {
44 | this.nodes[0].children.push({
45 | name: 'a new child'
46 | });
47 | tree.treeModel.update();
48 | }
49 |
50 | activateSubSub(tree: any) {
51 | // tree.treeModel.getNodeBy((node) => node.data.name === 'subsub')
52 | tree.treeModel.getNodeById(1001)
53 | .setActiveAndVisible();
54 | }
55 |
56 | activeNodes(treeModel: any) {
57 | console.log(treeModel.activeNodes);
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/projects/example-app/src/app/drag/drag.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { ITreeState, ITreeOptions } from 'angular-tree-component';
3 | import { v4 } from 'uuid';
4 |
5 | @Component({
6 | selector: 'app-drag',
7 | template: `
8 | Allowing to drag only leaf nodes; ctrl-drag to copy
9 |
10 | `,
11 | styles: []
12 | })
13 | export class DragComponent {
14 | state: ITreeState = {
15 | expandedNodeIds: {
16 | 1: true,
17 | 2: true
18 | },
19 | hiddenNodeIds: {},
20 | activeNodeIds: {}
21 | };
22 |
23 | options: ITreeOptions = {
24 | allowDrag: (node) => node.isLeaf,
25 | getNodeClone: (node) => ({
26 | ...node.data,
27 | id: v4(),
28 | name: `copy of ${node.data.name}`
29 | })
30 | };
31 |
32 | nodes = [
33 | {
34 | id: 1,
35 | name: 'root1',
36 | children: [
37 | { name: 'child1' },
38 | { name: 'child2' }
39 | ]
40 | },
41 | {
42 | name: 'root2',
43 | id: 2,
44 | children: [
45 | { name: 'child2.1', children: [] },
46 | { name: 'child2.2', children: [
47 | {name: 'grandchild2.2.1'}
48 | ] }
49 | ]
50 | },
51 | { name: 'root3' },
52 | { name: 'root4', children: [] },
53 | { name: 'root5', children: null }
54 | ];
55 | }
56 |
--------------------------------------------------------------------------------
/projects/angular-tree-component/src/lib/components/tree-node-children.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, ViewEncapsulation } from '@angular/core';
2 | import { TreeNode } from '../models/tree-node.model';
3 |
4 | @Component({
5 | selector: 'tree-node-children',
6 | encapsulation: ViewEncapsulation.None,
7 | styles: [],
8 | template: `
9 |
10 |
20 |
26 |
27 |
34 |
35 |
36 | `
37 | })
38 | export class TreeNodeChildrenComponent {
39 | @Input() node: TreeNode;
40 | @Input() templates: any;
41 | }
42 |
--------------------------------------------------------------------------------
/projects/example-app/src/app/dragover-styling/dragover-styling.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { ITreeState, ITreeOptions } from 'angular-tree-component';
3 | import { v4 } from 'uuid';
4 |
5 | @Component({
6 | selector: 'app-dragover-styling',
7 | template: `
8 | Disabled styling of nodes on dragover
9 |
15 | `,
16 | styles: []
17 | })
18 | export class DragOverStylingComponent {
19 | state: ITreeState = {
20 | expandedNodeIds: {
21 | 1: true,
22 | 2: true
23 | },
24 | hiddenNodeIds: {},
25 | activeNodeIds: {}
26 | };
27 |
28 | options: ITreeOptions = {
29 | allowDrag: node => true,
30 | allowDragoverStyling: false,
31 | getNodeClone: node => ({
32 | ...node.data,
33 | id: v4(),
34 | name: `copy of ${node.data.name}`
35 | })
36 | };
37 |
38 | nodes = [
39 | {
40 | id: 1,
41 | name: 'root1',
42 | children: [{ name: 'child1' }, { name: 'child2' }]
43 | },
44 | {
45 | name: 'root2',
46 | id: 2,
47 | children: [
48 | { name: 'child2.1', children: [] },
49 | { name: 'child2.2', children: [{ name: 'grandchild2.2.1' }] }
50 | ]
51 | },
52 | { name: 'root3' },
53 | { name: 'root4', children: [] },
54 | { name: 'root5', children: null }
55 | ];
56 | }
57 |
--------------------------------------------------------------------------------
/e2e/async.testcafe.js:
--------------------------------------------------------------------------------
1 | import { Selector } from 'testcafe';
2 | import { TreeDriver } from './helpers/tree.driver';
3 |
4 | fixture `Async`
5 | .page `http://localhost:4200/#/async`
6 | .beforeEach( async t => {
7 | t.ctx.tree = new TreeDriver('tree-root');
8 | t.ctx.root2 = t.ctx.tree.getNode('root2');
9 | });
10 |
11 | test('should show the tree', async t => {
12 | await t.expect(t.ctx.tree.isPresent()).ok();
13 | });
14 |
15 | test('should have 3 nodes', async t => {
16 | await t.expect(t.ctx.tree.getNodes().count).eql(3);
17 | });
18 |
19 | test('should not show loading before expanding', async t => {
20 | await t.expect(t.ctx.root2.getLoading().exists).notOk();
21 | });
22 |
23 | // TODO: find out why fails on saucelabs
24 | test.skip('should show loading', async t => {
25 | await t.ctx.root2.clickExpander(t)
26 | .expect(t.ctx.root2.getLoading().exists).ok();
27 | });
28 |
29 | test('should show children and then loading disappears', async t => {
30 | await t.ctx.root2.clickExpander(t)
31 | .expect(t.ctx.root2.getNode('child1').isPresent()).ok()
32 | .expect(t.ctx.root2.getLoading().exists).notOk();
33 | });
34 |
35 | test('should show not show loading the second time we expand the node', async t => {
36 | await t.ctx.root2.clickExpander(t)
37 | .expect(t.ctx.root2.getNode('child1').isPresent()).ok();
38 |
39 | await t.ctx.root2.clickExpander(t);
40 | await t.ctx.root2.clickExpander(t)
41 | .expect(t.ctx.root2.getLoading().exists).notOk();
42 | });
43 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/custom-elements/element-registry.ts:
--------------------------------------------------------------------------------
1 | import { InjectionToken, Type } from '@angular/core';
2 | import { LoadChildrenCallback } from '@angular/router';
3 |
4 | // Modules containing custom elements must be set up as lazy-loaded routes (loadChildren)
5 | // TODO(andrewjs): This is a hack, Angular should have first-class support for preparing a module
6 | // that contains custom elements.
7 | export const ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES = [
8 | {
9 | selector: 'code-example',
10 | loadChildren: () => import('./code/code-example.module').then(m => m.CodeExampleModule)
11 | },
12 | {
13 | selector: 'code-tabs',
14 | loadChildren: () => import('./code/code-tabs.module').then(m => m.CodeTabsModule)
15 | },
16 | ];
17 |
18 | /**
19 | * Interface expected to be implemented by all modules that declare a component that can be used as
20 | * a custom element.
21 | */
22 | export interface WithCustomElementComponent {
23 | customElementComponent: Type;
24 | }
25 |
26 | /** Injection token to provide the element path modules. */
27 | export const ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN = new InjectionToken>('aio/elements-map');
28 |
29 | /** Map of possible custom element selectors to their lazy-loadable module paths. */
30 | export const ELEMENT_MODULE_LOAD_CALLBACKS = new Map();
31 | ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES.forEach(route => {
32 | ELEMENT_MODULE_LOAD_CALLBACKS.set(route.selector, route.loadChildren);
33 | });
34 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/fundamentals/options/options.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-options',
5 | templateUrl: './options.component.html',
6 | styleUrls: ['./options.component.scss']
7 | })
8 | export class OptionsComponent implements OnInit {
9 |
10 | options = `
11 | import { TREE_ACTIONS, KEYS, IActionMapping, ITreeOptions } from '@circlon/angular-tree-component';
12 |
13 | class MyComponent {
14 | ...
15 | options: ITreeOptions = {
16 | displayField: 'nodeName',
17 | isExpandedField: 'expanded',
18 | idField: 'uuid',
19 | hasChildrenField: 'nodes',
20 | actionMapping: {
21 | mouse: {
22 | dblClick: (tree, node, $event) => {
23 | if (node.hasChildren) TREE_ACTIONS.TOGGLE_EXPANDED(tree, node, $event);
24 | }
25 | },
26 | keys: {
27 | [KEYS.ENTER]: (tree, node, $event) => {
28 | node.expandAll();
29 | }
30 | }
31 | },
32 | nodeHeight: 23,
33 | allowDrag: (node) => {
34 | return true;
35 | },
36 | allowDrop: (node) => {
37 | return true;
38 | },
39 | allowDragoverStyling: true,
40 | levelPadding: 10,
41 | useVirtualScroll: true,
42 | animateExpand: true,
43 | scrollOnActivate: true,
44 | animateSpeed: 30,
45 | animateAcceleration: 1.2,
46 | scrollContainer: document.documentElement // HTML
47 | }
48 | }
49 | `;
50 |
51 | constructor() { }
52 |
53 | ngOnInit(): void {
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 | .example-spacer {
2 | flex: 1 1 auto;
3 | }
4 |
5 | .toolbar {
6 | position: fixed;
7 | top: 0;
8 | right: 0;
9 | left: 0;
10 | z-index: 10;
11 | box-shadow: 0 2px 5px 0 rgba(0,0,0,.3);
12 | }
13 |
14 | .toolbar-link {
15 | display: flex;
16 | align-items: center;
17 | padding: 24px;
18 | margin: 0 -16px;
19 | }
20 |
21 | .main-logo {
22 | padding: 0 8px;
23 | }
24 |
25 | .sidenav-container {
26 | min-height: 100%;
27 | height: auto;
28 | max-width: 100%;
29 | margin: 0;
30 | transform: none;
31 |
32 | .sidenav {
33 | position: fixed;
34 | top: 64px;
35 | bottom: 0;
36 | left: 0;
37 | padding: 0;
38 | min-width: 260px;
39 | box-shadow: 6px 0 6px rgba(0,0,0,0.1);
40 |
41 | .sidenav-menu {
42 | :first-child {
43 | margin-top: 16px;
44 | }
45 |
46 | .menu-item {
47 | display: block;
48 | max-width: 260px;
49 | font-weight: 400;
50 | padding-left: 20px;
51 | }
52 | }
53 | }
54 |
55 | .sidenav-content {
56 | height: 100%;
57 |
58 | &.sidenav-open {
59 | margin-left: 278px;
60 | }
61 |
62 | .main-content {
63 | min-height: 100vh;
64 | padding: 80px 3rem 2rem;
65 | }
66 | }
67 | }
68 |
69 | footer {
70 | padding: 48px;
71 | font-weight: 300;
72 | background-color: #3f51b5;
73 | z-index: 0;
74 |
75 | .footer-content {
76 | display: flex;
77 | justify-content: center;
78 | margin: 0 0 40px;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/fundamentals/events/events.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-events',
5 | templateUrl: './events.component.html',
6 | styleUrls: ['./events.component.scss']
7 | })
8 | export class EventsComponent {
9 | event = `
10 |
15 |
16 |
17 | onEvent = ($event) => console.log($event);
18 | `;
19 |
20 | toggleExpanded = `
21 | {
22 | eventName: string;
23 | node: ITreeNode;
24 | isActive: boolean;
25 | }
26 | `;
27 |
28 | basicEvent = `
29 | {
30 | eventName: string;
31 | node: ITreeNode;
32 | }
33 | `;
34 |
35 | eventName = `
36 | {
37 | eventName: string;
38 | }
39 | `;
40 |
41 | move = `
42 | {
43 | eventName: string;
44 | node: ITreeNode; // The node that was moved
45 | to: {
46 | parent: ITreeNode; // The parent node that contains the moved node
47 | index: number; // Index in the parent where the node was moved
48 | }
49 | }
50 | `;
51 |
52 | copy = `
53 | {
54 | eventName: string;
55 | node: ITreeNode; // The node that was copied
56 | to: {
57 | parent: ITreeNode; // The parent node that contains the copied node
58 | index: number; // Index in the parent where the node was copied
59 | }
60 | }
61 | `;
62 |
63 | baseEvent = `
64 | {
65 | eventName: string;
66 | ...rest: corresponding to the original event
67 | }`;
68 |
69 | // TODO: add stateChange
70 | }
71 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/examples/columns-example/columns/columns.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { ITreeOptions } from 'angular-tree-component';
3 |
4 | @Component({
5 | selector: 'app-columns',
6 | templateUrl: './columns.component.html',
7 | styleUrls: ['./columns.component.scss']
8 | })
9 | export class ColumnsComponent implements OnInit {
10 |
11 | nodes = [
12 | {
13 | name: 'Region 1 Headquarter',
14 | city: 'Central City',
15 | zipCode: '00001',
16 | children: [
17 | { name: 'Region 1 Subdivision 1', city: 'Highway Town', zipCode: '00002' },
18 | { name: 'Region 1 Subdivision 2', city: 'Main Town', zipCode: '00003' }
19 | ]
20 | },
21 | {
22 | name: 'Region 2 Headquarter',
23 | city: 'Beach City',
24 | zipCode: '00010',
25 | children: [
26 | { name: 'Region 2 Subdivision 1', city: 'Palm Town', zipCode: '00011', children: [] },
27 | { name: 'Region 2 Subdivision 2', city: 'Sunny Town', zipCode: '00012', children: [
28 | { name: 'Customer Subdivision 2/2', city: 'Sunny Town', zipCode: '00012' }
29 | ]
30 | }
31 | ]
32 | },
33 | { name: 'Region 3 Headquarter', city: 'River City', zipCode: '00100' },
34 | ];
35 |
36 | options: ITreeOptions = {
37 | displayField: 'name',
38 | useVirtualScroll: false,
39 | nodeHeight: 25,
40 | allowDrag: false,
41 | allowDrop: false
42 | };
43 |
44 | columns = ['city', 'zipCode'];
45 |
46 | constructor() { }
47 |
48 | ngOnInit(): void {
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/e2e/template.testcafe.js:
--------------------------------------------------------------------------------
1 | import { Selector } from 'testcafe';
2 | import { TreeDriver } from './helpers/tree.driver';
3 |
4 | ['#tree1', '#tree2', '#tree3'].forEach((treeId) => {
5 | fixture `Templates ${treeId}`
6 | .page `http://localhost:4200/#/templates`
7 | .beforeEach( async t => {
8 | t.ctx.tree = new TreeDriver('#tree1');
9 | t.ctx.tree = new TreeDriver(treeId);
10 | });
11 |
12 | test('should show the tree', async t => {
13 | await t.expect(t.ctx.tree.isPresent()).ok();
14 | });
15 |
16 | test('should have 2 nodes', async t => {
17 | await t.expect(t.ctx.tree.getNodes().count).eql(2);
18 | });
19 |
20 | test('should use the template and pass it a node var', async t => {
21 | const root1Title = t.ctx.tree.selector.find('.root1Class').withText('root1');
22 |
23 | await t.expect(root1Title.exists).ok();
24 | });
25 |
26 | test('should use the template and pass it an index var', async t => {
27 | const root1Index = t.ctx.tree.selector.find('.root1ClassIndex').withText('0');
28 |
29 | await t.expect(root1Index.exists).ok();
30 | });
31 | });
32 | fixture `Templates loading component`
33 | .page `http://localhost:4200/#/templates`
34 | .beforeEach(async t => {
35 | t.ctx.tree = new TreeDriver('#tree1');
36 | t.ctx.root2 = t.ctx.tree.getNodeByIndex(1);
37 | });
38 |
39 | test('should show the loading template', async t => {
40 | await t.ctx.root2.clickExpander(t);
41 | const loadingComponent = t.ctx.tree.selector.find('.root2ClassLoading').withText('Loading root2...');
42 |
43 | await t.expect(loadingComponent.exists).ok();
44 | });
45 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | const { join } = require('path');
5 | const { constants } = require('karma');
6 |
7 | module.exports = () => {
8 | return {
9 | basePath: '',
10 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
11 | plugins: [
12 | require('karma-jasmine'),
13 | require('karma-chrome-launcher'),
14 | require('karma-jasmine-html-reporter'),
15 | require('karma-coverage-istanbul-reporter'),
16 | require('@angular-devkit/build-angular/plugins/karma'),
17 | require('@angular-devkit/build-angular/plugins/karma'),
18 | ],
19 | client: {
20 | clearContext: false, // leave Jasmine Spec Runner output visible in browser
21 | jasmine: {
22 | random: true
23 | }
24 | },
25 | coverageIstanbulReporter: {
26 | dir: join(__dirname, 'coverage'),
27 | reports: ['text-summary', 'html', 'lcovonly'],
28 | fixWebpackSourcePaths: true,
29 | thresholds: {
30 | emitWarning: false, // <- this is important to make karma fail
31 | global: {
32 | statements: 0,
33 | lines: 0,
34 | branches: 0,
35 | functions: 0
36 | }
37 | }
38 | },
39 | angularCli: {
40 | environment: 'dev'
41 | },
42 | reporters: ['progress', 'kjhtml', 'coverage-istanbul'],
43 | port: 9876,
44 | browserNoActivityTimeout: 1000000,
45 | colors: true,
46 | logLevel: constants.LOG_INFO,
47 | autoWatch: true,
48 | browsers: ['Chrome'],
49 | singleRun: false
50 | };
51 | };
52 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/examples/columns-example/columns-example.component.html:
--------------------------------------------------------------------------------
1 | Tree with columns
2 |
3 | Working tree
4 | Source Code
5 |
8 |
9 | How to implement
10 |
11 | To create columns inside the tree we need to use the templating options of the tree and style them with css.
12 |
13 | The example component contains two parts. The header and the tree itself. The header is responsible for displaying the column header.
14 | This is needed to give the tree the grid style. If you just want the tree to have columns without any header you don't need to build a header.
15 |
16 |
17 | #treeNodeWrapperTemplate
18 |
19 | To show the tree in columns it's not needed to use the full templating option. But at least the treeNodeWrapperTemplate is needed.
20 | See also the Template Fundamentals .
21 |
22 |
23 | Column definition
24 |
25 | The example uses a simple array columns = ['city', 'zipCode'] to handle the columns.
26 | This array includes the property names that should be shown as columns and does not include the name, which is the first column.
27 | The name is handled differently as seen in the example. The columns array is just looped over and shown.
28 | In a more advanced example you could have the columns array as an input and also handle the name dynamically.
29 |
30 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/fundamentals/api/api-demo/api-demo.component.html:
--------------------------------------------------------------------------------
1 |
7 |
8 | API:
9 | next node
10 | previous node
11 | drill down
12 | drill up
13 | allowDrag
14 |
15 |
18 | {{ tree.treeModel.getFocusedNode()?.isActive ? 'deactivate' : 'activate' }}
19 |
20 |
23 | {{ tree.treeModel.getFocusedNode()?.isExpanded ? 'collapse' : 'expand' }}
24 |
25 |
28 | blur
29 |
30 |
32 | Add Node
33 |
34 |
36 | Activate inner node
37 |
38 |
40 | Expand All
41 |
42 |
44 | Collapse All
45 |
46 |
48 | getActiveNodes()
49 |
50 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/guides/filter-guide/filter-guide.component.html:
--------------------------------------------------------------------------------
1 | Filtering
2 |
3 | Demo
4 |
5 | Source Code
6 |
7 |
10 |
11 | Intro
12 |
13 | Filtering on the tree will ensure that if a node is visible, then all its ancestors are also visible.
14 | This is being taken care of by the treeModel 'filterNodes' function.
15 |
16 |
17 | Filter by function
18 |
19 | The function receives the node and returns true if the node should be hidden, false otherwise.
20 |
21 | {{functionFilter}}
22 |
23 | Filter by string
24 | The function filters all nodes whose displayField ('name' by default) contains the given string. The comparison is done case insensitive.
25 | tree.treeModel.filterNodes("text", true);
26 |
27 | Note the second field - true by default.
28 | This flag makes sure all nodes are visible after searching (i.e. expand all relevant ancestors).
29 |
30 |
31 | Filtering by AP
32 |
33 | You can traverse the tree and do your own magic, and call hide(), show(), or setIsHidden(value) on all nodes as you wish.
34 |
35 |
36 | Filtering by 2-way binding
37 |
38 | You can bind to the tree state and supply a dictionary of hidden node IDs.
39 | See 2-way binding to state for more information.
40 |
41 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/fundamentals/nodes/nodes.component.html:
--------------------------------------------------------------------------------
1 | Nodes
2 |
3 | Inputs to Tree component:
4 |
5 | <tree-root [nodes]=nodes [options]="options"></tree-root>
6 |
7 | nodes
8 |
9 | Array of root nodes of the tree.
10 | Each node may contain the following fields:
11 |
12 |
13 |
14 | Property
15 | Description
16 |
17 |
18 |
19 | id
20 |
21 | Unique ID for the node.
22 | If one is not supplied it will be created by the tree library.
23 |
24 |
25 |
26 |
27 | name
28 | Will be displayed by default in the tree.
29 |
30 |
31 |
32 | children
33 | An array of the node's children.
34 | Each child is an object with the same structure as the parent node.
35 |
36 |
37 |
38 | hasChildren
39 | For async data load. Denotes that this node might have children, even when 'children' attr is empty.
40 |
41 |
42 |
43 | isExpanded
44 | Determines whether the node starts as expanded by default. Notice that this field is not bindable, meaning that changing it doesn't affect the tree and vice versa.
45 |
46 |
47 |
48 | Example:
49 |
50 | {{ nodes }}
51 |
52 | Focused
53 |
54 | Whether the tree should be focused. Key navigation only works when the tree is focused.
55 | Default value: false.
56 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { BrowserModule } from '@angular/platform-browser';
2 | import { NgModule } from '@angular/core';
3 |
4 | import { AppComponent } from './app.component';
5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
6 | import { MatToolbarModule } from '@angular/material/toolbar';
7 | import { MatIconModule } from '@angular/material/icon';
8 | import { MatSidenavModule } from '@angular/material/sidenav';
9 | import { MatButtonModule } from '@angular/material/button';
10 | import { HttpClientModule } from '@angular/common/http';
11 | import { GettingStartedComponent } from './getting-started/getting-started.component';
12 | import { AppRoutingModule } from './app-routing.module';
13 | import { CustomElementsModule } from './custom-elements/custom-elements.module';
14 | import { CodeExampleModule } from './custom-elements/code/code-example.module';
15 | import { ExamplesModule } from './examples/examples.module';
16 | import { TreeModule } from 'angular-tree-component';
17 | import { FundamentalsModule } from './fundamentals/fundamentals.module';
18 | import { LayoutModule } from './layout/layout.module';
19 | import { SharedModule } from './shared/shared.module';
20 | import { GuidesModule } from './guides/guides.module';
21 |
22 | @NgModule({
23 | declarations: [AppComponent, GettingStartedComponent],
24 | imports: [
25 | SharedModule,
26 | BrowserModule,
27 | BrowserAnimationsModule,
28 | HttpClientModule,
29 | MatToolbarModule,
30 | MatIconModule,
31 | MatSidenavModule,
32 | MatButtonModule,
33 | AppRoutingModule,
34 | CustomElementsModule,
35 | LayoutModule,
36 | CodeExampleModule,
37 | ExamplesModule,
38 | TreeModule,
39 | FundamentalsModule,
40 | GuidesModule,
41 | ],
42 | providers: [],
43 | bootstrap: [AppComponent]
44 | })
45 | export class AppModule {}
46 |
--------------------------------------------------------------------------------
/projects/example-app/src/app/checkboxes/checkboxes.component.ts:
--------------------------------------------------------------------------------
1 | import { ITreeOptions } from 'angular-tree-component';
2 | import { Component } from '@angular/core';
3 |
4 | @Component({
5 | selector: 'app-checkboxes',
6 | template: `
7 | tri-state checkboxes
8 |
12 |
13 | The tree is using flexbox.
14 | Switch expander and checkbox with CSS 'order' attribute:
15 |
20 |
21 | Disable tri-state checkboxes
22 |
26 |
27 | `,
28 | styles: [
29 | ]
30 | })
31 | export class CheckboxesComponent {
32 | nodes = [
33 | {
34 | name: 'root1',
35 | },
36 | {
37 | name: 'root2',
38 | children: [
39 | { name: 'child1' },
40 | { name: 'child2', children: [
41 | { name: 'grandchild1' },
42 | { name: 'grandchild2' }
43 | ] }
44 | ]
45 | },
46 | {
47 | name: 'asyncroot',
48 | hasChildren: true
49 | }
50 | ];
51 |
52 | options: ITreeOptions = {
53 | useCheckbox: true,
54 | getChildren: this.getChildren.bind(this)
55 | };
56 |
57 | optionsDisabled: ITreeOptions = {
58 | useCheckbox: true,
59 | getChildren: this.getChildren.bind(this),
60 | useTriState: false
61 | };
62 |
63 | getChildren(node: any) {
64 | const newNodes = [
65 | {
66 | name: 'child1'
67 | }, {
68 | name: 'child2'
69 | }
70 | ];
71 |
72 | return new Promise((resolve, reject) => {
73 | setTimeout(() => resolve(newNodes), 1000);
74 | });
75 | }
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/fundamentals/fundamentals.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { NodesComponent } from './nodes/nodes.component';
4 | import { CodeExampleModule } from '../custom-elements/code/code-example.module';
5 | import { OptionsComponent } from './options/options.component';
6 | import { ActionsComponent } from './actions/actions.component';
7 | import { TemplatesComponent } from './templates/templates.component';
8 | import { ActionsDemoComponent } from './actions/actions-demo/actions-demo.component';
9 | import { TreeModule } from 'angular-tree-component';
10 | import { TemplatesDemoComponent } from './templates/templates-demo/templates-demo.component';
11 | import { EventsComponent } from './events/events.component';
12 | import { StateBindingComponent } from './state-binding/state-binding.component';
13 | import { StateBindingDemoComponent } from './state-binding/state-binding-demo/state-binding-demo.component';
14 | import { ApiComponent } from './api/api.component';
15 | import { RouterModule } from '@angular/router';
16 | import { ApiDemoComponent } from './api/api-demo/api-demo.component';
17 | import { StylingComponent } from './styling/styling.component';
18 | import { FocusComponent } from './focus/focus.component';
19 | import { IssuesComponent } from './issues/issues.component';
20 |
21 |
22 | @NgModule({
23 | declarations: [
24 | NodesComponent,
25 | OptionsComponent,
26 | ActionsComponent,
27 | TemplatesComponent,
28 | ActionsDemoComponent,
29 | TemplatesDemoComponent,
30 | EventsComponent,
31 | StateBindingComponent,
32 | StateBindingDemoComponent,
33 | ApiComponent,
34 | ApiDemoComponent,
35 | StylingComponent,
36 | FocusComponent,
37 | IssuesComponent
38 | ],
39 | imports: [
40 | CommonModule,
41 | CodeExampleModule,
42 | TreeModule,
43 | RouterModule
44 | ]
45 | })
46 | export class FundamentalsModule { }
47 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/guides/filter-guide/filter/filter.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { TreeModel, TreeNode } from 'angular-tree-component';
3 |
4 | @Component({
5 | selector: 'app-filter',
6 | templateUrl: './filter.component.html',
7 | styleUrls: ['./filter.component.scss']
8 | })
9 | export class FilterComponent {
10 |
11 | options = {
12 | useCheckbox: true
13 | };
14 | nodes = [
15 | {
16 | name: 'North America',
17 | children: [
18 | { name: 'United States', children: [
19 | {name: 'New York'},
20 | {name: 'California'},
21 | {name: 'Florida'}
22 | ] },
23 | { name: 'Canada' }
24 | ]
25 | },
26 | {
27 | name: 'South America',
28 | children: [
29 | { name: 'Argentina', children: [] },
30 | { name: 'Brazil' }
31 | ]
32 | },
33 | {
34 | name: 'Europe',
35 | children: [
36 | { name: 'England' },
37 | { name: 'Germany' },
38 | { name: 'France' },
39 | { name: 'Italy' },
40 | { name: 'Spain' }
41 | ]
42 | }
43 | ];
44 |
45 | filterFn(value: string, treeModel: TreeModel) {
46 | treeModel.filterNodes((node: TreeNode) => fuzzysearch(value, node.data.name));
47 | }
48 | }
49 |
50 |
51 | function fuzzysearch (needle: string, haystack: string) {
52 | const haystackLC = haystack.toLowerCase();
53 | const needleLC = needle.toLowerCase();
54 |
55 | const hlen = haystack.length;
56 | const nlen = needleLC.length;
57 |
58 | if (nlen > hlen) {
59 | return false;
60 | }
61 | if (nlen === hlen) {
62 | return needleLC === haystackLC;
63 | }
64 | outer: for (let i = 0, j = 0; i < nlen; i++) {
65 | const nch = needleLC.charCodeAt(i);
66 |
67 | while (j < hlen) {
68 | if (haystackLC.charCodeAt(j++) === nch) {
69 | continue outer;
70 | }
71 | }
72 | return false;
73 | }
74 | return true;
75 | }
76 |
--------------------------------------------------------------------------------
/projects/angular-tree-component/src/lib/components/tree-node.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Component,
3 | Input,
4 | ViewEncapsulation,
5 | } from '@angular/core';
6 | import { TreeNode } from '../models/tree-node.model';
7 |
8 | @Component({
9 | selector: 'TreeNode, tree-node',
10 | encapsulation: ViewEncapsulation.None,
11 | styles: [],
12 | template: `
13 |
14 |
24 |
29 |
30 |
35 |
36 |
40 |
44 |
45 |
54 |
55 |
56 | `
57 | })
58 | export class TreeNodeComponent {
59 | @Input() node: TreeNode;
60 | @Input() index: number;
61 | @Input() templates: any;
62 | }
63 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/examples/columns-example/columns/columns.component.html:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
29 |
30 |
31 |
32 | {{ node.data.name }}
33 |
34 |
38 | {{node.data[columnName]}}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/projects/angular-tree-component/src/lib/directives/tree-drag.directive.ts:
--------------------------------------------------------------------------------
1 | import { AfterViewInit, Directive, DoCheck, ElementRef, HostListener, Input, NgZone, OnDestroy, Renderer2 } from '@angular/core';
2 | import { TreeDraggedElement } from '../models/tree-dragged-element.model';
3 |
4 | const DRAG_OVER_CLASS = 'is-dragging-over';
5 |
6 | @Directive({
7 | selector: '[treeDrag]'
8 | })
9 | export class TreeDragDirective implements AfterViewInit, DoCheck, OnDestroy {
10 | @Input('treeDrag') draggedElement;
11 | @Input() treeDragEnabled;
12 | private readonly dragEventHandler: (ev: DragEvent) => void;
13 |
14 | constructor(private el: ElementRef, private renderer: Renderer2, private treeDraggedElement: TreeDraggedElement, private ngZone: NgZone) {
15 | this.dragEventHandler = this.onDrag.bind(this);
16 | }
17 |
18 | ngAfterViewInit() {
19 | let el: HTMLElement = this.el.nativeElement;
20 | this.ngZone.runOutsideAngular(() => {
21 | el.addEventListener('drag', this.dragEventHandler);
22 | });
23 | }
24 |
25 | ngDoCheck() {
26 | this.renderer.setAttribute(this.el.nativeElement, 'draggable', this.treeDragEnabled ? 'true' : 'false');
27 | }
28 |
29 | ngOnDestroy() {
30 | let el: HTMLElement = this.el.nativeElement;
31 | el.removeEventListener('drag', this.dragEventHandler);
32 | }
33 |
34 | @HostListener('dragstart', ['$event']) onDragStart(ev) {
35 | // setting the data is required by firefox
36 | ev.dataTransfer.setData('text', ev.target.id);
37 | this.treeDraggedElement.set(this.draggedElement);
38 | if (this.draggedElement.mouseAction) {
39 | this.draggedElement.mouseAction('dragStart', ev);
40 | }
41 | }
42 |
43 | onDrag(ev) {
44 | if (this.draggedElement.mouseAction) {
45 | this.draggedElement.mouseAction('drag', ev);
46 | }
47 | }
48 |
49 | @HostListener('dragend') onDragEnd() {
50 | if (this.draggedElement.mouseAction) {
51 | this.draggedElement.mouseAction('dragEnd');
52 | }
53 | this.treeDraggedElement.set(null);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/projects/docs-app/src/app/fundamentals/actions/actions.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-actions',
5 | templateUrl: './actions.component.html',
6 | styleUrls: ['./actions.component.scss']
7 | })
8 | export class ActionsComponent {
9 |
10 | actionMapping = `
11 | import { TREE_ACTIONS, KEYS, IActionMapping } from 'angular-tree-component';
12 |
13 | const actionMapping: IActionMapping = {
14 | mouse: {
15 | click: TREE_ACTIONS.TOGGLE_SELECTED_MULTI
16 | },
17 | keys: {
18 | [KEYS.ENTER]: (tree, node, $event) => alert(\`This is \${node.data.name\}\`)
19 | }
20 | }
21 | `;
22 |
23 | mouseActions = `
24 | import { TREE_ACTIONS, IActionMapping } from 'angular-tree-component';
25 |
26 | actionMapping: IActionMapping = {
27 | mouse: {
28 | dblClick: (tree, node, $event) => // Open a modal with node content,
29 | click: TREE_ACTIONS.TOGGLE_SELECTED_MULTI,
30 | }
31 | }
32 | `;
33 |
34 | keys = `
35 | KEYS = {
36 | LEFT: 37,
37 | UP: 38,
38 | RIGHT: 39,
39 | DOWN: 40,
40 | ENTER: 13,
41 | SPACE: 32
42 | }
43 | `;
44 |
45 | keysExample = `
46 | import { TREE_ACTIONS, KEYS, IActionMapping } from 'angular-tree-component';
47 |
48 | actionMapping:IActionMapping = {
49 | keys: {
50 | 127: (tree, node, $event) => // do something to delete the node,
51 | [KEYS.ENTER]: TREE_ACTIONS.EXPAND
52 | }
53 | }
54 | `;
55 |
56 | defaultMapping = `
57 | const defaultActionMapping: IActionMapping = {
58 | mouse: {
59 | click: TREE_ACTIONS.TOGGLE_ACTIVE,
60 | dblClick: null,
61 | contextMenu: null,
62 | expanderClick: TREE_ACTIONS.TOGGLE_EXPANDED,
63 | checkboxClick: TREE_ACTIONS.TOGGLE_SELECTED,
64 | drop: TREE_ACTIONS.MOVE_NODE
65 | },
66 | keys: {
67 | [KEYS.RIGHT]: TREE_ACTIONS.DRILL_DOWN,
68 | [KEYS.LEFT]: TREE_ACTIONS.DRILL_UP,
69 | [KEYS.DOWN]: TREE_ACTIONS.NEXT_NODE,
70 | [KEYS.UP]: TREE_ACTIONS.PREVIOUS_NODE,
71 | [KEYS.SPACE]: TREE_ACTIONS.TOGGLE_ACTIVE,
72 | [KEYS.ENTER]: TREE_ACTIONS.TOGGLE_ACTIVE
73 | }
74 | };
75 | `;
76 |
77 | }
78 |
--------------------------------------------------------------------------------