0

I am building a website using Polymer (>= 1.2.0) based on the PSK (Polymer Starter Kit).

I am running into a (probably noob) problem with attempting to centralize and/or automatize my router configurations.

I have appended the following code to the end of the PSK's app.js file:

//note: app.baseUrl is set by PSK's original code earlier in the file: app.baseUrl = '/';
app.routeMap = [
  {name: "home", text: "Home", icon: "home", url: app.baseUrl},
  {name: "about", text: "About", icon: "face", url: app.baseUrl + "about"},
  {name: "portfolio", text: "Portfolio", icon: "build", url: app.baseUrl + "portfolio"},
  {name: "contact", text: "Contact", icon: "mail", url: app.baseUrl + "contact"}
];

I then replaced the original routing-configuration code in routing.html with the new version, that uses the routeMap:

page('*', scrollToTop, closeDrawer, function (ctx, next) {
  next();
});

page('/', function () {
  app.route = app.routeMap[0].name;
});

page(app.routeMap[0].url, function () {
  app.route = app.routeMap[0].name;
});

page(app.routeMap[1].url, function () {
  app.route = app.routeMap[1].name;
});

page(app.routeMap[2].url, function () {
  app.route = app.routeMap[2].name;
});

page(app.routeMap[3].url, function () {
  app.route = app.routeMap[3].name;
});

//404
page('*', function () {
  app.$.toast.text = 'Can\'t find: ' + window.location.href + '. Redirected you to Home Page';
  app.$.toast.show();
  page.redirect(app.baseUrl);
});

The above code works fine! But it breaks when I try to use a for-loop instead of hard-code:

page('*', scrollToTop, closeDrawer, function (ctx, next) {
  next();
});

page('/', function () {
  app.route = app.routeMap[0].name;
});

//Doesn't work with this for-loop...
for (i = 0; i < app.routeMap.length; i++) {
  //debug
  console.log("Registering route: " + i + " - Name: " + app.routeMap[i].name + " - URL: " + app.routeMap[i].url);
  page(app.routeMap[i].url, function () {
    app.route = app.routeMap[i].name;
  });
}

//404
page('*', function () {
  app.$.toast.text = 'Can\'t find: ' + window.location.href + '. Redirected you to Home Page';
  app.$.toast.show();
  page.redirect(app.baseUrl);
});

The debug console.log() prints the items of the routeMap as expected, but the routes don't work (the page(app.routeMap[i].url, function () { /*...*/ }); part doesn't work?), and I get an Uncaught TypeError: Cannot read property 'name' of undefined located at (anonymous function) (app/elements/routing.html:86:36)

Can anyone identify the problem here? It's probably a noob one, but it's flying straight over my head...

(I know a bit of the languages involved [HTML, CSS & JS], but this is my first time making a website, and putting that knowledge to serious use in a project, rather than in an exercise/learning-assignment)

CosmicGiant
  • 6,275
  • 5
  • 43
  • 58

1 Answers1

1

My first guess would be scoping. Take a look at the examples below (output into the console). Try doing a console.log on i inside your anonymous callback function. I expect they may not be what you think they are.

My expectation is that your i the length of your array which is out of bounds when used as an index (because index starts at 0).

I created some code below to show how the scope of the callback can effect what you expect. Basically, the scope of the callback is not the same scope as where you created the anonymous function (because it is called later).

var a = [11, 22, 33, 44, 55, 66];

function pass(arr) {
  for (var i = 0; i < arr.length; i++) {
    setTimeout(function(v) {   // This function returns a function with a copy of a current iterator value (non-object type)
      return function() {
        console.log("I am in pass: i=" + v);
      }
    }(i), 0)
  }
}

function noCB(arr) {
  for (var i = 0; i < arr.length; i++) {
    console.log("I am NOT in cb: i=" + i); // This is actually executed here
  }
}

function fail(arr) {
  for (var i = 0; i < arr.length; i++) {
    setTimeout(function() {               // This function is define but not run here
      console.log("I am in fail: i=" + i)  // The i here is the same as the i being iterated with the for loop.
    }, 0)                                  // This value will be looked at AFTER the loop is completed when the callback is called later
  }
}


fail(a);
noCB(a);
pass(a);

I changed i in pass() to v as it is actually a copy. You can use i in place there but it was to show that it is a copy and not the same reference.

Goblinlord
  • 3,290
  • 1
  • 20
  • 24
  • I'm coming to WebDev from a Java background, so this is proving quite confusing to me. I don't quite understand why the context has the outsider `app.routeMap` instance working, but the much closer `i` fails. As for you example and explanation, in the browser console, `noCB` works like `pass` for me (is this what's expected, or should `noCB` have gone OOB and failed like `fail()`? – CosmicGiant Feb 05 '16 at 14:28
  • Anyways, replacing the `pass` code's `arr` with `app.routeMap.length`, and putting `page(app.routeMap[i].url, function () { app.route = app.routeMap[i].name; });` within the `setTimeout` function has made things work, and I'll try to figure out why based on the same kind of testing as [this SO question](http://stackoverflow.com/questions/2130241/pass-correct-this-context-to-settimeout-callback), which seems to be explaining something similar, in similar manner. --- If you are willing to explain further, however, and help me understand this easier/faster, that would be greatly appreciated! =) – CosmicGiant Feb 05 '16 at 14:31
  • You shouldn't need setTimeout. Actually, the .apply statement I made was incorrect (apply called the function immediately). I also changed the var name inside pass so you know its actually different. I edited my answer to use a correct method. The problem is that the callback function in something like: ```someFunction(function(){})``` is defined and not run where you are defining it. It then has a reference to ```i``` (the same ```i``` you are iterating with). The ```i``` doesn't get used until after your for loop finishes (when the callback is actually called). – Goblinlord Feb 05 '16 at 14:50
  • Also, yes... noCB is what is expected. It is showing that it is immediately executing instead of being called a later time using a callback (cb). – Goblinlord Feb 05 '16 at 14:53
  • Tried it without setTimeout and without apply...It breaks the code again... --- As for my understanding, I think I'm starting to get it...the code inside the for-loop is actually defining a callback routine that's not executed within the loop, right? like `page(app.routeMap[i].url, function () { /*this gets called later, outside the loop context?*/ app.route = app.routeMap[i].name; });` and the `setTimeout` function is "setting a callback for the callback", thus bringing it back to the loop context? (...) – CosmicGiant Feb 05 '16 at 15:04
  • Based on that understanding (I don't know if it's correct), I've moved the `page()` function to inside a separate `registerRoute(url, name)` function that's called from the loop: `function registerRoute(routeUrl, routeName) { page(routeUrl, function () { app.route = routeName; }); }` and `for (var i = 0; i < app.routeMap.length; i++) { registerRoute(app.routeMap[i].url, app.routeMap[i].name) }`... It works, like the `setTimeout`+`apply` did, without making "callback of the callback". Don't know how "correct" this approach is, but it works... still, anything you can add is still very welcome! – CosmicGiant Feb 05 '16 at 15:07
  • Yeah sorry about the apply (apply executes immediately). I corrected that above... return a function from a function called immediately (corrected format above). .apply is actually executing. ```function(i){ return function(){/* "i" used here */}}(i)```. What is happening is the outer function is creating a new scope with a copy of the value of ```i```. otherwise it is using ```i``` in the scope of the for loop (which goes OOB before cb is run). – Goblinlord Feb 05 '16 at 15:10