How do I wait for users to click on certain buttons in component, then return a value from that in a function depending on which button was pressed? - javascript

I have an overlay component that appears when a user clicks on certain things in my page, and in this overlay it gives a warning and 2 buttons, one for yes and the other for no. What I want is to create a function that'll serve this component, and then it will wait for the user to respond, and subsequently return true or false based on what button was pressed. This boolean result can then be used to further progress to other code.
This is what I have tried already. It uses promises rather than rxjs observables.
A component will call this function to bring the overlay from the service, eg this.service.promptUser().then(res => if (res === true) { doSomething() }).
In the service:
didContinue: boolean = null;
async promptUser() {
this.showOverlay.next(true) //BehaviourSubject when true brings the popup
await waitForUser();
const decision = this.didContinue;
this.closeOverlay(); //sets didContinue back to null
return decision
}
The didContinue is a property inside of the service to indicate whether they have clicked yes or no using a boolean. Otherwise it will remain null. The click events from the overlay component will set the property didContinue to true or false.
The waitForUser function to wait for the user's input:
async waitForUser() {
while (this.didContinue === null) {setTimeout(() => {}, 50};
}
Currently it'll get stuck at the waitForUser() function but the popup will have not rendered at that stage, so the user can't input anything, the didContinue property will never change, and the application will freeze.
Please do send it forward if you know of an existing solution, I miss a lot of things with my google-foo. I am currently still new to Angular.

Create a service that will handle the result
inject service in your component where you want to use it like this
constructor(public popService: popService) { } // create functions accordingly in service
Call service method with the help of constructor on click() event like this
<button (click)="popService.success()">Done
<button (click)="popService.cancel()">Done
i hope this will help, let me know if you need further help :-)

Related

Prompt with callback when user tries to exit

I have a page which in it there is a Webrtc call between users.
And i want that when a user tries to go to another page/reload page/exit browser, there will be a prompt asking him if he's sure that he wants to leave the page, and if the user presses "yes", i want there to be a callback.
In the callback(not sure if relevant) i would close the peers and disconnect everything properly so that the call will get disconnected and the user won't hear audio anymore and the other side will know the user got disconnected also.
To do this on refresh i saw the window.onunloaded function
Is it even possible on browser exit?
I saw the Prompt option to do this on route change but i didn't see how to insert a callback on confirmation there.
I will be happy to get one working example as it feels to complicated for such a little thing which is weird..
Thanks.
Handling React's navigation events
I think what you're looking for is the getUserConfirmation prop of BrowserRouter component.
The function passed to the prop might look like (TypeScript example):
const getUserConfirmation = (
message: string,
callback: (ok: boolean) => void
): void => {
const answer = window.confirm("Do you want to navigate away?");
// do something depending on which button was pressed
callback(answer); // allow or disallow React's navigation
};
In the place where you define your routes:
<BrowserRouter getUserConfirmation={getUserConfirmation}>
... routes go here ...
</BrowserRouter>
You'll still need to use <Prompt>, though:
<Prompt
when={boolean}
message="This string will be supplied in the confirmation function above as second parameter"
/>
Handling browser refresh
You might want to add a callback like this
window.onbeforeunload = () => {
// some non-blocking logic
return true;
};
After you no longer need it, you can cleanup:
window.onbeforeunload = null;
Looking at this SO question, it doesn't seem to be possible out of the box to capture the user's response to the native confirm window in onbeforeunload.
Probably window.onunload would be the right place to clean up your stuff as it most probably means the user has opted to leave the page.

Angular4 - how to ensure ngOnDestroy finishes before navigating away

