Multiple ways to manage events in JS in progress
JS Event Handling
· One min read
Multiple ways to manage events in JS in progress
@Component({
selector: 'my-app',
standalone: true,
imports: [CommonModule],
templateUrl: './comp.html',
})
export class App implements OnInit {
name = 'Angular';
extras$ = of({ extra: 'extra data for event' });
array = ['A', 'B', 'C', 'D', 'E'];
event$ = interval(1000).pipe(
take(this.array.length),
map((i) => this.array[i])
);
ngOnInit() {
this.event$.subscribe((ev) => {
this.extras$.pipe(take(1)).subscribe((r) => {
console.log('Throw new event with: ', r, ev);
});
});
}
}
provideConfig({
tagManager: {
gtm: {
events: [TmsAnalyticsEvent],
gtmId: 'XXX'
}
}
} as TmsConfig)
export class TmsAnalyticsEvent extends CxEvent {
event: string;
_clear: boolean;
constructor(eventName: string) {
super();
this.event = eventName;
this._clear = true;
}
}
_clear flag is required to disable automatic object merging by GTM dataLayer on SPA Source: https://github.com/google/data-layer-helper#preventing-default-recursive-merge
export enum TmsEventName {
viewCart = 'view_cart',
addToCart = 'add_to_cart'
}
export interface TmsCustomPayload {
checkout_type: string;
currency: string;
items: TmsProduct[];
}
export class TmsCustomEvent extends TmsAnalyticsEvent {
ecommerce: TmsCustomPayload;
constructor(eventName: TmsEventName, payload: TmsCustomPayload) {
super(eventName);
this.ecommerce = payload;
}
}
export abstract class CustomAnalyticsEventService {
protected subscriptions = new Subscription();
abstract enableTracking(): void;
destroy(): void {
this.subscriptions.unsubscribe();
}
}
@Injectable({ providedIn: 'root' })
export class CustomViewCartEventService extends CustomAnalyticsEventService {
constructor(protected events: EventService, protected cartService: ActiveCartService) {
super();
}
enableTracking(): void {
this.subscriptions.add(
this.events.get(CartPageEvent).subscribe((_) => {
this.cartService.getActive().pipe(take(1)).subscribe(
data => {
//here we can map it to the required interface
const payload = data;
this.events.dispatch(new TmsCustomEvent(TmsEventName.viewCart, payload));
}
)
})
);
}
}
@Injectable({ providedIn: 'root' })
export class CustomAnalyticsService {
constructor(
protected viewCartEventService: CustomViewCartEventService,
protected addToCartEventService: CustomAddToCartEventService
) {}
trackEvents(): void {
this.viewCartEventService.enableTracking();
this.addToCartEventService.enableTracking();
}
destroy(): void {
this.viewCartEventService.destroy();
this.addToCartEventService.destroy();
}
}
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent implements OnInit, OnDestroy {
constructor(protected customAnalyticsService: CustomAnalyticsService) {}
public ngOnInit(): void {
if (ENVIRONMENT.analyticsEnabled) {
this.customAnalyticsService.trackEvents();
}
}
public ngOnDestroy(): void {
if (ENVIRONMENT.analyticsEnabled) {
this.customAnalyticsService.destroy();
}
}
}
How to implement the isLoading flag:
export interface HttpRequestState<T> {
isLoading: boolean;
value?: T;
error?: HttpErrorResponse | Error;
}
export class SomeComponent {
constructor(
private readonly activatedRoute: ActivatedRoute,
private readonly myDataService: MyDataService
) {}
readonly myDataState$: Observable<HttpRequestState<MyData>> = this.activatedRoute.params.pipe(
pluck('id'),
switchMap(
(id) => this.myDataService.getMyData(id).pipe(
map((value) => ({isLoading: false, value})),
catchError(error => of({isLoading: false, error})),
startWith({isLoading: true})
)
),
);
}
<ng-container *ngIf="myDataState$ | async as data">
<my-loading-spinner *ngIf="data.isLoading"></my-loading-spinner>
<my-error-component *ngIf="data.error" [error]="data.error"></my-error-component>
<my-data-component *ngIf="data.value" [data]="data.value"></my-data-component>
</ng-container>
// presentational component class
export class SomeLayoutComponent {
@Input()
state: HttpRequestState<MyData>;
}
<!-- Presentational component template -->
<my-loading-spinner *ngIf="state.isLoading"></my-loading-spinner>
<my-error-component *ngIf="state.error" [error]="state.error"></my-error-component>
<my-data-component *ngIf="state.value" [data]="state.value"></my-data-component>
<!-- Smart component template -->
<some-layout-component
[state]="myDataState$ | async"
></some-layout-component>
export class SomeComponent {
constructor(
private readonly activatedRoute: ActivatedRoute,
private readonly myDataService: MyDataService
) {}
readonly myDataState$: Observable<HttpRequestState<MyData>> = this.activatedRoute.params.pipe(
pluck('id'),
switchMap(
(id) => this.myDataService.getMyData(id).pipe(
map((value) => ({isLoading: false, value})),
catchError(error => of({isLoading: false, error})),
startWith({isLoading: true}),
shareReplay(1) // Added shareReplay to allow multicasting this
)
),
);
readonly loading$ = this.myDataState$.pipe(map(state => state.isLoading));
readonly error$ = this.myDataState$.pipe(map(state => state.error));
readonly myData$ = this.myDataState$.pipe(map(state => state.data));
}
shareReplay(1) is preferred over share() for robustness, as it ensures none of the subscriptions miss the initial state.
import { from } from 'rxjs';
import { pluck } from 'rxjs/operators';
const source = from([{ name: 'Joe', age: 30 }, { name: 'Sarah', age: 35 }]);
//grab names
const example = source.pipe(pluck('name'));
//output: "Joe", "Sarah"
const subscribe = example.subscribe(val => console.log(val));
Blog posts support Docusaurus Markdown features, such as MDX.
Use the power of React to create interactive blog posts.
<button onClick={() => alert('button clicked!')}>Click me!</button>