I have a shopping website made with Angular where you can either delete items from the same products page, or modify them by being sent to a form in another page, and then navigate back to products.
In both cases you have to manually reload the page to show the updated results, is there any way to avoid this and have it update dynamically?
These are the key pieces of code that i have right now:
Products page:
ngOnInit(): void {
this.productsService.getProducts().subscribe((res: any) => {
this.products = res;
})
}
edit(id: string){
this.productsService.getProductById(id).subscribe((res: any) => {
this.productsService.product = res;
this.router.navigate(['/edit']);
});
}
delete(id: string) {
this.productsService.deleteProduct(id).subscribe((res: any) => {
this.ngOnInit();
});
}
Products service:
getProducts(){
return this.http.get(this.url);
}
addProduct(newproduct: product){
return this.http.post(this.url, newproduct);
}
getProductById(id: string){
return this.http.get(this.url + '/' + id);
}
editProduct(newproduct: product){
return this.http.put(this.url + '/' + newproduct.id, newproduct);
}
deleteProduct(id: string){
return this.http.delete(this.url + '/' + id);
}
Edit page after you submit the new/edited product:
this.productRegisterForm.reset();
this.isSubmit = false;
this.router.navigate(['/products'])
I tried to use window.location.reload in many places (it would loop on the products page onInit, it wouldn't do anything on the Form page with a .then after the navigate and it wouldn't fetch the products correctly if added on the Products service (i wasn't able to in many cases)
The search term to research this is RxJs, reactive coding. Recommend looking up async pipe and learning more about observables.
Because you are updating state in your application you need to save the data somehow and replicate deletes and adds locally on this copy of the data. The simplest is a repository on the service you’ve created I.e. an array/object (internal management hidden and up to you) of products you maintain, when you add via api, you add to array etc. Using a RxJs subject and .next method you can expose a RxJs (behavioursubject) variable that emits for each of these changes.
In short, you change this,
ngOnInit(): void {
this.productsService.getProducts().subscribe((res: any) => {
this.products = res;
})
}
to this
products$: Observable<IProduct[]> = of()
ngOnInit(): void {
this.products$ = this.productsService.products$
this.productsService.getAll()
}
and then use this in the template using the async pipe
{{ products$ | async | json }}
If you want to reach for a library then ngrx is one of the most popular state libraries.
Edit,
You could have a separate repository service and inject into productService but easier to put on the service. You can always refactor later. e.g. https://stackoverflow.com/a/57355485/4711754
E.g.
<ng-container *ngFor="let product of (products$ | async)">
{{ product | json }}
</ng-container>
Brackets not needed, for clarity only, and you can replace ng-container with div or custom ui component.
Related
I am trying to display a routerlink name based on a condition. I want to display the div section routerLink name if condition is true.If i check {{isNameAvailable}}, first it displays false and after this.names got the values it shows true.Since in the component getDetails() method is asynchronous this.names getting the values after html template render.Therefore this routerLink does n't display.Therefore I want to display div section after some time. (That 's the solution i have) Don't know whether is there any other solution.
This is my html file code.
<main class="l-page-layout ps-l-page-layput custom-scroll bg-white">
{{isNameAvailable}}
<div class="ps-page-title-head" >
<a *ngIf ="isNameAvailable === true" [routerLink]="['/overview']">{{Name}}
</a>
{{Name}}
</div>
</main>
This is my component.ts file
names= [];
isNameAvailable = false;
ngOnInit() {
this.getDetails()
}
getDetails() {
this.route.params.subscribe(params => {
this.names.push(params.Names);
console.log(this.names);
this.getValues().then(() => {
this.isNameAvailable = this.checkNamesAvailability(this.names);
console.log(this.isNameAvailable);
});
});
}
resolveAfterSeconds(x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 900);
});
}
checkNamesAvailability(names) {
console.log(names);
return names.includes('Sandy');
}
async getValues() {
await this.resolveAfterSeconds(900);
}
And console.log(this.isLevelAvailable); also true. What I can do for this?
1.You do not have anything to show in the HTML only the isNameAvailable, because you do not have any assignment in the Name variable.
2.It is better to use the angular build-in async pipe,
when you want to show the returned value from observables.
3.When you are using the *ngIf directive you can skip *ngIf ="isNameAvailable === true" check because the variable is boolean type, you gust write *ngIf ="isNameAvailable", it will check also for null but NOT for undefined
It is working because the *ngIf directive is responsible for checking and rendering the UI, you can see how many times the directive is checking by calling an function and print and answer in the console.
By any chance do you have changeDetection: ChangeDetectionStrategy.OnPush docs set in component annotation? That might explain this behaviour. With it Angular run change detection only on component #Input()'s changes and since in your case there were non it did not run change detection which is why template was not updated. You could comment that line to check if that was cause of the issue. You are always able to run change detection manually via ChangeDetectorRef.detectChange() docs which should solve you problem
constructor(private cd: ChangeDetectorRef) {}
...
getDetails() {
this.route.params.subscribe(params => {
...
this.getValues().then(() => {
this.isNameAvailable = this.checkNamesAvailability(this.names);
this.cd.detectChanges(); // solution
console.log(this.isNameAvailable);
});
});
}
This stackblitz show this bug and solution. You can read more about change detection here
You could use RxJS timer function with switchMap operator instead of a Promise to trigger something after a specific time.
Try the following
import { Subject, timer } from 'rxjs';
import { takeUntil, switchMap } from 'rxjs/operators';
names= [];
isNameAvailable = false;
closed$ = new Subject();
ngOnInit() {
this.getDetails()
}
getDetails() {
this.route.params.pipe(
switchMap((params: any) => {
this.names.push(params.Names);
return timer(900); // <-- emit once after 900ms and complete
}),
takeUntil(this.closed$) // <-- close subscription when `closed$` emits
).subscribe({
next: _ => {
this.isNameAvailable = this.checkNamesAvailability(this.names);
console.log(this.isNameAvailable);
}
});
}
checkNamesAvailability(names) {
console.log(names);
return names.includes('Sandy');
}
ngOnDestroy() {
this.closed$.next(); // <-- close open subscriptions when component is closed
}
I implemented a infinite scroll and as part of this, I am triggering an API endpoint to get results in batches depending on the element position(used intersectionObserver). So in this case i would be hitting same API multiple times with different intervals. Sometime when scrolling fast i see the calls triggering very fast and view not rendering as expected. Is there a way in RXJS/Angular i can merge these responses OR wait for the first call to complete to trigger before initiating second call ? OR any ideas/suggestions how can i deal with situation ?
export class {
#Output() eventscroll = new EventEmitter();
#ViewChild('iscroll')
iscroll: ElementRef<HTMLElement>;
#Component({
selector: 'myinfinitescroll',
template: `
<div #iscroll></div>
<ng-content></ng-content>
`,
});
ngAfterViewInit() {
this.zone.runOutsideAngular(() => {
this.iob.observe(this.iscroll.nativeElement);
})
}
this.iob = new IntersectionObserver(this.handleScroll(),
{ root: null, rootMargin: '0px',threshold: 0 });
handleScroll([entry]){
if (entry.isIntersecting && entry.intersectionRatio >= 0) {
this.eventscroll.emit(); ///api call happens here, currently it is just a simple HTTP post call
}
}
****Edit1****
myservicemethod(a, b, c): void {
const reqdata = {
p1 : a
p2 : b
p3 : c
};
const debounceWindow = 1000;
this._callAPI$.pipe(
concatMap((data): Observable<any> => this.myAPI.getData(reqdata)),
bufferTime(debounceWindow),
map((resp: Array<Array<any>>) => resp.reduce((acc, item) => acc = [...acc, ...item], []))
).subscribe((resp)=>{
if (resp) {
this.updateRecords();
}
})
}
I am calling this.myservice.myservicemethod on component constructor and myservice._callAPI$.next($event) on component HTML
Well, I think the solution isn't related to the code you post, in the sense that you should emit any way (you cannot debounce your emission or some data will be missing and the scroll will have some weird holes of missing data).
So, someone will be using your component like this:
<myinfinitescroll (eventscroll)="_callAPI$.next($event)">
<my-panel [data]="_data$ | async"></my-panel>
</myinfinitescroll>
So, let's suppose your data comes in chunks and each chunk contains an array of data and you want to buffer the responses for sometime before emits them in a single array, using you service method:
grabSomeData(params: any): Observable<any[]>;
So, in the component typescript using you infinite scroll you'd have to do a few things:
Make the calls and receive the responses from the API in order
Buffer them for a while, let's say, 1 second.
Reemit all the received arrays of data as a single array
Stackblitz demo
import {BehaviorSubject, Observable, Subject} from 'rxjs';
import {bufferTime, concatAll, concatMap} from 'rxjs/operators';
_data$ = new BehaviorSubject<any[]>([]);
_callAPI$ = new Subject<any>();
constructor(private myService: MyService) {
this.scrollObservablesInit();
}
scrollObservablesInit() {
// time window to buffer arriving data before reemits it
const debounceWindow = 1000;
this._callAPI$.pipe(
// make the calls and receive the answers to the API
concatMap((params) => this.myService.grabSomeData(params)),
// buffer the arriving data for 1 second before reemit everything
// in the buffer
bufferTime(debounceWindow),
// from above, now you get an array of arrays emitted
// by bufferWindow and we need to flatten it
// obs.: there's an Array.prototype.flat operator
// on the recent versions on the ecma script, but
// let's not count on it here
map((data: Array<Array<any>>) => data
.reduce((acc, item) => acc = [...acc, ...item], []))
).subscribe(this._data$)
}
I created a service and I try call API method (HTTP GET) and I put my data in Observable, I don't understand why I don't see all data(object) from API GET.
angular-component.ts
public allUsers$: Observable<User[]>;
constructor(private usersService: UsersService) { }
ngOnInit() {
this.allUsers$ = this.getAllUsers();
console.log(this.allUsers$)
}
private getAllUsers(): Observable<User[]> {
return this.usersService.getUsers();
}
In console I have this message:
users.service.ts
public getUsers(): Observable<User[]> {
return this.apiService.get(this.type) as Observable<User[]>;
}
api.service.ts
public get(url: string): Observable<any> {
return this.http.get(environment.apiUrl + `/${url}`);
}
nodejs-route.js
app.get("/", async (req, res) => {
const getAllUsers = await User.find().populate("orders.order_id");
res.status(200).send(getAllUsers);
});
Always keep in mind that an Observable does nothing.
As the lead RxJS developer, Ben Lesh, once said:
Observables themselves are enert. They don't stream anything or do
anything. They are templates for streaming/actions/observations that
will be set up on a subscription.
And there are two basic ways to subscribe:
With an async pipe.
Using the subscribe method.
The async pipe in a template AUTOMATICALLY subscribes and unsubscribes for you.
So in your template, you'd have something like this:
<div class="card"
*ngIf="allUsers$ | async as users">
Then you will be able to access users in your template, such as in an *ngFor.
However, using an async pipe makes it a bit more difficult to access the data in your component code. So you can NOT just do this to see your data:
console.log(this.allUsers$)
All that will give you is information on the Observable, as you saw.
The other option is to subscribe in your component:
sub: Subscription
users: User[]
ngOnInit() {
this.sub = this.getAllUsers().subscribe(
users => {
this.users = users;
console.log(users);
);
}
The subscribe() method returns a Subscription that you can then use to manually unsubscribe.
You will then have an array of users User[], NOT an Observable<User[]> as your component property. Your template can then bind to this array.
The first technique (async pipe) is normally the recommended approach.
I suppose that the answer will be very obvious, but still it evades me. I'm new on working with observables, and now I'm facing issues assigning a value from one. I had success if I define it (this._apps) as an Observable and asking from the view to the service using subscribe (But for my taste is was way convoluted (three levels inside a map just to return another observable with the array and then another function to subscribe the previous to assign the variable and another subscription in the view to finally show the information), inefficient and on top of that I could not get it "right" again). The task is very simple. Given the class Application
export class Application {
name: string;
baseUrl: string;
deprecated: boolean;
}
And the service (just the relevant code)
private _apps: Application[] = [];
constructor(private _http: HttpClient) {
this.getAllApplications().subscribe(apps => {
console.log('Apps subscriber');
this._apps = apps;
console.log('Apps subscriber Ends ' + apps);
},
err => {
console.log(err.status); // 401
console.log(err.error.error); // undefined
console.log(JSON.parse(err.error).error); // unauthorized
});
}
private getAllApplications() {
return this._http.get<Application[]>('http://development:4300/api/v1/apps');
}
From the constructor the function which gets the information from WebAPI is triggered, and the remote call is successful, but the variable this._apps is an empty array if I try to call it from anywhere in the code. I could not determine the type of the parameter "apps" in the subscribe function, but for some reason it cannot be assigned and the best answer given is that it is a function (See my first update) in one of my tries. Currently it returns in the console "[object Object]", but apps[0] gives undefined, so it is an empty Array.
This is the console output, just starting the application:
Angular is running in the development mode. Call enableProdMode() to enable the production mode.
Refreshing apps cache calling http://development:4300/api/v1/atbc-apps
Apps subscriber
Apps subscriber Ends [object Object]
I was trying this solution among many others that I forget (to use the more modern HttpClient instead the Http I used before), so what I'm doing wrong?
Update 1
I changed the constructor to this:
constructor(private _http: HttpClient) {
this.getAllApplications().subscribe(apps => {
console.log('apps length ' + apps.length);
this._apps = apps; // Remember private _apps: Application[] = [];
console.log('Apps subscriber Ends ' + apps.toString);
},
err => {
console.log(err.status); // 401
console.log(err.error.error); // undefined
console.log(JSON.parse(err.error).error); // unauthorized
});
}
and the declaration of the function called into this:
private getAllApplications(): Observable<Application[]> {
// the exactly the same as before
}
And now I got from the console this:
apps length undefined
Apps subscriber Ends
function () {
if (this instanceof Promise) {
return PROMISE_OBJECT_TO_STRING;
}
return originalObjectToString.apply(this, arguments);
}
That is the function I was talking about. Any ideas about why even though there is no errors (nor at compile time, neither at runtime), the returning object is not a real Application array?
Change this line:
private _apps: Application[] = [];
to:
_apps: Application[] = [];
Which will default to making it public. Then this line will see it:
this._apps = apps;
At the end I suppose is a mindset to work with Observables, and I tried to build a kind of cache, so the only way I could do it (let me know if there is a better way) was using the view to fill-out the cache. I could not do it from the service itself because the calling the function from the view is synchronous and to fill out the array is async. So I had to create a public setApplicationCache procedure which is filled out after calling the service from the view, it call the setApplicationCache( Application[] ) function and the rest works because it takes just the cache to do filtering and other operations or use it from other pages w/o calling the database again and again.
This is the code from the first view called (main page)
ngOnInit() {
this._myService.getAllApplications().subscribe(arrObjApps => {
this._myService.setApplicationsCache(arrObjApps)
this.listApps = this._myService.getApplications(true);
});
And the service has this functions:
private _apps: Application[] = [];
getAllApplications(): Observable<Application[]> {
return this._http.get('http://development:4300/api/v1/atbc-apps').pipe(
map( (response: Response) => {
let results = response.json().data.map( app => {
return new Application(app.name, app.baseUrl, app.deprecated);
});
return results;
})
);
}
getApplication(appName: string): Application {
return this._apps.find(app => app.name == appName);
}
getApplications(onlyActives: boolean): Application[] {
if (onlyActives) {
return this._apps.filter(app => app.deprecated == false);
} else {
return this._apps;
}
}
And as I stated the solution should be obvious. Just again the async mindset required to work with observables.
When using a generic modal or toast with a confirm button, it becomes useful to be able to pass an action into this component so it can be dispatched when you click confirm.
The action may look something like this:
export function showConfirm({modalConfirm}) {
return {
type: 'MODALS/SHOW_MODAL',
payload: {
modalId: getUuid(),
modalType: 'CONFIRM',
modalConfirm : modalConfirm,
},
};
}
Where modalConfirm is another action object such as:
const modalConfirm = {
type: 'MAKE_SOME_CHANGES_AFTER_CONFIRM',
payload: {}
}
The modalConfirm action is dispatched inside the modal component using dispatch(modalConfirm) or even dispatch(Object.assign({}, modalConfirm, someResultFromTheModal)
Unfortunatley this solution only works if modalConfirm is a simple redux action object. This system is clearly very limited. Is there anyway you can pass a function (such as a thunk) in instead of a simple object?
Ideally, something full featured likes this:
const modalConfirm = (someResultFromTheModal) => {
return (dispatch, getState){
dispatch({
type: 'MAKE_SOME_UPDATES',
payload: someResultFromTheModal
})
dispatch({
type: 'SAVE_SOME_STUFF',
payload: http({
method: 'POST',
url: 'api/v1/save',
data: getState().stuffToSave
})
})
}
}
Funny, putting an action object in the store and passing it as a prop to a generic dialog is exactly the approach I came up with myself. I've actually got a blog post waiting to be published describing that idea.
The answer to your question is "Yes, but....". Per the Redux FAQ at http://redux.js.org/docs/FAQ.html#organizing-state-non-serializable , it's entirely possible to put non-serializable values such as functions into your actions and the store. However, that generally causes time-travel debugging to not work as expected. If that's not a concern for you, then go right ahead.
Another option would be to break your modal confirmation into two parts. Have the initial modal confirmation still be a plain action object, but use a middleware to watch for that being dispatched, and do the additional work from there. This is a good use case for Redux-Saga.
I ended up using string aliases to an actions library that centrally registers the actions.
Modal emmiter action contains an object with functionAlias and functionInputs
export function confirmDeleteProject({projectId}) {
return ModalActions.showConfirm({
message: 'Deleting a project it permanent. You will not be able to undo this.',
modalConfirm: {
functionAlias: 'ProjectActions.deleteProject',
functionInputs: { projectId }
}
})
}
Where 'ProjectActions.deleteProject' is the alias for any type of complicated action such as:
export function deleteProject({projectId}) {
return (dispatch)=>{
dispatch({
type: 'PROJECTS/DELETE_PROJECT',
payload: http({
method: 'DELETE',
url: `http://localhost:3000/api/v1/projects/${projectId}`,
}).then((response)=>{
dispatch(push(`/`))
}),
meta: {
projectId
}
});
}
}
The functions are registered in a library module as follows:
import * as ProjectActions from '../../actions/projects.js';
const library = {
ProjectActions: ProjectActions,
}
export const addModule = (moduleName, functions) => {
library[moduleName] = functions
}
export const getFunction = (path) => {
const [moduleName, functionName] = path.split('.');
// We are getting the module only
if(!functionName){
if(library[moduleName]){
return library[moduleName]
}
else{
console.error(`Module: ${moduleName} could not be found.`);
}
}
// We are getting a function
else{
if(library[moduleName] && library[moduleName][functionName]){
return library[moduleName][functionName]
}
else{
console.error(`Function: ${moduleName}.${functionName} could not be found.`);
}
}
}
The modalConfirm object is passed in to the modal by props. The modal component requires the getFunction function in the module above. The modalConfirm object is transformed into a function as follows:
const modalConfirmFunction = (extendObject, modalConfirm) => {
const functionFromAlias = getFunction(modalConfirm.functionAlias);
if(functionFromAlias){
dispatch(functionFromAlias(Object.assign({}, modalConfirm.functionInputs, extendObject)));
}
}
As you can see, this function can take in inputs from the modal. It can execute any type of complicated action or thunk. This system does not break time-travel but the centralized library is a bit of a drawback.