Ember Loading Template with Liquid Fire - javascript

I have been doing a lot of tinkering with this and can't seem to get it working. I am looking to show my loading template while waiting for my model promise to return.
My understanding is, by default, if I have app/templates/loading.hbs, this template will be rendered across all routes. However, even with that template in place whenever I switch between routes the old route remains displayed until the model returns, at which point my liquid fire transition occurs and you're taken to the next route.
I have tried various version of creating nested loading templates for each route, tried creating subroutes for each route for the loading template, and have even messed with the beforeModel/afterModel methods that are available but I am making no progress. This is the last hurdle I want to cross before launching and am perplexed as to why I can't get it working. Here is a bunch of my code I feel is relevant.
Note: I am using Ember CLI and Liquid Fire. My data is also being returned to the model from am Ember Service for the time being.
Router
Router.map(function() {
this.route('reviews', function() {
this.route('index', {path: '/'});
this.route('review', {path: '/:review_id'});
});
this.route('movies');
this.route('about');
});
app/template/loading.hbs
<div class="content-container">
<h1>Ish be loading</h1>
</div>
Slowest Model Route
export default Ember.Route.extend({
activate() {
this._super();
$("html,body").animate({scrollTop:0},"fast");
$("body").addClass('movies');
},
deactivate() {
$("body").removeClass('movies');
},
model() {
const movies = this.get('movies');
return movies.getMoviesInAlphOrder();
},
afterModel: function() {
$(document).attr('title', 'Slasher Obscura - Movie Database');
},
movies: Ember.inject.service()
});
app.js
App = Ember.Application.extend({
modulePrefix: config.modulePrefix,
podModulePrefix: config.podModulePrefix,
Resolver,
...
});
loadInitializers(App, config.modulePrefix);
Service Methods
sortReviewsByDateDesc(arr) {
return arr.slice().sort(function (a, b) {
return a.review.date > b.review.date ? -1 : 1;
});
},
getSetAmountOfMovies(num, arr) {
const movieList = arr ? null : this.getMovies();
const trimmedList = arr ? arr.slice(0, num) : movieList.slice(0, num);
return trimmedList;
},
setFirstReviewToFeatured(arr) {
arr[0].isFeatured = true;
return arr;
},
getLatestReviews(num) {
const movieList = this.getMovies();
const reviewList = movieList.filterBy('review');
const indexList = this.sortReviewsByDateDesc(reviewList);
const latestList = this.getSetAmountOfMovies(num, indexList);
return this.setFirstReviewToFeatured(latestList);
},
getMoviesInAlphOrder() {
const movieList = this.getMovies();
let lowerCaseA, lowerCaseB;
return movieList.sort(function(a, b) {
lowerCaseA = a.title.toLocaleLowerCase();
lowerCaseB = b.title.toLocaleLowerCase();
return lowerCaseA.localeCompare(lowerCaseB);
});
},
getMovies() {
return [{
id: 1,
title: '303 Fear Faith Revenge',
year: "1999",
imdb: "tt0219682",
youtube: "iFV1qaUWemA"
}
...
]
I have read the docs on Ember's site along with various other Google resources and can't seem to figure out why my loading template isn't rendering at all. Any help would be awesome! Thanks!

Loading templates trigger when your model hook returns a promise that takes a long time to resolve, however, your model hook is not returning a promise.
model() {
const movies = this.get('movies');
return movies.getMoviesInAlphOrder();
}
getMoviesInAlphOrder is returning a synchronous array. After talking with you further, it turns out that you've pre-filled this array client side with 540 items, so the issue here is that the loading template not only doesn't have a promise to wait for, but even if it did it would resolve immediately anyway.
Your time delay is very likely a performance issue stemming from rendering a long list of items. There are several Ember addons to help with this including one of my own: https://github.com/runspired/smoke-and-mirrors
Alternatively/ In addition you may want to consider "chunking" your array into smaller bits and render it 25-50 at a time, or setup some pagination.

Related

How to push to vue-router without adding to history?

I have the following sequence happening:
Main screen
Loading screen
Results screen
On homepage, when someone clicks a button, I send them to the loading screen, thru:
this.$router.push({path: "/loading"});
And once their task finishes, they are sent to the results screen via
this.$router.push({path: "/results/xxxx"});
The problem is, usually they want to go from results back to the main screen, but when they click back, they're sent to loading again which sends them back to results, so they're stuck in an infinite loop & unable to go back to main screen.
Any ideas how to fix this? I'd ideally like if there was an option like:
this.$router.push({path: "/loading", addToHistory: false});
which would send them to the route without adding it to history.
This should have a real answer using this.$router.replace:
// On login page
// Use 'push' to go to the loading page.
// This will add the login page to the history stack.
this.$router.push({path: "/loading"});
// Wait for tasks to finish
// Use 'replace' to go to the results page.
// This will not add '/loading' to the history stack.
this.$router.replace({path: "/results/xxxx"});
For further reading the Vue Router is using History.pushState() and History.replaceState() behind the scenes.
There is a perfect way to handle this situation
You can use in-component guard to control the route in granule level
Make the following changes in your code
In main screen component
Add this beofreRouteLeave guard in component options, before leaving to 'result screen' you are setting the route to go only
through loading screen
beforeRouteLeave(to, from, next) {
if (to.path == "/result") {
next('/loading')
}
next();
},
In loading screen component
If the route go backs from result to loading then , it should not land
here and directly jump to main screen
beforeRouteEnter(to, from, next) {
if (from.path == "/result") {
next('/main')
}
next();
},
In loading screen, The beforeRouteEnter guard does NOT have access to
this, because the guard is called before the navigation is confirmed,
thus the new entering component has not even been created yet. So taking the advantage of this, you won't get the infinite calls fired when routing from results screen
In result screen component
if you use go back then it should not land in loading and directly
jump to main screen
beforeRouteLeave(to, from, next) {
if (to.path == "/loading") {
next('/')
}
next();
},
I have just created small vue application to reproduce the same issue. It works in my local as per your question. Hope it resolves your issue as well.
I guess router.replace is the way to go - but still some lines of thought (untested):
Basically on $router change it renders the loading-component until it emits load:stop, then it renders the router-view
import { Vue, Component, Watch, Prop } from "vue-property-decorator";
#Component<RouteLoader>({
render(h){
const rv = (this.$slots.default || [])
.find(
child => child.componentOptions
//#ts-ignore
&& child.componentOptions.Ctor.extendedOptions.name === "RouterView"
)
if(rv === undefined)
throw new Error("RouterView is missing - add <router-view> to default slot")
const loader = (this.$slots.default || [])
.find(
child => child.componentOptions
//#ts-ignore
&& child.componentOptions.Ctor.extendedOptions.name === this.loader
)
if(loader === undefined)
throw new Error("LoaderView is missing - add <loader-view> to default slot")
const _vm = this
const loaderNode = loader.componentOptions && h(
loader.componentOptions.Ctor,
{
on: {
// "load:start": () => this.loading = true,
"load:stop": () => _vm.loading = false
},
props: loader.componentOptions.propsData,
//#ts-ignore
attrs: loader.data.attrs
}
)
return this.loading && loaderNode || rv
}
})
export default class RouteLoader extends Vue {
loading: boolean = false
#Prop({default: "LoaderView"}) readonly loader!: string
#Watch("$route")
loads(nRoute: string, oRoute: string){
this.loading = true
}
}
#Component<Loader>({
name: "LoaderView",
async mounted(){
await console.log("async call")
this.$emit("load:stop")
// this.$destroy()
}
})
export class Loader extends Vue {}
This is a tough call considering how little we know about what's occurring in your loading route.
But...
I've never had a need to build a loading route, only ever loading component(s) that gets rendered on multiple routes during init/data gathering stage.
One argument for not having a loading route would be that a user could potentially navigate directly to this URL (accidentally) and then it seems like it wouldn't have enough context to know where to send the user or what action to take. Though this could mean that it falls through to an error route at this point. Overall, not a great experience.
Another is that if you simplify your routes, navigation between routes becomes much simpler and behaves as expected/desired without the use of $router.replace.
I understand this doesn't solve the question in the way you're asking. But I'd suggest rethinking this loading route.
App
<shell>
<router-view></router-view>
</shell>
const routes = [
{ path: '/', component: Main },
{ path: '/results', component: Results }
]
const router = new VueRouter({
routes,
})
const app = new Vue({
router
}).$mount('#app')
Shell
<div>
<header>
<nav>...</nav>
</header>
<main>
<slot></slot>
</main>
</div>
Main Page
<section>
<form>...</form>
</section>
{
methods: {
onSubmit() {
// ...
this.$router.push('/results')
}
}
}
Results Page
<section>
<error v-if="error" :error="error" />
<results v-if="!error" :loading="loading" :results="results" />
<loading v-if="loading" :percentage="loadingPercentage" />
</section>
{
components: {
error: Error,
results: Results,
},
data() {
return {
loading: false,
error: null,
results: null,
}
},
created () {
this.fetchData()
},
watch: {
'$route': 'fetchData'
},
methods: {
fetchData () {
this.error = this.results = null
this.loading = true
getResults((err, results) => {
this.loading = false
if (err) {
this.error = err.toString()
} else {
this.results = results
}
})
}
}
}
Results Component
Basically the exact same results component you already have, but if loading is true, or if results is null, however you prefer, you can create a fake dataset to iterate over and create skeleton versions, if you'd like to. Otherwise, you can just keep things the way you have it.
Another option is to use the History API.
Once you are in the Results screen, you can utilize the ReplaceState to replace the URL in history of the browser.
This can be done with the beforeEach hook of the router.
What you need to do is you must save a variable globally or in localStorage in the loading component when the data is loaded (before redirecting to the results component):
export default {
name: "results",
...
importantTask() {
// do the important work...
localStorage.setItem('DATA_LOADED', JSON.stringify(true));
this.$router.push({path: "/results/xxxx"});
}
}
And then you should check for this variable in the beforeEach hook and skip to the correct component:
// router.js...
router.beforeEach((to, from, next) => {
const dataLoaded = JSON.parse(localStorage.getItem('DATA_LOADED'));
if (to.name === "loading" && dataLoaded)
{
if (from.name === "results")
{
next({ name: "main"} );
}
if (from.name === "main")
{
next({ name: "results"} );
}
}
next();
});
Also, do remember to reset the value to false in your main component when the query button is clicked (before routing to the loading component):
export default {
name: "main",
...
queryButtonClicked() {
localStorage.setItem('DATA_LOADED', JSON.stringify(false));
this.$router.push({path: "/loading"});
}
}
Your loading screen should not be controlled by vue-router at all.
The best option is to use a modal/overlay for your loading screen, controlled by javascript. There are lots of examples around for vue. If you cant find anything then vue-bootstrap has some easy examples to implement.