I have a list of objects. The user can click on one, which then loads a child component to edit that component.
The problem I have is that when the user goes back to the list component, the child component has to do some cleanup in the ngOnDestroy method - which requires making a call to the server to do a final 'patch' of the object. Sometimes this processing can be a bit slow.
Of course what happens is the user arrives back on the list, and that api call completes before the database transaction from the ngOnDestroy completes, and thus the user sees stale data.
ngOnDestroy(){
this.destroy$.next();
this.template.template_items.forEach((item, index) => {
// mark uncompleted items for deletion
if (!item.is_completed) {
this.template.template_items[index]['_destroy'] = true;
};
});
// NOTE
// We don't care about result, this is a 'silent' save to remove empty items,
// but also to ensure the final sorted order is saved to the server
this._templateService.patchTemplate(this.template).subscribe();
this._templateService.selectedTemplate = null;
}
I understand that doing synchronous calls is not recommended as it blocks the UI/whole browser, which is not great.
I am sure there are multiple ways to solve this but really don't know which is the best (especially since Angular does not support sync requests so I would have to fall back to standard ajax to do that).
One idea I did think of was that the ngOnDestroy could pass a 'marker' to the API, and it could then mark that object as 'processing'. When the list component does its call, it could inspect each object to see if it has that marker and show a 'refresh stale data' button for any object in that state (which 99% of the time would only be a single item anyway, the most recent one the user edited). Seems a bit of a crap workaround and requires a ton of extra code compared to just changing an async call to a sync call.
Others must have encountered similar issues, but I cannot seem to find any clear examples except this sync one.
EDIT
Note that this child component already has a CanDeactive guard on it. It asks the user to confirm (ie. discard changes). So if they click to confirm, then this cleanup code in ngOnDestroy is executed. But note this is not a typical angular form where the user is really 'discarding' changes. Essentially before leaving this page the server has to do some processing on the final set of data. So ideally I don't want the user to leave until ngOnDestroy has finished - how can I force it to wait until that api call is done?
My CanDeactive guard is implemented almost the same as in the official docs for the Hero app, hooking into a general purpose dialog service that prompts the user whether they wish to stay on the page or proceed away. Here it is:
canDeactivate(): Observable<boolean> | boolean {
console.log('deactivating');
if (this.template.template_items.filter((obj) => { return !obj.is_completed}).length < 2)
return true;
// Otherwise ask the user with the dialog service and return its
// observable which resolves to true or false when the user decides
return this._dialogService.confirm('You have some empty items. Is it OK if I delete them?');
}
The docs do not make it clear for my situation though - even if I move my cleanup code from ngOnDestroy to a "YES" method handler to the dialog, it STILL has to call the api, so the YES handler would still complete before the API did and I'm back with the same problem.
UPDATE
After reading all the comments I am guessing the solution is something like this. Change the guard from:
return this._dialogService.confirm('You have some empty items.
Is it OK if I delete them?');
to
return this._dialogService.confirm('You have some empty items.
Is it OK if I delete them?').subscribe(result => {
...if yes then call my api and return true...
...if no return false...
});
As you said, there are many ways and they depend on other details how your whole app, data-flow and ux-flow is setup but it feels like you might want to take a look at CanDeactivate guard method which ensures user cannot leave route until your Observable<boolean>|Promise<boolean> are resolved to true.
So, its a way for async waiting until your service confirms things are changed on server.
[UPDATE]
it depends on your user confirmation implementation but something along these lines...
waitForServiceToConfirmWhatever(): Observable<boolean> {
return yourService.call(); //this should return Observable<boolean> with true emitted when your server work is done
}
canDeactivate(): Observable<boolean> {
if(confirm('do you want to leave?') == true)
return this.waitForServiceToConfirmWhatever();
else
Observable.of(false)
}
One "workaround" I can think of is to have your list based in client. You have the list as a JS array or object and show the UI based on that. After editing in the details screen, have a stale flag on the item which the service called on ngOnDestroy clears while updating the other related data.

ngRx state update and Effects execution order

