Using Jinja2 template under Knockout attr binding

混江龙づ霸主 提交于 2020-01-05 08:22:09

问题


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 to that user profile

Here is the code

<a data-bind="attr: { href: '{{ url_for('user_profile')}}' + '/' + name() + '/' + uid() + '/'  }"><p data-bind="text: name"></p></a>

but in the above code there comes a Jinja2 Template error as

BuildError: ('user_profile', {}, None)

So I changed the above piece of code referred knockout attr documentation Knockout attr binding

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

but the same error as

BuildError: ('user_profile', {}, None)

It means that user_profile view is not getting the required variables. In knockout the way to do this is attr binding. I have referred this question

Jinja2: Flask's url_for() combined with Knockout's attr binding

But nothing working as expected


回答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>')
def user_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"><p data-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:

<a data-bind="attr: { href: '/user/' + name() + '/' + uid() + '/'  }"><p data-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>')
def user_profile(name = None, uid = None):
    if name is None or uid is None:
        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!




回答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/')
def posts_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:

<div data-bind="foreach: posts">
  <div class="post">
    <div data-bind="text: content" class="content"></div>
    <div class="author_info">
      <a data-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 -->
<script type="text/javascript">
    // autogenerated:
    POSTS_URL="{{ url_for('posts_list') }}";

    // initialise views:
    postsview = new PostsView( 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:

<script type="text/javascript">
    POSTS_DATA={{ posts |tojson|safe }};
    posts = new PostsView( 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!




回答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:

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

I hope it can help.

Best Regards




回答4:


I have another solution on my site

app.py:

@app.context_processor
def pass_context():
    return dict(
        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.)



来源:https://stackoverflow.com/questions/19784956/using-jinja2-template-under-knockout-attr-binding

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!