pippo icon indicating copy to clipboard operation
pippo copied to clipboard

Add route method helper in templates

Open decebals opened this issue 8 years ago • 15 comments

In my applications based on Pippo I feel that I'm missing a route(<template_name>, paramId_1:paramValue_1, ...) method in my html templates. For example, in companies.peb file I have something like:

<a href="{{ appPath }}/admin/companies/{{ company.id }}">{{ i18n('edit') }}</a>

and I wish something like (pseudo code):

<a href="{{ route("admin.companies.edit", "id":company.id) }}">{{ i18n('edit') }}</a>

and in .java file (I will don't use route group for simplicity):

GET("/admin/customers/{id}, ...).named("admin.companies.edit");

decebals avatar Mar 19 '16 23:03 decebals

I built a template function route that basically concatenates parameters (up to 5) with each other, smartly collapsing slashes if needed (but still allowing you to double up if needed). Parameters will treated as strings by calling .toString(), so they should implement that method. If they don't, Object does that anyway, but you might get weird output with custom objects.

Before I do a pull request, I want to verify the functionality that is desired. In your example, with my method, you would do this:

<a href="{{ route(appPath, "/admin/companies/", company.id) }}">{{ i18n('edit') }}</a>

and (if appPath = "/my/app/path" and company.id = 7) you would get:

<a href="/my/app/path/admin/companies/7">edit</a>

Is that the functionality that is desired?

It might be helpful to give 2 or 3 test cases showing input and desired output.

-gns

gregschmit avatar Jul 28 '17 03:07 gregschmit

My idea is to use Router.uriFor (reverse routing) with a route name in this template function. The implementation of template function do a simple job: read the function parameters, call Router.getUri and it returns the result (a string) of the method call. I prefer this approach with route name because is short (I don't write too much). As a bonus Router.getUri accept as first parameter nameOrUriPattern so in theory (you should check), this implementation resolved your case (pass uri pattern but without appPath that it's unnecessary because it is present in all scenarious and we can retrieve the value of this variable directly in template function, no need to carry on this variable any time). Your version is good but it's not the shortest one.

Code snippet for template:

  • variant with route name (you must give a name to your route)
<a href="{{ route("admin.companies.edit", "id":company.id) }}">{{ i18n('edit') }}</a>
  • variant with uri pattern
<a href="{{ route("/admin/companies", "id":company.id) }}">{{ i18n('edit') }}</a>

Make sense for you what I say?

decebals avatar Jul 28 '17 07:07 decebals

Not completely, but it could be because I'm not an expert. What output do you expect from the examples you gave?

-gns

gregschmit avatar Jul 28 '17 14:07 gregschmit

I do understand that you want to load appPath automatically (which makes sense to me), but it is the second part that I'm not entirely grasping. I think if you show me the expected output, then I will understand better.

-gns

gregschmit avatar Jul 28 '17 14:07 gregschmit

The expected output is:

<a href="/admin/companies/32">Edit</a>

where 32 is the value for id.

Take a look at javadoc of Router.uriFor method. This method is used extensive in Java code (back-end) when I wish to redirect a request to another route. The idea is that Pippo knows how to create an url for a route starting from the route name (or uri pattern) and parameters values (query and path parameters). For example:

// register a route (a route with name "user")
GET("/user/{id}", new MyRouteHandler()).named("user");

// register other route that redirect to "user" route
GET("/admin", routeContext -> {
    Map<String, Object> parameters = new HashMap<>();
    parameters.put("id", 0); // the admin user has id 0 (zero)
    routeContext.redirect(getRouter().uriFor("user", parameters)); // "user" is the name of the first declared route
}

In the second route instead of

routeContext.redirect(getRouter().uriFor("user", parameters)); // "user" is the name of the first declared route
//routeContext.redirect("user", parameters); // it's a shortcut of above line

you can use directly, the uri (url) generated by uriFor method

routeContext.redirect("/user/0"); // "user" is the name of the first declared 

but it's safe (against refactoring) to use the variant with getRouter().uriFor().

Is it a bit more clear for you? Do you have questions?

decebals avatar Jul 28 '17 15:07 decebals

Any interest for this feature?

decebals avatar Jan 11 '18 18:01 decebals

This sounds very attractive to me. Especially if it could be compile time safe, which it probably can't be, but at least fail loudly at run time then so we can unit test it.

Wavesonics avatar Aug 28 '18 07:08 Wavesonics

@Wavesonics I will try an implementation for Pebble template engine in first step (I use this template engine in my projects) and I will propose you a PR.

decebals avatar Aug 30 '18 13:08 decebals

I was sitting down to implement this for my self, and I realize after some looking that it doesn't actually look like it's possible to add more helpers to a template engine at run time?

Currently the builder happens entirely in the init() method with no hooks to modify it: https://github.com/pippo-java/pippo/blob/995773510b7ae19d45b8f881820d689d093655f6/pippo-template-parent/pippo-trimou/src/main/java/ro/pippo/trimou/TrimouTemplateEngine.java#L70

Not sure which way you'd guys prefer going about this, but might be nice to provide a method to override where you are passed the builder object and allowed to customize it?

Wavesonics avatar Sep 11 '18 00:09 Wavesonics

@Wavesonics

You can create the MyTrimouTemplateEngine class, like this:

public class MyTrimouTemplateEngine extends TrimouTemplateEngine {
    @Override
    protected void init(Application application, Configuration configuration) {
        // your custom logic here
    }
}

In your Application / ControllerApplication sub-class, you can do the following:

public class PippoApplication extends ControllerApplication {
    @Override
    protected void onInit() {
        setTemplateEngine(new MyTrimouTemplateEngine());
    }
}

I think this solves your problem. I needed to do this with the Freemarker and it worked.

mhagnumdw avatar Sep 11 '18 01:09 mhagnumdw

Oh man I totally missed that, and it's exactly what I was looking for. I apologize.

Wavesonics avatar Sep 11 '18 03:09 Wavesonics

I have a very basic Helper implemented for this that is working quite well: https://gist.github.com/Wavesonics/839b283eaf1245ffbe251f8d8a6e3016 It's currently implemented in Kotlin, but I can convert it to Java of course.

Which in use looks like this: <a href="{{route 'index.loginPage'}}">Login</a><br/>

Being able to handle variables would be very useful, I need to figure out how to go about that using the template system.

Wavesonics avatar Sep 11 '18 04:09 Wavesonics

Updated it to accept parameters for the routes: https://gist.github.com/Wavesonics/d1e65a4c094a04e0a7066d92545e9e4f

<a href="{{route 'user.userForName' 'name: user.userName'}}">User Page</a><br/>

Wavesonics avatar Sep 11 '18 06:09 Wavesonics

I think that it's a good practice to add the missing information in doc incremental, when someone find a good solution to his problems, solution that is not present in documentation. If a contribution with an update is not possible, it's enough an issue that point to solution, I think that documentation together with tests are things that are not sufficiently developed and from this reason the adoption of Pippo is not high enough.

decebals avatar Sep 11 '18 08:09 decebals

I implemented this feature in Pebble template engine (I use this template engine in my projects). The implementation is not complete (need to add more defense code - deal with route not found) but is complete functional.

How to use:

  • define a route with a name
GET("/quote", routeContext -> {
    ParameterValue symbol = routeContext.getParameter("symbol");
    if (symbol.isEmpty()) {
        routeContext.getResponse().badRequest().send("Specify a symbol as parameter");
    } else {
        Stock stock = dataStore.select(Stock.class).where(Stock.SYMBOL.eq(symbol.toString())).get().first();
        List<Quote> quotes = dataStore.select(Quote.class).where(Quote.SYMBOL.eq(stock.symbol)).get().toList();

        Map<String, Object> model = new HashMap<>();
        model.put("stock", stock);
        model.put("quotes", quotes);

        routeContext.render("quote", model);
     }
}).named("quote");
  • reference the route defined above using route method
<a href="{{ route('quote', {'symbol': stock.symbol }) }}">{{ stock.symbol }}</a>

I want to say that I consider this feature very nice. The html template is more clear and short :smile: .

decebals avatar Oct 27 '18 18:10 decebals