I want to have a bunch of data ready the very first thing after a user logs in my app (keeping it cached in a Service).
I thought implementing this data-loading logic before resolving the parent route the user will be redirected to if login is successful, as a Resolver.
To be more specific: before showing the user's home page I would like to have a list of data already loaded and, if the list is not empty, have the first element of that list set as the selected element by default. So, this means, two Services:
ItemsService: this Service will know how to request the list of items and keep it cached after the first request is done
ItemSelectedService: this Service will know which item has been set as selected at anytime
And the following Resolver:
#Injectable()
export class ItemsResolver implements Resolve<any> {
constructor(
private itemSelectedService: ItemSelectedService,
private itemsService: ItemsService
) { }
resolve() {
this.itemsService.getAll()
.subscribe((items) => {
if (items.length > 0) {
this.itemSelectedService.setAsSelected(items[0]);
}
});
}
}
As #resolve() needs to return an Observable (return this.itemsService.getAll() would have just been enough...) but I'm not returning any because I need to subscribe and call itemSelectedService#setAsSelected() once the item list has been fetched asynchronously... what would be the right way to achieve the desired behavior?
Try giving it a tap
resolve() {
return this.itemsService.getAll()
.pipe(
tap(
filter(items=>items.length > 0)
do(items=>this.itemSelectedService.setAsSelected(items[0]))
)
);
}
do / tap
Transparently perform actions or side-effects, such as logging.
https://www.learnrxjs.io/operators/utility/do.html
You can use flatmap to resolve the observable in chain
#Injectable()
export class ItemsResolver implements Resolve<any> {
constructor(
private itemSelectedService: ItemSelectedService,
private itemsService: ItemsService
) { }
resolve() {
return this.getAll()
.pipe(
flatmap(items => this.setSelectService(item[0]))
)
}
getAll() {
return this.itemsService.getAll();
}
setSelectService(item) {
return this.itemSelectedService.setAsSelected(item);
}
}
Related
I am new to JS, TS and Angular...
So I have this angular component:
export class AdminProductsMenuComponent implements OnInit{
constructor(private productService: ProductService,
private alertService: AlertService,
private router: Router) {
this.subscribeToDeleteProductEvents();
}
productsAdminModel: IGetProductAdminModel[] = [];
private productId: string;
ngOnInit(): void {
this.executeGetAllProductsAsAdmin();
}
executeGetAllProductsAsAdmin() {
this.productService.getAllProductsAsAdmin().subscribe({
next: (productData) => this.productsAdminModel = productData
})
}
private subscribeToDeleteProductEvents() {
this.alertService.getSubjectAlertEvent().subscribe({
next: (isConfirmed) => {
if (isConfirmed) {
this.productService.deleteProduct(this.productId).subscribe({
next: () => {
this.reloadCurrentResources();
}
});
}
}
});
}
private reloadCurrentResources(): void {
// save current route first
this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => {
this.router.navigate(['/AdminProducts']); // navigate to same route
});
}
executeProductDelete(id: string) {
this.productId = id;
this.alertService.confirmationAlertProductDelete();
}
}
Brief explanation:
I have subscription in the constructor which listens for events during the lifetime of the component.
An event is fired when the last method is called (through the template) which prompts a SweetAlert confirm dialog. Depending on the selected the event is true or false.
Now here is the tricky part - if I move the executeProductDelete() method above reloadCurrentResources() and subscribeToDeleteProductEvents() and invoke it (executeProductDelete) it will complete the action and throw error
I have a feeling that it executes again the subscribeToDeleteProductEvents() and reloadCurrentResources() .
If I move the executeDeleteProduct() as the last method, no error occurs.
Why is this behavior? I have a feeling that they continue to run synchronously. They are not invoked anywhere else.
There seems to be 2 main problems there:
Avoid at all costs "reloading" the same component, try to abstract the reload logic into methods. This could cause weird issues and unecessary loads, as the SPA is meant to be a single page application.
Since you are problably re-instancianting the component over and over again through your reloadResources, the alert service behaviour subjects creates new subscriptions. And since you haven't unsubscribed from them, they will keep listening forever.
My auth is based on NGRX
so when the page starts loading I got all roles and
then get logged in.
but when I start using route guard,
route gourd start working before user data get loading
how can I wait for user load action to be done then start using canActivate
I try below solution but it's not working
export class AuthGuard implements CanActivate, OnDestroy {
private unsubscribe: Subject<any>;
constructor(private store: Store<AppState>, private router: Router,
private alertService: ToastrService, private authService: AuthService) {
this.unsubscribe = new Subject();
}
ngOnDestroy(): void {
this.unsubscribe.next();
this.unsubscribe.complete();
}
getFromStoreOrAPI(): Observable<any> {
return this.store.pipe(
select(isUserLoaded),
tap((data: boolean) => {
if (!data) {
this.store.dispatch(new UserRequested());
}
}),
take(1)
);
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.getFromStoreOrAPI().pipe(
switchMap(() =>
this.store.pipe(
select(isLoggedIn),
map(loggedIn => {
if (!loggedIn) {
this.router.navigateByUrl('/auth/login');
return false;
} else {
this.store.pipe(
select(currentUserRoles),
map((userRoles: Role[]) => {
//.......
}),
takeUntil(this.unsubscribe)
).subscribe();
}
}),
)
),
catchError(() => this.router.navigateByUrl('/auth/login'))
);
}
}
You can use filter to wait until loaded flag is true.
Here is approach I took with my auth.guard.ts :
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> {
return this.authFacade.loaded$.pipe(
filter(loaded => !!loaded),
mergeMap(() => this.authFacade.userAccount$),
map(userAccount => {
if (!userAccount) this.authFacade.redirectLoginPage(state.url);
return !!userAccount;
}),
first()
);
}
In my case, main app component is dispatching an action CheckAuth to check if user is already authenticated, and then set loaded flag.
It should work with some adaptation for your need. But main difference is the use of filter which avoid to continue the workflow if user checking is not done, and force waiting for the value.
Of course, be sure to set loaded value in all the case after receiving response (authenticated or not), or in case of any error.
Here is a potential adaptation for your case :
authLoaded$ = this.store.pipe(select(authLoaded));
authAccount$ = this.store.pipe(select(authAccount));
canActivate(...) {
return userLoaded$.pipe(
tap(loaded => {
if (!loaded) {
this.store.dispatch(new UserRequested());
}
}),
filter(loaded => !!loaded),
mergeMap(() => authAccount$),
map(authAccount => {
if (!authAccount.loggedIn) {
this.router.navigateByUrl('/auth/login');
return false;
}
if (!authAccount.roles?.length) {
this.router.navigateByUrl('/auth/forbidden');
return false;
}
// do some extra stuff...
return true;
}),
first()
);
}
I renamed isUserLoaded to authLoaded to clearly indicate the status of authentication loading (you can use also ready or not for instance). But not necessary user.
I created also a new selector authAccount which returns an object with at least 2 things :
loggedIn : true/false if user is logged in
roles: array of user roles.
But you can add of course user property, with user details.
This is a composed selector from different parts of your state.
With it, your code is more clear and maintable, you receive a complete status of your current authentication user.
Maybe some typos is possible, I wrote the code directly in my answer without testing it.
Hope this will help you.
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);
})
)
);
};
Imagine we have the following factory:
#Injectable()
export class DataClassFactory {
constructor(
private dataService: DataService,
) { }
public createThing(initialData?: InitialData): AsyncSubject<DataClass> {
let dataClass: AsyncSubject<DataClass> = new AsyncSubject<DataClass>();
if (!!initialData) {
dataClass.next(new DataClass(initialData));
dataClass.complete();
} else {
this.dataService.getData().subscribe((dataResponse) => {
dataClass.next(new ReportRequest(dataResponse));
dataClass.complete();
});
}
}
return dataClass;
}
}
We inject this factory, invoke the createThing method, and subscribe to the response in some component. I originally tried to use a plain Subject, but then I realized that in the case where we already have initial data, next() is called before the response is returned, so the subscriber in the component never gets that value.
My question is: is this correct situation in which to use an AsyncSubject, or is there a different/better way to handle this sort of method that has potential synchronous and asynchronous timelines?
I would do something along these lines
public createThing(initialData?: InitialData): Observable<DataClass | ReportRequest> {
if (!!initialData) {
const data = new DataClass(initialData);
return of(data);
} else {
return this.dataService.getData()
.pipe(map(dataResponse => new ReportRequest(dataResponse));
}
}
Whoever calls createThing would get an Observable to which it would have to subscribe.
This Observable would emit an instance of DataClass if initialData is not null, otherwise it would return and instance of ReportRequest as soon as dataService responds.
Current routing configuration:
//...
{
path: 'foo/:id',
component: SomeComponent,
canActivate: [SomeGuard]
},
//...
Then in guard I call permission service to get access for component:
#Injectable()
export class SomeGuard implements CanActivate {
constructor(private service: Service) {
}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
const id = parseInt(next.params['id']);
return this.service.getPermissions(id).then(permissions => {
if (permissions.canView) {
return true;
}
return false;
});
}
}
But in the component I utilize the same permissions endpoint, which means I call it twice in a row to get to one page:
//...
constructor(private route: ActivatedRoute,
private service: Service) {
}
ngOnInit() {
this.id = parseInt(this.route.snapshot.params['id']);
this.service.getPermissions(this.id).then(permissions => {
// ...
});
}
//...
So it would be great to just save the permissions data in the route and utilize it by both guard and the component. I tried using resolve, but it turns out resolve only activates after the guards, which is no good. So how can i save permissions data?
This looks like the kind of task for a caching service. Permissions do not change often so they are the perfect candidate for caching. That way even multiple visits to the same resource would not trigger multiple HTTP requests for permission checks.
Edit: Since you need permissions to be loaded each time, you could listen for RouteNavigationStart and clear the cache. If this becomes too cumbersome to maintain in the PermissionsService you could extract the logic into a separate service.
You could something like this in the service you use to get your permissions:
// Permissions service
private permissionCache;
constructor(
router: Router,
) {
// clear cache when a route navigation starts
router.events
.filter(event => event instanceof NavigationStart)
.subscribe(event => this.permissionCache = {})
}
getPermissions(id) {
if (permissionCache[id]) {
return Observable.of(permissionCache[id]);
} else {
// get the permissions
permissionCache[id] = response;
return Observable.of(response);
}
});