I have my own opinion on this question, but it's better to double check and know for sure. Thanks for paying attention and trying to help. Here it is:
Imagine that we're dispatching an action which triggers some state changes and also has some Effects attached to it. So our code has to do 2 things - change state and do some side effects. But what is the order of these tasks? Are we doing them synchronously? I believe that first, we change state and then do the side effect, but is there a possibility, that between these two tasks might happen something else? Like this: we change state, then get some response on HTTP request we did previously and handle it, then do the side effects.
[edit:] I've decided to add some code here. And also I simplified it a lot.
State:
export interface ApplicationState {
loadingItemId: string;
items: {[itemId: string]: ItemModel}
}
Actions:
export class FetchItemAction implements Action {
readonly type = 'FETCH_ITEM';
constructor(public payload: string) {}
}
export class FetchItemSuccessAction implements Action {
readonly type = 'FETCH_ITEM_SUCCESS';
constructor(public payload: ItemModel) {}
}
Reducer:
export function reducer(state: ApplicationState, action: any) {
const newState = _.cloneDeep(state);
switch(action.type) {
case 'FETCH_ITEM':
newState.loadingItemId = action.payload;
return newState;
case 'FETCH_ITEM_SUCCESS':
newState.items[newState.loadingItemId] = action.payload;
newState.loadingItemId = null;
return newState;
default:
return state;
}
}
Effect:
#Effect()
FetchItemAction$: Observable<Action> = this.actions$
.ofType('FETCH_ITEM')
.switchMap((action: FetchItemAction) => this.httpService.fetchItem(action.payload))
.map((item: ItemModel) => new FetchItemSuccessAction(item));
And this is how we dispatch FetchItemAction:
export class ItemComponent {
item$: Observable<ItemModel>;
itemId$: Observable<string>;
constructor(private route: ActivatedRoute,
private store: Store<ApplicationState>) {
this.itemId$ = this.route.params.map(params => params.itemId);
itemId$.subscribe(itemId => this.store.dispatch(new FetchItemAction(itemId)));
this.item$ = this.store.select(state => state.items)
.combineLatest(itemId$)
.map(([items, itemId]: [{[itemId: string]: ItemModel}]) => items[itemId])
}
}
Desired scenario:
User clicks on itemUrl_1;
we store itemId_1 as loadingItemId;
make the request_1;
user clicks on itemUrl_2;
we store itemId_2 as loadingItemId;
switchMap operator in our effect cancells previous request_1 and makes request_2;
get the item_2 in response;
store it under key itemId_2 and make loadingItemId = null.
Bad scenario:
User clicks on itemUrl_1;
we store itemId_1 as loadingItemId;
make the request_1;
user clicks on itemUrl_2;
we store itemId_2 as loadingItemId;
we receive the response_1 before we made the new request_2 but after loadingItemId changed;
we store the item_1 from the response_1 under the key itemId_2;
make loadingItemId = null;
only here our effect works and we make request_2;
get item_2 in the response_2;
try to store it under key null and get an error
So the question is simply if the bad scenario can actually happen or not?
So our code has to do 2 things - change state and do some side
effects. But what is the order of these tasks? Are we doing them
synchronously?
Let's say we dispatch action A. We have a few reducers that handle action A. Those will get called in the order they are specified in the object that is passed to StoreModule.provideStore(). Then the side effect that listens to action A will fire next. Yes, it is synchronous.
I believe that first, we change state and then do the side effect, but
is there a possibility, that between these two tasks might happen
something else? Like this: we change state, then get some response on
HTTP request we did previously and handle it, then do the side
effects.
I've been using ngrx since middle of last year and I've never observed this to be the case. What I found is that every time an action is dispatched it goes through the whole cycle of first being handled by the reducers and then by the side effects before the next action is handled.
I think this has to be the case since redux (which ngrx evolved from) bills itself as a predictable state container on their main page. By allowing unpredictable async actions to occur you wouldn't be able to predict anything and the redux dev tools wouldn't be very useful.
Edited #1
So I just did a test. I ran an action 'LONG' and then the side effect would run an operation that takes 10 seconds. In the mean time I was able to continue using the UI while making more dispatches to the state. Finally the effect for 'LONG' finished and dispatched 'LONG_COMPLETE'. I was wrong about the reducers and side effect being a transaction.
That said I think it's still easy to predict what's going on because all state changes are still transactional. And this is a good thing because we don't want the UI to block while waiting for a long running api call.
Edited #2
So if I understand this correctly the core of your question is about switchMap and side effects. Basically you are asking what if the response comes back at the moment I am running the reducer code which will then run the side effect with switchMap to cancel the first request.
I came up with a test that I believe does answer this question. The test I setup was to create 2 buttons. One called Quick and one called Long. Quick will dispatch 'QUICK' and Long will dispatch 'LONG'. The reducer that listens to Quick will immediately complete. The reducer that listens to Long will take 10 seconds to complete.
I setup a single side effect that listens to both Quick and Long. This pretends to emulate an api call by using 'of' which let's me create an observable from scratch. This will then wait 5 seconds (using .delay) before dispatching 'QUICK_LONG_COMPLETE'.
#Effect()
long$: Observable<Action> = this.actions$
.ofType('QUICK', 'LONG')
.map(toPayload)
.switchMap(() => {
return of('').delay(5000).mapTo(
{
type: 'QUICK_LONG_COMPLETE'
}
)
});
During my test I clicked on the quick button and then immediately clicked the long button.
Here is what happened:
Quick button clicked
'QUICK' is dispatched
Side effect starts an observable that will complete in 5 seconds.
Long button clicked
'LONG' is dispatched
Reducer handling LONG takes 10 seconds. At the 5 second mark the original observable from the side effect completes but does not dispatch the 'QUICK_LONG_COMPLETE'. Another 5 seconds pass.
Side effect that listens to 'LONG' does a switchmap cancelling my first side effect.
5 seconds pass and 'QUICK_LONG_COMPLETE' is dispatched.
Therefore switchMap does cancel and your bad case shouldn't ever happen.

Error when using property that relies on ViewChildren

