Skip to content Skip to sidebar Skip to footer

Using Jinja2 Template Under Knockout Attr Binding

I am making a project using Flask and Knockoutjs. I am using Knockout to show comments and below the commentor's name would also be displayed which is when clicked takes the person

Solution 1:

You don't show the definition of your user_profile endpoint, but I bet it is more or less like this:

@app.route('/user/<name>/<uid>')defuser_profile(name, uid):
    # ...

The Jinja2 error that you get occurs because your user_profile endpoint requires two arguments that you are not giving to url_for().

The problem here is that url_for() generates URLs in the server, but you need the URLs generated in the client by Knockout instead. Your attempt at a solution is mixing both sides, but url_for() cannot work with partial information, you have to give it the whole thing.

I can think of two possible solutions for this problem. The first is the one I consider best, the second is closer to what you have done so far.

Solution #1

Do it the Knockout way. A click in a button or link controlled by Knockout should be handled by a Knockout controller. So you would write that <a> tag as something of this style:

<a data-bind="click: $parent.showProfile"class="btn"><pdata-bind="text: name"></a>

Then your Knockout controller will have a showProfile() method that will issue the redirect:

self.showProfile = function(user) {
    window.location = user.profileUrl
}

For this to work you need to add a profileUrl key to the JSON dictionary that Flask returns to the Knockout app. You don't show this part of your application, I imagine this should be easy to add, and since now the URLs are generated entirely in the server you are free to use url_for() and provide all the arguments.

Solution #2

If you prefer to fix the code you show above, then you have to give up the use of url_for() and build the URLs entirely in the client. I think the following example should work just fine:

<adata-bind="attr: { href: '/user/' + name() + '/' + uid() + '/'  }"><pdata-bind="text: name"></p></a>

If you don't want to give up url_for() completely, then you have to create a route that does not need the additional arguments, so that at least you get the base URL from the server:

@app.route('/user')@app.route('/user/<name>/<uid>')defuser_profile(name = None, uid = None):
    if name isNoneor uid isNone:
        abort(400) # bad request# ...

Now you can say url_for('user_profile') and you will get back /user, to which you can append the remaining arguments in Javascript. With this change in the Flask side I believe your first example should work.

As a side note, I hope you are aware that when the link is clicked that will blow away your Knockout app, which will be replaced with a new page (that may have another instance of the Knockout app, of course). Another option would be to use a single-page app, so then the change to the user profile happens entirely in the Javascript side. But of course this will move more of your app into the client.

I hope this helps!

Solution 2:

I think the problem is the separation of data & template information.

If you're using knockout, then you are usually generating data on the server, and sending it as json data to the template rendering on the client-side.

The url_for function is a server side function, and so you can't run it on the client within a client-side template.

As in @Miguel's answer, the easiest/best way is to generate the url on the server, as part of the data, not as part of the templating.

So your app.route for generating the data might be:

@app.route('/posts/')defposts_list():
    posts = []
    for post in get_posts():  #however you actually get your posts from your db
        author = get_author(post.author)  #however you actually get author info

        posts.append(
            {"id": post.id,
             "title":post.title,
             "author_name": author["display_name"],
             "author_uri":  url_for('user_profile', author["name"], author["id"]),
             "content": post.content})
    return jsonify({"posts": posts})

or whatever. You're generating all the posts data on the server side, so you have access to the url_for function. The client then only has to render pure data:

<divdata-bind="foreach: posts"><divclass="post"><divdata-bind="text: content"class="content"></div><divclass="author_info"><adata-bind="attr: { href: author_uri }, text: author_name"></a></div></div></div><!-- posts -->

Then in the HTML app.route, after all of the pure-knockout templating, you initialise your knockout views, and then request the data from the posts json route:

<!-- at the end of your HTML page/view, after including your models, and libs --><scripttype="text/javascript">// autogenerated:POSTS_URL="{{ url_for('posts_list') }}";

    // initialise views:
    postsview = newPostsView( data );
    ko.applyBindings(posts);

    // get initial data:
    $.getJSON(POSTS_URL, function (data) {
        // we've just got data sent to us from the posts_list route!
        postsview.posts(data.posts);
    });

</script>

If you try and mix jinja templating code (serverside) with knockout templating code (client-side) then you'll run into problems. You need to treat them as totally separate systems, and pass pure data (usually JSON) back and forth.

Alternatively, you can embed the data directly into your page, rather than making a separate async request. But again, remember to keep your knockout code completely free of jinja templating, then at the end of the page, instead of doing the $.getJSON stuff, you simply do:

<scripttype="text/javascript">POSTS_DATA={{ posts |tojson|safe }};
    posts = newPostsView( POSTS_DATA );
    ko.applyBindings(posts);
</script>

If you separate your data out this way, it makes it much easier to reason about, and also if later you decide to switch to a different javascript engine, you can, as the data contains everything you need. Or if you decide to re-write the python engine, or if you want to make a native app, or whatever.

I hope this helps!

Solution 3:

I do not have much experience with Knockout, but do not sounds right to put user_profile inside quotes, try to scape the quote character in the parameter for url_for function:

<adata-bind="attr: { href: '{{ url_for(\'user_profile\')}}', name: name , uid: uid  }"><pdata-bind="text: name"></p></a>

I hope it can help.

Best Regards

Solution 4:

I have another solution on my site

app.py:

@app.context_processordefpass_context():
    returndict(
        rules=[[rule.rule, rule.endpoint.replace('_', '-').replace('.', '-')] for rule in app.url_map.iter_rules()]
    )

templates/base.jinja:

<script>var routes = {};
{% for rule in rules -%}
    routes['{{ rule[1] }}'] = '{{ rule[0] }}';
{% endfor %}
</script>

and in my script.js:

var rurlarg = /<(?:\w+:)?([\w_-]+)>/;
var urlFor = function(page) {
    var url,
        urlArgs,
        _ = [];
    if (! (page in routes)) {
        throw {
            name: 'IndexError',
            message: 'no such page: ' + page
        };
    }
    url = routes[page];

    if (arguments.length > 1) {
        urlArgs = Array.prototype.slice.call(arguments, 1);
        urlArgs.forEach(function(val, i) {
            url = url.replace(rurlarg, val);
        });
    }

    return url;
}

It's not ideal function, but you get the idea. For example, you can add named arguments. Or filter rules for client side (hide admin endpoints etc.)

Post a Comment for "Using Jinja2 Template Under Knockout Attr Binding"