Managing available client side data with JSON (Not just for Python!)

filed under javascript

Use case: You have a URL you want to pull data from on a webpage, with Ajax. What do you do? Here's how it goes down, depending on the level of the developer:

  • Hard code it into the Javascript. Whenever a URL changes, you "remember" to go through your code base, and update all the URLs to the new ones.
  • You realise this is silly, and start putting hidden div tags on your page, with the content you need as the body.
  • You realise that this is silly too, and start using hidden input fields, with the value being what you want.

And, of course, after a while, you deem this silly too. By then, you've got a lot of these hidden input fields on a lot of your pages. You've got some in parent templates, and you use the same field in two different scripts, so if you change one field, you change multiple places. The worst part is, they're linked silently, so when it breaks, unless you've got some really rigorous unit tests, you want know until manual testing occurs, sometimes not even then. You duplicate input field names/ids, so you end up with unpredicable results when getting a value. It's ends up hellish, and painful.

This is only a small example, too. What about information about the currently logged in user? What about if there even IS a logged in user? All sorts of things that you'd like to use in Javascript from the server. So, what can you do? Inject everything of interest into Javascript, as an object attached to a global variable. That sounds confusing, so here's an example implementation in Flask:

# this will insert it into the template context
@app.context_processor
def add_js_context_to_template():
    return {
        'GLOBAL_JS': simplejson.dumps(g.js_context),
    }

# straight forward enough, initialise the context before each request...
@app.before_request
def add_js_context():
    g.js_context = {}

def add_to_context(values):
    g.js_context.update(values)

@app.route('/')
def home_page():
    add_to_js_context({
        'user_name': g.user.name,
    })
    return render_template('page.html', value='oh no that poor chicken')

That's all the code you need to be on your way to sanity. Providing your framework has a means of injecting values into your templates on each request, you shouldn't have to do much more than this to implement it in your framework of choice. Here's the same code in Django, using a custom middleware:

# middleware.py

class JSContextMiddleware(object):
    def process_request(self, request):
        request._js_context = {}

        def add_to_context(values):
            request._js_context.update(values)

        request.add_to_context = add_to_context

    def process_template_response(self, request, response):
        response.context_data.update({
            'GLOBAL_JS': request._js_context,
        })
        return response

# views.py

def some_view(request):
    request.add_to_context({
        'user_name': request.user.username,
    })

    return render_to_response('page.html', context='oh no that poor chicken')

This lets you inject values you want available in JS in your view functions to the page level context. In your base template, define something like this:

# base template

<script type="text/javascript">
    window.global_context = {{ GLOBAL_JS }};
</script>

The method of interest here is add_to_context(). Call this with whatever dict you want, and it will appear in the JS via a global variable, global_context, providing it can be serialised to JSON. You had that limitation putting it into your templates before, so it's not too much of a problem. You could add more to it, like making sure the context variables are unique and whatnot, but this is already a good base to start using.

What makes this solution so good is that not only is your code instantly improved by using something like this, you improve the efficiency of the Javascript that interacts with it as well. No more selectors, crawling the DOM, no more id collisions. The improvements are huge. The "downside" is that you can't define new variables in the template itself, but this should probably indicate to you that you shouldn't be doing that anyway.

That about does it. I couldn't find anything online recommending a solution like this. The best I managed to find was recommendations of using hidden input tags, which we know isn't the right solution. So, I thought this was worth putting online for people to find. If you have a better solution than this, let me know!

Related Posts