Hi is there a way I can have a base layout, like in express for node, see here , when using mustache for spring boot? So that I could do:
base layout:
<html>
<body>
<p>Title</p>
{{{body}}} // or similar, not sure about #
</html>
and then use the Controller's returned view name as content to render a view, so the outside is always the same?
You can do this via Mustache lambda functions. With this tutorial I was able to achieve what you need: https://spring.io/blog/2016/11/21/the-joy-of-mustache-server-side-templates-for-the-jvm#layout-abstractions-using-a-lambda
Create a controller advice like this:
#ControllerAdvice
class LayoutAdvice {
#ModelAttribute("layout")
public Mustache.Lambda layout() {
return new Layout();
}
}
class Layout implements Mustache.Lambda {
String body;
#Override
public void execute(Fragment frag, Writer out) throws IOException {
body = frag.execute();
}
}
This will make a lambda function named "layout" accessible for your Mustache templates. The lambda function itself receives the fragment (which will be inside the layout tags, see below) and store it in the layout object's body field. This will be accessible from the layout template:
<html>
<body>
{{{layout.body}}}
</body>
</html>
The filename must be "layout.html" as we used #ModelAttribute("layout") above.
Then you can use your layout by calling the lambda function and passing a fragment which will be the body of your layout:
{{#layout}}
<h1>Demo</h1>
<div>Hello World</div>
{{/layout}}
{{>layout}}
It will produce the desired output:
<html>
<body>
<h1>Demo</h1>
<div>Hello World</div>
</body>
</html>
Related
I am trying to call a REST endpoint and then display a ThymeLeaf template:
The Endpoint:
#GetMapping("/devices")
public String getDeviceDetailU(Model model) {
List<FinalDevice> devices = deviceService.getAll();
model.addAttribute("devices", devices);
return "deviceList";
}
For the endpoint I tried returning /deviceList, /deviceList.html, deviceList.html.
Whenever I navigate to the endpoint, I simply get the string that was returned.
Here is the template:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<body>
Hello World!
</body>
</html>
While I understand, at this point, it will not display the list, I just want to be forwarded to the template.
If I go to localhost:8080/deviceType I display that template. This to me indicates it is not a security or configuration issue.
Any ideas?
This should all work according to this tutorial.
You probably have #RestController instead of just a #Controller.
If you want templates to be rendered you need to use #Controller. #RestController means that all your #Mappings simply serialize the return value and output it as json or xml (which is why you are seeing the string deviceList instead of the template).
I'm trying to use ajax with thymeleaf. I designed a simple html page with two input field. I would like to use addEventHandler for the value of first input text, then I want to send it to controller and make calculation, after that I need to write it in same html form in the second field which returns from controller.
For example:
first input text value -> controller (make calculation) -> (write value) in second input text.
My html page is
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link rel='stylesheet prefetch' href='http://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css'>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<input type="text" name="Thing" value=""/>
<script th:inline="javascript">
window.onload = function () {
/* event listener */
document.getElementsByName("Thing")[0].addEventListener('change', doThing);
/* function */
function doThing() {
var url = '#{/testurl}';
$("#fill").load(url);
alert('Horray! Someone wrote "' + this.value + '"!');
}
}
</script>
<!-- Results block -->
<div id="fill">
<p th:text="${responseMsg}"/></div>
</div>
</body>
</html>
My controller
#RequestMapping(value = "/testurl", method = RequestMethod.GET)
public String test(Model model) {
model.addAttribute("responseMsg","calcualted value")
return "test";
}
However I cannot call controller from ajax. Could you help me?
There are a few issues with your code. First of all, it looks like you're using the same template for both the initial loading of the application, and returning the calculated result.
You should split these two into different calls if you're using AJAX, since one of the goals of AJAX is that you don't need to reload an entire page for one change.
If you need to return a simple value, you should use a separate request method like this:
#GetMapping("/calculation")
#ResponseBody
public int multiply(#RequestParam int input) {
return input * 2; // The calculation
}
What's important to notice here is that I'm using #ResponseBody and that I'm sending the input to this method as a #RequestParam.
Since you will be returning the calculated value directly, you don't need the Model, nor the responseMsg. So you can remove that from your original request mapping.
You can also remove it from your <div id="fill">, since the goal of your code is to use AJAX to fill this element and not to use Thymeleaf. So you can just have an empty element:
<div id="fill">
</div>
Now, there are also a few issues with your Thymeleaf page. As far as I know, '#{/testurl}' is not the valid syntax for providing URLs. The proper syntax would be to use square brackets:
var url = [[#{/calculation}]];
You also have to make sure you change the url to point to the new request mapping. Additionally, this doesn't look as beautiful since it isn't valid JavaScript, the alternative way to write this is:
var url = /*[[ #{/calculation} ]]*/ null;
Now, your script has also a few issues. Since you're using $().load() you must make sure that you have jQuery loaded somewhere (this looks like jQuery syntax so I'm assuming you want to use jQuery).
You also have to send your input parameter somehow. To do that, you can use the event object that will be passed to the doThing() function, for example:
function doThing(evt) {
var url = [[#{/calculation}]];
$("#fill").load(url + '?input=' + evt.target.value);
alert('Horray! Someone wrote "' + this.value + '"!');
}
As you can see, I'm also adding the ?input=, which will allow you to send the passed value to the AJAX call.
Finally, using $().load() isn't the best way to work with AJAX calls unless you try to load partial HTML templates asynchronously. If you just want to load a value, you could use the following code in stead:
$.get({
url: /*[[ #{/calculation} ]]*/ null,
data: { input: evt.target.value }
}).then(function(result) {
$('#fill').text(result);
});
Be aware that $.get() can be cached by browsers (the same applies to $().load() though). So if the same input parameter can lead to different results, you want to use different HTTP methods (POST for example).
I don't know how to solve the following: I'd like to let my Model generate real javascript dynamically based on some model logic.
This final piece of javascript code then should be added inside the $(document).ready { } part of my html page.
The thing is: If I use inline="javascript", the code gets quoted as my getter is a String (that is how it is mentioned in the Thymeleaf doc but it's not what I need ;-)
If I use inline="text" in is not quoted but all quotes are escaped instead ;-) - also nice but unusable 8)
If I try inline="none" nothing happens.
Here are the examples
My model getter created the following Javascript code.
PageHelper class
public String documentReady() {
// do some database operations to get the numbers 8,5,3,2
return "PhotoGallery.load(8,5,3,2).loadTheme(name='basic')";
}
So if I now try inline="javascript"
<script th:inline="javascript">
/*<![CDATA[*/
jQuery().ready(function(){
/*[[${pageHelper.documentReady}]]*/
});
/*]]>*/
</script>
it will be rendered to
<script>
/*<![CDATA[*/
jQuery().ready(function(){
'PhotoGallery.load(8,5,3,2).loadTheme(name=\'basic\')'
});
/*]]>*/
</script>
Which doesn't help as it is a String literal, nothing more (this is how Thymeleaf deals with it).
So if I try inline="text" instead
<script>
/*<![CDATA[*/
jQuery().ready(function(){
PhotoGallery.load(8,5,3,2).loadTheme(name='basic')
});
/*]]>*/
</script>
Which escapes the quotes.
inline="none" I do not really understand, as it does nothing
<script>
/*<![CDATA[*/
jQuery().ready(function(){
[[${pageHelper.documentReady}]]
});
/*]]>*/
</script>
To be honest I have no idea how to solve this issue and hopefully anybody out there knows how to deal with this.
Many thanks in advance
Cheers
John
I would change the approach.
Thymeleaf easily allows you to add model variables in your templates to be used in Javascript. In my implementations, I usually put those variables somewhere before the closing header tag; to ensure they're on the page once the JS loads.
I let the template decide what exactly to load, of course. If you're displaying a gallery, then render it as you would and use data attributes to define the gallery that relates to some JS code. Then write yourself a nice jQuery plugin to handle your gallery.
A relatively basic example:
Default Layout Decorator: layout/default.html
<!doctype html>
<html xmlns:layout="http://www.thymeleaf.org" xmlns:th="http://www.thymeleaf.org">
<head>
<title>My Example App</title>
<object th:remove="tag" th:include="fragments/scripts :: header" />
</head>
<body>
<div layout:fragment="content"></div>
<div th:remove="tag" th:replace="fragments/scripts :: footer"></div>
<div th:remove="tag" layout:fragment="footer-scripts"></div>
</body>
</html>
The thing to notice here is the inclusion of the generic footer scripts and then a layout:fragment div defined. This layout div is what we're going to use to include our jQuery plugin needed for the gallery.
File with general scripts: fragments/scripts.html
<div th:fragment="header" xmlns:th="http://www.thymeleaf.org">
<script type="text/javascript" th:inline="javascript">
/*<![CDATA[*/
var MY_APP = {
contextPath: /*[[#{/}]]*/,
defaultTheme: /*[[${theme == null} ? null : ${theme}]]*/,
gallery: {
theme: /*[[${gallery == null} ? null : ${gallery.theme}]]*/,
images: /*[[${gallery == null} ? null : ${gallery.images}]]*/,
names: /*[[${gallery == null} ? null : ${gallery.names}]]*/
}
};
/*]]>*/
</script>
</div>
<div th:fragment="footer" xmlns:th="http://www.thymeleaf.org">
<script type="text/javascript" src="/js/jquery.js"></script>
<script type="text/javascript" src="/js/my_app.js"></script>
</div>
In the scripts file, there are 2 fragments, which are included from the decorator. In the header fragment, a helpful context path is included for the JS layer, as well as a defaultTheme just for the hell of it. A gallery object is then defined and assigned from our model. The footer fragment loads the jQuery library and a main site JS file, again for purposes of this example.
A page with a lazy-loaded gallery: products.html
<html layout:decorator="layout/default" xmlns:layout="http://www.thymeleaf.org/" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Products Landing Page</title>
</head>
<body>
<div layout:fragment="content">
<h1>Products</h1>
<div data-gallery="lazyload"></div>
</div>
<div th:remove="tag" layout:fragment="footer-scripts">
<script type="text/javascript" src="/js/my_gallery.js"></script>
</div>
</body>
</html>
Our products page doesn't have much on it. Using the default decorator, this page overrides the page title in the head. Our content fragment includes a title in an h1 tag and an empty div with a data-gallery attribute. This attribute is what we'll use in our jQuery plugin to initialize the gallery.
The value is set to lazyload, so our plugin knows that we need to find the image IDs in some variable set somewhere. This could have easily been empty if the only thing our plugin supports is a lazyloaded gallery.
So the layout loads some default scripts and with cleverly placed layout:fragments, you allow certain sections of the site to load libraries independent of the rest.
Here's a basic Spring controller example, to work with our app: MyController.java
#Controller
public class MyController {
#RequestMapping("/products")
public String products(Model model) {
class Gallery {
public String theme;
public int[] images;
public String[] names;
public Gallery() {
this.theme = "basic";
this.images = new int[] {8,5,3,2};
this.names = new String[] {"Hey", "\"there's\"", "foo", "bar"};
}
}
model.addAttribute("gallery", new Gallery());
return "products";
}
}
The Gallery class was tossed inline in the products method, to simplify our example here. This could easily be a service or repository of some type that returns an array of identifiers, or whatever you need.
The jQuery plugin that we created, could look something like so: my_gallery.js
(function($) {
var MyGallery = function(element) {
this.$el = $(element);
this.type = this.$el.data('gallery');
if (this.type == 'lazyload') {
this.initLazyLoadedGallery();
}
};
MyGallery.prototype.initLazyLoadedGallery = function() {
// do some gallery loading magic here
// check the variables we loaded in our header
if (MY_APP.gallery.images.length) {
// we have images... sweet! let's fetch them and then do something cool.
PhotoGallery.load(MY_APP.gallery.images).loadTheme({
name: MY_APP.gallery.theme
});
// or if load() requires separate params
var imgs = MY_APP.gallery.images;
PhotoGallery.load(imgs[0],imgs[1],imgs[2],imgs[3]).loadTheme({
name: MY_APP.gallery.theme
});
}
};
// the plugin definition
$.fn.myGallery = function() {
return this.each(function() {
if (!$.data(this, 'myGallery')) {
$.data(this, 'myGallery', new MyGallery(this));
}
});
};
// initialize our gallery on all elements that have that data-gallery attribute
$('[data-gallery]').myGallery();
}(jQuery));
The final rendering of the products page would look like so:
<!doctype html>
<html>
<head>
<title>Products Landing Page</title>
<script type="text/javascript">
/*<![CDATA[*/
var MY_APP = {
contextPath: '/',
defaultTheme: null,
gallery: {
theme: 'basic',
images: [8,5,3,2],
names: ['Hey','\"there\'s\"','foo','bar']
}
};
/*]]>*/
</script>
</head>
<body>
<div>
<h1>Products</h1>
<div data-gallery="lazyload"></div>
</div>
<script type="text/javascript" src="/js/jquery.js"></script>
<script type="text/javascript" src="/js/my_app.js"></script>
<script type="text/javascript" src="/js/my_gallery.js"></script>
</body>
</html>
As you can see, Thymeleaf does a pretty good job of translating your model to valid JS and actually adds the quotes where needed and escapes them as well. Once the page finishes rendering, with the jQuery plugin at the end of the file, everything needed to initialize the gallery should be loaded and ready to go.
This is not a perfect example, but I think it's a pretty straight-forward design pattern for a web app.
instead of ${pageHelper.documentReady} use ${pageHelper.documentReady}
I am using a fresh build today of Laravel 4.
I have a dashboardController
class DashboardController extends BaseController {
protected $layout = 'layouts.dashboard';
public function index()
{
$this->layout->content = View::make('dashboard.default');
}
}
I have a simple route
Route::get('/', 'DashboardController#index');
I have a blade layout in views/layouts/dashboard.blade.php
For the sake of saving everyone from all of the actual HTML ill use a mock up.
<html>
<head>
<title></title>
</head>
<body>
#yield('content')
</body>
</html>
I have a default blade file in views/dashboard/ that has the following (edited for simplicity)
#section('content')
<p>This is not rocket science</p>
#stop
For some reason the content gets generated before the layout.
I am using a different approach to set the layouts globally to routes using a custom filter. Put the following filter into the app/filters.php
Route::filter('theme', function($route, $request, $response, $layout='layouts.default')
{
// Redirects have no content and errors should handle their own layout.
if ($response->getStatusCode() > 300) return;
//get original view object
$view = $response->getOriginalContent();
//we will render the view nested to the layout
$content = View::make($layout)->nest('_content',$view->getName(), $view->getData())->render();
$response->setContent($content);
});
and now instead of setting layout property in the controller class, you can group the routes and apply the filter as shown below.
Route::group(array('after' => 'theme:layouts.dashboard'), function()
{
Route::get('/admin', 'DashboardController#getIndex');
Route::get('/admin/dashboard', function(){ return View::make('dashboard.default'); });
});
When creating the views, make sure to use the #section('sectionName') in all the sub views and use #yield('sectionName') in the layout views.
I find it easier to do my layout like this for example. I would create my master blade file like so
<html>
<body>
#yield('content');
</body>
</html
And in the blade files that I want to use the master at the top i would put
#extends('master')
then content like so
#section('content')
// content
#stop
Hope this helps.
When you use controller layouts, i.e. $this->layout->..., then you get access to data as variables, not sections. So to access content in your layout you should use...
<html>
<head>
<title></title>
</head>
<body>
<?php echo $content; ?>
</body>
</html>
And in your partial, you would not use #section or #stop...
<p>This is not rocket science</p>
Reading through the Grails docs (see here http://grails.org/doc/latest/guide/theWebLayer.html#ajax), I was led to believe that I could use Ajax to update a div using the following syntax:
My view (Ajax/index.gsp)
<!doctype html>
<head>
<meta name="layout" content="main"/>
</head>
<body>
<div id="error"></div>
<div id="message"></div>
<g:remoteLink action="retrievePets" update="message">Ajax magic... Click here</g:remoteLink>
</body>
</html>
My controller (AjaxController):
package genericsite
class AjaxController {
def index() { }
def retrieveMessage() {
render "Weeee! Ajax!"
}
}
However, when I select the link, it just sends me to a page with "Weeee! Ajax!" I know how to do this the typical jQuery way. This is slightly more convenient...
The default "main" layout doesn't include a javascript library by default, so if you want to use remoteLink or any of its associates you'll need to add
<r:require module="jquery"/>
or (if you're on a pre-2.0 version of Grails or not using the resources plugin)
<g:javascript library="jquery"/>
to the <head> section of your GSP.