Skip to content

Commit 78abf94

Browse files
release: 0.0.8 (chips, typeahead, splitter, tree, docs)
1 parent cdbb032 commit 78abf94

17 files changed

Lines changed: 1659 additions & 362 deletions

README.md

Lines changed: 85 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# @angular-bootstrap/ngbootstrap
22

3-
Angular UI library providing datagrid, drag‑and‑drop, pagination, and stepper components with Bootstrap‑friendly styling.
3+
Angular UI library providing Datagrid, Drag‑and‑drop, Pagination, Stepper, Splitter, Tree, Typeahead, and Chips components with Bootstrap‑friendly styling.
44

55
## Features
66

@@ -10,8 +10,9 @@ Angular UI library providing datagrid, drag‑and‑drop, pagination, and steppe
1010
- Stepper – horizontal/vertical stepper with custom labels, error states, theming hooks, and keyboard support.
1111
- Splitter – resizable horizontal/vertical panes with collapsing, keyboard resizing, and ARIA semantics.
1212
- Tree – keyboard-accessible tree with optional checkboxes, JSON-style expanders, and expand/collapse helpers.
13-
- Typeahead – virtualized, debounced search with single/multi select, exact-match selection, and scroll hooks.
14-
- Angular + Bootstrap first – built for modern Angular (v17–20) and works with plain Bootstrap CSS; Material/Tailwind can be layered via custom styles.
13+
- Typeahead – Bootstrap dropdown overlay with debouncing, virtualization, single/multi select, chips/tags mode, custom templates, and Reactive Forms support.
14+
- Chips – small reusable chips/tags component used by Typeahead (can also be used standalone).
15+
- Angular + Bootstrap first – built for modern Angular (v21) and works with plain Bootstrap CSS; Material/Tailwind can be layered via custom styles.
1516

1617
## Installation
1718

@@ -21,11 +22,12 @@ npm install @angular-bootstrap/ngbootstrap
2122

2223
Make sure your app:
2324

24-
- Uses Angular 17–20.
25-
- Includes Bootstrap CSS (for example in `angular.json` or global styles):
25+
- Uses Angular 21 (peer deps: `>=21 <22`).
26+
- Includes Bootstrap CSS + Bootstrap Icons (for example in `angular.json` or global styles):
2627

2728
```css
2829
@import 'bootstrap/dist/css/bootstrap.min.css';
30+
@import 'bootstrap-icons/font/bootstrap-icons.css';
2931
```
3032

3133
## Usage overview
@@ -130,34 +132,6 @@ import { Component } from '@angular/core';
130132
import { NgbStepperComponent } from '@angular-bootstrap/ngbootstrap/stepper';
131133
import { NgbStepperStep } from '@angular-bootstrap/ngbootstrap/stepper';
132134

