scheduleOne('afterRender') in a Controller.init() does not work as I thought... Why?


#1

Hello,

My code :

// app/router.js
import EmberRouter from '@ember/routing/router';
import config from './config/environment';

const Router = EmberRouter.extend({
  location: config.locationType,
  rootURL: config.rootURL
});

Router.map(function() {
  this.route('test');
});

export default Router;
// app/pods/application/controller.js
import { scheduleOnce } from '@ember/runloop';
import Controller from '@ember/controller';

export default Controller.extend({
  init() {
    console.log('APPLICATION - INIT...');

    scheduleOnce('afterRender', () => {
      console.log('APPLICATION - AFTER RENDER...');
    });

    return this._super(...arguments);
  }
});
// app/pods/test/route.js
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import config from '../../config/environment';

export default Route.extend({
  ajax: service(),

  model() {
    return this.get('ajax').request(
      config.apiURL + '/api/application',
      {
        type     : 'GET',
        dataType : 'json'
      }
    );
  }
});
// app/pods/test/controller.js
import { scheduleOnce } from '@ember/runloop';
import Controller from '@ember/controller';

export default Controller.extend({
  init() {
    console.log('TEST - INIT...');

    scheduleOnce('afterRender', () => {
      console.log('TEST - AFTER RENDER...');
    });

    return this._super(...arguments);
  }
});

When a open my web console, I see:

Open http://localhost:8080/test

DEBUG: -------------------------------
DEBUG: Ember      : 3.5.1
DEBUG: Ember Data : 3.5.0
DEBUG: jQuery     : 3.3.1
DEBUG: -------------------------------

Attempting URL transition to /test
Preparing to transition from '' to 'test'
Transition #0: application: calling beforeModel hook
Transition #0: application: calling deserialize hook

APPLICATION - INIT...
TEST - INIT...

Transition #0: application: calling afterModel hook
Transition #0: test: calling beforeModel hook
Transition #0: test: calling deserialize hook

APPLICATION - AFTER RENDER...
TEST - AFTER RENDER...

Transition #0: test: calling afterModel hook
Transition #0: Resolved all models on destination route; finalizing transition.
Transitioned into 'test'
Transition #0: TRANSITION COMPLETE.

I don’t understand why my * - AFTER RENDER... logs are not at the end, after the Transition #0: TRANSITION COMPLETE..

If my model() in test/route.js does not return a Promise, all is good : “AFTER RENDER” logs are at the end…

I thought Ember was waiting for the promises to be resolved to go to the next hook (afterModel here) but my AFTER RENDER logs before it!?

Could you explain me ? Thanks.

ps: I would like to use scheduleOne('afterRender') to add/remove some classes/DOM elements but I need to do this after all rendering templates.


#2

Controllers are singletons and you shouldn’t be doing dom stuff in controllers anyway.

In fact, if you use init in ember objects at all, with the exception of avoiding ember object self troll, you are probably doing something unintended.

You should probably be using template bindings to add and remove classes and dom elements anyway, so it sounds like this whole approach could be improved - using runloop scheduling for this does not sound like the right path to be taking.

If you have to use runloop scheduling for some reason, you should always be doing any dom stuff in components.


#3

Thank you very much for your reply.

I’m in a special case of JS initialization :frowning: (Metronic template) but I understand what you said.

But I would like to understand how it works :slight_smile:


#4

@alexspeller is correct, but I will add some meta-detail.

Even when you understand perfectly the internal implementation, it’s still a mistake to make your code depend on implementation over interface.

We have a declarative way to ensure that code will run after a template has rendered (didInsertElement). If you use that, your code will work not just today, but even under future versions of Ember that could have entirely different, optimized timings.

Whereas if you carefully observe Ember’s current timing and then assume it will always be true, it will eventually not be true and your code may break.

Now, that doesn’t mean it’s not beneficial to study how things really work inside! And I encourage you to do so, and I’m happy to share more detail if that will help. But I needed to preface with the warning that it’s perilous to over-optimize against the present internals.

Re-rendering is a rather low-level concept that has no direct relation to router transitions. Any time anything changes that could invalidate the DOM, glimmer schedules a re-render. (And it’s very cheap to do so, when little has actually changed.) While your promise is waiting to resolve, glimmer can choose to re-render any number of times.

One reason it might do so is because some link-to is eagerly updating it’s classes as soon as the transition begins. Another is that after the router encounters your promise, it settles into a loading state (whether or not you actually have a loading template), and that in itself probably pushes new information down to the outlets, causing invalidations that would schedule a glimmer re-render.


#5

Thank you very much :slightly_smiling_face: !

I need to refactor my code with some components and use didInsertElement. OK

End of 1st render

So there is no safe way to ensure the end of the first render :thinking:? We display a “splash-screen” in our index.html like this while Ember loads, starts, etc:

...
<body>
	<div id="splash-screen">
		<div class="sSpiningWrapper">
			<div class="sSpin2"></div>
		</div>
	</div>

	{{content-for 'body'}}
...

And we used our bad “init” :yum: code in our Application Controller like this:

init() {
	this._super(...arguments);

	schedule('afterRender', () => {
		$('#splash-screen').fadeOut(200, function(){
			$(this).remove();
		});
	});
},

Could you show the right way to do that?

Another question about didInsertElement

(maybe should i open a new topic?..)

I often read on some sites:

didInsertElement() {
    scheduleOne('afterRender', () => {
        // do some things with this.$()
    }
},

Do I need to use scheduleOne('afterRender', () => {}) inside didRenderElement() {...} OR just didRenderElement() {...}?

Thank you!


#6

You can put a component in your application.hbs template like {{cleanup-splash-screen}}, and in that component’s didInsertElement, you can do the $('#splash-screen').fadeout().

That works no matter what other things may block rendering.

You should not need to schedule afterRender. There are very rare situations where that is appropriate, but usually when people do that they are trying to evade the backtracking re-render assertion, which means they are probably just hiding bugs.

(The backtracking re-render assertion throws an error if rendering a component causes data in one of the component’s ancestors to change, which would therefore trigger another render. It’s bad because it can be arbitrarily expensive and it means there is a data cycle, which can lead to hard-to-debug logic race conditions.)


#7

Thank you very much for all these answers.