I'm currently developing an application with Angular using redux principle with ngrx.
I'm looking for a best practice for reacting to state changes and call some component logic depending on this state. I'll give you an (simplified) example to make clear what I mean:
reducers.ts
import {createSelector} from 'reselect';
export const getViewTypeOrFilterChanged = createSelector(isLoading, getActiveViewType, getActiveFilter, (isLoading, activeViewType, activeFilter) => {
// ensure that data is loaded
if (!isLoading) {
return {
activeViewType: activeViewType,
activeFilter: activeFilter
};
}
});
example-component.ts
#Component({ ... })
export class ExampleComponent implements OnInit {
// properties ...
constructor(private store: Store<fromRoot.AppState>) {
}
ngOnInit() {
this.subscriptions.push(
this.store.select(fromRoot.getViewTypeOrFilterChanged).subscribe((result) => {
if (result) {
this.property1 = result.activeType;
this.dependentHelperClass.method1(result.activeFilter);
this.method1();
this.method2(result.activeFilter);
this.method3();
}
})
);
}
ngOnDestroy() {
this.subscriptions.forEach((subscription: Subscription) => {
subscription.unsubscribe();
});
}
// methods ...
}
As you can see I'm also using reselct to combine three different slices of state within a selector (getViewTypeOrFilterChanged). In the subscription to this selector I then want to take some actions according to the combined state.
The thing is, I'm feeling like using ngrx store and subscriptions more in a way of publish/subscribe pattern here and it feels not quite correct. Also the subscriptions (I have multiple ones) in ngOnInit and unsubscriptions in ngOnDestroy bother me, but I can't think of a way achieving the same results using e.g. async pipe.
Is there maybe a more elegant way of reacting to (combined) state changes?
Thanks!
With RxJS you should think of everything as a stream - the following code is just as an example, because I don't really know any of your UI-logic so just look at the structure and not at the logic of the code, since it's more like a very wild guess of mine:
#Component({ ... })
export class ExampleComponent implements OnInit {
private destroyed$ = new Subject<boolean>();
// the following streams can be used in the controller
// as well as in the template via | async
// the .share() is just so the | async pipe won't cause unneccessary stream-creations (the result should be the same regardless of the .share(), it's just a minor performance-enhancement when using multiple | async)
isLoading$ = this.store.select(fromRoot.isLoading).share();
activeViewType$ = this.store.select(fromRoot.getActiveViewType).share();
activeFilter$ = this.store.select(fromRoot.getActiveFilter).share();
activeViewTypeAndFilter$ = Observable.combineLatest(this.activeViewType$, this.activeFilter$).share();
constructor(private store: Store<fromRoot.AppState>) {
}
ngOnInit() {
this.isLoading$
.filter(isLoading => !isLoading) // the initial stream will not emit anything until "loading" was false once
.switchMapTo(this.activeViewTypeAndFilter$)
.do([viewType, filter] => {
this.dependentHelperClass.method1(activeFilter);
this.method1();
this.method2(activeFilter);
this.method3();
})
.takeUntil(this.destroyed$) //this stream will automatically be unsubscribed when the destroyed$-subject "triggers"
.subscribe();
}
ngOnDestroy() {
this.destroyed$.next(true);
this.destroyed$.complete();
}
// methods ...
}
As I said: logic-wise I cannot say if this is what you need, but that's just a question of using different operators and/or a different order to arrange your "main-stream" differntly.
Related
I used to have one banner at the top of the page for all events in my app (like some Errors, Warnings, and Success) and used for that BehaviorSubject.
For example:
in the main app.component.html file I had:
<baner [alerts]="alerts$ | async"></baner>
and alerts get from bannerService:
ngOnInit(): void { this.alerts$ = this.bannerService.alerts$; }
the service looks next:
alertSub$ = new BehaviorSubject<string>('');
alerts$ = this.alertSub$.asObservable();
showWarning(message: string): void {
const newAlert = { message, type: 'Warning' };
this.alertSub$.next([...this.alertSub$.getValue(), newAlert]);
setTimeout(() => this.dismiss(newAlert), 500);
}
dismiss(alert): void {
const updatedAlerts = this.alertSub$.getValue().filter(alertSub => alertSub !== alert);
this.alertSub$.next(updatedAlerts);
}
...and so on...
So when I wanted to add some warning, I called this.bannerService.showWarning('some msg') and everything was fine.
But now I need to add a banner inside another component for its own warnings, and it should be independent. This means that global warnings would be still on the top of the app, but warnings of this component are only inside the component.
I understand, that I should create a new BehaviorSubject, but how to re-use all functions correctly?
For now, I've added to all functions a parameter, that pass proper BehaviorSubject, but in that case, I need to make changes in all places, where bannerService was used.
Service with my new changes:
alertSub$ = new BehaviorSubject<string>('');
alerts$ = this.alertSub$.asObservable();
componentSub$ = new BehaviorSubject<string>('');
componentAlerts$ = this.componentSub$.asObservable();
showWarning(message: string, banner: BehaviorSubject<string>): void {
const newAlert = { message, type: 'Warning' };
banner.next([...banner.getValue(), newAlert]);
setTimeout(() => this.dismiss(newAlert, banner), 500);
}
dismiss(alert, banner: BehaviorSubject<string>): void {
const updatedAlerts = banner.getValue().filter(alertSub => alertSub !== alert);
banner.next(updatedAlerts);
}
...and so on...
Would be really grateful for any idea, on how to use old functions for different BehaviorSubjects.
I think it can be a bit easier than that. Your baner component is responsible of rendering the messages, right? What if you modify this component to take in two instances of bannerService instead of just one? Let's suppose this is our BannerComponent:
export class BannerComponent implements OnInit {
bannerService: BannerService;
constructor(
#Host() #Optional() parentBannerService: BannerService,
#Inject() globalBannerService: BannerService
) {
this.bannerService = parentBannerService ?? globalBannerService;
}
This allows us to ask the injector for an (optional) instance of BannerService that is provided by the parent component (the component that renders the BannerComponent component).
In case we don't have such a thing, we still want the BannerService to be injected from somewhere, hence the second parameter, globalBannerService.
Now all that is left for us to do, is to provide a BannerService instance from our custom component that displays the banner:
#Component({
selector: 'app-component-with-its-own-banner',
// template, css, etc
providers: [BannerService]
})
export class ComponentWithItsOwnBanner {
// ...
}
The template of this component also includes the banner component selector:
<baner [alerts]="bannerService.alerts$ | async"></baner>
Everything else can stay exactly the same. You don't need to create any additional behavior subjects.
The component has variable applicationObs as observer:
export class OrderDetailsComponent implements OnInit {
public applicationObs: Observable<ApplicationResponse>;
public getSections(): SidebarMenuItem[] {
const fabricApplicationSidebar = fabricApplicationSidebarMenu(this.application); // Here
}
}
How to pass applicationObs to fabricApplicationSidebarMenu() when this.application is not observer, but ApplicationResponse.
How to pass async data as variable in function?
I'd suggest you think about this a little differently, what you actually want to do is add a new function onto your observable pipe. so I'd do something like this:
export class OrderDetailsComponent implements OnInit {
public applicationObs: Observable<ApplicationResponse>;
public getSections(): Observable<SidebarMenuItem[]> {
return applicationObs.pipe(
map(m => fabricApplicationSidebarMenu(m))
);
}
}
Here were adding a new pipe onto the existing observable and returning that. this is still async so in itself doesn't do anything. To get the sections you'll still need to subscribe to the resulting observable:
this.getSections().subscribe((s: SidebarMenuItem[]) => {
});
You could also potentially use an async pipe if you wished. See here
Note that the above will trigger the applicationObs and whatever fabricApplicationSidebarMenu does on subscribe. This may or may not be desirable depending on what your planning on doing and what these functions do.
what do you want to do?
something like this?
this.applicationObs.subscribe(
(applicationResponse: ApplicationResponse) => {
const fabricApplicationSidebar = fabricApplicationSidebarMenu(applicationResponse); // Here
});
In TypeScript / Angular, you would usually call a function that returns an observable and subscribe to it in a component like this:
this.productsService.getProduct().subscribe((product) => { this.product = product });
This is fine when the code runs in a class that manages data, but in my opinion this should not be handled in the component. I may be wrong but i think the job of a component should be to ask for and display data without handling how the it is retrieved.
In the angular template you can do this to subscribe to and display the result of an observable:
<h1>{{ product.title | async }}</h1>
Is it possible to have something like this in the component class? My component displays a form and checks if a date is valid after input. Submitting the form is blocked until the value is valid and i want to keep all the logic behind it in the service which should subscribe to the AJAX call, the component only checks if it got a valid date.
class FormComponent {
datechangeCallback(date) {
this.dateIsValid$ = this.dateService.checkDate(date);
}
submit() {
if (this.dateIsValid$ === true) {
// handle form submission...
}
}
}
You can convert rxjs Observables to ES6 Promises and then use the async-await syntax to get the data without observable subscription.
Service:
export class DateService {
constructor(private http: HttpClient) {}
async isDateValid(date): Promise<boolean> {
let data = await this.http.post(url, date, httpOptions).toPromise();
let isValid: boolean;
// perform your validation and logic below and store the result in isValid variable
return isValid;
}
}
Component:
class FormComponent {
async datechangeCallback(date) {
this.dateIsValid = await this.dateService.isDateValid(date);
}
submit() {
if (this.dateIsValid) {
// handle form submission...
}
}
}
P.S:
If this is a simple HTTP request, which completes on receiving one value, then using Promises won't hurt. But if this obersvable produces some continuous stream of values, then using Promises isn't the best solution and you have to revert back to rxjs observables.
The cleanest way IMHO, using 7.4.0 < RxJS < 8
import { of, from, tap, firstValueFrom } from 'rxjs';
const asyncFoo = () => {
return from(
firstValueFrom(
of('World').pipe(
tap((foo) => {
console.info(foo);
})
)
)
);
};
asyncFoo();
// Outputs "World" once
asyncFoo().subscribe((foo) => console.info(foo));
// Outputs "World" twice
The "more cleanest" way would be having a factory (in some service) to build these optionally subscribeable function returns...
Something like this:
const buildObs = (obs) => {
return from(firstValueFrom(obs));
};
const asyncFoo = () => {
return buildObs(
of('World').pipe(
tap((foo) => {
console.info(foo);
})
)
);
};
I have an Angular 2/4 service which uses observables to communicate with other components.
Service:
let EVENTS = [
{
event: 'foo',
timestamp: 1512205360
},
{
event: 'bar',
timestamp: 1511208360
}
];
#Injectable()
export class EventsService {
subject = new BehaviorSubject<any>(EVENTS);
getEvents(): Observable<any> {
return this.subject.asObservable();
}
deleteEvent(deletedEvent) {
EVENTS = EVENTS.filter((event) => event.timestamp != deletedEvent.timestamp);
this.subject.next(EVENTS);
}
search(searchTerm) {
const newEvents = EVENTS.filter((obj) => obj.event.includes(searchTerm));
this.subject.next(newEvents);
}
}
My home component is able to subscribe to this service and correctly updates when an event is deleted:
export class HomeComponent {
events;
subscription: Subscription;
constructor(private eventsService: EventsService) {
this.subscription = this.eventsService.getEvents().subscribe(events => this.events = events);
}
deleteEvent = (event) => {
this.eventsService.deleteEvent(event);
}
}
I also have a root component which displays a search form. When the form is submitted it calls the service, which performs the search and calls this.subject.next with the result (see above). However, these results are not reflected in the home component. Where am I going wrong? For full code please see plnkr.co/edit/V5AndArFWy7erX2WIL7N.
If you provide a service multiple times, you will get multiple instances and this doesn't work for communication, because the sender and receiver are not using the same instance.
To get a single instance for your whole application provide the service in AppModule and nowhere else.
Plunker example
Make sure your Component is loaded through or using its selector. I made a separate component and forgot to load it in the application.
I previously worked with React/Mobx with the action concept. This allows to change some model properties in one transaction without firing multiple events to update UI state (only one event will be triggered after an action method will be executed).
Is there any approach or may be patterns to achieve the same behavior in Angular 2?
I'm using a service like this to control UI:
#Injectable()
export class UIService {
private buffer: any = {};
private dispatcher: Subject<any> = new Subject();
constructor() {
this.dispatcher
.asObservable()
.map(state => this.buffer = { ...this.buffer, ...state })
.debounceTime(50)
.subscribe(() => { /* do something */));
}
set(key: string, value?: any) {
this.dispatcher.next({ [key]: value });
}
}
and in different components in ngOnInit() I set different options:
this.uiService.set('footer', false); // in base component
this.uiService.set('footer', true); // in extended component
this.uiService.set('sidebar', true); // in other component
this.uiService.set('title', 'My Page'); // elsewhere...
This way I have only one object that reflects my current UI state...
Note that MobX can be used with Angular 2 as well: https://github.com/500tech/ng2-mobx