Skip to content

Commit 74c7a92

Browse files
committed
feat: enhance content projection via m-template
1 parent 099b255 commit 74c7a92

6 files changed

Lines changed: 187 additions & 93 deletions

File tree

projects/mantic-ui/src/lib/components/template/template.component.ts

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { Component, effect, inject, input, OnDestroy, TemplateRef, viewChild } from '@angular/core';
1+
import { Component, effect, inject, input, model, OnDestroy, TemplateRef, untracked, viewChild } from '@angular/core';
2+
import { toBoolean } from '../../helpers/to-boolean';
3+
import { BooleanLike } from '../../models/boolean-like';
24
import { Template } from '../../models/template';
35
import { TemplateService } from '../../services/template.service';
46

@@ -14,17 +16,45 @@ export class TemplateComponent implements OnDestroy {
1416
protected readonly contentTemplate = viewChild<TemplateRef<unknown>>('contentTemplate');
1517
public readonly name = input.required<string | string[]>();
1618
public readonly class = input<string>();
19+
public readonly visible = model(true);
20+
21+
/**
22+
* When used, it sets the default visibility of the template to false.
23+
* @example
24+
* ```html
25+
* <m-template hidden />
26+
* ```
27+
*/
28+
public readonly hidden = input<boolean, '' | undefined>(false, { transform: toBoolean });
29+
30+
/* When set to true, it set its visible state to false when another template is shown in the same outlet */
31+
public readonly autoHide = input<boolean, BooleanLike>(false, { transform: toBoolean });
1732

1833
public constructor() {
1934
effect(() => {
2035
const ref = this.contentTemplate();
21-
if (this.template) {
22-
this.templateService.hide(this.name(), this.template);
36+
untracked(() => {
37+
if (this.template) {
38+
this.templateService.hide(this.name(), this.template);
39+
}
40+
this.template = ref ? { ref, class: this.class(), visible: this.visible, autoHide: this.autoHide() } : undefined;
41+
if (this.template && this.visible()) {
42+
this.templateService.show(this.name(), this.template);
43+
}
44+
});
45+
});
46+
effect(() => this.visible.update(value => this.hidden() ? false : value));
47+
effect(() => {
48+
const isVisible = this.visible();
49+
if (!this.template) {
50+
return;
2351
}
24-
this.template = ref ? { ref, class: this.class() } : undefined;
25-
if (this.template) {
52+
if (isVisible) {
2653
this.templateService.show(this.name(), this.template);
2754
}
55+
else {
56+
this.templateService.hide(this.name(), this.template);
57+
}
2858
});
2959
}
3060

@@ -33,4 +63,22 @@ export class TemplateComponent implements OnDestroy {
3363
this.templateService.hide(this.name(), this.template);
3464
}
3565
}
66+
67+
public show(): void {
68+
if (this.template) {
69+
this.templateService.show(this.name(), this.template);
70+
}
71+
}
72+
73+
public hide(): void {
74+
if (this.template) {
75+
this.templateService.hide(this.name(), this.template);
76+
}
77+
}
78+
79+
public toggle(): void {
80+
if (this.template) {
81+
this.templateService.toggle(this.name(), this.template);
82+
}
83+
}
3684
}

projects/mantic-ui/src/lib/helpers/to-boolean.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@
33
export const toBoolean = (value: BooleanLike): boolean => {
44
return value === '' || value === true || value?.toString().toLowerCase() === 'true';
55
};
6+
7+
export const toBooleanWithUndefined = (value: BooleanLike): boolean | undefined => {
8+
return value === undefined ? undefined : toBoolean(value);
9+
};
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { TemplateRef } from '@angular/core';
1+
import { TemplateRef, WritableSignal } from '@angular/core';
22

33
export interface Template<T = unknown> {
44
ref: TemplateRef<T>;
55
class?: string;
6+
visible: WritableSignal<boolean>;
7+
autoHide?: boolean;
68
}

projects/mantic-ui/src/lib/services/template.service.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,22 @@ export class TemplateService {
1818
list = [];
1919
this.templates.set(name, list);
2020
}
21-
const index = list.indexOf(template);
21+
let index = list.indexOf(template);
2222
if (index >= 0) {
2323
list.splice(index, 1);
2424
}
25+
for (const entry of list.slice()) {
26+
if (!entry.autoHide) {
27+
continue;
28+
}
29+
entry.visible.set(false);
30+
index = list.indexOf(entry);
31+
if (index >= 0) {
32+
list.splice(index, 1);
33+
}
34+
}
2535
list.push(template);
36+
template.visible.set(true);
2637
this.notify(name, template, subscriptionSubject);
2738
}
2839
}
@@ -36,9 +47,26 @@ export class TemplateService {
3647
}
3748
const index = list.indexOf(template);
3849
if (index >= 0) {
39-
list.splice(index, 1);
50+
const template = list.splice(index, 1)[0];
51+
template.visible.set(false);
52+
}
53+
const lastTemplate = list[list.length - 1];
54+
lastTemplate?.visible.set(true);
55+
this.notify(name, lastTemplate);
56+
}
57+
}
58+
59+
public toggle(names: string | string[], template: Template): void {
60+
names = typeof names === 'string' ? [names] : names ?? [];
61+
for (const name of names) {
62+
const list = this.templates.get(name);
63+
const index = list?.indexOf(template);
64+
if (list && index !== undefined && index >= 0) {
65+
this.hide(name, template);
66+
}
67+
else {
68+
this.show(name, template);
4069
}
41-
this.notify(name, list[list.length - 1]);
4270
}
4371
}
4472

