Server Blocks
SaneJS file-based routing works great for pages which don't require any server-side actions. But what if your page is displaying data from a database or needs to do some other server-side processing? That's where Server Blocks come into play.
By simply adding a <script server>...</script>
block to the top of your template, you can define Express route handlers for this route. An instance of the Express Router
object is available as server
. So anything that could be written in Express routes can be written here instead.
<script server>
const Book = require('~/models/Book')
server.get('/', async (req, res) => {
const books = await Book.find()
res.render(self, { books })
})
</script>
Line by line explanation
- Using the
server
attribute in a script tag tells the SaneJS middleware to strip this block from the template and parse it as server-side code. - The handy NPM package
wavy
is used to provide a quick reference to the project root forrequire
statements as inrequire('~/models/Book')
- In the third line, we define our Express router handler for
'/'
which refers to the top level for this route as determined by file-side routing. So if this file is located at/routes/books.html
, then'/'
will set the route handler to handle requests for/books
. - The
res.render
method takes the path to the template to render as its first arg and an Object to parse into the template as its second. As a convenience,self
can be used as a reference to the current route.
Note that the second argument passed to res.render
must be an Object, not a simple variable. Example routes are included in the starter-kit. All values rendered into a template can be referenced in the template like: {= foo }
or with Object dot-syntax like: {= foo.bar.baz }
. These variables can also be referenced for template conditionals, or loops.
Multiple methods, same file
If you take a look at routes/admin/users/edit.html
, you'll see that there are three Express router handlers in the same template. This is a common pattern. This template handles a GET request to initially show the populated form, a PATCH request to handle submitting the form to update the record, as well as a DELETE request to handle deleting the record. They all use the same route to handle different HTTP methods on the same resource.
Here's an example of loading a book by id and handling a request to update that form in the same template.
<script server>
const Book = require('~/models/Book')
// GET /book/:id
server.get('/:id', async (req, res) => {
const book = await Book.findById(req.params.id)
res.render(self, { book })
})
// PATCH /book/:id
server.patch('/:id', async (req, res) => {
const { title, author } = req.body
const book = await Book.findByIdAndUpdate(req.params.id, { title, author })
if (!book) res.error500()
res.redirect('/books')
})
</script>
<form hx-patch="/books/">...</form>
Swapping out only certain elements of a template by id
Say you have an "Add to cart" button which updates the sidebar to show items in your cart and also updates the cart icon badge in the site header. Normally, using res.render
will render the entire page. By default, SaneJS uses HTMX's handy hx-boost
method so that only the body
of the document is replaced – but that's still basically the whole page getting reloaded. What if there were a way render the whole page on the server with all the appropriate variables populated with new data, then have the server only send us the those elements that need to be swapped out? In our example, we might have an element with id="sidebar"
and another element with id="cart-icon"
. By simply supplying an array of id names as the third argument to res.render
, we tell the middleware to extract only those elements from the compiled template and send them down the line as "HTMX Out of Band Swap" elements. Then HTMX will handle those on the client side automatically.
Example replacing entire page
res.render(self, { myVars })
Example replacing only the sidebar
res.render(self, { myVars }, 'sidebar')
IMPORTANT: Remember that HTMX attributes like
hx-get
orhx-post
will replace the element that triggered the request by default. So in the sidebar example above, let's say the element that triggered that was a button like this:<button hx-post="add-to-cart/123">Add to Cart</button>button>
The response would swap the sidebar as expected, but it would also replace this button with an empty string. This is normal HTMX behavior. So if you want to swap only other elements on the page without swapping the element that triggered the action, you need to add one more attribute:
<button hx-post="add-to-cart/123" hx-swap="none">Add to Cart</button>button>
That extra
hx-swap="none"
tells HTMX to handle the response swaps for specified ids, but don't do anything to this element which triggered the action.
Route methods
The SaneJS middleware decorates Express req
and res
objects with helpful properties and methods. We've already seen res.render
. Here's the full list.
res.render(route[, obj][, swapIds])
Accepts a path to template relative to top-level routes/
directory and an optional object containing values to be interpolated into template. If a third argument is passed either as a single string or an array of strings, only elements with those ids will be rendered. Renders the page with nearest ancestor _layout.html
by default. In server blocks, a reference to the current route is available as self
.
res.partial(route[, obj])
Renders a template ignoring any parent sublayouts or layouts.
res.error404()
Uses routes/_404.html
and renders it without redirecting.
res.error500()
Uses routes/_500.html
and renders it without redirecting.
res.redirect(path)
For Express native res.redirect()
, use res.exRedirect
which does a standard server-side redirect. SaneJS overrides the natvie res.redirect
so that if the client request is an HTMX call, then the browser will simply render the redirected page into whatever chunk of DOM it was planning to update. Using res.redirect()
instead will first check the HTTP headers to see if this was an HTMX request. If so, it will add a special HTMX header to tell the browser to do a full redirect instead of rendering the AJAX response. If the request wasn't an HTMX request, then the standard res.exRedirect()
method will be called.
res.retarget(selector)
Sometimes depending on what happens on the server-side, you might want to override the original HTMX target so that the response is rendered somewhere else on the page. This will set a header and return the res
object so that it can be chained.
Example: res.retarget('#sidebar').partial('sidebar')
res.trigger(eventName)
Include a response header telling HTMX to trigger a custom Javascript event on the client.
req.isHtmx
Requests from HTMX include a special HTTP header to let us know that this is an HTMX request as opposed to a regular request from the browser. Sometimes this can be helpful to know when crafting our response routes. When the header is present (and not an HTMX Boosted request), this property will be set to true.