Flow Router doesn't render on a route change

Yeah, thats a point of view discussion. Google’s spider wouldn’t think that “/hello” and “/about” are the sames routes, also the browser wouldn’t do. I’m coming from PHP, so it’s normally for me that a catch all route will end in a rerender of the sites and the behavior of FlowRouter here feels a little bit different for me. On the one side I’m calling the same route, because FlowRouter is always calling this “catch all” route and so it’s the same, on the other side it isn’t the same.

The root of the problem is single page applications. When you suddenly flip the script on how to render HTML by bundling up all of it into a single HTML-file with javascript and the works, you suddenly have to deal with issues that didn’t exist before if you want crawlers and browsers to behave nicely with your app.

With traditional server side rendered forms-navigation and multiple html pages, everything’s great, simply because that’s how it’s been done for decades. submit a form = new url = new server request = new html

Having an SPA imitate traditional browser navigation CAN come with the cost of weird interaction with crawlers and browsers. You’ll need to do some extra work to achieve the same interactions…

Mozilla has provided a nice article about the history API :sunny: pushState and replaceState() are the holy grails of SPA routing AFAIK.

3 Likes

Yes, but this issue is not existant with the iron router. I recently moved from iron router to flow router and run into a similar issue: https://github.com/kadirahq/blaze-layout/issues/65

As I pointed out in the discussion, I think it’s because here ( https://github.com/kadirahq/blaze-layout/blob/master/lib/client/layout.js#L70-L74 ) no attention is paid to the value of the query parameter. I’ve found it quite annoying and then mover to React and used another template renderer that works fine.

2 Likes

With Flow Router, params and queryParams are not reactive by default. It was designed this way to get rid of some annoying behaviours of Iron Router, but it creates other annoying behaviours this way.

I agree that the issue you have is typical to single page applications in general. I had to deal with it with Angular too, before I moved to Meteor.

Now that’s interesting.

How I deal with that is I keep the state at the app’s level (while showing proper params and queryParams for the user to be able to share the link with others). This way subscriptions and methods rerun when my app’s state changes, not my routing.

1 Like

Very interesting! Could you expand on that? Some example?

@avalanche1 here’s an example. It may be an overkill, but I wrote it as an experiment and doesn’t work bad so far. Also, it may be a bad practice to reset queryParam value when it’s equal to default, who knows? I prefer it that way.

External means that you use this function in your template. Internal means that this function is only used internally so the template doesn’t have to address it in any way.

Data template is a template responsible for keeping the data for the particular view component. Each such view component may have different default settings, that’s why we allow the template to override them.

Query field template is a template responsible for managing a particular form field which I use to change the value of query properties - it may be a select field, a checkbox or whatever. @param() is a string representation of the property’s name, set in the template, @value() is the value of form field.

ViewModel.share
  query:
    filter: ''
    sort: 'createdAt'
    order: '-1'
    limit: 10

ViewModel.mixin

  #injected into data templates
  prepareQuery:

  #external

    #overwrites defaults and sets initial property values
    #called in onCreated
    getQueryParams: (sort, order, limit) ->
      @defaultQueryParams().sort = sort
      @defaultQueryParams().order = order
      @defaultQueryParams().limit = limit

    #passes values of queryParams to properties
    #called in onCreated
    checkQueryParams: ->
      @filter @checkQueryParam 'filter'
      @sort @checkQueryParam 'sort'
      @order @checkQueryParam 'order'
      @limit @checkQueryParam 'limit'

  #internal

    #sets queryParam value or resets it when default
    setQueryParam: (param, value) ->
      unless value is @defaultQueryParams()[param]
        FlowRouter.setQueryParams 
          "#{param}": value
      else
        FlowRouter.setQueryParams 
          "#{param}": null

    #sets default values for properties
    defaultQueryParams: ->
      filter: ''
      sort: 'createdAt'
      order: '-1'
      limit: 10

    #returns queryParam value if set or default if not
    checkQueryParam: (param) ->
      if FlowRouter.getQueryParam param
        FlowRouter.getQueryParam param
      else
        @defaultQueryParams()[param]

  #injected into query field templates
  queryField:

  #internal

    #sets queryParam and property value every time the field's value changes
    autorun: (c) ->
      @setQueryParam @param(), @value()
      @parent()[@param()] @value()

I used this opportunity to do some tweaks that previously I was too lazy to do. But now it’s ready.

I simply can’t seem to find a solution that reliably works every time for a particular scenario. I’m running Meteor 1.6.0.1 with FlowRouter. Specifically, when the page reloads after the route change, I want to ‘update’ an iCheck checkbox using jQuery. I have that checkbox in it’s own template:

<template name="activeCheck">
    <div class="i-checks">
        <label>
            <input name="activeCheck" type="checkbox" value="1" checked="{{isChecked active true}}">
            Active (Test: {{active}})
        </label>
    </div>
</template>

I do get the underlying checkbox updated when the route changes, but I can’t find an event that consistently triggers every time on the route change. Specifically:

Tracker.afterFlush(function() {
    console.log('afterFlush')
    $('input[name="activeCheck"]').iCheck('update')
})

Does NOT fire on every route change.

this.autorun(function () {
    console.log('autorun')
    $('input[name="activeCheck"]').iCheck('update')}, 2000)
});

DOES fire on every route change, but a race condition appears to exist such that I am updating the iCheck checkbox before the DOM has had a chance to redraw?

After testing every combo of autorun and afterFlush, I have the following code which works, but is less than ideal by injecting a 2 second pause to give the DOM time to update before running the jQuey iCheck update routine.