Lines changed: 60 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,66 @@
11
<app-header header="Template" subHeader="An component to project content from anywhere in your app to somewhere else" />
22

33
<m-tab-group secondary pointing class="page" selectByRoute>
4-
<m-tab label="Examples">
5-
<m-example header="Template" description="A default template projection with template and outlet">
6-
<m-template name="my.template.id">
7-
This content is projected
8-
</m-template>
9-
<m-segment>
10-
<m-template-outlet name="my.template.id" />
11-
to here
12-
</m-segment>
13-
<m-example-code [code]="code1" />
14-
</m-example>
4+
<m-tab label="Examples">
5+
<m-example header="Template" description="A default template projection with template and outlet">
6+
<m-template name="my.template.id">
7+
This content is projected
8+
</m-template>
9+
<m-segment>
10+
<m-template-outlet name="my.template.id" />
11+
to here
12+
</m-segment>
13+
<m-example-code [code]="code1" />
14+
</m-example>
1515

16-
<m-example header="Multiple Template for one outlet" description="Multiple templates can stack on one outlet. Only the last template is rendered">
17-
@if (!showA) {
18-
<m-button primary (click)="showA = true">Show A</m-button>
19-
}
20-
@if (showA) {
21-
<m-button primary (click)="showA = false">Hide A</m-button>
22-
}
23-
@if (showA) {
24-
<m-template name="stacked-templates">A is the best.</m-template>
25-
}
26-
@if (!showB) {
27-
<m-button secondary (click)="showB = true">Show B</m-button>
28-
}
29-
@if (showB) {
30-
<m-button secondary (click)="showB = false">Hide B</m-button>
31-
}
32-
@if (showB) {
33-
<m-template name="stacked-templates">B is even better!</m-template>
34-
}
35-
@if (!showC) {
36-
<m-button positive (click)="showC = true">Show C</m-button>
37-
}
38-
@if (showC) {
39-
<m-button positive (click)="showC = false">Hide C</m-button>
40-
}
41-
@if (showC) {
42-
<m-template name="stacked-templates">C is the is the bestestestest!!1!!</m-template>
43-
}
44-
<m-template name="stacked-templates">No result available. Press a button</m-template>
45-
<m-segment>
46-
<div>Result:</div>
47-
<m-template-outlet name="stacked-templates" />
48-
</m-segment>
49-
<m-example-code [code]="code2" />
50-
</m-example>
16+
<m-example header="Multiple Template for one outlet" description="Multiple templates can stack on one outlet. Only the last template is rendered">
17+
<!-- A uses template methods and is per default hidden. -->
18+
<!-- It gets hidden when other templates are shown. That allows bring A in front via button. -->
19+
<m-button primary (click)="templateA.toggle()">
20+
{{ templateA.visible() ? 'Hide' : 'Show' }} A
21+
</m-button>
22+
<m-template name="stacked-templates" hidden autoHide #templateA>A is the best.</m-template>
5123

