Skip to content

Commit 5c523ab

Browse files
committed
Added lazy/deferred content directives to be used in collapsible
patterns
1 parent fd78ed8 commit 5c523ab

File tree

10 files changed

+248
-5
lines changed

10 files changed

+248
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ng_web_test_suite")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ng_module(
6+
name = "deferred-content",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
deps = [
12+
"@npm//@angular/core",
13+
],
14+
)
15+
16+
ng_test_library(
17+
name = "unit_test_sources",
18+
srcs = glob(
19+
["**/*.spec.ts"],
20+
exclude = ["**/*.e2e.spec.ts"],
21+
),
22+
deps = [
23+
":deferred-content",
24+
"@npm//@angular/platform-browser",
25+
],
26+
)
27+
28+
ng_web_test_suite(
29+
name = "unit_tests",
30+
deps = [":unit_test_sources"],
31+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import {Component, DebugElement, Directive, effect, inject, signal} from '@angular/core';
2+
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
3+
import {DeferredContent, DeferredContentAware} from './deferred-content';
4+
import {By} from '@angular/platform-browser';
5+
6+
describe('DeferredContent', () => {
7+
let fixture: ComponentFixture<TestComponent>;
8+
let collapsible: DebugElement;
9+
10+
beforeEach(waitForAsync(() => {
11+
TestBed.configureTestingModule({
12+
imports: [TestComponent],
13+
});
14+
}));
15+
16+
beforeEach(() => {
17+
fixture = TestBed.createComponent(TestComponent);
18+
collapsible = fixture.debugElement.query(By.directive(Collapsible));
19+
});
20+
21+
it('removes the content when hidden.', async () => {
22+
collapsible.injector.get(Collapsible).contentVisible.set(false);
23+
await fixture.whenStable();
24+
expect(collapsible.nativeElement.innerText).toBe('');
25+
});
26+
27+
it('creates the content when visible.', async () => {
28+
collapsible.injector.get(Collapsible).contentVisible.set(true);
29+
await fixture.whenStable();
30+
expect(collapsible.nativeElement.innerText).toBe('Lazy Content');
31+
});
32+
33+
describe('with preserveContent', () => {
34+
let component: TestComponent;
35+
36+
beforeEach(() => {
37+
component = fixture.componentInstance;
38+
component.preserveContent.set(true);
39+
});
40+
41+
it('creates the content when hidden.', async () => {
42+
collapsible.injector.get(Collapsible).contentVisible.set(false);
43+
await fixture.whenStable();
44+
expect(collapsible.nativeElement.innerText).toBe('Lazy Content');
45+
});
46+
47+
it('creates the content when visible.', async () => {
48+
collapsible.injector.get(Collapsible).contentVisible.set(true);
49+
await fixture.whenStable();
50+
expect(collapsible.nativeElement.innerText).toBe('Lazy Content');
51+
});
52+
});
53+
});
54+
55+
@Directive({
56+
selector: '[collapsible]',
57+
hostDirectives: [{directive: DeferredContentAware, inputs: ['preserveContent']}],
58+
})
59+
class Collapsible {
60+
private readonly _deferredContentAware = inject(DeferredContentAware);
61+
62+
contentVisible = signal(true);
63+
64+
constructor() {
65+
effect(() => this._deferredContentAware.contentVisible.set(this.contentVisible()));
66+
}
67+
}
68+
69+
@Directive({
70+
selector: 'ng-template[collapsibleContent]',
71+
hostDirectives: [DeferredContent],
72+
})
73+
class CollapsibleContent {}
74+
75+
@Component({
76+
template: `
77+
<div collapsible [preserveContent]="preserveContent()">
78+
<ng-template collapsibleContent>
79+
Lazy Content
80+
</ng-template>
81+
</div>
82+
`,
83+
imports: [Collapsible, CollapsibleContent],
84+
})
85+
class TestComponent {
86+
preserveContent = signal(false);
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
computed,
11+
Directive,
12+
effect,
13+
inject,
14+
input,
15+
TemplateRef,
16+
signal,
17+
ViewContainerRef,
18+
} from '@angular/core';
19+
20+
/**
21+
* A container directive controls the visibility of its content.
22+
*/
23+
@Directive()
24+
export class DeferredContentAware {
25+
contentVisible = signal(false);
26+
readonly preserveContent = input(false);
27+
}
28+
29+
/**
30+
* DeferredContent loads/unloads the content based on the visibility.
31+
* The visibilty signal is sent from a parent directive implements
32+
* DeferredContentAware.
33+
*
34+
* Use this directive as a host directive. For example:
35+
*
36+
* ```ts
37+
* @Directive({
38+
* selector: 'ng-template[cdkAccordionContent]',
39+
* hostDirectives: [DeferredContent],
40+
* })
41+
* class CdkAccordionContent {}
42+
* ```
43+
*/
44+
@Directive()
45+
export class DeferredContent {
46+
private readonly _deferredContentAware = inject(DeferredContentAware);
47+
private readonly _templateRef = inject(TemplateRef);
48+
private readonly _viewContainerRef = inject(ViewContainerRef);
49+
private _isRendered = false;
50+
51+
constructor() {
52+
effect(() => {
53+
if (
54+
this._deferredContentAware.preserveContent() ||
55+
this._deferredContentAware.contentVisible()
56+
) {
57+
if (this._isRendered) return;
58+
this._viewContainerRef.clear();
59+
this._viewContainerRef.createEmbeddedView(this._templateRef);
60+
this._isRendered = true;
61+
} else {
62+
this._viewContainerRef.clear();
63+
this._isRendered = false;
64+
}
65+
});
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
export * from './public-api';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
export {DeferredContentAware, DeferredContent} from './deferred-content';

src/cdk-experimental/tabs/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ ng_module(
99
exclude = ["**/*.spec.ts"],
1010
),
1111
deps = [
12+
"//src/cdk-experimental/deferred-content",
1213
"//src/cdk-experimental/ui-patterns/tabs",
1314
"//src/cdk/a11y",
1415
"//src/cdk/bidi",

src/cdk-experimental/tabs/public-api.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
export {CdkTabs, CdkTablist, CdkTab, CdkTabpanel} from './tabs';
9+
export {CdkTabs, CdkTablist, CdkTab, CdkTabpanel, CdkTabcontent} from './tabs';

src/cdk-experimental/tabs/tabs.ts

+26
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import {
1111
computed,
1212
contentChildren,
1313
Directive,
14+
effect,
1415
ElementRef,
1516
inject,
1617
input,
1718
model,
1819
} from '@angular/core';
1920
import {Directionality} from '@angular/cdk/bidi';
21+
import {DeferredContent, DeferredContentAware} from '@angular/cdk-experimental/deferred-content';
2022
import {TabPattern} from '@angular/cdk-experimental/ui-patterns/tabs/tab';
2123
import {TablistPattern} from '@angular/cdk-experimental/ui-patterns/tabs/tablist';
2224
import {TabpanelPattern} from '@angular/cdk-experimental/ui-patterns/tabs/tabpanel';
@@ -189,12 +191,22 @@ export class CdkTab {
189191
exportAs: 'cdkTabpanel',
190192
host: {
191193
'role': 'tabpanel',
194+
'tabindex': '0',
192195
'class': 'cdk-tabpanel',
193196
'[attr.id]': 'pattern.id()',
194197
'[attr.inert]': 'pattern.hidden() ? true : null',
195198
},
199+
hostDirectives: [
200+
{
201+
directive: DeferredContentAware,
202+
inputs: ['preserveContent'],
203+
},
204+
],
196205
})
197206
export class CdkTabpanel {
207+
/** The DeferredContentAware host directive. */
208+
private readonly _deferredContentAware = inject(DeferredContentAware);
209+
198210
/** The parent CdkTabs. */
199211
private readonly _cdkTabs = inject(CdkTabs);
200212

@@ -215,4 +227,18 @@ export class CdkTabpanel {
215227
id: () => this._id,
216228
tab: this.tab,
217229
});
230+
231+
constructor() {
232+
effect(() => this._deferredContentAware.contentVisible.set(!this.pattern.hidden()));
233+
}
218234
}
235+
236+
/**
237+
* A Tabcontent container for the lazy-loaded content.
238+
*/
239+
@Directive({
240+
selector: 'ng-template[cdkTabcontent]',
241+
exportAs: 'cdTabcontent',
242+
hostDirectives: [DeferredContent],
243+
})
244+
export class CdkTabcontent {}

src/components-examples/cdk-experimental/tabs/cdk-tabs/cdk-tabs-example.html

+9-3
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,14 @@
4545
<li cdkTab class="example-tab">tab 3</li>
4646
</ul>
4747

48-
<div cdkTabpanel class="example-tabpanel">Tabpanel 1</div>
49-
<div cdkTabpanel class="example-tabpanel">Tabpanel 2</div>
50-
<div cdkTabpanel class="example-tabpanel">Tabpanel 3</div>
48+
<div cdkTabpanel class="example-tabpanel">
49+
<ng-template cdkTabcontent>Tabpanel 1</ng-template>
50+
</div>
51+
<div cdkTabpanel class="example-tabpanel">
52+
<ng-template cdkTabcontent>Tabpanel 2</ng-template>
53+
</div>
54+
<div cdkTabpanel class="example-tabpanel">
55+
<ng-template cdkTabcontent>Tabpanel 3</ng-template>
56+
</div>
5157
</div>
5258
<!-- #enddocregion tabs -->

src/components-examples/cdk-experimental/tabs/cdk-tabs/cdk-tabs-example.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import {Component} from '@angular/core';
22
import {MatCheckboxModule} from '@angular/material/checkbox';
3-
import {CdkTabs, CdkTablist, CdkTab, CdkTabpanel} from '@angular/cdk-experimental/tabs';
3+
import {
4+
CdkTabs,
5+
CdkTablist,
6+
CdkTab,
7+
CdkTabpanel,
8+
CdkTabcontent,
9+
} from '@angular/cdk-experimental/tabs';
410
import {MatFormFieldModule} from '@angular/material/form-field';
511
import {MatSelectModule} from '@angular/material/select';
612
import {FormControl, ReactiveFormsModule} from '@angular/forms';
@@ -16,6 +22,7 @@ import {FormControl, ReactiveFormsModule} from '@angular/forms';
1622
CdkTablist,
1723
CdkTab,
1824
CdkTabpanel,
25+
CdkTabcontent,
1926
MatCheckboxModule,
2027
MatFormFieldModule,
2128
MatSelectModule,

0 commit comments

Comments
 (0)