Rethinking CRUD Interactions0:00
Now that we've built out our database tables and the tooling around them, it's time to ask the question of how do our users want to interact with the data in those tables? What do they come to our site to do or to ask? By default, we as programmers think of our applications as offering some primary ways of interactions, and they're often called CRUD. CREATE, READ, UPDATE, and DELETE. And these kind of describe some basic things where you're going to say, well, I'm going to create one of my resources. I'm going to read, which means I'm going to usually look at a list of them, and maybe I'm going to filter and search, and maybe go to a show page for one potentially. Update means you're going to make changes to an existing one. And delete, meaning you're going to delete an existing one. In fact, knowing I was going to be talking about this episode, you can actually take a listen to me in previous episodes, and I keep saying, when we build out the CRUD. And even if I don't think it's just going to be CRUD operations, that's still how I tend to talk about these early stages of building out the user's possible interactions with the app.
In fact, knowing I was going to be talking about this episode, you can actually take a listen to me in previous episodes, and I keep saying, when we build out the CRUD. And even if I don't think it's just going to be CRUD operations, that's still how I tend to talk about these early stages of building out the User's possible interactions with the app. And there's something to be said there. You know, list out all the talks. Create a new talk. Update a talk. Delete a talk. But thinking too much along the lines of CRUD also often makes us build some mega pages with these really complicated set of interactions that users don't always need. You know, data tables with filtering and sorting and searching and all that kind of stuff is not always actually the user's best interaction with the application. So let's first think about how our users would interact with talks.
Introducing Jobs To Be Done1:15
You know, data tables with filtering and sorting and searching and all that kind of stuff is not always actually the user's best interaction with the application. So let's first think about how our users would interact with talks. Interestingly enough, Symposium is actually the first open source project I worked on together with Adam Wathen while he still worked at Tighten. And while we were working on that project, he asked a lot of these sort of questions around why exactly we should be building particular pieces of the interface. And in these conversations, he introduced me to a concept called Jobs to be Done. And Jobs to be Done has as a core tenet the idea that we as consumers want to interact with new innovations around a job that we want it to do for us. And no one in Adam's idea would go to an app and say, you know what I really want? Filtering. You know what I really want? I want a data table.
You know what I really want? I want a data table. I just, you know, love data tables. That's why I sign up for apps. And so what we really want is to ask the question of why would our users want to come here? What are we actually trying to provide for them in the first place? Is it just a site that lists things or is it the ability to do or see or understand certain things? What are the jobs that we're trying to allow people to get done with this app? When I first made Symposium, what I wanted more than anything else was a way for myself as a conference speaker to have a way to track all of the talk abstracts or proposals that I'd written. So that when I went to speak at another conference, it was really easy for me to say, well, what talks have I given before?
Defining User Talk Needs2:26
When I first made Symposium, what I wanted more than anything else was a way for myself as a conference speaker to have a way to track all of the talk abstracts or proposals that I'd written. So that when I went to speak at another conference, it was really easy for me to say, well, what talks have I given before? Do I want to pull one of them? Do I want to write a new talk? But also to look at previous conferences and figure out where have I applied and what talks did I apply with and which were accepted to or what conferences do I want to potentially apply to the future and think about what talks I was going to use there to apply to that one. And some of this I could just keep track of locally. You know, I could just have something in my notes app where it's all my talk descriptions, but that plus the conferences, it was just getting really difficult to keep track of all these things at once. Now, a lot of the things I just mentioned have to do with getting information out of the system about my talks, which means we have to make it possible for me to get those talks in there in the first place. Now, there could be some sort of existing system to import a list of talks I have somewhere else. But in reality, we're going to really just have to build interfaces, allowing people to add their own talks into our system.
Building Talk Form Template3:21
Now, there could be some sort of existing system to import a list of talks I have somewhere else. But in reality, we're going to really just have to build interfaces, allowing people to add their own talks into our system. So it looks like we are at the sea of crud, even though we didn't just decide to start there. Breeze has already given us a decent starter shell for our app. So what we really need to do is figure out what goes inside of this shell. Most of the time I'm going to be working with the designer, but for the sake of this one, we're just going to grab some components I already pulled from Tailwind UI, and we're just going to throw them in here and say that's good enough for now because this course is mainly around building the whole application, not spending the next three hours in HTML and CSS. So I threw together this template just from Tailwind UI, and it's basically just one of the example pages, the form pages from Tailwind UI. The only thing I did was I just changed the examples that they have, which was signing up for this site, workation.com, and I just brought in the basic fields that we had. And remember, if we go to talks, migration, we've got title, length, type, abstract, and organizer notes, and we're going to attach it to the User by default. And timestamps are generated by default.
And remember, if we go to talks, migration, we've got title, length, type, abstract, and organizerNotes, and we're going to attach it to the User by default. And timestamps are generated by default. ID is created by default. So these are the only ones we're going to allow the user to interact with. So we've got the title right here. We've got the type right here. We have the length right here. I also use this one as the example for us when we want to build in error handling. And then we've got the abstract and the organizerNotes. Now, there's no php in here at all, which is why, for example, we are not pulling directly from the enum here.
And then we've got the abstract and the organizer notes. Now, there's no php in here at all, which is why, for example, we are not pulling directly from the enum here. This is just HTML and CSS. So this is called template. So let's go just take a look at what this looks like to make sure it actually works correctly. I'm going to include, I think it was talks.template is where I put it temporarily. Let's take a look. Okay, great. So this is what our basic talk create and edit page is going to look like. Title, dropdown type, length.
Wiring Routes and Controller5:12
So this is what our basic talk create and edit page is going to look like. Title, dropdown type, length. We're not going to constrain it because, remember, we might allow them to do this or something like that. So totally fine. And then an abstract and organizer notes. I'm going to cancel the save button. So now let's plug it in. We want to create the route. And remember, when we were creating our models, we created the controllers by default. So if you go to app/Http/Controllers, we already have a ConferenceController and a TalkController.
And remember, when we were creating our models, we created the controllers by default. So if you go to app/Http/Controllers, we already have a ConferenceController and a TalkController. And the create page right here is where we show the form for creating it. And then the store is where we accept the post of that form. So we could do the mapping here where we hook things directly in to a resource controller. But I like to actually manually map every single one of our routes. We'll say Route::get('talks/create'). And we're going to pass it off to the TalkController class and the create method. And then we'll do the same thing, except we'll do Route::post('talks'). And that will be the store method.
And then we'll do the same thing, except we'll do Post to Talk. And that will be the store method. Oh, and we want to name this as well. Now we can go to our TalkController and right here. And then this is where we're going to process. So let's build out that view. So we said views/talk/create.blade.php. This view in general, remember, it's very similar to this application structure that we have in the dashboard. We're just going to include this kind of create form. We'll just copy it directly from the dashboard.
We're just going to include this kind of create form. We'll just copy it directly from the dashboard. And we'll change it from saying dashboard to createTalk. And right now we'll just leave this in this template file. Let's just take a look at what that looks like. Great. Obviously, this error message will need to get removed. But in general, we have things the way we want. And if we were to submit it, it wouldn't go anywhere because the form on the template is just form. This is where you want to set your CSRF token.
And if we were to submit it, it wouldn't go anywhere because the form on the template is just form. This is where you want to set your CSRF token. And you also want to set your method and your action. So our method will be POST and our action will be the store routes. We'll just say route. We'll say talks.store. And we have to make sure we add a CSRF token. And now we should be able to come in here and submit. And there we go. So we've now submitted our form.
And there we go. So we've now submitted our form. We've got a title. We've got a type. We've got a length, abstract, and organizer notes. All's null. So we need to add validation. But we've got the basics in there. And we also want to make sure we put something in there. We get it for all of them.
Adding Validation and Errors7:49
And we also want to make sure we put something in there. We get it for all of them. And there we go. It's all going through. So let's get to validation. There's a lot of different ways Laravel has done validation over the years, but the latest recommended one in Laravel 11 is to call validate directly on the request. And you get an array of your validated data back from this, so we can use this to save it to the database. So we want to take all of our fields and make sure every single one of them has one here.
so we can use this to save it to the database. So we want to take all of our fields and make sure every single one of them has one here. So we've got our title already. And this is just copied directly from the docs. And then let's figure out what else we've got. We've got length, type, abstract, and organizerNotes. Even if something does not have any rules that you want to apply to it, you should still put it in here so that the validator knows it exists and pulls it out. So you could potentially have a talk without having an abstract or organizerNotes for it. So we don't want to make those required.
So you could potentially have a talk without having an abstract or organizer notes for it. So we don't want to make those required. But we at least want to set these ones up, especially because type is a dropdown. I guess you could potentially not know the length either. So it's really just these. It doesn't need to be unique in posts. I guess we can set a maximum in all of them. But really, I would just say it's required. It's required. We'll do an enum on this one in just a second.
It's required. We'll do an enum on this one in just a second. And it's kind of maxed to 55 just because, I guess, you know, because that's the VARCHAR max in MySQL. And then here, this is where we say create talk. And then this is where we direct to the route. Our route is named talks.index. Oops, redirect. Now, this will break because we don't have an index page right now. So let's just go at it really quickly.
Now, this will break because we don't have an index page right now. So let's just go at it really quickly. Even though we have not built that out yet. Route::get('talks', 'TalkController@index')->name('talks.index'); So that will no longer break that page. We should be creating a talk, but right now let's just see what happens when we pass in failing validation state. So remember the title is required. So we hit save, and we get redirected back here, which means we need to handle our errors.
So we hit save, and we get redirected back here, which means we need to handle our errors. As you probably know, we get an errors object available to us. So if we were to hit it again and hit save, we're going to see that there's a single one here. It's the default error bag. It's got a message for the title. And the message is the title field is required. So the Laravel documentation gives some examples for how to work through these. We're working with the template with an error message on each one specifically.
So the Laravel documentation gives some examples for how to work through these. We're working with the template with an error message on each one specifically. So we don't want to just do the old school Laravel thing where we say fullErrors any. We actually want to get the specific errors for the specific field. And the thing is, we're going to do that here and here and here and here. So why don't we come up with a little bit of a component helper to allow us to do that more easily? And one of the great things that Breeze does for us is it gives us some useful components by default.
And one of the great things that Breeze does for us is it gives us some useful components by default that are useful across various aspects of the Laravel app, not just the things that Breeze sets up. And one of the things you might see is you've got a basic text input that we can potentially work with. But we also have one called input error. And input error is basically what we were about to potentially build, which is the ability to say pass particular error messages in. And if they have any, put them in an ordered list.
which is the ability to say pass particular error messages in. And if they have any, put them in an ordered list. That's exactly what we need. And we already saw it takes messages. And so we want to pass in just the messages for the specific one. So at this particular place, we are looking at the length properties. We would say errors->get(length). And that just says give me an array of all of the errors for the length field. And let's see how it looks. So we should see nothing.
And let's see how it looks. So we should see nothing. And it's not required. So let's make it required real quickly. So now if it's required, there we go. Length field is required. So we can bring that same thing in for all of our other fields. I'll do that real quickly off camera, and we'll keep moving. So now that I've done that, let's actually make this work. Auth::user->create(validated);
So now that I've done that, let's actually make this work. Auth user talks create validated. Let's import the Auth facade and try it out. Ah, okay. So this is one of the things I was talking about earlier when I said we don't want to be precious. And I talked about it way too many times. We don't want to be precious about our database. As I was creating this talks table, I thought I should probably make some of these nullable.
As I was creating this talks table, I thought I should probably make some of these nullable. But let's just see as we go because I know that I can just go back and change it. We haven't pushed up in Git yet. We're still in the middle of working on it. So let's think through which of these we're requiring to be something. I think I'm okay with a nullable length. And I'm a nullable abstract and organizerNotes because realistically someone can have a talk and just throw it in here.
because realistically someone can have a talk and just throw it in here. And at least to start, it's just a title. And I'm requiring type. You can even make dropdowns be optional, but I'm just forcing them into it for now. php artisan migrate:fresh --seed. They'll probably force us to log in again. Yeah, page expires and that's fine. That's why you make sure you have your seeded user.
Yeah, page expires and that's fine. That's why you make sure you have your seeded User. All right. This is a title. And here we end up on the talks index page, which right now is nothing. But if we go to our TalkController, the index method, we can just say return list talks. You can see we're there. User talks.
You can see we're there. User talks. And this User, of course, already has these talks. But the most recent one should be the one that we created. And there we go. This is a title. So we are actually creating a talk directly in the database when we enter that information in. Now, there are a few places we can go from here. One of the things I would often want to point out
Now, there are a few places we can go from here. One of the things I would often want to point out is the fact that anytime you're doing something in your app and then checking the output in TablePlus or checking the output in an output page or something like this over and over, that is a great opportunity to add a test. So one thing we could do right now is we can add some tests for this create and store method. Another thing that makes it easy for us to see the outcome
for this create and store method. Another thing that makes it easy for us to see the outcome is to say, well, where is it actually going to end up on the site? You know, maybe we're going to build the create page and then we'll build the list page. And from there, we'll then say, okay, once we get those working the way we want, then we'll write our tests. In this particular moment, because we don't have a ton of time because we're in a course and I don't want this video to be too long,
Creating the Talks List14:04
In this particular moment, because we don't have a ton of time because we're in a course and I don't want this video to be too long, we're going to jump straight to that list page. Now, I don't want to make the assumption that we want a list page just because we're doing CRUD. But let's say we are thinking about the jobs to be done, the needs of this actual user, and one of the things they want to do in this context is say, I'm about to submit a talk to a new conference. What do they want to do?
I'm about to submit a talk to a new conference. What do they want to do? Think about the talks that they've given before to see if any of those are talks that they want to submit again. So we do need to list all the talks out in one place. But that doesn't mean it needs to be a massive paginated page with data tables and everything. Most people won't have but 10, maybe 15 talks at any given time. So we're just going to throw those talks up in a page and just see, does this work? Does this make sense?
So we're just going to throw those talks up in a page and just see, does this work? Does this make sense? And if we need to add pagination, we can. If we need other things, we can. But right now, all we need is just a place to see a list of all of them with some really basic details. So let's get started there. So we're going to go over to this page, and instead of allowing it to be just this, we're going to pass that data as talks,
and instead of allowing it to be just this, we're going to pass that data as talks, and we're going to say return view('talks.index')->with('talks', $talks); Now, again, that view does not exist. So let's take our other existing talks view, create. We're going to duplicate it, and we're going to call it index. And so right here, we're going to say foreach $talks as $talk and unordered list. Just keep it real simple.
and unordered list. Just keep it real simple. All right, so what do we get here? Great, we got all of our talks in an unstyled unordered list with no link. This is a title. And then you ask the questions about, well, when I was looking at the list of talks that I've given before that I might want to give again, what other information would I need?
that I've given before that I might want to give again, what other information would I need? And importantly, can I put all of that information on this page, or do we need an individual show page for every single one of them? Well, unfortunately, because we've got these long abstracts and these long organizer notes, we probably do need an individual page for each talk. So let's think about what do we need here to help the person just skim it.
So let's think about what do we need here to help the person just skim it. And this is where a UI and UX designer would come in very handy, because as programmers, we tend to just put everything here. Or even if we don't put everything in here, we do it in a way that's kind of ugly. And UI, UX designers are often the people who have the best ideas for how to build those out. Again, that's not the point of this particular course,
who have the best ideas for how to build those out. Again, that's not the point of this particular course, but I would say that like a part of building a SaaS is understanding what should go where and how people are going to interact with it. So I would highly encourage you, you dig into jobs to be done, some of this UX design stuff, and if ever possible, work with a designer. So let's at least get some basic designs.
and if ever possible, work with a designer. So let's at least get some basic designs. So for each of these, let's think about what we might need. We might want to show their length and type. Right now, we'll be simple. Just real simple stuff here. Oops, you all saw that, I'm sure. If this were our final user interface, we would say only show the / if it's null, but we're just getting the data out there.
we would say only show the slash if it's null, but we're just getting the data out there. I mean, I can even show you what this page looks like on the real app to see kind of where we landed. So you've got the title of the talk, you've got edit buttons, you've got a blah, blah, blah, and then blah, blah, blah, and then blah, blah, blah. And I think in this particular one, we forced the length to be in minutes.
And I think in this particular one, we forced the length to be in minutes. We set the difficulty level to be here, one of these, and then the type of talk is these. We're able to build this string right here from it. So that's one way to do it, creation dates and everything like that. But this is just how we landed on it in the actual Symposium app. What you want to know is what is the most important
in the actual Symposium app. What you want to know is what is the most important to people as they're asking those questions of, well, what talks have I given before? Is it important that this was created in June 27, 2015? Well, let's keep it there. Is it important how long it's going to be or whether it's a beginner, or does none of that matter? Is all they care about when they're skimming just the name of it?
Linking to Talk Show18:10
Is all they care about when they're skimming just the name of it? I'm curious whether that's the case and whether we might put other things there just for fun. So let's just, let's do this. We said we don't want the abstracts here, so let's just do a talk show page. This will break because that route's not defined yet. And we can make that... go here.
And we can make that... go here. And so now it's a link, and because it's Tailwind, you need to style the link. Great. And you click on it, and it takes you to an already filled method with nothing in it. So we've got route model binding passing us the talk already, a view that doesn't exist, talk.showNotFound. So then we create talk.show.
a view that doesn't exist, talk.showNotFound. So then we create talk.show. And then right in here is where our talk information happens. All right, so talkTitle. Go up here. And then all our talk details can go in here. And because, again, this is not a UI UX course, I'm not going to spend the time to look into what it looks like to actually build this up the whole way. If you click into it in Symposium,
to actually build this up the whole way. If you click into it in Symposium, you can see the abstract here, you can see the organizer notes there, you can see those specific details there. And in Symposium, we've got revision history, so you can see all of that there. Again, I'm not going to make you sit and watch me build that whole thing out, but we have a template with the data around it with the correct URL structure.
but we have a template with the data around it with the correct URL structure with talks that we're able to build ourselves. So we've got the basics to be able to do what the User wanted from a Talk perspective, which is to ask the question of what Talks have I given before so that I can figure out which Talks to propose at the next conference. So in this video, we tackled effectively
at the next conference. So in this video, we tackled effectively the C for create and the R for read of CRUD. And in the next video, we're going to tackle some of the more complicated views and also do a couple updates on these ones that we didn't get to when we were first building it. Stay tuned. Thank you.
Thank you.
