Skip to content

Commit f3fdd92

Browse files
feat(command): add canExecuteFromSignals + change canExecuteFromNgForm (#14)
1 parent 7aaaa8a commit f3fdd92

File tree

7 files changed

+64
-20
lines changed

7 files changed

+64
-20
lines changed

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
## 3.1.0 (2025-02-16)
2+
3+
### 🚀 Features
4+
5+
- **command:** add `canExecuteFromSignals`
6+
7+
### 🩹 Fixes
8+
9+
- **command:** change `canExecuteFromNgForm` to use pristine/status events which handles state more accurately
10+
111
## 3.0.0 (2025-02-11)
212

313
### 🚀 Features

libs/ngx.command/README.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ saveCmd = command(() => this.save(), isValid);
4343
saveCmd = commandAsync(() => Observable.timer(2000), isValid);
4444

4545
// can execute diff ways
46-
saveCmd = command(() => this.save(), isValid); // signal
4746
saveCmd = command(() => this.save(), () => isValid()); // reactive fn (signal)
47+
saveCmd = command(() => this.save(), isValid); // signal
4848
saveCmd = command(() => this.save(), isValid$); // rx
4949
```
5050

@@ -133,12 +133,12 @@ Command creator ref, directive which allows creating Command in the template and
133133

134134
## Utils
135135

136-
### canExecuteFromNgForm
136+
### canExecuteFromNgForm/canExecuteFromSignals
137137
In order to use with `NgForm` easily, you can use the following utility method.
138138
This will make canExecute respond to `form.valid` and for `form.dirty` - also can optionally disable validity or dirty.
139139

140140
```ts
141-
import { commandAsync, canExecuteFromNgForm } from "@ssv/ngx.command";
141+
import { commandAsync, canExecuteFromNgForm, canExecuteFromSignals } from "@ssv/ngx.command";
142142

143143
loginCmd = commandAsync(x => this.login(), canExecuteFromNgForm(this.form));
144144

@@ -147,6 +147,8 @@ loginCmd = commandAsync(x => this.login(), canExecuteFromNgForm(this.form, {
147147
dirty: false
148148
}));
149149

150+
// similar functionality using custom signals (or form which provide signals)
151+
loginCmd = commandAsync(x => this.login(), canExecuteFromSignals({dirty: $dirty, valid: $valid}));
150152
```
151153

152154

libs/ngx.command/src/command.directive.xspec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// import {Observable} from "rxjs/Observable";
1+
// import {of} from "rxjs";
22
// import {inject, addProviders, async} from "@angular/core/testing";
33
// import {TestComponentBuilder, ComponentFixture} from "@angular/compiler/testing";
44
// import {Component, provide} from "@angular/core";
@@ -12,7 +12,7 @@
1212
// ]
1313
// })
1414
// class TestContainer {
15-
// saveCmd = new Command(() => Observable.of("yey").delay(2000), null, true);
15+
// saveCmd = new Command(() => of("yey").delay(2000), null, true);
1616
// }
1717

1818
// @Component({

libs/ngx.command/src/command.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function command(
5555

5656
/**
5757
* Command object used to encapsulate information which is needed to perform an action.
58-
* @deprecated Use {@link command} or {@link commandAsync} instead.
58+
* @deprecated Use {@link command} or {@link commandAsync} instead for creating instances.
5959
*/
6060
export class Command implements ICommand {
6161

libs/ngx.command/src/command.util.ts

+44-12
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { AbstractControl, AbstractControlDirective, FormControlStatus } from "@angular/forms";
2-
import { Observable, of, map, distinctUntilChanged, startWith, delay } from "rxjs";
1+
import { AbstractControl, PristineChangeEvent, StatusChangeEvent } from "@angular/forms";
2+
import { Observable, map, distinctUntilChanged, startWith, filter, combineLatest, of } from "rxjs";
33

44
import { CommandCreator, ICommand } from "./command.model";
55
import { Command } from "./command";
6+
import { type Signal, computed } from "@angular/core";
67

78
/** Determines whether the arg object is of type `Command`. */
89
export function isCommand(arg: unknown): arg is ICommand {
@@ -27,23 +28,54 @@ export interface CanExecuteFormOptions {
2728
dirty?: boolean;
2829
}
2930

30-
/** Get form is valid as an observable. */
31+
const CAN_EXECUTE_FORM_OPTIONS_DEFAULTS = Object.freeze<CanExecuteFormOptions>({
32+
validity: true,
33+
dirty: true,
34+
})
35+
36+
/** Get can execute from form validity/pristine as an observable. */
3137
export function canExecuteFromNgForm(
32-
form: AbstractControl | AbstractControlDirective,
38+
form: AbstractControl,
3339
options?: CanExecuteFormOptions
3440
): Observable<boolean> {
35-
const opts: CanExecuteFormOptions = { validity: true, dirty: true, ...options };
41+
const opts: CanExecuteFormOptions = options ?
42+
{ ...CAN_EXECUTE_FORM_OPTIONS_DEFAULTS, ...options }
43+
: CAN_EXECUTE_FORM_OPTIONS_DEFAULTS;
44+
45+
const pristine$ = opts.dirty
46+
? form.events.pipe(
47+
filter(x => x instanceof PristineChangeEvent),
48+
map(x => x.pristine),
49+
distinctUntilChanged(),
50+
startWith(form.pristine),
51+
) : of(true);
3652

37-
return form.statusChanges
38-
? (form.statusChanges as Observable<FormControlStatus>).pipe( // todo: remove cast when working correctly
39-
delay(0),
40-
startWith(form.valid),
41-
map(() => !!(!opts.validity || form.valid) && !!(!opts.dirty || form.dirty)),
53+
const valid$ = opts.validity
54+
? form.events.pipe(
55+
filter(x => x instanceof StatusChangeEvent),
56+
map(x => x.status === "VALID"),
4257
distinctUntilChanged(),
43-
)
44-
: of(true);
58+
startWith(form.pristine),
59+
) : of(true);
60+
61+
return combineLatest([pristine$, valid$]).pipe(
62+
map(([pristine, valid]) => !!(!opts.validity || valid) && !!(!opts.dirty || !pristine)),
63+
distinctUntilChanged(),
64+
);
4565
}
4666

67+
/** Can executed based on valid/dirty signal inputs. */
68+
export function canExecuteFromSignals(
69+
signals: { valid: Signal<boolean>, dirty: Signal<boolean> },
70+
options?: CanExecuteFormOptions
71+
): Signal<boolean> {
72+
const opts: CanExecuteFormOptions = options ?
73+
{ ...CAN_EXECUTE_FORM_OPTIONS_DEFAULTS, ...options }
74+
: CAN_EXECUTE_FORM_OPTIONS_DEFAULTS;
75+
return computed(() => !!(!opts.validity || signals.valid()) && !!(!opts.dirty || signals.dirty()));
76+
}
77+
78+
4779
function isAssumedType<T = Record<string, unknown>>(x: unknown): x is Partial<T> {
4880
return x !== null && typeof x === "object";
4981
}

libs/ngx.command/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ export { provideSsvCommandOptions, COMMAND_OPTIONS, type CommandOptions } from "
33
export { command, commandAsync, Command, CommandAsync, type CanExecute, type CommandCreateOptions } from "./command";
44
export { CommandDirective } from "./command.directive";
55
export { CommandRefDirective } from "./command-ref.directive";
6-
export { type CanExecuteFormOptions, isCommand, isCommandCreator, canExecuteFromNgForm } from "./command.util";
6+
export { type CanExecuteFormOptions, isCommand, isCommandCreator, canExecuteFromNgForm, canExecuteFromSignals } from "./command.util";
77
export type { CommandCreator, ICommand } from "./command.model";
88
export { VERSION } from "./version";

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ssv/ngx",
3-
"version": "3.0.0",
3+
"version": "3.1.0",
44
"packageManager": "[email protected]",
55
"volta": {
66
"node": "20.18.1"

0 commit comments

Comments
 (0)