133-
### Splitter
134-
135-
```ts
136-
import { Component } from '@angular/core';
137-
import { NgbSplitterComponent, NgbSplitterPaneComponent } from '@angular-bootstrap/ngbootstrap/splitter';
138-
139-
@Component({
140-
standalone: true,
141-
selector: 'app-splitter',
142-
imports: [NgbSplitterComponent, NgbSplitterPaneComponent],
143-
template: `
144-
<ngb-splitter orientation="horizontal">
145-
<ngb-splitter-pane size="30%" min="200px" [collapsible]="true" (collapsedChange)="onCollapse($event)">
146-
<div class="p-3">Navigation</div>
147-
</ngb-splitter-pane>
148-
<ngb-splitter-pane>
149-
<div class="p-3">Main content</div>
150-
</ngb-splitter-pane>
151-
</ngb-splitter>
152-
`,
153-
})
154-
export class SplitterExampleComponent {
155-
onCollapse(collapsed: boolean) {
156-
// persist pane state if needed
157-
}
158-
}
159-
```
160-
161135
@Component({
162136
standalone: true,
163137
selector: 'app-wizard',
@@ -205,11 +179,29 @@ Stepper highlights:
205179
- Controlled navigation (`allowRevisit`, `next()`, `prev()`, `reset()` and events).
206180
- Theming hooks via `theme` and CSS classes (`bootstrap`, `material`, `tailwind`).
207181

208-
### Drag & drop
182+
### Splitter
209183

210184
```ts
211185
import { Component } from '@angular/core';
212-
import { DndListDirective, DndItemDirective } from '@angular-bootstrap/ngbootstrap/drag-drop';
186+
import { NgbSplitterComponent, NgbSplitterPaneComponent } from '@angular-bootstrap/ngbootstrap/splitter';
187+
188+
@Component({
189+
standalone: true,
190+
selector: 'app-splitter',
191+
imports: [NgbSplitterComponent, NgbSplitterPaneComponent],
192+
template: `
193+
<ngb-splitter orientation="horizontal" [handleThickness]="10">
194+
<ngb-splitter-pane size="25%" collapsible>
195+
<div class="p-3">Navigation</div>
196+
</ngb-splitter-pane>
197+
<ngb-splitter-pane>
198+
<div class="p-3">Main content</div>
199+
</ngb-splitter-pane>
200+
</ngb-splitter>
201+
`,
202+
})
203+
export class SplitterExampleComponent {}
204+
```
213205

214206
### Tree
215207

@@ -222,35 +214,73 @@ import { NgbTreeComponent, NgbTreeNode } from '@angular-bootstrap/ngbootstrap/tr
222214
selector: 'app-tree',
223215
imports: [NgbTreeComponent],
224216
template: `
225-
<ngb-tree
226-
[nodes]="nodes"
227-
[showCheckbox]="true"
228-
type="json"
229-
(expand)="onExpand($event)"
230-
(collapse)="onCollapse($event)"
231-
(selectionChange)="onSelection($event)"
232-
></ngb-tree>
217+
<ngb-tree [nodes]="nodes" type="default"></ngb-tree>
233218
`,
234219
})
235220
export class TreeExampleComponent {
236221
nodes: NgbTreeNode[] = [
237-
{
238-
id: 'parent',
239-
label: 'Parent',
240-
expanded: true,
241-
children: [
242-
{ id: 'child-1', label: 'Child 1' },
243-
{ id: 'child-2', label: 'Child 2' },
244-
],
245-
},
222+
{ id: 'a', label: 'Parent', expanded: true, children: [{ id: 'a-1', label: 'Child 1' }] },
246223
];
224+
}
225+
```
226+
227+
### Typeahead
228+
229+
```ts
230+
import { Component } from '@angular/core';
231+
import { FormControl, ReactiveFormsModule } from '@angular/forms';
232+
import { NgbTypeaheadComponent, NgbTypeaheadItem } from '@angular-bootstrap/ngbootstrap/typeahead';
247233

