mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 05:43:41 +00:00
feat: improve async actions docs and guards (#4215)
* docs: clarify submit handler in standalone example * docs: clarify protection against re-running actions * docs: clarify that these directives replace click and ngSubmit * docs: clarify `void` * feat: disable action directive on bitsubmit disable * docs: fix grammar * docs: change to note * feat: guard against double running bitAction
This commit is contained in:
@@ -18,6 +18,8 @@ export class BitActionDirective implements OnDestroy {
|
|||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
private _loading$ = new BehaviorSubject<boolean>(false);
|
private _loading$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
disabled = false;
|
||||||
|
|
||||||
@Input("bitAction") protected handler: FunctionReturningAwaitable;
|
@Input("bitAction") protected handler: FunctionReturningAwaitable;
|
||||||
|
|
||||||
readonly loading$ = this._loading$.asObservable();
|
readonly loading$ = this._loading$.asObservable();
|
||||||
@@ -39,7 +41,7 @@ export class BitActionDirective implements OnDestroy {
|
|||||||
|
|
||||||
@HostListener("click")
|
@HostListener("click")
|
||||||
protected async onClick() {
|
protected async onClick() {
|
||||||
if (!this.handler) {
|
if (!this.handler || this.loading || this.disabled || this.buttonComponent.disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ import { BitActionDirective } from ".";
|
|||||||
* - Activates the button loading effect while the form is processing an async submit action.
|
* - Activates the button loading effect while the form is processing an async submit action.
|
||||||
* - Disables the button while a `bitAction` directive on another button is being processed.
|
* - Disables the button while a `bitAction` directive on another button is being processed.
|
||||||
*
|
*
|
||||||
* When attached to a standalone button with `bitAction` directive:
|
* When attached to a button with `bitAction` directive inside of a form:
|
||||||
* - Disables the form while the `bitAction` directive is processing an async submit action.
|
* - Disables the button while the `bitSubmit` directive is processing an async submit action.
|
||||||
|
* - Disables the button while a `bitAction` directive on another button is being processed.
|
||||||
|
* - Disables form submission while the `bitAction` directive is processing an async action.
|
||||||
*/
|
*/
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: "button[bitFormButton]",
|
selector: "button[bitFormButton]",
|
||||||
@@ -48,6 +50,10 @@ export class BitFormButtonDirective implements OnDestroy {
|
|||||||
actionDirective.loading$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => {
|
actionDirective.loading$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => {
|
||||||
submitDirective.disabled = disabled;
|
submitDirective.disabled = disabled;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
submitDirective.disabled$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => {
|
||||||
|
actionDirective.disabled = disabled;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,14 +16,16 @@ Adding async actions to submit buttons requires the following 3 steps
|
|||||||
### 1. Add a handler to your `Component`
|
### 1. Add a handler to your `Component`
|
||||||
|
|
||||||
A handler is a function that returns a promise or an observable. Functions that return `void` are also supported which is
|
A handler is a function that returns a promise or an observable. Functions that return `void` are also supported which is
|
||||||
useful for aborting an action.
|
useful because `return;` can be used to abort an action.
|
||||||
|
|
||||||
**NOTE:**
|
**NOTE:** Defining the handlers as arrow-functions assigned to variables is mandatory if the handler needs access to the parent
|
||||||
|
component using the variable `this`.
|
||||||
|
|
||||||
- Defining the handlers as arrow-functions assigned to variables is mandatory if the handler needs access to the parent
|
**NOTE:** `formGroup.invalid` will always return `true` after the first `await` operation, event if the form is not actually
|
||||||
component using the variable `this`.
|
invalid. This is due to the form getting disabled by the `bitSubmit` directive while waiting for the async action to complete.
|
||||||
- `formGroup.invalid` will always return `true` after the first `await` operation, event if the form is not actually
|
|
||||||
invalid. This is due to the form getting disabled by the `bitSubmit` directive while waiting for the async action to complete.
|
**NOTE:** Handlers do not need to check if any previous requests have finished because the directives have built in protection against
|
||||||
|
users attempting to trigger new actions before the previous ones have finished.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
@Component({...})
|
@Component({...})
|
||||||
@@ -52,6 +54,8 @@ Add the `bitSubmit` directive and supply the handler defined in step 1.
|
|||||||
**NOTE:** The `directive` is defined using the input syntax: `[input]="handler"`.
|
**NOTE:** The `directive` is defined using the input syntax: `[input]="handler"`.
|
||||||
This is different from how submit handlers are usually defined with the output syntax `(ngSubmit)="handler()"`.
|
This is different from how submit handlers are usually defined with the output syntax `(ngSubmit)="handler()"`.
|
||||||
|
|
||||||
|
**NOTE:** `[bitSubmit]` is used instead of `(ngSubmit)`. Using both is not supported.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<form [formGroup]="formGroup" [bitSubmit]="submit">...</form>
|
<form [formGroup]="formGroup" [bitSubmit]="submit">...</form>
|
||||||
```
|
```
|
||||||
@@ -60,6 +64,8 @@ This is different from how submit handlers are usually defined with the output s
|
|||||||
|
|
||||||
Add both `bitButton` and `bitFormButton` directives to the button.
|
Add both `bitButton` and `bitFormButton` directives to the button.
|
||||||
|
|
||||||
|
**NOTE:** A summary of what each directive does can be found inside the source code.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<button type="submit" bitButton bitFormButton>{{ "submit" | i18n }}</button>
|
<button type="submit" bitButton bitFormButton>{{ "submit" | i18n }}</button>
|
||||||
```
|
```
|
||||||
@@ -76,21 +82,22 @@ useful for aborting an action.
|
|||||||
**NOTE:** Defining the handlers as arrow-functions assigned to variables is mandatory if the handler needs access to the parent
|
**NOTE:** Defining the handlers as arrow-functions assigned to variables is mandatory if the handler needs access to the parent
|
||||||
component using the variable `this`.
|
component using the variable `this`.
|
||||||
|
|
||||||
|
**NOTE:** Handlers do not need to check if any previous requests have finished because the directives have built in protection against
|
||||||
|
users attempting to trigger new actions before the previous ones have finished.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
@Component({...})
|
@Component({...})
|
||||||
class Component {
|
class Component {
|
||||||
formGroup = this.formBuilder.group({...});
|
formGroup = this.formBuilder.group({...});
|
||||||
|
|
||||||
submit = async () => {
|
submit = async () => {
|
||||||
// not relevant for this example
|
// contents of this handler are not relevant for this example
|
||||||
|
// as this handler will not be trigger by standalone buttons using
|
||||||
|
// `bitAction`
|
||||||
}
|
}
|
||||||
|
|
||||||
// action can also return Observable instead of Promise
|
// action can also return Observable instead of Promise
|
||||||
handler = async () => {
|
handler = async () => {
|
||||||
if (/* perform guard check */) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.apiService.post(/* ... */);
|
await this.apiService.post(/* ... */);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -98,7 +105,7 @@ class Component {
|
|||||||
|
|
||||||
### 2. Add directive to the `form` element
|
### 2. Add directive to the `form` element
|
||||||
|
|
||||||
The `bitSubmit` directive is required beacuse of its coordinating role.
|
The `bitSubmit` directive is required beacuse of its coordinating role inside of a form.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<form [formGroup]="formGroup" [bitSubmit]="submit">...</form>
|
<form [formGroup]="formGroup" [bitSubmit]="submit">...</form>
|
||||||
@@ -108,6 +115,8 @@ The `bitSubmit` directive is required beacuse of its coordinating role.
|
|||||||
|
|
||||||
Add `bitButton`, `bitFormButton`, `bitAction` directives to the button. Make sure to supply a handler.
|
Add `bitButton`, `bitFormButton`, `bitAction` directives to the button. Make sure to supply a handler.
|
||||||
|
|
||||||
|
**NOTE:** A summary of what each directive does can be found inside the source code.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<button type="button" bitFormButton bitButton [bitAction]="handler">Do action</button>
|
<button type="button" bitFormButton bitButton [bitAction]="handler">Do action</button>
|
||||||
<button type="button" bitFormButton bitIconButton="bwi-star" [bitAction]="handler"></button>
|
<button type="button" bitFormButton bitIconButton="bwi-star" [bitAction]="handler"></button>
|
||||||
|
|||||||
@@ -14,21 +14,20 @@ Adding async actions to standalone buttons requires the following 2 steps
|
|||||||
### 1. Add a handler to your `Component`
|
### 1. Add a handler to your `Component`
|
||||||
|
|
||||||
A handler is a function that returns a promise or an observable. Functions that return `void` are also supported which is
|
A handler is a function that returns a promise or an observable. Functions that return `void` are also supported which is
|
||||||
useful for aborting an action.
|
useful because `return;` can be used to abort an action.
|
||||||
|
|
||||||
**NOTE:** Defining the handlers as arrow-functions assigned to variables is mandatory if the handler needs access to the parent
|
**NOTE:** Defining the handlers as arrow-functions assigned to variables is mandatory if the handler needs access to the parent
|
||||||
component using the variable `this`.
|
component using the variable `this`.
|
||||||
|
|
||||||
|
**NOTE:** Handlers do not need to check if any previous requests have finished because the directives have built in protection against
|
||||||
|
users attempting to trigger new actions before the previous ones have finished.
|
||||||
|
|
||||||
#### Example using promises
|
#### Example using promises
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
@Component({...})
|
@Component({...})
|
||||||
class PromiseExampleComponent {
|
class PromiseExampleComponent {
|
||||||
handler = async () => {
|
handler = async () => {
|
||||||
if (/* perform guard check */) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.apiService.post(/* ... */);
|
await this.apiService.post(/* ... */);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -40,10 +39,6 @@ class PromiseExampleComponent {
|
|||||||
@Component({...})
|
@Component({...})
|
||||||
class Component {
|
class Component {
|
||||||
handler = () => {
|
handler = () => {
|
||||||
if (/* perform guard check */) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.apiService.post$(/* ... */);
|
return this.apiService.post$(/* ... */);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -56,6 +51,8 @@ Add the `bitAction` directive and supply the handler defined in step 1.
|
|||||||
**NOTE:** The `directive` is defined using the input syntax: `[input]="handler"`.
|
**NOTE:** The `directive` is defined using the input syntax: `[input]="handler"`.
|
||||||
This is different from how click handlers are usually defined with the output syntax `(click)="handler()"`.
|
This is different from how click handlers are usually defined with the output syntax `(click)="handler()"`.
|
||||||
|
|
||||||
|
**NOTE:** `[bitAction]` is used instead of `(click)`. Using both is not supported.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<button bitButton [bitAction]="handler">Do action</button>
|
<button bitButton [bitAction]="handler">Do action</button>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user