در حال بارگذاری ...

Scaffolding a Nova Tool0:00

So far, when we've talked about customization in Nova, we've discussed how it allows you to edit small parts of the UI, a card here, a field there, but what if you need something much bigger, much more custom? For example, the New York Times has an amazing API that allows you to grab the latest bestsellers. That would be perfect for our administrators, but it doesn't fit into the CRUD model that Nova provides by default. Ideally, we'd want to be able to grab the bestselling categories, grab the books in those categories, and output them with purchase links, but we don't want to store anything in our database. Well, thankfully, when you need something super custom, you can reach for custom tools, and Nova will just hand the reins over to you. It will give you the entire page to play with, and you can do whatever you want. Let me show you. From the terminal, we'll run php artisan nova:tool, and again, you need to format this as if it was a composer package. So I'll prefix it with my application name, Reddit, and then we'll use NYT-bestsellers in this instance, and it will ask you questions. Once more, answer yes to all of them. Your life will be a lot easier if you do. Once everything's scaffolded, I will again cd into the Nova components, and this time the NYT-bestsellers directory, and I can run npm run watch so that whilst ever I'm working on this, I don't have to recompile.

Registering Tool in Nova1:16

Your life will be a lot easier if you do. Once everything's scaffolded, I will again cd into the NYT-bestsellers directory, and I can run npm run watch so that whilst ever I'm working on this, I don't have to recompile. I don't have to remember to update my assets. It will happen automatically. Now, unlike fields and cards, we have to register our tool inside Nova. Jump into the NovaServiceProvider, and you'll find a method called tools, which returns an array. We just have to instantiate our tool in this array. So new NYT-bestsellers, that's a class that ships with your custom tool. If we jump into there, you can see that it has a menu method, so your tool internally controls its menu. If you're using Nova's built-in menu, you'll see it straight away, but if you're using a custom menu, we're going to have to do one more thing.

If we jump into there, you can see that it has a menu method, so your tool internally controls its menu. If you're using Nova's built-in menu, you'll see it straight away, but if you're using a custom menu, we're going to have to do one more thing. Jump back into your NovaServiceProvider, and we'll head up to the menu method here where we've defined the custom menu. Perhaps underneath books, I'm going to new up an instance of NYT-bestsellers, and then I'll call the menu method directly, and I'll pass in the currentRequest. And when I do that, if I refresh, I now have an NYT-bestsellers item in the Nova menu down here. I don't really like that icon, so let's update that quickly in the menu method inside my new tool. I'll change this to newspaper, perhaps, and if we come back and refresh, yeah, we now have a nice newspaper instead. Again, that's Heroicons, Heroicons.com. You can choose any icon and enter the name, and it will appear in your menu. If we click that menu item, you'll see we have a little bit of a blank slate.

Exploring Tool Structure2:50

You can choose any icon and enter the name, and it will appear in your menu. If we click that menu item, you'll see we have a little bit of a blank slate. It's not trying to push any resources on us. We're not forced into any form of UI. We have a black hole, as Nova calls it, and it tells us where we can find this view component. Let's take a look at the directory structure for this tool. So here is our NYT-bestsellers component, our custom tool. It has a resources directory, much like fields and cards. There's a JS folder where we have pages. This takes up a whole page, so it makes sense that we have a pages directory.

There's a JS folder where we have pages. This takes up a whole page, so it makes sense that we have a pages directory. We also have our CSS directory, which we'll touch on later. We have a roots file. We have our API.php file, as we had in our custom card, and we also have an inertia.php file where we can display multiple pages. By default, it will just show the one, which is the page we see here in the browser, but you're free to define as many pages as you'd like. We also then have our source directory, where you can see we have our NYT-bestsellers class, as well as a service provider if you need to reach for that.

Connecting to NYT API3:51

We also then have our source directory, where you can see we have our NYT-bestsellers class, as well as a service provider if you need to reach for that. Let's jump into the API.php file, and whilst we're here, we'll create our connection to the New York Times API. I'll uncomment this root here, and we'll leave it as the default get endpoint, the root of our tool. Inside here, I'll make use of the HTTP facade, and I've created a macro called NYT, which will connect us to the correct base URL and authenticate with the New York Times API.

which will connect us to the correct base URL and authenticate with the New York Times API. It's called the get method, and I want to go to the lists/overview.json file on the New York Times API, and once I've retrieved the results, I'll grab just a certain key from that result set, which is under results.lists. Now, this is actually quite an intensive query. There's a lot of information that the New York Times API returns, so I'm going to wrap this in a closure.

There's a lot of information that the New York Times API returns, so I'm going to wrap this in a closure. We'll say getBooks, and we can use an inline closure for this, and then I'm going to use the Cache facade in order to remember this data for a day, so I can say now I want to add a day instead of minutes, and then we use the getBooks closure in order to actually execute that, and that is what I'll return from this endpoint here. That's all we actually have to do to set up our connection to the New York Times endpoint. We should now be able to retrieve books. Obviously, we need to see if it works,

Rendering Bestsellers in Vue5:17

