Passing JavaScript arguments via the src attribute
The dual <script>
idiom—covered in Remotely called JavaScript with local configuration–which is generally necessary in JavaScript
based web services is awkward and unfriendly. When simple
configuration is all that’s needed, the terse and well-known API that the query
string affords would be superior if only JavaScript could use it. With
just a few lines added to the remote script, it can.
We’d like to write something direct like this.
<script src="http://elektrum.org/cal.js?show=appts;user=veri" type="text/javascript"></script>
Instead we must call two scripts. A configuration script to pass arguments to an imported script which uses them.
<script type="text/javascript"> <!--// user = 'veri'; show = 'appts'; //--> </script> <script src="http://elektrum.org/cal.js" type="text/javascript"></script>
A disappointing fact of JavaScript is that an executing script is not
itself an object—even though the <script> that contains it
is—like everything in the DOM. Therefore, a script doesn’t have direct
knowledge of its calling conditions, including its source URI and query string.
The location
and document
objects don’t
directly do us any good in this regard.
That’s why we must resort to two scripts when we want to provide a service plus user configuration. Google ads, the omnipresent example, do this. Fifteen lines or so of code to get an ad into a page. Not hard per se but not as easy as one.
Ease is part of affordance in UI and design. The easier path will be the path
that users stay on. The separation of code from client is part of
that. You could provide your user with some kind of
lib.js
used with another script call but what if you find
a bug or they have problems and your pager number? What if you want to
launch a simple enhancement? Getting all your users to install it would
be difficult; some would never know v2 was actually worth keeping.
The obstacle challenge
How do we make a script self-aware? The answer is deceptively simple.
When a script with a src
is loaded in a page, the rest of
the page is not yet written. The implication is that no matter how
many scripts a page contains, the one currently starting to execute is
the last one; so far. So the following reliably makes a script aware
of its caller and not just its caller’s page.
var scripts = document.getElementsByTagName('script'); var index = scripts.length - 1; var myScript = scripts[index]; // myScript now contains our script object
This works in all current JavaScript enabled web browsers. Including
but not limited to IE for
PC and Mac, FireFox, Safari, and Opera. Another, perhaps obvious, tack
is using the id
attribute in the script
tag,
does not, and should not. It’s natural to try to use the
id
with getElementById()
but the HTML specification for
script
doesn’t contain the id
and even
though a browser or two supports it, they shouldn’t and you can’t rely
on it.
Once past that bit of thinking like the browser, getting the query
string is easy because we have access to the script’s source via its
myScript.src
attribute. We take a perl-ish approach because
JavaScript’s regular expressions are powerful and often overlooked.
Getting the query string out
// myScript.src is "http://elektrum.org/cal.js?show=appts;user=veri" var queryString = myScript.src.replace(/^[^\?]+\??/,''); // queryString is "show=appts;user=veri"
We take the script’s src
and erase—replace with the
empty ”—everything from the start, ^
, that is not
a question mark, [^\?]+
, up through a question mark, if
there is one, \??
. This also safely produces an empty
string in queryString
if, for whatever reason, there is
no query string.
Get the arguments out
We got our query string out. It’s now in our queryString
variable. We need to be able to use the arguments programmatically. We
parse the query string into an associative array Object
by breaking key=value pairs out on the semi-colon (;) or ampersand1 (&), and then splitting them apart on the equals
(=).
var params = parseQuery( queryString ); function parseQuery ( query ) { var Params = new Object (); if ( ! query ) return Params; // return empty object var Pairs = query.split(/[;&]/); for ( var i = 0; i < Pairs.length; i++ ) { var KeyVal = Pairs[i].split('='); if ( ! KeyVal || KeyVal.length != 2 ) continue; var key = unescape( KeyVal[0] ); var val = unescape( KeyVal[1] ); val = val.replace(/\+/g, ' '); Params[key] = val; } return Params; }
With the newly returned variable params
a source like
"http://elektrum.org/snow.js?state=nm;town=Taos"
will
parse out the query string as the equivalent of the following code.
params = new Object({state:'nm' ,town:'Taos'});
It can be used like so.
document.write('Ski conditions in ' + params['town'] + ', ' + params['state'].toUpperCase() + '...');
Which would render–
The rub
This implementation is not just naïve, it’s technically wrong.
Multiple parameters of the same name are allowed in CGI; POST or GET.
fish=coelacanth;fish=khuli
should pass both values along
to the key fish
. Our implementation would drive the
ceolocanth where 360 million years hasn’t been able: extinction.
The fix
It’s easy enough to parse the values into arrays. That way the key looks up a list of values, which might well only contain one item, instead of a single one. Only one change, and two new lines are needed to make this work.
Params[key] = val;if ( ! Params[key] ) Params[key] = new Array (); Params[key].push( val );
push
in JS isn’t supported by all older browsers though
and it introduces a wrinkle when using the values. They’re arrays now,
whether their param[key].length == 1
or not. So you have
to be conscious of that when you manipulate or write them out. If you
don’t need to support multiple values, don’t bother with the change.
Okay, another potential rub
Anyone who has dealt with JavaScript and caching knows that a good
trick to keep a script and its output from getting cached is to put a
random query string on it. The server recognizes it isn’t the same
call as before and reloads the entire script. This means that if you
use the same service many times in a single page with different
configuration query strings and the script is large, it might be a
page load performance hit as the file is fetched and parsed for every
call with a different src
.
All together now
Find script, get query string, parse arguments out
var scripts = document.getElementsByTagName('script'); var myScript = scripts[ scripts.length - 1 ]; var queryString = myScript.src.replace(/^[^\?]+\??/,''); var params = parseQuery( queryString ); function parseQuery ( query ) { var Params = new Object (); if ( ! query ) return Params; // return empty object var Pairs = query.split(/[;&]/); for ( var i = 0; i < Pairs.length; i++ ) { var KeyVal = Pairs[i].split('='); if ( ! KeyVal || KeyVal.length != 2 ) continue; var key = unescape( KeyVal[0] ); var val = unescape( KeyVal[1] ); val = val.replace(/\+/g, ' '); Params[key] = val; } return Params; }
Unexpected bonuses
A great side-effect to this style of calling is you can dispense with
object scoped checks for imported variables. We’re not importing any
script variables at all, like we had to before, Remotely called JavaScript with local configuration. So we also don’t need to fear that we’re
going to throw errors by trying to use non-existent, never created or
declared, variables. With the dual script idiom we need to check
imported variables through another guaranteed object for safety like
checking for pangram
through window.pangram
.
It also means that since you’re no longer polluting the namespace, you can reuse a script over and over in the same page with different parameters. No variable bleed from one invocation to the next like we got in Pangrams in action.
And lastly, it is self-tracking. The query string goes into your weblog so you can see who called the service with what variables at what time. When you use the dual script idiom, you don’t get automatic logging of the variables. They’re only inside the JavaScript. It’s possible to track them with something like Pixeling & usage tracking but that’s more difficult and really only a win for services that are interactive, ie, the user on the client side can interact with dynamic content or a form.
A minor loss sneaked in
When you count on the client to cut and paste a bit of code—or when
you’re writing for your own hosts where you have control—you should
be using <noscript>
tags. That way you can build a
failsafe service. If the client’s browser has JavaScript off, you still
deliver an image or a text message and link, or even a mini form.
The remote nature of our clients precludes the use of delivering
<noscript>
tags. If JavaScript in not enabled, then
we obviously can’t write out a <noscript>
message
to say something about why the service isn’t working. This is a
noticeable loss.
That said, you should not count on a client for something like the
content of <noscript>
tags. They could leave it
out, or worse, alter it. A major reason to deliver content from a
remote source with JavaScript is so it can’t be tinkered with.
What now?
Now that we’ve gone to the trouble to make it possible, when would you really want to use this? Any time where a site is providing dynamic content non-programmatically. By non-programmatically, we mean the end user is only configuring the script once (with the query string). No call to get results to parse alla SOAP or even RSS. Though, that’s not impossible, it would be overly complicated and we’d lose everything we’ve gained in the simplicity JavaScript affords us.
Examples
- Simplistic web services like weather alerts.
- Advertisements like Google ads.
- Link circles.
- A web site award or certification service.
- A joke of the day (set for blue or tame).
- A code snippet of the day (pick a language).
- Key based configuration where only one variable, like a developer’s token, is necessary to look up a service for a client.
- A remotely run tracking, site metrics service.
- A front end of another language, like mod_perl or PHP, which accepts the request, does heavier processing like database work, and returns JavaScript customized for the client, arguments, or date.
Or how about a greeking service to help webdevs, which is exactly what we’ll develop next in Configurable greeking.