很高兴您回来!在上一篇文章中,我们谈论了在Laravel应用程序中关联不同的模型和定义关系。最好的部分是,现在我们项目中的每个用户都可以拥有个性化的任务。
在这篇新文章中,我们将进一步迈出一步,并探索多一到多面的关系。我们将介绍一个名为标签的全新模型,该模型将使我们的应用程序中的每个任务都具有多个标签。
但是,还有更多!在本文中,我们还将创建一个出色的搜索选择刀片组件,旨在使我们的表格中的标签工作。所以准备好,因为我们要深入研究一些令人兴奋的东西!
在刀片中创建搜索选择输入
现在,让我们谈论一个我决定添加的新功能:一个搜索选择输入,用于在任务表格中添加和选择标签。可能有一些可用的软件包可以在应用程序中对此有所帮助,但是让我们首先使用纯JavaScript实施。
所以,让我们开始!
首先,我创建了一个简单的输入组件,用于搜索文本,位于刀片组件的共同目录中。然后,我将此输入组件添加到我们的创建任务视图中,如下图所示:
现在,如果我们想在共同输入中使用纯JavaScript,我们通常需要定义resources/js
目录中的app.js
中的所有JavaScript函数。但是,我更喜欢这样做的一种更方便的方法,那就是使用堆叠的脚本。为了实现这一目标,我们在称为push的刀片语法中具有另一个有用的功能。您可以这样使用:
@push('scripts')
<script type="module">
// Your JavaScript code goes here
</script>
@endpush
通过使用此语法,您可以在@push('scripts')
和@endpush
标签中定义脚本,并将其视为模块。这使得在您的应用程序中更容易管理和组织您的JavaScript代码。
但是,现在这些脚本将无法使用。为了使它们栩栩如生,我们需要在我们的主要布局中添加堆叠的脚本:
现在,如果您在浏览器内打开控制台,您会看到我们的脚本有效:
现在,第一步是聆听用户在输入字段中输入的每个更改和字符。为了实现这一目标,您需要将事件侦听器分配给输入元素。
要查看此操作,您可以将以下代码段添加到脚本中。当您运行它时,您会注意到您输入的每个字符都会登录到您的控制台:
@push('scripts')
<script type="module">
let tagSearchInput = document.getElementById("search-tag");
tagSearchInput.addEventListener("input", async function (e) {
console.log(tagSearchInput.value);
});
</script>
@endpush
现在,让我们继续下一步,这是根据搜索输入找到所有标签。
但是,等等,我们甚至没有在应用程序中定义标签模型!它实际上很简单!您需要做的就是运行以下命令,它将为您创建模型,迁移和控制器:
php artisan make:model Tag -mc
此模型很简单,因为它只有一个属性,即名称。因此,通过添加简单的名称列来更新迁移文件。同样,更新请求文件,如下所示:
最后,让我们回到控制器上,因为这是新事物发生的地方,我们需要对此进行工作。
首先,让我们以略有不同的方式利用我们的索引功能。我们不简单地列出视图中的所有标签,而是将其修改为返回JSON响应。这将使我们能够在JavaScript代码中检索所有数据:
public function index()
{
return response()->json([
'tags' => []
]);
}
然后,我们需要在标签记录中搜索基于搜索输入的标签。为此,我们可以利用Laravel中的常用语法在模型中进行搜索。 Laravel提供了一个有用的功能,称为WHERE(),它允许我们根据特定属性或列从数据库表中检索特定记录。
例如,如果我们想找到一个名称的标签,则查询看起来像这样:
$tag = Tag::where('name', 'job')->get();
不忘记在末端使用get()执行SQL命令。
现在,如果我们想找到包含指定字母或字母的名称的所有标签怎么办?在这里,我们在搜索键的开头和结束时用作第二个参数和%
。例如:
# 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();
所以我们的index
函数看起来像鲍泽:
public function index()
{
$query = Tag::where('name', 'LIKE', "%" . request('name') . "%")->get();
return response()->json($query);
}
就是这样!通过使用此语法,我们可以轻松地从数据库中搜索和检索所需的标签。
现在让我们定义新路线:
Route::get('tags', [TagController::class, 'index'])->name('tags.search');
如下图所示,我只添加一些示例数据并提出一个请求:
好吧,让我们看看如何在JavaScript中使用它。为了提出HTTP请求,我们可以使用一个名为Axios的流行软件包。好消息是,默认情况下,Axios已在Laravel中安装。但是,如果没有它,则可以使用以下命令轻松安装它:
npm install axios
设置了Axios后,让我们回到我们的JavaScript代码并提出请求:
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);
});
首先,让我解释“异步”和“等待法规”的含义。简单地说,将它们视为在特定行中等待请求结果的听众。
现在,我们需要显示查询中的结果列表。为了实现这一目标,我们可以添加一个HTML元素并使用所有找到的项目填充列表。
<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>
该过程涉及通过结果迭代。如果有任何数据,则显示我们的项目列表,我们将找到标签的列表附加到我们的搜索列表中。
现在,我们需要为列表中的每个标签项目实现点击功能。单击标签项目时,其标签ID应附加到指定的输入字段。该输入字段具有特定的ID,例如标签选择,其目的是将所选标签存储在我们的表单中。在提交所选标签以及任务创建时,我们将使用此输入字段。
为了实现这一目标,我添加了三个HTML元素,如图所示。第一个元素是带有ID tags-select
的输入字段,第二个元素是无序列表(UL),它是我们列出的搜索结果标签的父容器。第三个元素是视觉代表当前选择的标签的DIV。
接下来,当用户单击列表中的标签项目时,我们要执行以下操作:
- 创建并附加一个代表所选标签的元素,此外,我们添加了一个关闭的按钮元素,我们稍后将实现其功能。
- 触发另一个函数,该功能通过添加选定标签的ID来更新标签 - 选择输入字段。
- 隐藏显示搜索结果的下拉列表。
- 最后,清除标签搜索输入元素以准备添加另一个标签。
这是处理每个列出的标签的点击事件的函数:
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));
}
这是更新我们的标签选择输入字段的功能:
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;
}
在这里,我们必须将选定标签ID的数组转换为字符串,因为我们的输入接受字符串值。
现在,让我们讨论删除选定标签的功能。当用户单击“关闭”按钮时,需要从tags-select-list
和我们的输入字段中删除相应的元素。这是删除功能的代码:
const removeSelectedTag = (el) => {
tagSelectInput.value = inputTag(parseInt(el.id));
tagSelectWrap.removeChild(el);
}
惊人的让我们看看我们的出色结果:
最后一步是实现用户按Enter的功能来添加新标签,并且在我们现有的记录中找不到输入的标签。在这种情况下,我们将将输入的标签发送到服务器并将其存储为新标签。结果,我们将重复单击新添加的标签的列表项的整个过程。
要添加一个新标签,您只需要在标签控制器中包含存储功能即可。这是可以实现存储功能的方式:
public function store(StoreTagRequest $request)
{
$tag = Tag::create($request->validated());
return response()->json($tag);
}
要确保正确设置所有内容,请确保添加适当的路由和简单的StoreTagRequest
类。我们较早地介绍了这一步骤,并且相对简单。
现在让我们返回我们的搜索选择输入并处理Enter关键事件。首先,我们需要检查记录中是否没有标签。如果没有现有标签,我们将使用以下步骤来存储一个新标签:
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);
}
}
});
现在,我们具有在应用程序中添加新标签的令人兴奋的功能!不是那么惊人吗?
好吧,让我们返回任务创建过程。在这里,我们只需要在发送请求时从输入字段中检索输入的标签。
让我们更新我们的askStoreRequest
以包括选定的标签:
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();
}
我刚刚重构了经过验证的方法将ID字符串转换为ID数组(我们不能合并,因为合并可能会导致冲突,因为任务模型中没有列的列列标记)。
现在,让我们使用两个给定标签来测试其功能。
是!
用给定标签存储一个任务
现在如何将此标签与我们的任务联系起来?
正如我之前提到的,我们需要一种方法来管理标签和任务之间的关系。在上一篇文章中,我们讨论了此主题,您可以在这里找到:
要定义标签和任务之间的关系,我们将需要另一个表,该表将记录这些模型之间的所有连接。该中间表将具有两个主要属性,这些属性是每个模型的ID。
所以,让我们开始使用迁移来定义表:
php artisan make:migration create_tag_task_table
这是我们的迁移设置:
Schema::create('tag_task', function (Blueprint $table) {
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->foreignId('task_id')->constrained()->cascadeOnDelete();
});
现在,我们需要为这两个模型定义关系功能,这非常有用,并且包含许多有用的功能,这些功能使与Laravel模型中的关系合作直接直接。对于这两种模型,我们都需要声明每个模型属于另一个模型。
class Task extends Model
{
public function tags()
{
return $this->belongsToMany(Tag::class);
}
// ...
}
class Tag extends Model
{
public function tasks()
{
return $this->belongsToMany(Task::class);
}
// ...
}
让我们回到我们的控制器,我们需要在其中定义创建任务和选定标签之间的关系,在这里我们将使用另一个函数呼叫附件,就像bellow:
public function store(StoreTaskRequest $request)
{
$task = auth()->user()->tasks()->create($request->validated());
$task->tags()->attach($request->validated('tags'));
return redirect("/tasks", 201);
}
选择一些标签后注意到并向服务器存储请求后,您会注意到创建的任务与所选标签相关联。
更新任务标签
现在,为给定任务更新标签的内容,
主要要求是使用提供的标签启用选择输入。幸运的是,在Laravel刀片中,有一个称为“ Props”的有用功能,该功能允许在组件中插入变量。在我们的项目中,我们需要定义一个标签变量,默认值为null。这将使我们能够有效地利用该组件:
@props([ 'tags' => null ])
然后,我们需要将选定的标签添加到标签选择输入:
<input
type="text"
name="tags"
id="tags-select"
value="{{ isset($tags) ? $tags->pluck('id') : ''}}"
hidden
>
并在我们的标签选择列表中显示选定的标签:
<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>
您可以看到,在可能有一个空列表的情况下,使用了forelse
。您也可以将简单的foreach
循环和if
语句一起使用,但是forelse
提供了更简单的替代方案。
现在,您可以将此组件用于我们的编辑视图。当您这样做时,您会注意到所有选定的标签都正确显示。但是,仍然需要解决一件小事:这些标签的关闭功能。
在这种情况下,我们要做的就是在列表中搜索。如果找到任何跨度元素,我们可以将关闭处理程序功能分配给关闭按钮。此过程很简单,您可以参考以下代码以实现它:
// Get all the tags inside the parent element add close event
[...tagSelectWrap.getElementsByTagName('span')].forEach(spanTag => {
spanTag.getElementsByTagName('i')[0].onclick = () => removeSelectedTag(spanTag);
})
是!
然后,我将我们的搜索组件添加到任务编辑视图中,然后将我们的更新方法重构为bellow:
现在,如果我们添加其他任务并进行更新,您会注意到,不仅会添加新标签,而且最后一个附件的标签也将再次附加。
一种可能的解决方案是首先分离所有先前附加的标签,然后将它们全部连接在一起。但是,另一个选项是使用称为同步的辅助函数,该功能在一个步骤中处理整个过程:
// 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'));
让我们看到结果:
我们刚刚完成了一篇令人难以置信且广泛的文章,其中涵盖了广泛的主题,包括多到许多关系的迷人领域。此外,我们引入了一个名为“ Tag”的全新模型,并在用户任务和关联标签之间建立了关系。为了增强用户体验,我们甚至开发了专门设计用于管理我们表格中标签的刀片组件。
这是我们项目的repo。在整个材料中,我们将采取逐步步骤将其转换为功能齐全的应用。
快乐编码!