pebble icon indicating copy to clipboard operation
pebble copied to clipboard

Feature Request: Fragment Renderer

Open jochen777 opened this issue 8 years ago • 7 comments

I think we need a "fragment renderer" mechanism in pebble. It acts like an include/macro but has an additional Java-Class attached, that can execute business logic or queries the database.

Example:

In template: {% render "latestPosts" with {"count":5} %}

In Java:

public class LatestPosts implements PebbleFramgmentRenderer {
  public Map render (Object... args) {
     ...
     model.put("posts", getLatestsPosts());
  }

  public String provideTemplateName() {
    return "/framgements/latestPosts"; 
}

In Template: (/framgements/latestPosts)

{% for post in posts %}
    <h3>{{ post.title }}</h3>
{% endfor %}

In configuration:

public class PebbleExtension extends AbstractExtension {
@Override
public Map<String, FragmentRenderer> getFragmentRenderer () {
 Map<String, FragmentRenderer> renderer = new HashMap();
 renderer.put("latestPosts", new LatestsPosts());
 return renderer;
}
}

.net has View Components (https://docs.asp.net/en/latest/mvc/views/view-components.html), symfony has Subrequests (http://symfony.com/doc/current/templating/embedding_controllers.html). I think, the .net approach is very good for this.

if the current HttpServletRequest could be passed to the "render" method, this would be even more useful!

jochen777 avatar Oct 16 '16 11:10 jochen777

I do something similar to this using a plain function. In the function, I use a PebbleEngine to render a "fragment" and use the result to generate the result of the function itself.

The only difference I see from your example is that the current PebbleEngine could be more easily available from inside a function/filter. I inject mine using Guice since my custom Extension is part of the Guice context.

electrotype avatar Oct 16 '16 15:10 electrotype

That is a nice idea. I'll try this - thank you!

jochen777 avatar Oct 17 '16 07:10 jochen777

@jochen777 Have you come to a conclusion? Can you share some code?

decebals avatar Oct 26 '16 19:10 decebals

At first I really liked this idea but the more I think about it the more it feels like we would just be implementing a new technique for something that is already possible. I think I'm of the opinion that this feature is better implemented at the application/framework level instead of within the Pebble library (similar to how this feature exists in Symfony but does not exist in the Twig library). A framework may already have something that is synonymous with a "fragment renderer" (ex. a controller in an MVC app) and implement it with that in mind, similar to how Symfony did theirs. I may put more thought into implementing this feature into our pebble-spring4 library.

Below I've listed a couple alternate methods of how we can access native Java code prior to rendering a sub-template; the first being suggested by @electrotype. Neither of which require a lot of code and both could easily be expanded or made more generic.

Custom Pebble function that internally invokes a PebbleEngine

main.html:

{{ latestPosts() }}

custom pebble function:

public String latestPosts(Map<String, Object> args) {
  Map<String, Object> context = new HashMap<>();
  context.put("posts", database.query("SELECT * FROM posts")); 
  PebbleEngine engine = new PebbleEngine.Builder().build();
  return engine.getTemplate('/fragments/latestPosts').evaluate(new StringWriter(), context);
}

Logic contained within the provided context

main.html:

{% include '/fragments/latestPosts' %}

/fragments/latestPosts.html:

{% for post in latestPosts.posts %} {{ post }} {% endfor %}

LatestPostsRenderer.java:

public class LatestPostsRenderer {
  public List<Post> posts(){
    return database.query("SELECT * FROM posts");
  }
} 

rendering the main template:

Map<String, Object> context = new HashMap<>();
context.put("latestPosts", new LatestPostsRenderer()); 
PebbleEngine engine = new PebbleEngine.Builder().build();
return engine.getTemplate("main").evaluate(new StringWriter(), context);

mbosecke avatar Nov 14 '16 03:11 mbosecke

Thank you for your answer!

The problem with the custom-function: It does not get a handle to the current http-request. So you can't react on cookies or the current path...

And the problem with provided context: Your controller must be aware of that and collect all data.

I agree with you, that this may be better included in the webframework-integration layer. So I really would love to have it in the pebble-spring4 project!

But in the first place, the custom function can solve a lot of problems like this for me. I can easily access the Pebble-engine within the spring - context.

jochen777 avatar Nov 17 '16 16:11 jochen777

The problem with the custom-function: It does not get a handle to the current http-request. So you can't react on cookies or the current path...

Maybe you could use RequestContextHolder?

ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();

electrotype avatar Nov 17 '16 17:11 electrotype

Yes, I know RequestContextHolder. But it is kind of ugly and hard to test... (But of course better than nothing!)

jochen777 avatar Nov 17 '16 21:11 jochen777