52-
<m-example header="Hide wrapper on empty template" description="It is possible to hide a wrapping element when the template is not set">
53-
@if (!visible) {
54-
<m-button primary (click)="visible = true">Show</m-button>
55-
}
56-
@if (visible) {
57-
<m-button primary (click)="visible = false">Hide</m-button>
58-
}
59-
@if (visible) {
60-
<m-template name="onlyFilledTemplates">
61-
This content is projected and also hides it wrapping m-segment
62-
</m-template>
63-
}
64-
<m-segment *mHideOnEmptyTemplate="'onlyFilledTemplates'">
65-
<m-template-outlet name="onlyFilledTemplates" />
66-
</m-segment>
67-
<m-example-code [code]="code3" />
68-
</m-example>
69-
</m-tab>
24+
<!-- B uses a local signal bound to template's visible property -->
25+
<!-- It gets also hidden when other templates are shown. That allows bring B in front via button. -->
26+
@if (showB()) {
27+
<m-button secondary (click)="showB.set(false)">Hide B</m-button>
28+
} @else {
29+
<m-button secondary (click)="showB.set(true)">Show B</m-button>
30+
}
31+
<m-template name="stacked-templates" [(visible)]="showB" autoHide>B is even better!</m-template>
32+
33+
<!-- C uses a local signal and shows/hides the template via @if. -->
34+
<!-- But button state does not change when other templates are in front. -->
35+
<!-- So all other templates has to hide, to make C visible again. -->
36+
@if (showC()) {
37+
<m-button positive (click)="showC.set(false)">Hide C</m-button>
38+
<m-template name="stacked-templates">C is the bestestestest!!1!!</m-template>
39+
} @else {
40+
<m-button positive (click)="showC.set(true)">Show C</m-button>
41+
}
42+
43+
<!-- Fallback template is always visible. So it recovers its visible state when the outlet is empty -->
44+
<m-template name="stacked-templates">No result available. Press a button</m-template>
45+
46+
<!-- Projection target -->
47+
<m-segment>
48+
<div>Result:</div>
49+
<m-template-outlet name="stacked-templates" />
50+
</m-segment>
51+
52+
<m-example-code [code]="code2" />
53+
</m-example>
54+
55+
<m-example header="Hide wrapper on empty template" description="It is possible to hide a wrapping element when the template is not set">
56+
<m-button primary (click)="onlyFilledTemplates.toggle()">Toggle</m-button>
57+
<m-template name="onlyFilledTemplates" #onlyFilledTemplates>
58+
This content is projected and also hides it wrapping m-segment
59+
</m-template>
60+
<m-segment *mHideOnEmptyTemplate="'onlyFilledTemplates'">
61+
<m-template-outlet name="onlyFilledTemplates" />
62+
</m-segment>
63+
<m-example-code [code]="code3" />
64+
</m-example>
65+
</m-tab>
7066
</m-tab-group>

src/app/examples/template/template.component.ts

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
1-
2-
import { Component } from '@angular/core';
3-
import { ButtonComponent, HideOnEmptyTemplateDirective, SegmentComponent, TabComponent, TabGroupComponent, TemplateComponent, TemplateOutletComponent } from '@mantic-ui/angular';
1+
import { Component, signal } from '@angular/core';
2+
import { ButtonComponent, FlexComponent, HideOnEmptyTemplateDirective, SegmentComponent, TabComponent, TabGroupComponent, TemplateComponent, TemplateOutletComponent } from '@mantic-ui/angular';
43
import { ExampleCodeComponent, ExampleComponent } from '@mantic-ui/angular-doc';
54
import { HeaderComponent } from '../../components/header/header.component';
65

