Clickable Article Cards with Datastar
The video demonstrates how to make entire article cards clickable using data-on with a click event. Instead of wrapping cards in anchor tags, a GET request is issued via Datastar, triggering navigation through server-sent events (SSE). A pointer cursor is added to improve UX.
Partial Page Updates for Pagination
Pagination is refactored to update only the latest articles section instead of reloading the entire page. A dedicated articleCardsGrid component is introduced, and updates are targeted using consistent element IDs so Datastar can morph the correct DOM node.
Backend Integration with Datastar SSE (Go SDK)
A new controller method is created to return only the article grid using Datastar’s Go SDK. Server-sent events are used to patch specific elements in the UI, and a new route is registered to handle these partial updates.
URL State Management
Since partial updates do not automatically modify the browser URL, history state is manually updated using window.history.pushState. On the backend, replaceURL is used to ensure pagination state persists across refreshes and navigation.
Signal-Based UI Updates
A currentPage signal is introduced to dynamically display the active page number. Signals are defined higher in the DOM and updated from the backend using marshalAndPatchSignals, allowing UI elements to change immediately without waiting for full request completion.
Dynamic Page Metadata
The hardcoded total page count is replaced with backend-provided pagination metadata. The UI now reflects accurate page numbers (e.g., “2 out of 2”) and remains consistent after refreshes.
Advantages of Server-Sent Events
The approach highlights how SSE enables incremental UI updates, multiple sequential patches, and backend-driven state changes. This allows efficient, fine-grained reactivity without full page reloads.
So I'm in home.tmbl here, and I want to do two things on this page. I want to make it so that we can update only the latest articles section of the page, whenever we navigate back and forth. I also want to be able to click this article card anywhere on the card itself. And we could technically just rabbit everything in an anchor tag and have the same functionality.
But I think it's just nicer to keep it as is and then use data star to issue the request. So the first thing we're going to do is we're going to specify in data on with a click. And if we go in here and look at the reference, we can see a data dash on attaches an event listener to an element and then execute whatever we provide to.
to this attribute here, whenever this expression, or whenever this event is triggered, the expression gets executed right. This works with a bunch of different types of events. And if we go here to the Mozilla docs, and we can see DOM events, this is a list of all the events that are fired. And the data on attribute will work with all of these. So for our needs here, we just need to work with a click event, which we will have on the dev.
So this means we can say, oh, I need to be in here, fmt, sprint. Then we need to specify the type of request we want to send using the ads. We want to send a get request. Inside of here, we want to specify where the request will go to. And that will be the exact same as what we have in here.
So let's just try and do this. Give that a save. So what are we complaining about? Strength replace. Needs one, but has two arguments. Right, we don't need to read me. Right, let's go in here. Let's give this a refresh. And let's just try and click any of the cards. You can see we sent our crest here with
The initiator is the data star library, and we load the article page just as what we did expect. The type of response is still HTML, but the request is an event stream. And that is because, as we talked about in the previous episode, the data star uses servers and events to deal with
replacing elements in the UI. And we also have a slash page we need to fix. But now we can click the cards. We actually don't have. OK, that's great. So we can click the card and we can go back and forth. Let's just add an indicator because now the card is clickable. So we do cursor dash pointer. And now we get a little pointer.
So this is just making it a little bit more friendly. We could technically wrap the entire card in an anchor tag that will also work. I feel like this is nicer, it's easier to work with. Right, so this is our article card now working and we didn't change anything. We didn't need to update our controllers, at least for it for to load the article page. But if you do check, you can see that the URL doesn't change, which is not the nicest experience.
What we will need to do is we will need to push a state, a UL state change whenever we click these cards here because we do click read me, we get the changes that we expect. We're gonna fix that when we get to dealing with the controllers. But for now, let's start working on this navigation or pagination button here. So we only update the latest articles part of the UI.
And to do that, I'm back in home.temple. I'm gonna grab this button link here and I'm gonna create a new component called data star link. And since we don't need an anchor tag now to send get request, we can just change it into a button. And then again here, we're gonna grab data on click, replace all of this. And we also need to say,
Femt Sprint because we need to send our get request or close it and then everything else stays the same. I still need the data on click event listener. Now let's go back and give it a refresh and see what happens when we click the next page. So we still navigate. And of course we still navigate because I haven't updated this part yet.
Now let's go back and try. Let's now see what happens. So we can still navigate. And we see again that we sent the data star request or everything works. We haven't broken anything yet. But if you go to this page right here and give it a refresh, you can see we go back to the original or the first page because we haven't updated our URL to include what page we are on. So that's a little bit of a
not so nice user experience. That again, we're going to fix in the control layer. But first, I want to do some, let's call it plumbing, because I want to have an article, cards, grid components. So we only send that back whenever we have this pagination page change. So we can be very specific about what we update. So let me create an article, cards, grids.
that will take in paginated articles. And then what I want to do is I want to, I want to grab all of this. Yes. Paste it up here and then say article grid, cards grid, paste the paginated articles. And can we clean this up a little bit?
Right. It would be nicer if you do like this. Yeah. Right. The way that Datastar figures out what elements to replace is through IDs. We can overwrite this if we want to with selectors in the backends. But if we have an ID, it will just morph into a change that ID. Sorry. Let me start over. If you send it back,
are some HTML with an ID from the server. Data star will automatically find that element in the UI or in the DOM and change it if the ID matches. So we need to have an ID or alternatively specify another selector in the backend. Here, we're just going to say article, cards, credits. Because this is going to stay consistent throughout every change we make every time we navigate the pagination.
This will stay the same, so we don't have the same situation that we had with our toast messages, where we needed to have a specific ID. Also want to add the pagination to this wrapper here. So actually I need to, I made a mistake. We need to add a container def that has this ID that wraps everything. There we go. So now,
Our page should not have changed. No, we can still navigate. Yes. Great. But we're still replacing the entire page whenever we click this, this next page button. So what we will do is go into controllers. And then our home page here is going to stay the same. But we're going to be adding another controller called load article.
Cards, Article Cards Grid was the name, right? There go Context, like always, no comma. And in here we're gonna grab this, all of this. And we're gonna change this to be, no, I need to make it a method on the controller struct.
Right. Then we can add the Datastar SDK for Go. And if I just try to say Datastar, no, we need to import it. So let me give this a save. Let me just return null and then jump out. And if we go back into Datastar, we can see they have SDKs down here. You can go to the version for
Go, which is also written by the author, Delaney, of the library. Run this, go, get in the terminal. So now we have the go lang data star SDK to enter equity on the back ends. Great. Now am I able to say data star? It doesn't want to say data star. Okay. Right. Let's just try and go back and we're going to grab
this here and say data star new SSE. You're gonna pass in the response and the request and create a new variable called SSE. Now are we still having errors up here? Yes, let's just jump out and say go mod tidy.
and see if that didn't fix the issue. It did. Great. So now we have this server-send event generator. And using this, instead of returning null here, we can say SSE, Hatch element, temple, which is just a helper method to use with the temple library that the author of the SDK have added to the library. And here we simply say views.
article cards generator, and then we pass in the paginated articles. And that's basically it. We'd need to, of course, add this to the routes as well, so we can actually load the grid. So we go into router routes here. Let's grab this and say article cards, grids, and let's just say that it's gonna be
ArticleCardsGrid is going to be the endpoint that we're going to be hitting. Go all into router and then we can register it. So get request articleCardsGrid and load articleCardsGrid. Finally, we go down to our articleCardsGrid here and instead of sending the
Request to the routes.home page. We're going to send it to article.card.grid. And now everything should work as before. Only we are not updating the entire page. We're only updating these latest articles here. So click and we are only updating this. And you can see in the network tab that we are sending this to
slash article script. So we know we're hitting the right controller, but we still need to fix the URL up here because reversing it now is not really a good experience. So back in controller.go here, we can say that we want to have if error, we sse, replace URL. If there's an error, we return the
return error and in here we need to pass a url.url which needs the value of path and then we say routes home page and raw query is going to be equal to pages or page equal and then we do a string convert and say ito a to convert the
page program that we have up here, back into a string. Right, let's give this a save and go in here and say next page. Now you should be able to refest the page and we stay on the second page because we updated the URL up here. Nice, and we go back to page one, we click here, we go to page without any pages. So sorry, so we go back to
Here, let me see the URL changes as we expected to. Right. We also need to deal with the fact that we can now click our whole cards, but we need the page, the URL to be updated. So we can do a similar thing as what we did with our, with our load articles here and update the URL.
from here, from the article controller. And to make our article card work now when you click the diff instead of the link is that we need to push some URL change into the history. And what we can do with Datastar is that on this event list we can specify multiple things. We just separate them with whatever
I forgot what these ones are called, semicolons. So we are repeating ourselves a bit here. I'm going to create a little replace param function. This is route param and param value. It just returns us a string. And then we want to grab all of this up here, return it.
And then we wanna replace, we don't want to do this, we want to replace the route and the param, param value, which of course only works when we have one param, but that's fine for now. So what we instead are gonna be doing here is we're gonna onclick, we can use JavaScript just like regular,
And we can use JavaScript, just like regular JavaScript, which sounds weird to say, but we can say window, history, push state. And then we're going to add parentheses, brackets, empty quotes. And then we're going to say in here, we're going to add a route. We're going to close it and we're going to use the semicodons to differentiate. So we need to
move this here and then we need all of this to be in like this. So we need to, it can be a bit weird to navigate all of this. So we actually don't need this right. So now we have this thing, yes, and we have the yes. So we just say,
We place param. We don't need this one anymore, but we still need to add another call to this because we need to do it twice. Because we need to push the URL and we also need, yeah.
I think we're holding it right. Let's just try and add one more and then see what happens here. Now, if I go back here, give a refresh and click, we change the URL correctly and we load the article correctly. And if we click the read me, we also see the correct article. Great.
Quite a few things here. We are now only sending back what we need whenever we are navigating through the patination component. We can click the entire card. We can also click the read me if we want to. I just want to show you one more thing of what we can do with the signals. And this is a little bit of a thought-op example that you probably wouldn't need. But I want to really illustrate that you can update anything on the page and you can send
the same type of signals that we use to build our disappearing toast component. We can use them on the back end as well as in signals and update completely different elements of the page. And we can even send them and then do something else and then send another one. It doesn't finish when we send back one response because the server's in the vent. So this really opens up for a lot of cool UI elements that you can build. So in this,
latest articles here in home dot temple. I want to add another span where I want to say is showing page and then we want to add a paragraph tag. Let's just say one out of three. Give that a safe as a class flex text base content and on the
paragraph tag, let's just add class. Let's wait with that. Yeah, let's just see how this looks like in the browser. We have nothing. Why don't we have anything? Let me try and restart the development server now. Okay, we are still not seeing. Oh, we are seeing in the wrong place. Okay, so what I need is we need the section and then we need
Diff that wraps the entire thing, of course. So we say class, flex, item center, justify between. Then we close off the diff here, take the article grid and move it below. Yes, now what are we seeing? We are seeing it show up where we expected to.
So what I want to do is I want to add a signal here. And it's important that when we reference a signal, it has to be defined higher up in the DOM. So if you want to do something here with a signal, we need to specify it somewhere else, higher up in the DOM or in the tree of the DOM. So what we can do is say data, signals, and now we're going to say current page. And remember, this gets transformed to camel case. This is just the standard behavior of
of data star and we can set this to one and now on the paragraph tag here we can say data text equals to current page current page writes and then we can't showcase this anymore so what we need is to say slash three here and
Let's see what we have. We have the same thing. Let's just add a little bit of space on the left side, some marking. This is looking a little bit better. Let's do this. Great. So now we're showing one out of three pages. We do not have three pages right now, and this one doesn't update. What we can do now is that we can target this
signal from the from the controller so that whenever we do our load articles or call to the load artist controller we can update this so if we go into controller here and have the load articles before we change the or replace the url you can say if error equals to sse marshal and patch signals you're going to pass it on map
type string int, and we're going to say current page. Remember this camera case, pass the page, and if there's an error, we just return the error. Go back, give it a refresh, and now if I click next page, you can see we update to page two. And this comes directly from the server. If I try and do something like time.sleep here, let's do
Just do five seconds, I think that's enough. Yes, let's go back to home. Notice the URL now is just the clean one without the page program. We're still showing page one out of three. If I click next page now, we will see this update. So you can see we have page two here. We have nothing up here. We have changed nothing up here. And the sleep finishes and we actually
finish the rest of the code here in the controller. So I think this is super powerful because you can also send down a bunch of these signals so you can update whatever you want on the page and you can do it immediately without having to wait for the entire request to finish. Great. So let's remove the sleep. Let's go back here. And now if I give this a refresh, we have one out of three, which is obviously not right. So we need to do a little bit more work in there.
View Layer, so the latest article here should probably accept our current page argument of type int, I believe we want to have. And then we can pass it here, current page. Then we also need to pass the current page here and accept it as a parameter in an argument here, current page int.
That should be the front end layer. Now we go back to controller. We start the LSP. Do we get an error somewhere? We do. So here in home, we need to pass in page. Do we need to do it anywhere else? I do not think so. So now we see two out of three. Even if you refresh the page, then we can still update it here. Final thing.
we need to add a field to our paginated articles and just say total pages. It's going to be 32. We already have total pages. So total pages becomes total pages. We jump back and to the controller. Right, we now are still returning paginated articles.
So in our home here, instead of saying slash free hardcoded, we can say patinated articles, patinated articles, and it was total pages. Now we should see hopefully two we do. We click next page here and we get to showing two out of two.
So a little bit of a thought of example, but hopefully it illustrates how much you can now do from the back end and have all just stayed in one place. I will link to a video down in the episode notes of a guy using HTMLX, but illustrating some of the same principles where he will update one page of the UI and that will then result in a bunch of other components also being updated with reflecting to that change it. I think that's really illustrated what you can do.
And you can do it even more efficiently with Datastar, in my opinion, also easier because they use this server-centered event approach.