Custom Cards Overview0:00
Alright, let me start by saying, if you've skipped straight to this episode, please go and watch the previous episode on custom fields first, because many of the basic concepts of creating anything custom in Nova are discussed inside that episode. With that prerequisite out of the way, let's dive into the example of building custom cards in Nova. So, our current experience when it comes to cards has been metrics. All metrics show as cards, either on a dashboard, or at the top of a resource index, or even on a resource detail page. But we're not limited to displaying metrics. We can actually display anything we want inside a card by building a custom card. In our case, I want to go beyond the basic chart types that are available inside Nova and bring in a library like Chart.js to create a custom bar chart. But again, this is just an example. The sky really is the limit when it comes to cards. Let's go ahead and create our very own bar chart for displaying the most popular books on loan in our library. Now, much in the same way we created a custom field in the last episode, we can use the same syntax to create a custom card, php artisan nova:card this time, and formatting the name as a composer package. We'll say reddit/popular-books-bar-chart in this case. I'll hit enter. Again, it will ask you various questions during installation, and the easiest possible thing you can do is reply yes to all of them.
Scaffold Card and Install1:21
We'll say reddit / popular books bar chart in this case. I'll hit enter. Again, it will ask you various questions during installation, and the easiest possible thing you can do is reply yes to all of them. And now our card has been scaffolded. I'm going to create a new terminal tab, cd into the Nova Components/popular-books-bar-chart directory, and run the npm run watch command in order to continually build the card assets as I update them in the IDE. Okay, now let's install Chart.js as a node dependency. And remember that our custom cards, our custom fields, anything custom has its own package.json file and its own node_modules folder. So, inside the popular-books-bar-chart card, I'm going to say npm install chart.js. With all of the installation steps complete, why don't we jump into the book Nova resource, and down here where we have our cards method, I'll add our brand new card, new PopularBooksBarChart, like so. If we jump into the browser and refresh our book index, we now see a brand new card in the second slot here. So, our card has been successfully registered, and we're ready to start editing it to actually show our Chart.js graph. Now, I will say this is a little narrow to show the graph we want. We want to make it a bit wider, so let's handle the card width first of all.
Adjust Card Width2:29
So, our card has been successfully registered, and we're ready to start editing it to actually show our chart.js graph. Now, I will say this is a little narrow to show the graph we want. We want to make it a bit wider, so let's handle the card width first of all. If we jump into the PopularBooksBarChart php class, you'll see it extends this Card component, and it ships with a width property that is by default set to one third of the screen's width. We can use the constants that are available on the Card class in order to change this to one half width. Now, our custom card takes up just that little bit of extra width, which gives us a little bit more room to play with inside the chart itself. So, much like our custom field from our last episode, the custom card has a resources directory where we have the js components directory, and in there we see card.vue. The card has an outlying Card class that will give us the UI, the nice white card that has the drop shadow on it. Then we have an internal div with a little bit of padding, and then this h1, this title that says popular books bar chart at the moment. And of course, that is what we're seeing inside the browser. If we scroll down to the script here at the bottom, you'll see we define a single prop called card.
And of course, that is what we're seeing inside the browser. If we scroll down to the script here at the bottom, you'll see we define a single prop called card. This contains some information about the card itself. And if you're on a detail page, you can also access the resource, resource ID, and resource name properties. However, our card displays on the book index, so I'm actually going to remove these entirely. I'm going to convert this to the composition API, which I'm much more comfortable with. We'll create a constant called props where we define the props, and the only prop we're interested in is card. And then I can use an unmounted hook in order to replicate the functionality we have out of the box. Alright, if I remove this now, we are ready to begin using our component. Now, this is not a lesson on chart.js, so don't expect me to go into great detail on how it works.
Build Chart Component4:19
Alright, if I remove this now, we are ready to begin using our component. Now, this is not a lesson on chart.js, so don't expect me to go into great detail on how it works. But the first thing I'll do is replace this title here with a canvas element that has a ref of canvas at the top, and simply has a width of 100% to take up the full width of our custom card. I can grab a reference to that canvas by creating a constant called canvas, which is equal to just an empty ref. And when you do that, well, it will automatically determine that this must be a ref in HTML, and it will go up here, and it will grab this canvas element at the top. I'm going to define two reactive pieces of data. One is the books that we'll load in through an external API call. We'll come to that in a moment.
One is the books that we'll load in through an external API call. We'll come to that in a moment. And the second is the chart that is currently being rendered on the page. Obviously, at the start, there is no chart, so we'll set that to null. In our onMounted hook, I'm going to grab the Chart class from chart.js, and I need to call the register method in order to grab the registerables that come along with the chart.js library. This is just a prerequisite to make sure things don't break a little later on. And you can see it's been imported from the chart.js library here at the top of our script. The next thing I want to do is actually load in books. The reason I've not created books as a prop is that I want to do it asynchronously.
Create Card API Route5:34
The next thing I want to do is actually load in books. The reason I've not created books as a prop is that I want to do it asynchronously. I want the page to load as quickly as possible, and then we make an external API call to go and grab the list of books we're interested in. Now, each custom card actually ships with an api.php file, so you can create very custom routes for your cards that don't have to spill over into your standard application logic. Let me show you how simple it is to create one for our books. We'll create a route GET endpoint. We'll call this one /book,
We'll create a route GetEndpoint. We'll call this one /book, seen as it's isolated to its current environment, our custom card. And this is going to be a function. The function is going to return our Book model after doing a query. What query do we want to perform? Well, why don't we withCount, and rather than passing the allLoans relationship directly, I'll wrap it inside an array so that I'm able to make an edit to the query that's executed. We have a function here that receives the subquery,
I'll wrap it inside an array so that I'm able to make an edit to the query that's executed. We have a function here that receives the subquery, and the subquery can say query where, and we want the bookCustomer.createdAt column is greater than or equal to now, and let's sub one year off of the current date. So the most popular books in the last year. Now that we have the count, we can order by descending based on that count column, which is going to be allLoansCount. We don't want all the books. I think the top five will be more than enough,
We don't want all the books. I think the top five will be more than enough, so I'll call limit and pass 5. I'll grab that information, and that is what we return from our books endpoint. Let's jump back into the card now that we've defined the route, and we'll add it to the onMounted hook here. I'm going to use a front-end class that Nova provides called Nova, and we can call a request method on there to perform an Axios request under the hood scoped to our Nova administration panel, and you'll see here that our API route that we created, our books route,
scoped to our Nova administration panel, and you'll see here that our API route that we created, our books route, has an automated prefix of NovaVendor followed by the kebab case version of our custom card, in this case popular-books-bar-chart. Obviously, that returns a promise, so I'll call then, and then I'll update our reactive books property with whatever was returned from that API endpoint. So rather than loading the data up front, we now have an API route that we can hit as many times as we want.
So rather than loading the data up front, we now have an API route that we can hit as many times as we want to grab the latest information about the most popular books in our library. All right, now we have a way of updating the books. We need to watch for when those books change and draw a chart onto the page, so I'll use the watch helper in the composition API. I'm going to pass it a closure that will look for data.books, and then obviously the second argument will be a closure that receives the latest books and performs a set piece of logic, and I'm just going to paste in something that I created earlier for this.
that receives the latest books and performs a set piece of logic, and I'm just going to paste in something that I created earlier for this so that we're not wasting too much time here on the syntax for chart.js, but basically I'm newing up a BarChart. The labels are being set to the book titles, and then I'm setting the data to the loanCount for each book. Let's see what that looks like in the browser. I'll refresh, and we seem to have an error. Let's take a look at what's gone wrong. Reactive is not defined.
Let's take a look at what's gone wrong. reactive is not defined. Okay, we need to import reactive. Obviously, phpStorm was unable to import it properly. There we go. It's now yellow. Let's see if that works. I'll refresh, and here we have a bar chart, a custom chart.js bar chart where we can hover over, and we can see these different books and how many loans they've had in the past year.
Make Chart Clickable9:09
a custom chart.js bar chart where we can hover over, and we can see these different books and how many loans they've had in the past year. How cool was that? We have full customizability thanks to the power of custom cards. One last thing I want to touch on in this episode is making this bar chart clickable, and when we click one of the books, actually visiting the route, going to that particular book in our administration panel. It's easier than you might think. Again, this is not a chart.js tutorial,
It's easier than you might think. Again, this is not a chart.js tutorial, so I'm not going to go into detail on the line-by-line workings of this, but I'll create an onclick handler, which will be for clicking our chart, which is obviously going to be a closure that receives the event, and I'll paste this code in here, which I'll very briefly explain. We check to see if there actually is a chart. If there isn't, we're not going to do anything, and then we're using this getElementsAtEventForNode that chart.js ships with.
If there isn't, we're not going to do anything, and then we're using this getElementsAtEventForNode that chart.js ships with in order to grab the result that we've actually clicked. If the result is nothing, we won't do anything. Otherwise, and here's the interesting bit for Nova, I'm going to call the nova.visit method, which will, without reloading the page, redirect us to the resources books and then the ID of the book that we've actually clicked on in order to take us to that page.
and then the ID of the book that we've actually clicked on in order to take us to that page. I need to wire this handler up to our canvas, so at the top here, I can say atclick equals onclick, and once I have that defined, I should be able to see this working in the browser. Let's head over to For Whom the Bell Tolls and click, and look at that. We are taken straight to the details page for For Whom the Bell Tolls. I can do the same with this book here.
We are taken straight to the details page for For Whom the Bell Tolls. I can do the same with this book here. I'm taken straight to the details page. I can do the same for this book here, straight to the details page. So using a mixture of custom cards, whatever we want to create inside the view component that's provided for us, and the tool set that Nova provides, we're able to build more or less anything we can think of and place it directly into our administration panel alongside everything that Nova provides out of the box.
and place it directly into our administration panel alongside everything that Nova provides out of the box. Thank you.