We should now be able to retrieve books. Obviously, we need to see if it works, so let's go and update the view component and make sure we can get books from the API. So inside the view component, I'm going to update this heading here. I'll say New York Times Bestsellers just so it's a bit more clear what that acronym means, and then I'm going to delete everything that ships by default inside this card to give us a little bit of a blank slate to work with. Obviously, I want to change to the composition API as I did in the previous episode, and let's define a constant called data, which is reactive, and it's going to contain a simple prop called items, which is an empty array by default.

and let's define a constant called data, which is reactive, and it's going to contain a simple prop called items, which is an empty array by default. I'll then use the onMounted hook in order to perform an action when this component is first mounted into the DOM, and we can use the nova.request method again as we did in the previous episode in order to perform an API request that is scoped to our component, and the URL will be nova-vendor, and then the kebab case version of whatever our tool is called. In this case, nyt-bestsellers, and seeing as our API endpoint is at the root here, we don't actually need to include books as has been suggested by GitHub Copilot,

In this case, NYT-bestsellers, and seeing as our API endpoint is at the root here, we don't actually need to include books as has been suggested by GitHub Copilot, but the rest is correct. Once we have retrieved the information, we want to set data.items to the response data. We can make sure this actually works correctly by outputting it directly inside the HTML, and let's jump into the browser and see if this worked, and of course it did. You can see in this card here, here is all the JSON that was returned from our books endpoint. So now why don't we pass the information we've received here and create something a little bit nicer to look at. The NYT-bestsellers API actually groups books by categories,

and create something a little bit nicer to look at. The NYT-bestsellers API actually groups books by categories, so perhaps we want a separate card for each category, category in data.items, and then down here maybe just for now we'll output the category directly so that we can check that that is working. Maybe we'll add a class of mb6 to each card to give ourselves a little bit of spacing. Let's refresh that, and hopefully, yeah, you can see we now have multiple cards, and each card contains the JSON of the category. Now instead of outputting the entire category as JSON, we could just output the category name at the top,

Now instead of outputting the entire category as JSON, we could just output the category name at the top, which would be category.displayName according to the API documentation for New York Times. Let's remove that and check that that works, and yeah, that's looking great. We can now see the title of each category. Now we need to loop over all of the books in that category and output those on the screen as well. So let's create an unordered list and then list items inside that unordered list, which will be a v-for, and the v-for will be book in category.books. Again, let's create a heading,

which will be a V4, and the V4 will be book in category.books. Again, let's create a heading, and for now we'll just output the book.title to make sure that works correctly, and when we refresh, yeah, sure enough, we have the category, and then we have all of the books inside that category. Now the New York Times API actually ships with book covers, so we can see the covers directly in the browser. Let's make use of that. Perhaps above the heading we'll create an image tag. We can set the source of this image tag to book.bookimage,

Perhaps above the heading we'll create an image tag. We can set the source of this image tag to book.bookImage, and we should now see that in the browser, which we do, and maybe we could output the author as well, so that would be book.author, and again, we should now see, yeah, Colleen Hoover under Too Late here, and finally, if you take a look in the API documentation, you'll see that a book has buy links. This would be brilliant. So our administrators, our librarians, would be able to see these best-selling books.

So our administrators, our librarians, would be able to see these best-selling books and immediately click to purchase them for our library. Now here's the cool thing. You don't have to try and recreate the components that Nova ships with. So in this case, if we want to use a Nova button, we simply look through the Vendor folder, so Vendor, Laravel, Nova, Resources, JS, Components, and you can use any of these components directly in your custom tools, your custom fields, your custom cards.

and you can use any of these components directly in your custom tools, your custom fields, your custom cards. So let's use the defaultButton in our case. Maybe I'll wrap it in a div so that it takes up its own line. We don't even have to import it. We just use defaultButton. Obviously, we don't want to use a button. We want to use an anchor tag. So I'll set the component attribute, and then I can set the href,

So I'll set the component attribute, and then I can set the href, which was book.buy links, if I remember correctly. We want to grab the first one, and if it exists, we want the URL. Maybe the name of this button is just Buy. Let's see if that works. Refresh the page, and sure enough, there's now a Buy button under each book. Hopefully, if we click it, we're taken straight to Amazon.

there's now a Buy button under each book. Hopefully, if we click it, we're taken straight to Amazon where we would be able to purchase this book. So that's the functionality of our custom tool sorted. Maybe we could just add a few Tailwind CSS classes now to style it and make it look appropriate. We'll start by making the title here a little bigger, so maybe text-xl, font-bold, and let's add a little bit of spacing underneath, maybe a mb-6,

and let's add a little bit of spacing underneath, maybe a margin-bottom of 6, and the URL needs to be a flex. I'd like the books to be in line, in a row, so we'll add a class there as well. We can say flex. We could say space-x-4. The card should probably have a little bit of padding-right, so we've already done mb-6, but maybe we could do px-2 and py-2,

so we've already done MB6, but maybe we could do PX2 and PY2, and then let's see how that looks. Okay, now we're seeing our books in line, which is great. I don't like that the titles sometimes take up two lines, so maybe we could ellipsize those titles. Let's override the H3 here. We can say maybe MT2 to give it a little bit of spacing before the image, Font Bold again, and then obviously we want to truncate that,

