Skip to main content

· One min read
@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);
});
});
}
}

· 2 min read
    provideConfig({
tagManager: {
gtm: {
events: [TmsAnalyticsEvent],
gtmId: 'XXX'
}
}
} as TmsConfig)

Custom event that extends CxEvent

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

Custom interface to map SPA with TMS

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;
}
}

Custom services that capture default events and dispatch TmsAnalyticsEvent

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));
}
)
})
);
}
}

Core service that enable required events

@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();
}
}

Enable analytics service from app component

@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();
}
}
}

· 2 min read

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>

Improved separation of concerns

// 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>

Or separated observables

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.

· One min read
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));