How to get the latest data from parent to child components after page refresh

I am working on a project and using Vue.js for the frontend. I have following code in the main.js file.
new Vue({ // eslint-disable-line no-new
//el: '#app',
router,
data () {
return {
friends: []
}
},
methods: {
getFriends: function () {
return this.friends;
}
},
created: function () {
this.$http.get('/user/' + this.getUserIDCookie('userID') +
'/friends').then(function (response) {
this.friends = response.data;
});
},
components: {
'nav-bar': require('./components/Navigation.vue')
},
template: `
<div id="app">
<nav-bar></nav-bar>
<router-view class="router-view"></router-view>
</div>`
}).$mount('#app');
In one of the pages(for ex. when the page is redirected to localhost/#/user/1/details, I am retrieving the friends' list from main.js like below:
<script type="text/babel">
export default {
name: 'profile',
data: function () {
return {
user: {},
friends: []
}
},
methods: {
// Some methods
},
created: function () {
this.friends = this.$root.getFriends();
}
}
</script>
The problem arises when I refresh the current page. After page refresh, this.friends is null/undefined because this.$root.getFriends() is returning null/undefined. I can move it to user component, but I want to keep it in main.js so that GET call is used once and data will be available to the whole application.
Any input regarding how to solve this issue would be great. I am using Vue 2.0.1
Really, what you want to do, is pass the data the component needs as props.
The dirt simple easiest way to do it is this.
<router-view class="router-view" :friends="friends"></router-view>
And in your profile component,
export default {
props:["friends"],
name: 'profile',
data: function () {
return {
user: {},
friends: []
}
},
methods: {
// Some methods
}
}
If you want to get more sophisticated, the later versions of VueRouter allow you to pass properties to routes in several ways.
Finally, there's always Vuex or some other state management tool if your application gets complex enough.
The problem is that when you refresh the page, the whole app reloads, which includes the get, which is asynchronous. The router figures out that it needs to render details, so that component loads, and calls getFriends, but the asynchronous get hasn't finished.
You could work around this by saving and pulling the Promise from the get, but Bert's answer is correct: the Vue Way is to send data as props, not to have children pull it from parents.

react router redux express API loading state

How do you guys usually go about doing the loading part of your react? Since pages load really fast now, it's my API request who's having a hard time keeping up. For express, I have a Promise to wait for the API's return data before serving the pages.
export default function fetchComponentData(dispatch, components, params) {
const needs = components.reduce((prev, current) => {
return current ? (current.needs || []).concat(prev) : prev;
}, []);
const promises = needs.map(need => dispatch(need(params)));
return Promise.all(promises);
}
...
fetchComponentData(store.dispatch, props.components, _.merge({}, props.params, props.location.query))
.then(setupHTML)
.then(html => res.end(html))
But should I do this to react as well? Or how do you guys usually display the pages while it's loading? My current is I display the page without the data and start fetching and then rerender to display the page with the content, but then I have a small flickering page which I bet would be annoying if it's deployed already. I usually have my reducers (states) in this format:
const defaultState = {
ui: {
loading: false
}, metadata: {
},
data: { }
}
I was wondering how do you guys usually approach this?

Meteor collection always returns undefined on client

Im new to meteor and have been trying to learn the framework via the discover meteor book. Im having a few issue understanding what exactly is going on in my application (found here https://github.com/Themitchell/Sound-wav.es).
Essentially, my understanding is that on my server side I allow publications for certain collections which take arguments from my client side subscribe calls. For this part on my server i have this in my server/publications.js file:
Meteor.publish('studios', function (options) {
return Studios.find({
userId: this.userId
}, options);
});
Meteor.publish('studio', function (id) {
return id && Studios.find({
userId: this.userId,
_id: id
});
});
Next we would need a controller to handle the routing and deal with waiting for any subscriptions required, then, once the subscriptions are ready (hence the waitOn) go and render the template providing the data function as a reactive data source for the templates.
So I then set up my 2 routes for indexing studios and showing one studio using iron router and 'Controllers' as follows:
StudiosIndexController = RouteController.extend({
template: 'studiosIndex',
increment: 20,
limit: function () {
return parseInt(this.params.studiosLimit) || this.increment;
},
findOptions: function () {
return {
sort: this.sort,
limit: this.limit()
};
},
waitOn: function () {
return Meteor.subscribe('studios', this.findOptions());
},
studios: function () {
return Studios.find({}, this.findOptions());
},
data: function () {
return {
studios: this.studios()
};
}
});
ShowStudioController = RouteController.extend({
template: 'showStudio',
waitOn: function () {
return Meteor.subscribe('studio', this.params._id);
},
studio: function () {
return Studios.findOne(this.params._id);
},
data: function () {
return {
studio: this.studio()
};
}
});
Router.map(function () {
this.route('studiosIndex', {
path: '/',
controller: StudiosIndexController
});
this.route('showStudio', {
path: '/studios/:_id',
controller: ShowStudioController
});
});
Now this is great and works fine when displaying my index page. I get a list of collections which is reactive and the as i introduce new studios to the collection i see them get created on the server and on the client respectively. However in my show view when the view is rendered the data always seems to be empty. On dropping into my show controller's data function with a debugger I tried querying the Studios and this always returns undefined even when i try to fetch everything possible. Oddly I know that my publications are logging the correct id for a studio (using console.log). It seems that i get all the correct data up until the routing on the client side. The parameter ids are all correct but a find() call on studios always returns nothing. Am I missing something obvious here.
Its also worth noting i deleted my helpers for 'studios' and 'studio' in views/studios/index.js and views/studios/show.js respectively as my understanding is that this is what im doing with studios and studio in the controller. These helpers are now defined at the controller level and passsed to the reactive 'data' function. IS this correct?
TL;DR
With the code above my index action works however my show action fails where the data function always returns undefined and in fact on the show page i cannot get access to any of my studio information (Studios.find({}).count() always returns 0). I'm unsure how the 2 routes differ. Can anyone spot the issue?
Edit: Its also worth noting having looked through some of the iron router issues on github i have tried using this.ready(). The first time the route is run i hit data and this is false but then even wrapping my data helpers to wait for this.ready() gets an undefined return value when this.ready() returns true.
Extra Notes
Im running meteor 0.8.0 with latest 0.7.0 release of iron router and collection2 with simple schema, just in case you are interested / require this info.
EDIT: Possible solution
So having fiddled around it seems like my helpers are the issue. By using the 'studio' section and then calling this.studio() inside my data function this seems to break. If I change the code below:
ShowStudioController = RouteController.extend({
template: 'showStudio',
waitOn: function () {
return Meteor.subscribe('studio', this.params._id);
},
studio: function () {
return Studios.findOne(this.params._id);
},
data: function () {
return {
studio: this.studio()
};
}
});
to this
ShowStudioController = RouteController.extend({
template: 'showStudio',
waitOn: function () {
return Meteor.subscribe('studio', this.params._id);
},
data: function () {
return Studios.findOne(this.params._id);
}
});
My view renders correctly again. Im unsure where i saw this pattern however I had assumed the function assigned to 'studio' was essentially the same as doing
Template.showStudio.helpers({
studio: function () {
return Studios.findOne(this.params._id)
}
});
but it seems not!

Iron-Router randomly returning undefined in Meteor

For some odd reason, iron-router randomly returns undefined.
this.route('pollyShow', {
path: '/polly/:_id',
template: 'polly_show',
notFoundTemplate: 'notFound',
before: function () {
var id = this.params._id;
var poll = Polls.findOne({_id: id});
console.log(poll);
var ip_array = poll.already_voted;
$.getJSON("http://smart-ip.net/geoip-json?callback=?", function(data){
ip_voted = ip_array.indexOf(data.host);
if (ip_voted > -1) {
Router.go('pollyResults', {_id: id});
}
});
},
data: function() {
return Polls.findOne({_id: this.params._id});
}
});
Sometimes it is returning normally while other times it just returns undefined.
Is there any reason behind this?
The problem occurs because the Polly collection is sometimes populated and at other times unpopulated when the route executes.
This problem can be prevented by explicitly waiting on a subscription using waitOn option in the route configuration.
From the docs:
By default, a new Meteor app includes the autopublish and insecure packages, which together mimic the effect of each client having full read/write access to the server's database. These are useful prototyping tools, but typically not appropriate for production applications. When you're ready, just remove the packages.
To remove the packages, call meteor remove <package-name>.
Then you need to explicitly publish records which you want to see on the client on the server:
server/publications.js:
Meteor.publish('all_of_polly', function () { return Polls.find({}); });
And subscribe to it on the client:
this.route('pollyShow', {
path: '/polly/:_id',
template: 'polly_show',
notFoundTemplate: 'notFound',
waitOn: function () { return Meteor.subscribe('all_of_polly'); }
// ...
});

Categories