248-
onExpand(node: NgbTreeNode) {}
249-
onCollapse(node: NgbTreeNode) {}
250-
onSelection(selected: NgbTreeNode[]) {}
234+
@Component({
235+
standalone: true,
236+
selector: 'app-typeahead',
237+
imports: [ReactiveFormsModule, NgbTypeaheadComponent],
238+
template: `
239+
<ngb-typeahead [data]="countries" [showDropdownButton]="true" [multiSelect]="true" [chips]="true"></ngb-typeahead>
240+
<ngb-typeahead [data]="countries" [showDropdownButton]="true" [formControl]="country"></ngb-typeahead>
241+
`,
242+
})
243+
export class TypeaheadExampleComponent {
244+
country = new FormControl<string | null>('IN');
245+
countries: NgbTypeaheadItem[] = [
246+
{ id: 'IN', label: 'India', value: 'IN' },
247+
{ id: 'US', label: 'United States', value: 'US' },
248+
];
251249
}
252250
```
253251

252+
### Chips
253+
254+
```ts
255+
import { Component } from '@angular/core';
256+
import { NgbChipsComponent } from '@angular-bootstrap/ngbootstrap/chips';
257+
258+
@Component({
259+
standalone: true,
260+
selector: 'app-chips',
261+
imports: [NgbChipsComponent],
262+
template: `
263+
<ngb-chips [items]="items" (remove)="onRemove($event)"></ngb-chips>
264+
`,
265+
})
266+
export class ChipsExampleComponent {
267+
items = [
268+
{ id: 1, label: 'One' },
269+
{ id: 2, label: 'Two' },
270+
];
271+
272+
onRemove(item: { id: number; label: string }) {
273+
this.items = this.items.filter((x) => x.id !== item.id);
274+
}
275+
}
276+
```
277+
278+
### Drag & drop
279+
280+
```ts
281+
import { Component } from '@angular/core';
282+
import { DndListDirective, DndItemDirective } from '@angular-bootstrap/ngbootstrap/drag-drop';
283+
254284
@Component({
255285
standalone: true,
256286
selector: 'app-drag-list',

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@angular-bootstrap/ngbootstrap",
3-
"version": "0.0.7",
4-
"description": "Angular UI library providing datagrid, drag-and-drop, pagination, and stepper components with Bootstrap-friendly styling.",
3+
"version": "0.0.8",
4+
"description": "Angular UI library providing datagrid, drag-and-drop, pagination, stepper, splitter, typeahead and chips components with Bootstrap-friendly styling.",
55
"author": {
66
"name": "Harmeet Singh"
77
},

src/chips/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './public-api';
2+

src/chips/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "ngbootstrap/chips",
3+
"ngPackage": {
4+
"lib": {
5+
"entryFile": "index.ts"
6+
}
7+
}
8+
}
9+

src/chips/public-api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './src/chips/chips.component';
2+
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { CommonModule } from '@angular/common';
2+
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
3+
4+
export type NgbChip = {
5+
id: string | number;
6+
label: string;
7+
disabled?: boolean;
8+
};
9+
10+
@Component({
11+
selector: 'ngb-chips',
12+
standalone: true,
13+
imports: [CommonModule],
14+
changeDetection: ChangeDetectionStrategy.OnPush,
15+
styles: [
16+
`
17+
:host {
18+
display: block;
19+
}
20+
21+
.ngb-chips {
22+
display: flex;
23+
flex-wrap: wrap;
24+
gap: 0.35rem;
25+
align-items: center;
26+
}
27+
28+
.ngb-chip {
29+
display: inline-flex;
30+
align-items: center;
31+
gap: 0.25rem;
32+
max-width: 100%;
33+
}
34+
35+
.ngb-chip-label {
36+
overflow: hidden;
37+
text-overflow: ellipsis;
38+
white-space: nowrap;
39+
}
40+
41+
.ngb-chip-remove {
42+
border: 0;
43+
background: transparent;
44+
padding: 0;
45+
line-height: 1;
46+
cursor: pointer;
47+
font-size: 1rem;
48+
opacity: 0.85;
49+
}
50+
51+
.ngb-chip-remove:disabled {
52+
cursor: default;
53+
opacity: 0.4;
54+
}
55+
`,
56+
],
57+
template: `
58+
<div class="ngb-chips" [attr.aria-label]="ariaLabel">
59+
<span
60+
*ngFor="let item of items; trackBy: trackById"
61+
class="badge rounded-pill bg-secondary ngb-chip"
62+
[class.opacity-50]="!!item.disabled"
63+
>
64+
<span class="ngb-chip-label">{{ item.label }}</span>
65+
<button
66+
*ngIf="removable"
67+
type="button"
68+
class="ngb-chip-remove"
69+
[disabled]="!!item.disabled"
70+
(click)="remove.emit(item)"
71+
[attr.aria-label]="removeLabel || 'Remove'"
72+
>
73+
&times;
74+
</button>
75+
</span>
76+
</div>
77+
`,
78+
})
79+
export class NgbChipsComponent {
80+
@Input() items: NgbChip[] = [];
81+
@Input() removable = true;
82+
@Input() ariaLabel?: string;
83+
@Input() removeLabel?: string;
84+
85+
@Output() remove = new EventEmitter<NgbChip>();
86+
87+
trackById = (_: number, item: NgbChip) => item.id;
88+
}
89+

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from './stepper';
66
export * from './splitter';
77
export * from './typeahead';
88
export * from './tree';
9+
export * from './chips';

src/splitter/public-api.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
export * from './src/splitter/splitter.component';
2-
export * from './src/splitter/splitter.types';
1+
export { NgbSplitterComponent } from './src/splitter/splitter.component';
2+
export { NgbSplitterPaneComponent } from './src/splitter/splitter-pane.component';
3+
export { NgbSplitterOrientation, NgbSplitterPaneSize } from './src/splitter/splitter.types';
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Component, ViewChild } from '@angular/core';
2+
import { ComponentFixture, TestBed } from '@angular/core/testing';
3+
import { NgbSplitterPaneComponent } from './splitter-pane.component';
4+
5+
@Component({
6+
standalone: true,
7+
imports: [NgbSplitterPaneComponent],
8+
template: `
9+
<ngb-splitter-pane
10+
[collapsible]="collapsible"
11+
[collapsed]="collapsed"
12+
[scrollable]="scrollable"
13+
(collapsedChange)="events.push($event)"
14+
>
15+
<div class="stub-content">Pane slot</div>
16+
</ngb-splitter-pane>
17+
`,
18+
})
19+
class HostComponent {
20+
@ViewChild(NgbSplitterPaneComponent) pane!: NgbSplitterPaneComponent;
21+
collapsible = true;
22+
collapsed = false;
23+
scrollable = true;
24+
events: boolean[] = [];
25+
}
26+
27+
describe('NgbSplitterPaneComponent', () => {
28+
let fixture: ComponentFixture<HostComponent>;
29+
let host: HostComponent;
30+
31+
beforeEach(async () => {
32+
await TestBed.configureTestingModule({
33+
imports: [HostComponent],
34+
}).compileComponents();
35+
36+
fixture = TestBed.createComponent(HostComponent);
37+
host = fixture.componentInstance;
38+
fixture.detectChanges();
39+
});
40+
41+
it('should expose its projected content through template', () => {
42+
expect(host.pane.template).toBeTruthy();
43+
const view = host.pane.template.createEmbeddedView(null);
44+
view.detectChanges();
45+
const wrapper = view.rootNodes[0] as HTMLElement | undefined;
46+
const projected = wrapper?.querySelector?.('.stub-content') as HTMLElement | null | undefined;
47+
expect(projected?.textContent).toContain('Pane slot');
48+
});
49+
50+
it('should wire collapsedChange output', () => {
51+
host.pane.collapsedChange.emit(true);
52+
host.pane.collapsedChange.emit(false);
53+
expect(host.events).toEqual([true, false]);
54+
});
55+
56+
it('should render scrollable template wrapper', () => {
57+
const view = host.pane.template.createEmbeddedView(null);
58+
view.detectChanges();
59+
60+
const wrapper = view.rootNodes[0] as HTMLDivElement | undefined;
61+
expect(wrapper?.style.overflow).toBe('auto');
62+
63+
host.scrollable = false;
64+
fixture.detectChanges();
65+
const viewAfter = host.pane.template.createEmbeddedView(null);
66+
viewAfter.detectChanges();
67+
68+
const wrapperAfter = viewAfter.rootNodes[0] as HTMLDivElement | undefined;
69+
expect(wrapperAfter?.style.overflow).toBe('hidden');
70+
});
71+
});

0 commit comments

Comments
 (0)