I have created a custom component that contains a form <address></address>. And I have a parent component that has an array of these:
#ViewChildren(AddressComponent) addressComponents: QueryList<AddressComponent>;
So the parent can contain a collection of these elements and the user can add and remove them based on the number of addresses they will be entering.
The parent also has a button to proceed after the user has entered all desired addresses. However, the <address> component must be filled out correctly so I have a public getter on the <address> component:
get valid(): boolen {
return this._form.valid;
}
Back to the button on the parent. It needs to be disabled if any of the <address> components are invalid. So I wrote the following:
get allValid() {
return this.addressComponents && this.addressComponents.toArray().every(component => component.valid);
}
And in the parent template:
<button [disabled]="!allValid" (click)="nextPage()">Proceed</button>
But angular doesn't like this because addressComponents are not defined in the parent until ngAfterViewInit lifecycle event. And since it immediately runs ngOnViewInit() I get two different values for the expression check which causes the error. (At least that's what I think is going on).
How do I use a property in my template that depends on ngAfterViewInit? Or what is the best way to inform my parent that all of its children are valid?
The Error Message:
Expression has changed after it was checked. Previous value: 'false'.
Current value: 'true'
Update:
So I console.loged the return value of allValid and noticed the first time it was undefined. This was to be expected as this.addressComponents are undefined until ngAfterInit. The next log it was true and this was surprising as I didn't have any <address> components on the page (yet). I am using mock data (all valid, though) in ngOnInit of the parent component to create a component. I did learn that ([].every... returns true on an empty array). So the third call to the console.log was returning false. Again, I am a little surprised because all my data is valid. On the 4th log it was returning true which is what I expected. So I'm assuming this final value being returned is what Angular disliked.
Anyway, I was able to sort of solve this. I don't know if I'm actually fixing the problem or just suppressing the error. I do not like this solution so I am going to keep the question open for a better solution.
get allValid() {
return this.addressComponents && this.addressComponents.length > 0 && this.addressComponents().toArray().every(component => component.valid);
}
So what I think is happening:
The first wave of change detection gets you a false for your function, then your parent component finds out this information after the view is instantiated (then returns true). In "dev" mode, Angular runs change detection twice to ensure that changes don't happen AFTER change detection (as change detection should detect all of the changes, of course!)
According to the answer found here:
Angular2 - Expression has changed after it was checked - Binding to div width with resize events
using AfterViewInit can cause these issues, as it may run after the change detection has completed.
Wrapping your assignment in a timeout will fix this, as it will wait a tick before setting the value.
ngAfterViewInit(){
setTimeout(_ => this.allValid = this.addressComponents && this.addressComponents.toArray().every(component => component.valid));
}
Due to these reasons, I would not use a getter on a template variable like that, as the view initializing may change the value after change detection has finished.
If I understand, you'll probably need to come up with a way in the parent to track how many instances of the child there are, and the child will need an EventEmitter that informs the parent when it's valid or becomes invalid.
So in the parent you could use an array to track how many address instances there are..
Parent Component
addressForms: Array<any> = [{ valid: false }];
addAddressForm() {
this.addressForms.push({ valid: false ));
}
checkValid() {
// somehow loop through the addressForms, make sure all valid
let allValid: boolean = false;
for (var i = this.addressForms.length - 1; i >= 0; i--) {
if (this.addressForms[i].value && allValid === false)
allValid = true;
}
return allValid;
}
Parent Template
<div *ngFor="let form of addressForms; let i = index">
<address (valid)="form.valid = true" (invalid)="form.valid = false"></address>
</div>
<button [disabled]="checkValid()">Next</button>
Address Component
#Output() valid: EventEmitter<any> = new EventEmitter();
#Output() invalid: EventEmitter<any> = new EventEmitter();
isValid: boolean = false;
check() {
// call this check on field blurs and stuff
if ("it's valid now" && !this.isValid) {
this.isValid = true;
this.valid.emit(null);
}
if ("it's not valid anymore" && this.isValid) {
this.isValid = false;
this.invalid.emit(null);
}
}
That's the basic idea anyway, with some holes that are obvious enough to fill in. Hope that has some relevancy with what you're doing and I understood the question to begin with. Good luck!
I faced the same issue when have been using that realy handy pattern :( Only short way I found atm to solve it is the next kind of a hack:
#ViewChild(DetailsFormComponent) detailsForm: DetailsFormComponent;
isInitialized = false;
get isContinueBtnEnabled(): boolean {
return this.isInitialized && this.detailsForm?.isValid();
}
and
ngAfterViewInit() {
setTimeout(() => { // required
this.isInitialized = true;
});
}

How can I ask users to confirm before leaving the page on the Aurelia.js

I have an long operation on the page. How can I said user wait a bit when he make the navigation change?
The idea is to show the promt dialog. But the trouble prevent default Aurelia router behavior.
How make it works in Aurelia.js?
You could use the canDeactivate hook of your view-model. Like this:
canDeactivate() {
return confirm('Are you sure you want to leave this page?');
}
canDeactivate() - Implement this hook if you want to control whether or not the router can navigate away from your view-model when moving to a new route. Return a boolean value, a promise for a boolean value, or a navigation command.
More information at http://aurelia.io/docs.html#/aurelia/framework/1.0.0-beta.1.2.2/doc/article/cheat-sheet/7

Categories

Resources