Skip to content

Commit 594f696

Browse files
refactor(command): signal refactor (#21)
1 parent 31f11c0 commit 594f696

22 files changed

+308
-318
lines changed

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
## 4.0.0 (2025-11-05)
2+
3+
### Features
4+
5+
- **command:** refactor to signal based add `$isExecuting` and `$canExecute`
6+
- **command:** change `provideSsvCommandOptions` to `Provider[]` instead of `EnvironmentProviders`
7+
- **command:** deprecated `Command` `autoDestroy`, `subscribe`, `unsubscribe` (redundant when using `command`/`commandAsync`)
8+
- **command:** deprecated `Command` `canExecute`, `isExecuting` in favor of signal based
9+
10+
### Refactor
11+
12+
- **all:** remove all `OnDestroy` in favor of `DestroyRef`
13+
14+
### BREAKING CHANGES
15+
16+
- **command:** remove `Command` `isExecuting$` and `canExecute$`
17+
- **command:** remove `CommandOptions` `hasDisabledDelay`
18+
- **command:** rename `CommandDirective` to `SsvCommand`
19+
- **command:** rename `CommandRefDirective` to `SsvCommandRef`
20+
- **ux:** rename `SsvViewportMatcherVarDirective` to `SsvViewportMatcherVar`
21+
- **ux:** rename `SsvViewportMatcherDirective` to `SsvViewportMatcher`
22+
123
## 3.3.0 (2025-10-28)
224

325
### Chore

apps/test-app/src/app/app.config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ export const appConfig: ApplicationConfig = {
1414

1515
provideSsvCommandOptions({
1616
executingCssClass: "is-busy",
17-
hasDisabledDelay: false
1817
}),
1918

2019
provideSsvUxViewportOptions({

apps/test-app/src/app/command/example-command.component.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,7 @@ <h1>Command</h1>
139139
mat-raised-button
140140
color="primary"
141141
disabled
142-
[ssvCommand]="saveCmdNoValidation"
143-
[ssvCommandOptions]="{ hasDisabledDelay: true }">
142+
[ssvCommand]="saveCmdNoValidation">
144143
@if (saveCmdNoValidation.isExecuting) {
145144
<i class="ai-circled ai-indicator ai-dark-spin small"></i>
146145
} Save

apps/test-app/src/app/command/example-command.component.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { MatIconModule } from "@angular/material/icon";
55
import { MatProgressSpinnerModule } from "@angular/material/progress-spinner";
66

77
import { BehaviorSubject, timer, Observable, tap, filter, map, distinctUntilChanged } from "rxjs";
8-
import { CommandAsync, commandAsync, SsvCommandModule } from "@ssv/ngx.command";
8+
import { commandAsync, SsvCommand, SsvCommandRef } from "@ssv/ngx.command";
99
import { CommonModule } from "@angular/common";
1010

1111
interface Hero {
@@ -31,11 +31,15 @@ interface HeroPausedState {
3131
MatIconModule,
3232
MatButtonModule,
3333
MatProgressSpinnerModule,
34-
SsvCommandModule,
34+
SsvCommand,
35+
SsvCommandRef,
3536
]
3637
})
3738
export class ExampleCommandComponent {
3839

40+
readonly #cdr = inject(ChangeDetectorRef);
41+
readonly #destroyRef = inject(DestroyRef);
42+
3943
isValid = true;
4044
isExecuting = false;
4145

@@ -45,12 +49,12 @@ export class ExampleCommandComponent {
4549
readonly $isValid = signal(false);
4650
readonly $containerVisibility = signal(true);
4751

48-
readonly saveCmd = new CommandAsync(() => this.save$(), this.isValid$);
49-
readonly saveSignalCmd = new CommandAsync(() => this.save$(), this.$isValid);
50-
readonly saveCmdNoValidation = new CommandAsync(() => this.save$());
51-
readonly removeHeroCmd = new CommandAsync(this.removeHero$.bind(this), this.isValidHeroRemove$);
52-
readonly pauseHeroCmd = new CommandAsync(this.pauseHero$.bind(this), this.isValidHeroRemove$);
53-
readonly saveReduxCmd = new CommandAsync(
52+
readonly saveCmd = commandAsync(() => this.save$(), this.isValid$);
53+
readonly saveSignalCmd = commandAsync(() => this.save$(), this.$isValid);
54+
readonly saveCmdNoValidation = commandAsync(() => this.save$());
55+
readonly removeHeroCmd = commandAsync(this.removeHero$.bind(this), this.isValidHeroRemove$);
56+
readonly pauseHeroCmd = commandAsync(this.pauseHero$.bind(this), this.isValidHeroRemove$);
57+
readonly saveReduxCmd = commandAsync(
5458
this.saveRedux.bind(this),
5559
this.isValidRedux$,
5660
);
@@ -71,26 +75,22 @@ export class ExampleCommandComponent {
7175
isInvulnerable: true
7276
} as Hero);
7377

74-
// saveCmdSync: ICommand = new Command(this.save$.bind(this), this.isValid$, true);
75-
// saveCmd: ICommand = new Command(this.save$.bind(this), null, true);
78+
// saveCmdSync = command(this.save$.bind(this), this.isValid$, true);
79+
// saveCmd = command(this.save$.bind(this), null, true);
7680
private _state = new BehaviorSubject({ isLoading: false });
7781
private _pauseState = new BehaviorSubject<HeroPausedState>({});
7882

79-
private readonly cdr = inject(ChangeDetectorRef);
80-
private readonly destroyRef = inject(DestroyRef);
81-
8283
constructor() {
83-
this.destroyRef.onDestroy(() => {
84+
this.#destroyRef.onDestroy(() => {
8485
console.warn("destroyRef.onDestroy");
8586
});
86-
// this.containerDestroySaveCmd.autoDestroy = false;
8787
}
8888

8989
save() {
9090
this.isExecuting = true;
9191
setTimeout(() => {
9292
this.isExecuting = false;
93-
this.cdr.markForCheck();
93+
this.#cdr.markForCheck();
9494
console.warn("save", "execute complete");
9595
}, 2000);
9696
}
@@ -145,7 +145,7 @@ export class ExampleCommandComponent {
145145
map(x => !x || !x.isPaused),
146146
distinctUntilChanged(),
147147
tap(x => console.warn(">>>> canPauseHero$ change", x, hero)),
148-
tap(() => this.cdr.markForCheck()),
148+
tap(() => this.#cdr.markForCheck()),
149149
);
150150
}
151151

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,58 @@
11
<h1>Viewport</h1>
2-
<p>Experimental area for viewport
3-
</p>
2+
<p>Experimental area for viewport</p>
43
<div class="container">
54
<fieldset class="viewport__field-container">
65
<legend class="viewport__field-header">Size info</legend>
76
<div class="viewport__field">
87
<span class="type variable">Size Info</span>
9-
<span class="type object value">{{sizeInfo | json}}</span>
8+
<span class="type object value">{{ sizeInfo | json }}</span>
109
</div>
1110
</fieldset>
1211
<fieldset class="viewport__field-container">
1312
<legend class="viewport__field-header">Size</legend>
1413
<div class="viewport__field">
1514
<span class="type variable">Size</span>
16-
<span class="type object value">{{size | json}}</span>
15+
<span class="type object value">{{ size | json }}</span>
1716
</div>
1817
</fieldset>
1918
</div>
2019
<div class="container">
2120
<fieldset>
2221
<legend class="viewport__field-header">Viewport Matcher Container</legend>
23-
<div *ssvViewportMatcher="'large'"
22+
<div
23+
*ssvViewportMatcher="'large'"
2424
class="matcher-dummy-container">
2525
Dummy container #1 <strong class="matcher-dummy-container__target">only (large)</strong>
2626
</div>
27-
<div *ssvViewportMatcher="['large', 'xlarge']"
27+
<div
28+
*ssvViewportMatcher="['large', 'xlarge']"
2829
class="matcher-dummy-container">
2930
Dummy container #2 <br />
30-
<strong class="matcher-dummy-container__target">
31-
only (large, xlarge)
32-
</strong>
31+
<strong class="matcher-dummy-container__target"> only (large, xlarge) </strong>
3332
</div>
34-
<div *ssvViewportMatcher="''; exclude ['xsmall', 'small']"
33+
<div
34+
*ssvViewportMatcher="''; exclude: ['xsmall', 'small']"
3535
class="matcher-dummy-container">
3636
Dummy container #3
3737
<strong class="matcher-dummy-container__target"> exclude (xsmall, small) </strong>
3838
</div>
3939
<div>
40-
<div *ssvViewportMatcher="{size: 'xlarge', operation: '<='}; else elseB"
40+
<div
41+
*ssvViewportMatcher="{ size: 'xlarge', operation: '<=' }; else elseB"
4142
class="matcher-dummy-container">
4243
Dummy container #4
4344
<strong class="matcher-dummy-container__target">(<= xlarge>)</strong>
4445
</div>
4546
<ng-template #elseB>
4647
<div class="matcher-dummy-container">
4748
Dummy container #5
48-
<strong class="matcher-dummy-container__target">(<= xlarge
49-
[else]>)</strong>
49+
<strong class="matcher-dummy-container__target">(<= xlarge [else]>)</strong>
5050
</div>
5151
</ng-template>
5252
</div>
5353

54-
<div *ssvViewportMatcher="['>=', 'xlarge']"
54+
<div
55+
*ssvViewportMatcher="['>=', 'xlarge']"
5556
class="matcher-dummy-container">
5657
Dummy container #6 <strong class="matcher-dummy-container__target">(>= xlarge>)</strong>
5758
<small>tuple syntax *recommended*</small>
@@ -69,32 +70,35 @@ <h1>Viewport</h1>
6970
<legend class="viewport__field-header">Viewport Data</legend>
7071
<div class="viewport__field">
7172
<span class="type variable">data</span>
72-
<span class="type object value">{{dataConfig | json}}</span>
73+
<span class="type object value">{{ dataConfig | json }}</span>
7374
</div>
7475
<div class="viewport__field">
7576
<span class="type variable">value</span>
7677
<span class="type string value">
77-
{{ dataConfig | ssvViewportData: "closestSmallerFirst"}}
78+
{{ dataConfig | ssvViewportData : "closestSmallerFirst" }}
7879
</span>
7980
</div>
8081
</fieldset>
8182

8283
<fieldset class="viewport__var-container viewport__field-container">
8384
<legend class="viewport__field-header">Viewport Matcher Var</legend>
84-
<div class="viewport__field"
85-
*ssvViewportMatcherVar="let isMediumDown when ['<=', 'medium']">
85+
<div
86+
class="viewport__field"
87+
*ssvViewportMatcherVar="let isMediumDown; when: ['<=', 'medium']">
8688
<span class="type variable">['<=', 'medium' ]</span>
87-
<span class="type boolean value">{{isMediumDown}}</span>
89+
<span class="type boolean value">{{ isMediumDown }}</span>
8890
</div>
89-
<div class="viewport__field"
90-
*ssvViewportMatcherVar="let isLarge when 'large'">
91+
<div
92+
class="viewport__field"
93+
*ssvViewportMatcherVar="let isLarge; when: 'large'">
9194
<span class="type variable">'large'</span>
92-
<span class="type boolean value">{{isLarge}}</span>
95+
<span class="type boolean value">{{ isLarge }}</span>
9396
</div>
94-
<div class="viewport__field"
95-
*ssvViewportMatcherVar="let isLargeOrSmall when ['small', 'large']">
97+
<div
98+
class="viewport__field"
99+
*ssvViewportMatcherVar="let isLargeOrSmall; when: ['small', 'large']">
96100
<span class="type variable">['small', 'large']</span>
97-
<span class="type boolean value">{{isLargeOrSmall}}</span>
101+
<span class="type boolean value">{{ isLargeOrSmall }}</span>
98102
</div>
99103
</fieldset>
100-
</div>
104+
</div>

libs/ngx.command/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Choose the version corresponding to your Angular version:
2020

2121
| Angular | library |
2222
| ------- | ------- |
23+
| 17+ | 4.x+ |
2324
| 17+ | 3.x+ |
2425
| 10+ | 2.x+ |
2526
| 4 to 9 | 1.x+ |
@@ -163,7 +164,6 @@ export const appConfig: ApplicationConfig = {
163164
providers: [
164165
provideSsvCommandOptions({
165166
executingCssClass: "is-busy",
166-
hasDisabledDelay: false
167167
}),
168168
],
169169
};

libs/ngx.command/src/command-ref.directive.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { Observable } from "rxjs";
2-
import { Directive, OnInit, OnDestroy, Input } from "@angular/core";
1+
import { Directive, OnInit, inject, Injector, DestroyRef, input } from "@angular/core";
32

4-
import { ICommand, CommandCreator } from "./command.model";
3+
import type { ICommand, CommandCreator, CanExecute } from "./command.model";
54
import { isCommandCreator } from "./command.util";
6-
import { Command } from "./command";
5+
import { command } from "./command";
76

87
const NAME_CAMEL = "ssvCommandRef";
98

@@ -29,29 +28,35 @@ const NAME_CAMEL = "ssvCommandRef";
2928
exportAs: NAME_CAMEL,
3029
standalone: true,
3130
})
32-
export class CommandRefDirective implements OnInit, OnDestroy {
31+
export class SsvCommandRef implements OnInit {
3332

34-
@Input(NAME_CAMEL) commandCreator: CommandCreator | undefined;
33+
readonly #injector = inject(Injector);
34+
35+
readonly commandCreator = input.required<CommandCreator>({
36+
alias: `ssvCommandRef`
37+
});
3538

3639
get command(): ICommand { return this._command; }
3740
private _command!: ICommand;
3841

42+
constructor() {
43+
const destroyRef = inject(DestroyRef);
44+
destroyRef.onDestroy(() => {
45+
this._command?.unsubscribe();
46+
});
47+
}
48+
3949
ngOnInit(): void {
40-
if (isCommandCreator(this.commandCreator)) {
41-
const isAsync = this.commandCreator.isAsync || this.commandCreator.isAsync === undefined;
50+
if (isCommandCreator(this.commandCreator())) {
51+
const commandCreator = this.commandCreator();
52+
const isAsync = commandCreator.isAsync || commandCreator.isAsync === undefined;
53+
54+
const execFn = commandCreator.execute.bind(commandCreator.host);
4255

43-
const execFn = this.commandCreator.execute.bind(this.commandCreator.host);
44-
this._command = new Command(execFn, this.commandCreator.canExecute as Observable<boolean> | undefined, isAsync);
56+
this._command = command(execFn, commandCreator.canExecute as CanExecute, { isAsync, injector: this.#injector });
4557
} else {
4658
throw new Error(`${NAME_CAMEL}: [${NAME_CAMEL}] is not defined properly!`);
4759
}
4860
}
4961

50-
ngOnDestroy(): void {
51-
// console.log("[commandRef::destroy]");
52-
if (this._command) {
53-
this._command.destroy();
54-
}
55-
}
56-
5762
}

0 commit comments

Comments
 (0)