before the image, Font Bold again, and then obviously we want to truncate that, but in order to truncate, we need to set a width. Let's set a width of 32, and we'd obviously want the same applied to the image above here, so let's set a width of 32, and let's set an aspect ratio as well, right? So I think the aspect ratio for a book is 1 over 1.5. Let's see how that looks. And that's not really worked.

Adding Tailwind Build Pipeline11:39

Let's see how that looks. And that's not really worked. Okay, this is actually a great point to talk about this. So far, we've made use of Tailwind CSS, and it just happens to have worked correctly, only because Nova already has the utility classes we've used compiled, but now we're starting to use Tailwind CSS that isn't part of the Nova CSS file. So how can we fix the problem of it not existing? Well, remember, inside our custom tool,

So how can we fix the problem of it not existing? Well, remember, inside our custom tool, we have a package.json file, we have a post.css file. We can just install Tailwind into our tool and have it compile its own CSS. Let's do that now. We'll start by installing the npm dependencies, Tailwind CSS and autoprefixer. Once they're installed, let's run mpx tailwindcss init in order to create our Tailwind config file.

Once they're installed, let's run mpx tailwindcss in it in order to create our tailwind.config.js file. And then inside the project, I'll open up the post.css config file in order to add our plugins. So the plugins node will be an object, and inside we'll have tailwindcss, which is an empty object, and I'll have autoprefixer, which is also an empty object. Let's now jump into our tailwind.config.js file where we can update the content array. I want the resources directory.

where we can update the content array. I want the resources directory. I'm interested in any subdirectory and any file that ends in .vue, which should cover us for our use case here. We also need to update the resources/css/tool.css file with the Tailwind directives. We want to include the @tailwind components and @tailwind utilities. Now, we could include @tailwind base, but Nova already has Tailwind base styles installed for us,

Now, we could include Tailwind base, but Nova already has Tailwind base styles installed for us, so there's no real need for us to do that. So long as you're still executing npm run watch in the background, you should be able to head to dist/css/tool.css and see our compiled CSS file here with the utility classes we're using in our tool.view file. There is still some duplication here. MT2, for example, definitely exists inside Nova. If this little bit of duplication doesn't bother you

MT2, for example, definitely exists inside Nova. If this little bit of duplication doesn't bother you and it's not causing any visual inconsistencies on the front end, I would actually say leave it as is, but I am going to show you a workaround just in case it's causing you issues. I actually have a postcss plugin called unique-styles. I'll say off the bat, this is not my idea. Aaron Francis came up with the original implementation and all props go to him for the work he's done here.

Aaron Francis came up with the original implementation and all props go to him for the work he's done here. But let's go ahead and copy unique styles from npm and we'll install it inside our custom tool. And once that's installed, from the postcss.config.js file, I'll add the configuration for unique styles. We will need to grab a path from Node, so I'll make sure that's required at the top here. But basically, we give it a list of CSS files that we want it to diff against.

But basically, we give it a list of CSS files that we want it to diff against. In this case, the Nova app CSS. And with our post CSS plugin configured, our final CSS is very small indeed. We are not including any classes that already exist inside Nova, so we avoid conflicts and we also make sure that the shipped CSS is as small as possible. Let's hard refresh this page and yeah, you can now see that our books take up

Let's hard refresh this page and yeah, you can now see that our books take up the correct aspect ratio and width. Tailwind is compiling as we would expect. Let's add a few more classes just to clean things up over here. Maybe we could do the same thing where we set a width of 32 and call truncate on the book's author. And maybe we'll add a little bit of margin-top above the default button here. And hard refresh.

above the default button here. And hard refresh. Okay, this isn't quite working. Maybe that's because it's a span. Maybe I could set that to block. And yeah, that's much better. Now our author is truncated where necessary. Everything takes up the same amount of space and we have just that little bit of margin above the buy button. I think we could probably do with just a little bit more padding.

and we have just that little bit of margin above the buy button. I think we could probably do with just a little bit more padding on larger displays on the left of these cards. So let's say on medium displays and above, we'll have a px of 4. And yeah, with the styling done, I'm pretty happy with this. This looks great. We have a page where our administrator could come to purchase new books for the library. And yet we didn't have to do this through standard CRUD. We didn't have to use any trickery.

And yet we didn't have to do this through standard CRUD. We didn't have to use any trickery. We have full control over this page and its content. We're able to create our own API routes. We're able to style it however we want. We're able to pull in any front-end dependencies we need. And yet we can still make use of things like the Nova buttons to give it that Nova look and feel. It fits into the Nova Chrome. It's a single-page application still, but with full control on our end.

It fits into the Nova Chrome. It's a single-page application still, but with full control on our end. So yes, by default, Nova is for CRUD. Nova will help you quickly scaffold out all of your models and resources along with their relationships. But it isn't limited to CRUD. If you need to do something completely custom, then Nova's tools has you covered.

دوست دارید گاهی خبرهای Laracasts را ایمیل کنیم؟