76
@Component({
87
selector: 'app-template',
9-
imports: [ExampleCodeComponent, ExampleComponent, HeaderComponent, SegmentComponent, TabComponent, TabGroupComponent, TemplateComponent, TemplateOutletComponent, ButtonComponent, HideOnEmptyTemplateDirective],
8+
imports: [ExampleCodeComponent, ExampleComponent, HeaderComponent, SegmentComponent, TabComponent, TabGroupComponent, TemplateComponent, TemplateOutletComponent, ButtonComponent, HideOnEmptyTemplateDirective, FlexComponent],
109
templateUrl: './template.component.html',
1110
styleUrl: './template.component.scss'
1211
})
1312
export class TemplateExampleComponent {
14-
protected showA = false;
15-
protected showB = false;
16-
protected showC = false;
17-
protected visible = false;
13+
protected readonly showB = signal(false);
14+
protected readonly showC = signal(false);
1815

1916
protected readonly code1 = `<m-template name="my.template.id">
2017
This content is projected
@@ -24,24 +21,43 @@ export class TemplateExampleComponent {
2421
to here
2522
</m-segment>`;
2623

27-
protected readonly code2 = `<m-button *ngIf="!showA" primary (click)="showA = true">Show A</m-button>
28-
<m-button *ngIf="showA" primary (click)="showA = false">Hide A</m-button>
29-
<m-template *ngIf="showA" name="stacked-templates">A is the best.</m-template>
30-
<m-button *ngIf="!showB" secondary (click)="showB = true">Show B</m-button>
31-
<m-button *ngIf="showB" secondary (click)="showB = false">Hide B</m-button>
32-
<m-template *ngIf="showB" name="stacked-templates">B is even better!</m-template>
33-
<m-button *ngIf="!showC" positive (click)="showC = true">Show C</m-button>
34-
<m-button *ngIf="showC" positive (click)="showC = false">Hide C</m-button>
35-
<m-template *ngIf="showC" name="stacked-templates">C is the is the bestestestest!!1!!</m-template>
24+
protected readonly code2 = `<!-- A uses template methods and is per default hidden. -->
25+
<!-- It gets hidden when other templates are shown. That allows bring A in front via button. -->
26+
<m-button primary (click)="templateA.toggle()">
27+
{{ templateA.visible() ? 'Hide' : 'Show' }} A
28+
</m-button>
29+
<m-template name="stacked-templates" hidden autoHide #templateA>A is the best.</m-template>
30+
31+
<!-- B uses a local signal bound to template's visible property -->
32+
<!-- It gets also hidden when other templates are shown. That allows bring B in front via button. -->
33+
@if (showB()) {
34+
<m-button secondary (click)="showB.set(false)">Hide B</m-button>
35+
} @else {
36+
<m-button secondary (click)="showB.set(true)">Show B</m-button>
37+
}
38+
<m-template name="stacked-templates" [(visible)]="showB" autoHide>B is even better!</m-template>
39+
40+
<!-- C uses a local signal and shows/hides the template via @if. -->
41+
<!-- But button state does not change when other templates are in front. -->
42+
<!-- So all other templates has to hide, to make C visible again. -->
43+
@if (showC()) {
44+
<m-button positive (click)="showC.set(false)">Hide C</m-button>
45+
<m-template name="stacked-templates">C is the bestestestest!!1!!</m-template>
46+
} @else {
47+
<m-button positive (click)="showC.set(true)">Show C</m-button>
48+
}
49+
50+
<!-- Fallback template is always visible. So it recovers its visible state when the outlet is empty -->
3651
<m-template name="stacked-templates">No result available. Press a button</m-template>
52+
53+
<!-- Projection target -->
3754
<m-segment>
3855
<div>Result:</div>
3956
<m-template-outlet name="stacked-templates" />
4057
</m-segment>`;
4158

42-
protected readonly code3 = `<m-button *ngIf="!visible" primary (click)="visible = true">Show</m-button>
43-
<m-button *ngIf="visible" primary (click)="visible = false">Hide</m-button>
44-
<m-template *ngIf="visible" name="onlyFilledTemplates">
59+
protected readonly code3 = `<m-button primary (click)="onlyFilledTemplates.toggle()">Toggle</m-button>
60+
<m-template name="onlyFilledTemplates" #onlyFilledTemplates>
4561
This content is projected and also hides it wrapping m-segment
4662
</m-template>
4763
<m-segment *mHideOnEmptyTemplate="'onlyFilledTemplates'">

0 commit comments

Comments
 (0)