Great to have you back! In our previous article, we talk about associating different models and defining relationships within our Laravel application. The best part is that now every user in our project can have personalized tasks.
In this new article, we’re going to take things a step further and explore the many-to-many relationship. We’ll introduce a brand new model called tags, which will enable each task in our application to have multiple tags.
But wait, there’s more! In this article, we're also going to create a fantastic search select blade component, designed to make working with tags inside our forms. So get ready, because we’re about to dive into some seriously exciting stuff!
Create a search select input in blade
Now, let’s talk about a new feature I’ve decided to add: a search select input for adding and selecting tags within the task forms. There might be some packages available that can help with this in the application, but let’s start by implementing it using pure JavaScript.
So, let’s begin!
First, I created a simple input component for searching text, located in the common directory of our blade components. Then, I added this input component to our create tasks view, as shown in the following picture:
Now, if we want to work with pure JavaScript within our common input, we usually need to define all the JavaScript functions within our app.js
inside resources/js
directory. However, there’s a more convenient way to do this that I prefer, and that is by using a stacked scripts. To achieve this, we have another helpful function within the blade syntax called push. You can use it like this:
@push('scripts')
<script type="module">
// Your JavaScript code goes here
</script>
@endpush
By using this syntax, you can define your script inside the @push('scripts')
and @endpush
tags, and it will be treated as a module. This makes it easier to manage and organize your JavaScript code within your application.
However, right now these scripts won’t work. In order to bring them to life, we need to add the stacked scripts inside our main layout:
Now if you open your console inside your browser you’ll see that our script works:
Right now, the first step is to listen to every change and character that the user enters in the input field. To achieve this, you need to assign an event listener to the input element.
To see this in action, you can add the following code snippet to your scripts. When you run it, you’ll notice that every character you enter in the input will be logged to your console:
@push('scripts')
<script type="module">
let tagSearchInput = document.getElementById("search-tag");
tagSearchInput.addEventListener("input", async function (e) {
console.log(tagSearchInput.value);
});
</script>
@endpush
Now, let’s move on to the next step, which is finding all the tags based on the search input.
But wait we don’t even define our tag model inside our application! It’s actually quite simple! All you need to do is run the following command, and it will take care of creating the model, migration, and controller for you:
php artisan make:model Tag -mc
This model is straightforward because it only has one attribute, which is the name. So, update the migration file by adding a simple name column. Similarly, update the request file as shown below:
Finally, let’s move back to the controller because this is where the new things happen, and we need to work on it.
First, let’s utilize our index function in a slightly different way. Instead of simply listing all the tags in the view, we’ll modify it to return a JSON response. This will allow us to retrieve all the data in our JavaScript code:
public function index()
{
return response()->json([
'tags' => []
]);
}
Then we need to search inside our tags records to find tags based on searching input. To accomplish this, we can utilize a commonly used syntax in Laravel for searching within model. Laravel provides a useful function called where() that allows us to retrieve specific records from our database tables based on specific attributes or columns.
For instance, if we want to find a tag where its name is “job”, the query would look like this:
$tag = Tag::where('name', 'job')->get();
Don’t forget to use get() at the end to execute the SQL command.
Now, what if we want to find all the tags that have a name containing the specified letter or letters? Here we use LIKE as a 2nd parameter and %
at the beginning and end of our search key. For instance:
# all tags start with a 's'
$tags = Tag::where('name', 'LIKE', 's%')->get();
# all tags end with a 's'
$tags = Tag::where('name', 'LIKE', 's%')->get();
# all tags contains a 's'
$tags = Tag::where('name', 'LIKE', '%s%')->get();
So our index
function could be look like bellow:
public function index()
{
$query = Tag::where('name', 'LIKE', "%" . request('name') . "%")->get();
return response()->json($query);
}
That’s it! By using this syntax, we can easily search and retrieve the desired tags from our database.
Now Lets define the new route:
Route::get('tags', [TagController::class, 'index'])->name('tags.search');
As you see in following pic I just add some sample data and make a request:
Well, let’s see how we can use it in JavaScript. To make the HTTP request, we can use a popular package called Axios. The good news is that Axios is already installed by default in Laravel. However, if you don’t have it, you can easily install it using the following command:
npm install axios
Once you have Axios set up, let’s go back to our JavaScript code and make the request:
let tagSearchInput = document.getElementById("search-tag");
tagSearchInput.addEventListener("input", async function (e) {
const response = await axios.get(`/tags?name=${tagSearchInput.value}`);
console.log(response.data);
});
First, Let me explain what “async” and “await” mean in the code. In simple word, think of them as listeners that wait for the result of a request at a specific line.
Now, we need to display the list of results from our query. To achieve this, we can add an HTML element and populate the list with all the found items.
<script type="module">
let tagSearchInput = document.getElementById("search-tag");
let tagSearchList = document.getElementById("search-tag-list");
tagSearchInput.addEventListener("input", async function (e) {
const response = await axios.get(`/tags?name=${tagSearchInput.value}`);
if (response.data.length) {
tagSearchList.classList.remove("hidden");
// remove all item inside list
while (tagSearchList.firstChild) {
tagSearchList.removeChild(tagSearchList.firstChild);
}
// add found tags inside the list
response.data.forEach((tag) => {
let li = document.createElement("li");
li.appendChild(document.createTextNode(tag.name));
li.className = "tag-list-item";
// todo: uncomment after add tagSelected()
// li.onclick = () => tagSelected(tag);
tagSearchList.appendChild(li);
});
} else tagSearchList.classList.add("hidden");
});
</script>
The process involves iterating through the results. If there is any data, our item list is displayed, and we append a list of found tags to our search list.
Now, we need to implement click functionality for each tag item in the list. When a tag item is clicked, its tag id should be appended to a designated input field. This input field has a specific id, such as tags-select and its purpose is to store the selected tags within our form. We will utilize this input field when submitting the selected tags along with the task creation.
To achieve this, I have added three HTML elements as shown in the picture. The first element is an input field with the id tags-select
, The second element is an unordered list (ul) which serves as the parent container for our search result tags that are listed. The third element is a div that visually represents the currently selected tags.
Next, when a user clicks on a tag item in the list, we want to perform the following actions:
- Create and append an element that represents the selected tag to the
tags-select-list
Additionally, we add a close button element, which we will later implement its functionality. - Trigger another function that updates our tags-select input field by adding the id of the selected tag.
- Hide the dropdown list that displays the search results.
- Finally, clear the tag search input element to prepare it for adding another tag.
Here is the function that handles the click event for each listed tag:
const tagSelected = (tag) => {
let span = document.createElement("span");
span.id = tag.id;
span.className = "tag-select-item";
let closeBtn = document.createElement("i");
// todo: uncomment after add removeSelectedTag()
// closeBtn.onclick = () => removeSelectedTag(span);
closeBtn.appendChild(document.createTextNode('×'));
span.appendChild(closeBtn);
span.appendChild(document.createTextNode(tag.name));
tagSelectWrap.appendChild(span);
tagSearchList.classList.add("hidden");
tagSearchInput.value = "";
tagSelectInput.value = inputTag(parseInt(tag.id));
}
And this is the function that updates our tags-select input field:
function inputTag(tagId) {
let tags = tagSelectInput.value.length
? JSON.parse(tagSelectInput.value)
: [];
if (tags.includes(tagId)) {
const indexToRemove = tags.indexOf(tagId);
tags.splice(indexToRemove, 1);
tags = JSON.stringify(tags);
} else {
tags.push(tagId);
tags = JSON.stringify(tags);
}
return tags;
}
Here we have to convert array of selected tags id to string since our input accept string value.
Now, let’s discuss the functionality for removing selected tags. When a user clicks on the close button, the corresponding element needs to be removed from both the tags-select-list
and our input field. Here is the code for the removal functionality:
const removeSelectedTag = (el) => {
tagSelectInput.value = inputTag(parseInt(el.id));
tagSelectWrap.removeChild(el);
}
Amazing let see our fantastic result:
The final step is to implement the functionality for adding a new tag when the user presses Enter and the entered tag is not found in our existing records. In this case, we will send the entered tag to the server and store it as a new tag. As a result, we will repeat the entire process of clicking on a list item for the newly added tag.
To add a new tag, you simply need to include a store function inside the tag controller. Here is how the store function can be implemented:
public function store(StoreTagRequest $request)
{
$tag = Tag::create($request->validated());
return response()->json($tag);
}
To ensure that everything is set up correctly, please make sure to add the appropriate route and a simple StoreTagRequest
class. We have covered this step earlier, and it is relatively straightforward.
Now let’s return to our search select input and handle the Enter key events. First, we need to check if there are no tags in our records. If there are no existing tags, we will proceed to store a new tag using the following steps:
tagSearchInput.addEventListener("keydown", async function (e) {
if (e.key === "Enter") {
e.preventDefault();
const response = await axios.get(
`/tags?name=${tagSearchInput.value}`
);
if (!response.data.length) {
const { data } = await axios.post("/tags", {
name: tagSearchInput.value
});
tagSelected(data);
}
}
});
Now, we have the exciting capability of adding new tags within our app! Isn’t that amazing?
Well, Let’s return to the task creation process. Here, we simply need to retrieve the entered tags from the input fields when the request is sent.
let’s proceed with updating our askStoreRequest
to include selected tags:
public function rules(): array
{
return [
'title' => 'required|min:3|max:120',
'description' => 'nullable|min:3|max:255',
'expired_at' => 'nullable|date|after:now'
];
}
public function validated($key = null, $default = null)
{
if ($key == 'tags') return json_decode(request('tags'));
return $this->validator->validated();
}
I have just refactored the validated method to convert a string of ids into an array of IDs (We cannot merge because merging could lead to conflicts, as there is no column named “tags” inside the tasks model).
Now, let's test its functionality by using two given tags.
That’s it!
Store a Task with given tags
Now how to relates this tags to our tasks?
As I mentioned before, we need a way to manage the relationship between tags and tasks. In a previous article, we discussed this topic you can find out here:
To define the relationship between tags and tasks, we’ll need another table that will record all the connections between these models. This middle table will have two main attributes, and those are the id of each model.
So, let’s get started and define the table using a migration:
php artisan make:migration create_tag_task_table
And this is our migration setup:
Schema::create('tag_task', function (Blueprint $table) {
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->foreignId('task_id')->constrained()->cascadeOnDelete();
});
Right now we need to define relation function for both model that is is incredibly useful and includes many helpful functions that make working with relations in Laravel models straightforward. For both models, we need to declare that each model belongs to the other one.
class Task extends Model
{
public function tags()
{
return $this->belongsToMany(Tag::class);
}
// ...
}
class Tag extends Model
{
public function tasks()
{
return $this->belongsToMany(Task::class);
}
// ...
}
Let’s back to our controller where we need to define relation between created task and selected tags, Here we will use another function call attach just like bellow:
public function store(StoreTaskRequest $request)
{
$task = auth()->user()->tasks()->create($request->validated());
$task->tags()->attach($request->validated('tags'));
return redirect("/tasks", 201);
}
As you notice after selecting some tags and post store request to server you will notice that a created task is associated with the chosen tags.
Update a task’s tags
Now what’s about updating tags for a given task,
The main requirement is to enable the select input using the provided tags. Fortunately, in the Laravel blades, there is a useful feature called “props” that allows for the insertion of variables within components. In our project, we need to define a tags variable, with a default value of null. This will allow us to utilize the component effectively:
@props([ 'tags' => null ])
Then we need to add selected tags to tags-select input:
<input
type="text"
name="tags"
id="tags-select"
value="{{ isset($tags) ? $tags->pluck('id') : ''}}"
hidden
>
And show selected tags inside our tags-select-list :
<div class="flex flex-wrap space-x-1" id="tags-select-list">
@forelse($tags ?? [] as $tag)
<span id="{{ $tag->id }}" class="tag-select-item">
<i>×</i>{{ $tag->name }}
</span>
@empty
@endforelse
</div>
As you can see, the forelse
is used in cases where there may be an empty list. You can also use a simple foreach
loop along with an if
statement, but the forelse
provides a simpler alternative.
Now, you can use this component into our edit view. When you do so, you will notice that all the selected tags are displayed correctly. However, there is one small thing that still needs to be addressed: the closing functionality for these tags.
In this situation, all we need to do is search within our list. If any span elements are found, we can assign the close handler function to the close button. This process is straightforward, and you can refer to the following code to implement it:
// Get all the tags inside the parent element add close event
[...tagSelectWrap.getElementsByTagName('span')].forEach(spanTag => {
spanTag.getElementsByTagName('i')[0].onclick = () => removeSelectedTag(spanTag);
})
That’s it!
Then I add our search components into the task edit view, and refactor our update method as bellow:
Now if we add an additional task and make updates, you will notice that not only will new tags be added, but the last attached tags will also be attached again.
One possible solution is to first detach all previously attached tags and then attach them all together. However, another option is to use a helper function called sync which handles the entire process in one step:
// option 1:
$tags_id = $task->tags()->pluck('id');
$task->tags()->detach($tags_id);
$task->tags()->attach($request->validated('tags'));
// option 2:
$task->tags()->sync($request->validated('tags'));
That’s it Let’s see the result:
We’ve just completed an incredible and extensive article that covers a wide range of topics, including the fascinating realm of many-to-many relationships. In addition, we introduced a brand new model called “tag” and and establish a relationship between user tasks and the associated tags. To enhance the user experience, we even developed blade components specifically designed to manage tags within our forms.
And again this is the repo of our project. Throughout this series of materials, we’ll be taking gradual steps to transform it into a fully functional application.
Happy coding!
Top comments (2)
Thank u man that was awesome! I like your minimal design
Thanks