Tracker.autorun(function () {
    /* Todo there appears to be a race condition. This is a less than ideal fix that appears to work */
    FlowRouter.watchPathChange()
    setTimeout(function() {$('input[name="activeCheck"]').iCheck('update')}, 2000)
});

Any ideas as to how to get around this? @dr.dimitru might you have some insight into whether flow-router-extra might fix or address this?

Hello,

Thank you for pinging me.

  1. Have you tried to use .onRendered() hook?
  2. Could you check if .onRendered() hook fires when path is changed?
  3. How do you update path?

Yes, I tried .onRendered() for both the main template as well as the checkbox template.

However, I just discovered a pattern that had eluded me before: if I navigate to a different route, .onRendered() does fire, but if I navigate back to a route I’d already visited, the DOM does update, but .onRendered() does NOT fire.

In other words:

FlowRouter.go('/doc/1') <= Initial load, .onRendered() fires

FlowRouter.go('/doc/2') <= DOM updates & .onRendered() fires

FlowRouter.go('/doc/1') <= DOM updates & .onRendered() DOES NOT fire

FlowRouter.go('/doc/3') <= DOM updates & .onRendered() fires

FlowRouter.go('/doc/2') <= DOM updates & .onRendered() DOES NOT fire

@cormip need more info from you:

  • Are you on original flow-router or on one of its forks?
  • Version of flow-router you’re experiencing this issue
  • Version of Meteor you’re experiencing this issue
  • Browser name and its version (Chrome, Firefox, Safari, etc.)?
  • Platform name and its version (Win, Mac, Linux)?

@dr.dimitru, original: kadira:flow-router@2.12.1

Meteor 1.6.0.1

Chrome: Version 64.0.3282.119 (Official Build) (64-bit)

Development: Win10

Deployed to Galaxy where the problem is also observed.

This issue might be fixed using flow-router-extra as drop-in replacement (except it should be imported).

I replaced kadira:flow-router with ostrio:flow-router-extra and it did not solve the problem. :disappointed: I verified that onRendered still does not fire when revisiting a previously visited route that uses the same template as I described earlier.

My guess it’s a feature. Did action hook fire on path change?

Yes, the action hook fires every time the path changes, the same as autorun. However, I cannot use that hook since it creates a race condition between my code to render the checkbox, and the DOM being updated by the data change.

It does not seem likely that this is a feature since the DOM does update on every path change. I even added the checkbox status to the checkbox label as shown in a previous comment, and I can see that the DOM is updated on every path change. There is no reason why Tracker.afterFlush shouldn’t run. That’s the event that is needed and which should fire after the DOM has been updated.

My guess it’s BlazeLayout feature to not rerender tempates, but insted update it’s data. As I understood /doc/1 and /doc/x are using the same template, right? So, only data is updated.

Try to use this.render() method of flow-router-extra.

OK, I’m not sure you understood the problem. The template DOES rerender on every route change, it’s the onRender and afterFlush events that don’t always fire after the template rerenders, so I can’t effectively redraw my fancy checkbox.

As you suggested, I tried using this.render(), but it’s not working for me. Nothing at all happens when the route is changed. I set a breakpoint, and this.render() does run, but nothing happens, and no errors are thrown. Apparently it’s not a simple swap-out like below. Did I miss something?

Unchanged

const authenticatedRoutes = FlowRouter.group({
	name: 'authenticated'
})
...
const adminRoutes = authenticatedRoutes.group({
  prefix: '/admin', 
  triggersEnter: [() => {
  	if (!Meteor.loggingIn() && !Roles.userIsInRole(Meteor.user(), [ 'admin' ])) {
		FlowRouter.go('/unauthorized')
	}
  }]      
})
...

WAS (flow-router, worked):

adminRoutes.route( '/doc/:doc_id', {
    name: 'docs',
    action: () => {
        BlazeLayout.render( 'defaultLayout', { content: 'documents' } )
    }
})

IS (flow-router-extra, does not work):

adminRoutes.route( '/doc/:doc_id', {
    name: 'docs',
    action: function() {
        this.render( 'defaultLayout', 'documents')
    }
})

this.render isn’t equal to BlazeLayout, and has different API.

It isn’t clear what your’re trying to accomplish, and why template should be re-rendered, while it’s already rendered, and :doc_id isn’t passed into the template, so nothing is changed.

To work on Template level you can use:

this.autorun(function () {
    FlowRouter.watchPathChange();
    setTimeout(function() {
       $('input[name="activeCheck"]').iCheck('update');
    }, 2000)
});

Better to have .iCheck at onRendered and at change event of input[name="activeCheck"].

That’s what I’ve been saying. That’s the way I originally had it, but onRendered doesn’t fire every time, so .iCheck is unable to refresh.

The template works just fine except for the checkbox:

/doc/1 First load , displays document 1 contents just fine. onRendered fires, iCheck correctly displayed

navigate to:

/doc/2 Displays document 2 contents just fine using the same template as previous. onRendered fires, iCheck correctly displayed

navigate back to:

/doc/1 Displays document 1 contents just fine using the same template as previous. However, onRendered DOESN’T fire, so iCheck is NOT refreshed so the iCheck checkbox status is not correctly displayed.

The autorun and setTimeout is a kludge to work around the race condition created by using autorun instead of onRendered and afterFlush which SHOULD be firing after updating the DOM but don’t.

Also, the change event is not trigger by DOM manipulation, so that is not a viable solution either.