How to do polling in Ember js component? - ember.js

In ember js 3.24 LTS, I can create a component that receive data from an API call inside a route. The data from the API is an activity logs. The complexity arrives when I try to keep updating the logs with the latest logs, I need to keep polling the API every few seconds and I need to keep updating the data that being displayed inside the component.
How do I keep reloading the API call every few seconds and then
update the display of the component?
I can use the ember later to keep looping/requery the API ever 2 seconds or 2000 ms, but then how do I tell my component to update the display?
This component gets data from an API call which is in the routes.
// addon/routes/single-scan/index.js
import { later } from '#ember/runloop';
...
async model() {
let phaseActivityLog = await this.pollTestActivity(testId);
return { phaseActivityLog };
}
async pollTestActivity(testId) {
later(
this,
function () {
this.pollTestActivity(testId);
},
2000
);
let phaseActivityLog = await this.store.query('phase-activity-log', testId, { reload: true }); // poll this
return phaseActivityLog;
}
And my component hbs
// components/single-scan/phase-activity-log.hbs
{{#each #phaseActivityLog as |row|}}
{{row.timeStamp}} - {{row.event}} - {{row.userName}}
{{/each}}

In controller Initialize #tracked variable and set phaseActivityLog on this #tracked variable after query promise is finished. Then pass this #tracked variable in component as an argument.
If You use #tracked decorator when #tracked variable value change its render automatically.
// addon/controllers/single-scan/index.js
import { tracked } from '#glimmer/tracking';
#tracked phaseActivityLog = null;
async pollTestActivity(testId) {
later(
this,
function () {
this.pollTestActivity(testId);
},
2000
);
await this.store.query('phase-activity-log', testId, { reload: true
}).then(result=>{
this.phaseActivityLog = result;
});
}
// components/single-scan/phase-activity-log.hbs
{{#each #phaseActivityLog as |row|}}
{{row.timeStamp}} - {{row.event}} - {{row.userName}}
{{/each}}

Related

Ember centralised error handling within an Action

Inside an Ember controller I have a number of actions which perform data events (create update and delete) on the store.
Each of these requests has the same error handling logic which is repeated many times. If there a way of centralising this error logic?
Given this logic will be used by many routes, what would be the best location in Ember for it to live?
How can the context be preserved so this is still available to send toast type of messages to the DOM ?
Here is an example of what I have now
createEntryRecord() {
// This action is a 'create' button on a modal
set(this, 'isLoadingModal', true); // shows loading spinner
let dbid = this.get('dbidModal');
let type = this.get('entryModalType');
this.get('store').createRecord('my-store', {
type,
dbid
})
.save()
.then((record) => {
get(this, 'flashMessages').success(`Created`); // Toast message
})
.catch((e) => {
// Begin of the error handling logic
let errors = get(e, 'errors');
if (errors.length) {
errors.forEach(err => {
let field = err.source.pointer.split("/").pop();
get(this, 'flashMessages').danger(`${field}: ${err.detail}`);
});
} else {
let message = get(e, 'message');
get(this, 'flashMessages').danger(`${message}`);
}
})
.finally(() => {
set(this, 'isLoadingModal', false); // stop the loading spinner
set(this, 'isEntryModalShown', false);
});
},
I would suggest a solution that utilises Ember's Object Model Mixins.
First you create a common Mixin for all your models:
// app/mixins/flashing-model.js
import Mixin from '#ember/object/mixin';
import { inject as service } from '#ember/service';
export default Mixin.create({
flashMessages: service(), // can be accessed via this.get('flashMessages')
save(...args) {
const superSavePromise = this._super(...args);
superSavePromise.then(() => {
// your positive logic
}).catch(e => {
// your negative logic
});
return superSavePromise;
}
});
Then you include it in each of the models. For example:
// app/models/some-model.js
import FlashingModel from '{your-app-name}/mixins/flashing-model';
import DS from 'ember-data'
export default DS.Model.extend(FlashingModel, {
// ...
});
After that you can edit your controller:
this.get('store').createRecord('my-store', {
type,
dbid
})
.save()
.finally(() => {
// ...
});
So regarding your questions:
1. Ember Mixins (under app/mixins directory) can be a good place to host common logic.
2. When implementing the above solution you don't really need to preserve the controller's this context, since the flashMessages service is injected to each model. Hence, you can access the service within the save method's context, which is the this of model.

Auto Calculate Component in EmberJS

I'm trying to create a component in Ember that shows you how many comments a post has. I pull the comments from the API. The problem right now is, if there are new comments, it doesn't re-query the API.
Is there a way to have the Ember component automatically check every 15 or so seconds for new comments to update the count?
Could call a method in init hook that triggers new comments fetching and also calls for itself when 15 sec passes.
commentsCount: Ember.computed.alias('comments.length'), // Use in template for items count
init: function() {
this._super(...arguments);
this.getNewComments();
},
getNewComments: function() {
Ember.run.later(() => {
this.get('store').query('comments', { post: this.get('post.id') }).then(newItems => {
this.get('comments').pushObjects(newItems);
this.getNewComments(); // Calls itself out
});
}, 15000);
}

Ember: How to cleanly replace model data and have progress indicators

I have a certain route that shows a list of projects, and it gets initial data from my RESTAdapter based on who the user is.
I am now implementing a search function that will issue a new API call so the user can get records besides the default ones for them, and the response should replace the model for that route. I have all that working, but I'm not sure how to do a loading or progress indicator (as the response from the database could potentially take 5-10 seconds depending on the amount of data). I know about loading substates, but in this case I'm not transitioning between routes. I just want to have at minimum a spinner so the user knows that it's working on something.
Would anyone that's done this before be willing to share how they handled a)replacing the model with new data, and b)keeping the user informed with a spinner or something?
Form action called when user clicks the Search button
searchProjects: function() {
var query = this.get('queryString');
if (query) {
var _this = this;
var projects = this.store.find('project', {q: query});
projects.then(function(){
_this.set('model', projects);
});
}
}
a) replacing the model with new data
You don't need to do anything. If you sideload records properly from the backend, Ember will automatically update them on the frontend.
b) keeping the user informed with a spinner or something
The loading substate is an eager transition. Ember also supports lazy transitions via the loading event.
You can use that event in order to display the spinner.
Here's an example from the docs:
App.ApplicationRoute = Ember.Route.extend({
actions: {
loading: function(transition, route) {
showSpinner();
this.router.one('didTransition', function() {
hideSpinner();
});
return true; // Bubble the loading event
}
}
});
UPD1
I need to do at least what I'm doing right? Setting the model to the response?
You need to reflect the search in the URL via query params. This will let the router automatically update the model for you.
what I would put in showSpinner to affect stuff on the page (like, can I use jQuery to show or hide a spinner element?), or show the actual loading substate.
I would set a property on that page's controller:
App.IndexRoute = Ember.Route.extend({
queryParams: {
search: {
refreshModel: true
}
},
model () {
return new Ember.RSVP.Promise( resolve => setTimeout(resolve, 1000));
},
actions: {
loading (transition, route) {
this.controller.set('showSpinner', true);
this.router.one('didTransition', () => {
this.controller.set('showSpinner', false);
});
return true;
}
}
});
App.IndexController = Ember.Controller.extend({
queryParams: ['search'],
search: null,
showSpinner: false,
});
Demo: http://emberjs.jsbin.com/poxika/2/edit?html,js,output
Or you could simply put the spinner into the loading template, which will hide obsolete data:
http://emberjs.jsbin.com/poxika/3/edit?html,js,output
Or you could put your spinner into the loading template:
Just in case others want to see, here's my working code based on #lolmaus's answers.
These Docs pages were helpful as well
Route's queryParams and Find method
Controller
//app/controllers/project.js
export default Ember.ArrayController.extend({
queryParams: ['q'],
q: null,
actions: {
searchProjects: function() {
var query = this.get('queryString');
if (query) {
this.set('q', query);
}
}
}
})
Route
export default Ember.Route.extend(AuthenticatedRouteMixin, {
model: function(params) {
if (params.q) {
return this.store.find('project', params);
} else {
return this.store.findAll('project');
}
},
queryParams: {
q: {
refreshModel: true
}
},
actions: {
loading: function(/*transition, route*/) {
var _this = this;
this.controllerFor('projects').set('showSearchSpinner', true);
this.router.one('didTransition', function() {
_this.controllerFor('projects').set('showSearchSpinner', false);
});
return true; // Bubble the loading event
}
}
});
My issue now is that when I use the parameter query, it works great, but then if I clear the query (with an action, to effectively "go back") then the records fetched by the query stay in the store, so when it does a findAll() I have both sets of records, which is not at all what I want. How do I clear out the store before doing findAll again?

Ember force template sync before longer operation

I have some actions that take some time and I wan't to indicate that to the user by showing a loading spinner. I know realized that sometimes the longer running action is directly triggered before the loading spinner is shown:
this.set('betRound.isLoading', true);
var _this = this;
Ember.run.sync();
Ember.run.later(function(){
_this.transitionToRoute('betround.edit', _this.get('betRound.content'));
}, 50);
I tried to achieve this by delaying the transition with 50ms, but on some slower mobile devices, the loading spinner that depends on "isLoading" is not shown.
I would go with some kind of callback mechanism, where you'd put your loading spinner in a view or component and notify the controller when DOM is ready. Here's an example of a Mixin you can attach to any view or component to make it send a notice after it loads.
App.LoadAware = Ember.Mixin.create({
didInsertElement: function () {
this._super();
var target = this.get("loadedNoticeTarget");
Ember.assert("We must have loadedNoticeTarget in LoadAware component or view", target);
target.send("loadAwareLoaded");
}
});
You would then apply it like this:
App.LoadingSpinnerComponent = Ember.Component.extend(App.LoadAware);
In your template:
{{#if isLoading}}
{{loading-spinner loadedNoticeTarget=this}}
{{/if}}
And then, in your controller:
App.IndexController = Ember.Controller.extend({
actions: {
goToSlowRoute: function () {
this.set("_waitingForSpinner", true);
this.set("isLoading", true);
},
loadAwareLoaded: function () {
Ember.assert("We must be waiting on spinner at this point", this.get("_waitingForSpinner"));
this.set("_waitingForSpinner", false);
this.transitionToRoute("slowRoute");
}
},
isLoading: false
});
In this example, you would initiate transition by sending goToSlowRoute message to the controller. Full JSBin here.

Ember: Get route instance from the controller

I have a multi-step flow that the user can go through sequentially or jump straight to a section (if the sections in between are completed). I think this logic should be in the Route object. However, from within the controller, how do I access the route instance. For example, it would be ideal to be able to do something like this in the controller:
App.Flow = Em.ObjectController.extend({
submit: function(){
// Validation and XHR requests
// ...
// Go to the next step
route.goToNextStep();
}
}
From within a controller, you can access the router via this.get('target'). So this.get('target').send('goToNextStep') should work.
Like so:
App.Flow = Em.ObjectController.extend({
submit: function(){
// ...
this.get('target').send('gotoNextStep');
}
}
App.FlowRoute = Ember.Route.extend({
events: {
gotoNextStep: function(){
// ...
this.transitionTo(routeName);
}
}
}
You need to get the route for such conditions,
so from the controller just say,
App.Flow = Em.ObjectController.extend({
submit: function(){
var self =this;
// Validation and XHR requests
// ...
// Go to the next step
self.send('goToNextStep');
}
}
and define your goToNextStep event in your route's event hash
'this' is what points to the router, but you shouldn't add any methods to that prototype. Instead, make some sort of event that triggers the transition to the next step.
In addition to target, another way to do this in Ember now is with getOwner.
For example, to send an action to your application route:
import Component from '#ember/component';
import { action } from '#ember/object'; // https://github.com/pzuraq/ember-decorators-polyfill
import { getOwner } from '#ember/application';
export default class MyTopLevelComponent extends Component {
#action
closeModal() {
getOwner(this).lookup('route:application').send('closeModal');
}
});