Skip to content

Commit 8507272

Browse files
authored
Breaking: ColumnComponent supports new declarative syntax for dynamic components (#1117)
* breaking: dynamic components now use declarative syntax in columns
1 parent 486d162 commit 8507272

File tree

9 files changed

+97
-52
lines changed

9 files changed

+97
-52
lines changed

projects/swimlane/ngx-ui/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## HEAD (unreleased)
44

5+
- Breaking (`ngx-column`): Dynamic Components now follow declarative syntax to support outputs, two-way data binding
56
- Fix (`ngx-date-range-picker`): Error handling for invalid custom input and updated preset list.
67

78
## 50.0.0-alpha.3 (2025-07-14)

projects/swimlane/ngx-ui/src/lib/components/column/column/column.component.html

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
@if (!column?.content) {
1+
@if (!column()?.content) {
22
<header>
3-
<h4>{{ column?.title }}</h4>
3+
<h4>{{ column()?.title }}</h4>
44
</header>
55
}
66
<div class="column">
7-
@if (column?.children) {
7+
@if (column()?.children) {
88
<section class="column-list">
99
<div class="search">
1010
<ngx-icon fontIcon="search" class="search-icon pull-left"></ngx-icon>
@@ -16,7 +16,7 @@ <h4>{{ column?.title }}</h4>
1616
name="searchInputValue"
1717
placeholder="Search"
1818
(keyup)="onInputChange($event)"
19-
[disabled]="!column.children.length"
19+
[disabled]="!column().children.length"
2020
></ngx-input>
2121
</div>
2222
@if (list) {
@@ -43,15 +43,12 @@ <h4>{{ column?.title }}</h4>
4343
</cdk-virtual-scroll-viewport>
4444
}
4545
</section>
46-
} @if (activeChild?.content) {
47-
<section class="column-expanded" #expandedSection [ngStyle]="{ width: activeChild.content.width }">
48-
<ng-container
49-
*ngComponentOutlet="
50-
activeChild.content.component;
51-
inputs: activeChild.content.inputs ? activeChild.content.inputs : {};
52-
module: activeChild.content.module ? activeChild.content.module : {}
53-
"
54-
></ng-container>
55-
</section>
5646
}
47+
<section
48+
class="column-expanded"
49+
[ngStyle]="{ width: activeChild?.content?.width }"
50+
[ngClass]="{ hidden: !activeChild?.content }"
51+
>
52+
<ng-container #expandedSection></ng-container>
53+
</section>
5754
</div>

projects/swimlane/ngx-ui/src/lib/components/column/column/column.component.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@
112112

113113
section.column-expanded {
114114
overflow: auto;
115+
&.hidden {
116+
display: none;
117+
}
115118
}
116119

117120
&:not(.expanded) {

projects/swimlane/ngx-ui/src/lib/components/column/column/column.component.spec.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,20 @@ describe('ColumnComponent', () => {
122122
describe('column events', () => {
123123
beforeEach(() => {
124124
fixture = TestBed.createComponent(ColumnComponent);
125-
component = fixture.componentInstance;
126-
component.column = column;
125+
fixture.componentRef?.setInput('column', column);
127126
fixture.detectChanges();
127+
component = fixture.componentInstance;
128128
});
129129

130130
it('should emit column id on click', () => {
131-
const activeColumn = column.children[0].children[0];
131+
const activeColumn = {
132+
id: '3o',
133+
active: false,
134+
title: 'Column 3o',
135+
content: {
136+
component: ColumnTestContentComponent
137+
}
138+
};
132139
spyOn(component.tabClick, 'emit');
133140
component.ngOnChanges({
134141
column: {
@@ -151,7 +158,14 @@ describe('ColumnComponent', () => {
151158
});
152159

153160
it('should emit column id on keyup', () => {
154-
const activeColumn = column.children[0].children[0];
161+
const activeColumn = {
162+
id: '3o',
163+
active: false,
164+
title: 'Column 3o',
165+
content: {
166+
component: ColumnTestContentComponent
167+
}
168+
};
155169
spyOn(component.tabClick, 'emit');
156170
component.ngOnChanges({
157171
column: {
@@ -174,7 +188,14 @@ describe('ColumnComponent', () => {
174188
});
175189

176190
it('should emit column id on keyup with space bar', () => {
177-
const activeColumn = column.children[0].children[0];
191+
const activeColumn = {
192+
id: '3o',
193+
active: false,
194+
title: 'Column 3o',
195+
content: {
196+
component: ColumnTestContentComponent
197+
}
198+
};
178199
spyOn(component.tabClick, 'emit');
179200
component.ngOnChanges({
180201
column: {

projects/swimlane/ngx-ui/src/lib/components/column/column/column.component.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import {
22
Component,
3-
EventEmitter,
4-
Input,
3+
ComponentRef,
4+
input,
55
OnChanges,
6-
Output,
6+
output,
77
signal,
88
SimpleChanges,
9+
viewChild,
10+
ViewContainerRef,
911
ViewEncapsulation
1012
} from '@angular/core';
1113
import { Column } from './column.types';
@@ -22,14 +24,15 @@ import { Column } from './column.types';
2224
}
2325
})
2426
export class ColumnComponent implements OnChanges {
25-
@Input() column: Column = null;
26-
@Input() height: string;
27-
@Output() tabClick = new EventEmitter<{ columnId: string }>();
27+
column = input<Column | null>(null);
28+
height = input<string>('');
29+
tabClick = output<{ columnId: string }>();
30+
vcr = viewChild('expandedSection', { read: ViewContainerRef });
2831
scrollerHeight = signal('300');
29-
3032
activeChild: Column = null;
3133
list: Column[] = [];
3234
searchInputValue = '';
35+
componentRef: ComponentRef<any> | null = null;
3336

3437
ngOnChanges(changes: SimpleChanges): void {
3538
if (changes.column?.currentValue) {
@@ -48,28 +51,42 @@ export class ColumnComponent implements OnChanges {
4851
}
4952

5053
onChildClick(columnId: string) {
51-
this.activeChild = this.column.children.find(child => child.id === columnId);
54+
this.activeChild = this.column().children.find(child => child.id === columnId);
5255
this.tabClick.emit({ columnId });
56+
if (this.activeChild?.content && this.activeChild?.content.component) {
57+
this.vcr()?.clear();
58+
this.componentRef = this.vcr()?.createComponent(
59+
this.activeChild.content.component,
60+
this.activeChild.content.options || {}
61+
);
62+
}
5363
}
5464

5565
onChildKeyup(event: KeyboardEvent, columnId: string) {
5666
if (event.key === 'Enter' || event.key === ' ') {
57-
this.activeChild = this.column.children.find(child => child.id === columnId);
67+
this.activeChild = this.column().children.find(child => child.id === columnId);
5868
this.tabClick.emit({ columnId });
69+
if (this.activeChild?.content && this.activeChild?.content.component) {
70+
this.vcr()?.clear();
71+
this.componentRef = this.vcr()?.createComponent(
72+
this.activeChild.content.component,
73+
this.activeChild.content.options || {}
74+
);
75+
}
5976
}
6077
}
6178

6279
onInputChange(event: KeyboardEvent) {
6380
const change = (event.target as HTMLInputElement).value;
6481
if (!change.length) {
65-
this.list = this.column.children ? this.column.children : [];
82+
this.list = this.column().children ? this.column().children : [];
6683
} else {
6784
const query = change.toLowerCase();
68-
const results = this.column.children.filter((child: Column) => {
85+
const results = this.column().children.filter((child: Column) => {
6986
return child.title.toLowerCase().includes(query);
7087
});
7188
if (!results.length) {
72-
this.list = this.column.children ? this.column.children : [];
89+
this.list = this.column().children ? this.column().children : [];
7390
this.activeChild = this.list.find(child => child.active);
7491
} else {
7592
this.list = results;
Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
import { Binding, EnvironmentInjector, Injector } from '@angular/core';
2+
3+
export interface ColumnCustomComponentOptions {
4+
index?: number;
5+
injector?: Injector;
6+
ngModuleRef?: any;
7+
environmentInjector?: EnvironmentInjector | any;
8+
projectableNodes?: Node[][];
9+
directives?: any[];
10+
bindings?: Binding[];
11+
}
12+
113
export interface Column {
214
id: string;
315
active: boolean;
@@ -7,8 +19,6 @@ export interface Column {
719
content?: {
820
width?: string;
921
component: any;
10-
inputs?: any;
11-
outputs?: any;
12-
module?: any;
22+
options?: ColumnCustomComponentOptions;
1323
};
1424
}

projects/swimlane/ngx-ui/src/lib/components/column/columns.component.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ describe('ColumnsComponent', () => {
5353

5454
beforeEach(() => {
5555
fixture = TestBed.createComponent(ColumnsComponent);
56+
fixture.componentRef?.setInput('column', column);
57+
fixture.detectChanges();
5658
component = fixture.componentInstance;
57-
component.column = column;
5859
component.columns = [];
59-
fixture.detectChanges();
6060
});
6161

6262
it('should be defined', () => {

projects/swimlane/ngx-ui/src/lib/components/column/columns.component.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, Input, OnChanges, signal, SimpleChanges, ViewEncapsulation } from '@angular/core';
1+
import { Component, input, OnChanges, signal, SimpleChanges, ViewEncapsulation } from '@angular/core';
22
import { ColumnComponent } from './column/column.component';
33
import { Column } from './column/column.types';
44

@@ -15,8 +15,8 @@ import { Column } from './column/column.types';
1515
}
1616
})
1717
export class ColumnsComponent implements OnChanges {
18-
@Input() column: Column;
19-
@Input() height: string;
18+
column = input<Column | null>(null);
19+
height = input<string>('');
2020
columns: Array<Column>;
2121
columnComponent = ColumnComponent;
2222
columnHeight = signal('');
@@ -66,7 +66,7 @@ export class ColumnsComponent implements OnChanges {
6666

6767
getCurrentColumns(): Array<Column> {
6868
const columns = [];
69-
return this.traverseActivePath(this.column, columns);
69+
return this.traverseActivePath(this.column(), columns);
7070
}
7171

7272
onColumnNavigation(col: { columnId: string }): void {

src/app/components/column-page/column-page.component.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class ColumnPageComponent {
2929
title: 'Column 1d',
3030
content: {
3131
component: ColumnTestContentComponent,
32-
inputs: {}
32+
options: {}
3333
}
3434
},
3535
{
@@ -48,7 +48,7 @@ export class ColumnPageComponent {
4848
title: 'Column 1g',
4949
content: {
5050
component: ColumnTestContentComponent,
51-
inputs: {}
51+
options: {}
5252
}
5353
},
5454
{
@@ -57,7 +57,7 @@ export class ColumnPageComponent {
5757
title: 'Column 1h',
5858
content: {
5959
component: ColumnTestContentComponent,
60-
inputs: {}
60+
options: {}
6161
}
6262
}
6363
]
@@ -116,7 +116,7 @@ export class ColumnPageComponent {
116116
title: 'Column 3f',
117117
content: {
118118
component: ColumnTestContentComponent,
119-
inputs: {}
119+
options: {}
120120
}
121121
},
122122
{
@@ -135,7 +135,7 @@ export class ColumnPageComponent {
135135
title: 'Column 3i',
136136
content: {
137137
component: ColumnTestContentComponent,
138-
inputs: {}
138+
options: {}
139139
}
140140
},
141141
{
@@ -144,7 +144,7 @@ export class ColumnPageComponent {
144144
title: 'Column 3j',
145145
content: {
146146
component: ColumnTestContentComponent,
147-
inputs: {}
147+
options: {}
148148
}
149149
}
150150
]
@@ -517,9 +517,7 @@ export class ColumnPageComponent {
517517
title: 'Column 1c',
518518
content: {
519519
component: 'ColumnTestContentComponent',
520-
inputs: {},
521-
outputs: {},
522-
module: {}
520+
options: {}
523521
}
524522
}
525523
]
@@ -540,9 +538,7 @@ export class ColumnPageComponent {
540538
title: 'Column 2c',
541539
content: {
542540
component: 'ColumnTestContentComponent',
543-
inputs: {},
544-
outputs: {},
545-
module: {}
541+
options: {}
546542
}
547543
}
548544
]

0 commit comments

Comments
 (0)