I have a component that on a swipe will send an action upward to the parent route/controller to handle some ajax functionality. This component has some UI that gets set to a loading state to show the user things are happening. When working gets set back to false the loading animation in UI stops and user interaction can happen again.
callAjaxAction() {
this.setProperties({working:true});
Ember.RSVP.cast(this.attrs.action()).finally(() => {
this.$('.draggable').animate({
left: 0
});
this.setProperties({working:false});
});
}
In this case the controller catches the action specified on the component definition and calls an ajax function to get some data to display in the page
// in the controller action
return Ember.RSVP.Promise((resolve,reject) => {
Ember.$.ajax({
type: 'get',
dataType: 'json',
url: `http://***/api/paysources/user/697?key=${ENV.APP.api_key}`
}).then((response)=>{
this.setProperties({
'cards':response.user_paysources,
'showCards': true
});
},(reason)=>{
reject();
this.get('devlog').whisper(reason);
})
})
This will show a new popup type of component that will allow the user to pick a card to pay with. If the user clicks away, or if they click a card and the ajax action completes I need to reset the UI not only on this page (change it so it says the cart has been paid for) but also send the swipe component (the one that now has a loading animation) something that tells it it's done loading.
Basically, the way I see it in my head, is there a way to fire an action on a component from the parent controller/route?
To answer your question "is there a way to fire an action on a component from the parent controller/route?": No there is not. However there are two idiomatic ways I can think of for doing this in Ember:
You could potentially get around it by moving the Ajax request into the component.
Follow the 'data down, actions up' pattern. See the following as an example of how to implement it.
You could fire an action on the card component that changes a property on your controller. Then listen to that property into the original component so it can update.
original-component.js
reset: computed('transactionComplete', function() {
// cleanup stuff here...
})
original-component-template.hbs
{{#if transactionComplete}}
{{! stuff to show when transaction is complete... }}
{{/if}}
controller.js
transactionComplete: false,
actions: {
completeTransaction() {
this.toggleProperty('transactionComplete');
}
}
controller-template.hbs
{{original-component
transactionComplete=transactionComplete
}}
{{cart-component
transactionComplete=(action 'completeTransaction')
}}
cart-component.js
actions: {
processTransaction() {
// cleanup stuff here...
this.attrs.transactionComplete();
}
}
There might be different – and better – ways of doing this, but it depends on exactly what you need to do when resetting the original component.
Also, separately, have you considered using ember data and routing for loading the cards?
Related
I want to refresh my cardData array that gets its value by a get request from the database after I send a post request the the database. I have written this saveDraft() function that upon click adds values to cardData array but I need to refresh the page or click on it twice to see the changes. Is there a way to do it dynamically without refreshing the page?
saveDraft() {
Api.createComment({
// it's a post request
})
.then(res => {
if (response.status == 200) {
Api.getComments().then(res => {
// it's a get request to update the cardData
if (res.status === 200) {
this.cardData = res.data;
} else {
// catches error
}
});
} else {
// catches error
}
});
},
I can't exactly see how you've implemented the saveDraft() function but I'll work with what you gave!
To generally answer your question, I think it depends on what you mean by clicking on it twice or refreshing the page (i.e. 'clicking' meaning you've bound it to a user event such as #dblclick or #click?). But in Vue, you can handle dynamic events through the following:
1) Is this function in a child component? If yes, then consider using v-on directive to listen to user events (i.e. #dblclick, #click, #mouseover, etc.), emitting a custom event, or creating a watcher that 'updates' the data in a parent.
If this is confusing, you can check out the video by Vue Mastery on event handling that goes over the foundations of emitting events from the child component to the parent.
2) You can consider using Vuex as a state-manager. This can help with reactivity - of updating a component or piece of data being passed around through mutations and actions. Check out a general overview of Vuex and reactivity in Vue on Vue Mastery
If this still doesn't make sense, let me know here or update your question if needed :)
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.
I have a form and on submit I dispatch an action which is caught by an effect. The effect then does the http call. I'm wondering how off the back of this action completing / failing I would do the following:
show a success message once the action completes
reset all the fields ready for when the form is next used
show an error off the back of the action failing
I understand I could re-dispatch an action to populate the store with several flags success, error etc. However, resetting the form will probably be done by calling a function. Would it be acceptable to subscribe to the store and calling the relevant reset function in the child? It's almost as if I'd like components to be able to listen to actions just like effects can.
If your effect-rest does not affect the store, but only should display a notification - then there are two ways I'd say:
1) Inject Actions in your component as you suggested yourself:
class SomeComponent {
constructor(actions: Actions) {
actions.ofType(SOME_EVENT)
.do(myThing)
.takeUntil(componentIsDestroyed)
.subscribe();
}
}
2) Or don't go the effects- and actions-way at all and just call a simple service-method:
class SomeComponent {
constructor(myService: CoolService) {
}
onClick(): void {
mySevice.makeRequest()
.do(myThing)
.subscribe();
}
}
In my index route, I have an observer that fires an action when the user object is set in the session service, as shown below. This works fine- the console logs 'index route observer fired' and the action fires.
routes/index
session: Ember.inject.service(),
sendTheAction: function() {
console.log('index route observer fired');
this.send('checkLicense');
}.observes('session.user'),
actions: {
checkLicense: function() {
if (this.get('session.user.email)) {
//get the user's email and send off an AJAX request.
}
},
}
I also have a logout route which (among other things) sets session.user to an empty object, and then transitions to the login route. I simply use a link-to helper pointing to 'logout' to initiate this.
routes/logout
session: Ember.inject.service(),
actions: {
didTransition: function() {
this.set('session.user', {});
this.transitionTo('login');
}
}
If I am in the index route and I click logout, the transition to logout begins. Then, when session.user is set to {} in the logout route's didTransition hook, the observer in the index route fires. The console logs 'index route observer fired' but then I get a console error saying
Error while processing route: logout Nothing handled the action 'checkLicense'.
I'm not sure how the observer in the previous route can still be fired by a change in the logout route's didTransition hook, as I thought didTransition only fired when the transition was fully resolved.
I think that it is then looking for the action "checkLicense" in the logout route.
Can anyone clarify why that observer still fires, and how to get around this?
The observer still fires and this is the expected behavior. The reason is; routes are SINGLETONs; they are not destroyed upon transitions. So; if you define an observer within a route and watch for values that might be updated by other routes, components, controllers, etc. then your observer will still work!!! So; be careful about using observers within singletons (controllers, services, routest, etc.).
I created a twiddle that shows the exact case you mentioned. The valid question here in fact is "why is the send action within index.js is delegated to the other route?" Here is the answer from the Ember.js api. It says "Sends an action to the router, which will delegate it to the currently active route..." in send method definition. So; the error occurs; because current active route is not index anymore; action is not found!
So; what can you do? Why are throwing an action to yourself within index.js? Just make a normal function call!!! If you still need the action; you can also call the same function from within the action. See the modified twiddle in order to see what I mean.
To sum up; observers within singletons will work no matter what if the dependent key(s) get(s) updated! Hope; it is all clear now. Best Regards.
From what I've read the pattern is the Components pass data to the Actions which Pass to the Store whose value changes trigger updates in Components that subscribe to the Stores. My question is how to "react" to these triggered updates in the form of a notification? ( ie a successfully saved notification )
Ie do I add logic to the render of this notification component that only displays itself if there is a some flag attribute in the object that its subscribed to? Then deletes itself after a time. This sounds wrong.
UPDATE
Thanks to Hannes Johansson I think I have a better grasp of a pattern. What I have working is the following:
Component passes data through action to the Store
The Store interacts with the api and adds a flag to the model that the component is now notified of an updated model.
createItem: function (item) {
$.ajax({
url: '/items',
method: 'POST',
data: item,
success: function (item) {
CurrentBrandActions.addCampaign(item);
this.item = item;
item.newlyCreated = true;
this.trigger(item);
}.bind(this)
})
}
The Component sees the flag and renders a "Notification Child Component"
var newlyCreated = this.state.item.newlyCreated === true;
if (newlyCreated) {
newlyCreated = <ItemCreatedNotification item={this.state.item} />
} else {
newlyCreated = '';
}
return (
<form onSubmit={this.createItem} className="form">
{newlyCreated}
Something needs to move the app to a new place based on this event. Should this be a) the Notification Child Component b) Parent Component c) The Store?
According to Colin Megill's talk on flux api patterns the api interaction should occur in the Action, but reflux doesn't really allow for that.
UPDATE 2
Component passes data to an Action called createItemRequest
The Action has a preEmit hook that actually does the api call. The createItemRequest continues to the Store so that the store can change the model to reflect the state of sending which is then displayed in the component( maybe show a spinner ). The Action is also responsible for firing two other events depending on the api result.
ItemActions.createItemRequest.preEmit = function (data) {
$.ajax({
url: '/items',
method: 'POST',
data: data,
success: function (item) {
ItemActions.itemCreatedSuccess(item);
},
error: function (error) {
ItemActions.itemCreatedError(error);
}
});
}
There are different approaches to this. For example, in Reflux it's very easy to listen directly to actions if you choose to, since each action is actually a "dispatcher".
However, the general, purist Flux principle is that only stores register with the dispatcher and that components only listen to store updates. And the store just trigger an event that notifies that something has changed, not providing any payload. Then it's up to the component to read the store's state and determine how to render it.
One approach would be the one you describe, put some flag on the items in the store to signal that an update has happened, but it would violate the Flux principle if the components themselves then update the stored items' flags, since only stores are meant to mutate state, and only in response to actions and not from any other source. So in that case the "Flux thing" to do would probably be to trigger yet another event that signals that the newly added item has been noted so that the store can then reset the flag in response to that action.
Another approach I can think of would be to diff the state in your component when it gets notified of a store update. Then keep the flags only in the component, or even keeping newly added items in a separate list in the state and render them separately.
There are no hard rules here, except that if you want to follow the core Flux principle, components should never directly mutate stores' state, because that should only be mutated by the stores themselves in response to actions. That allows for a uni-directional data flow and a single source of truth about the data, which is the main goal of Flux.