Laravel101:探索具有多一关系和标签的高效任务管理
#javascript #教程 #php #laravel

很高兴您回来!在上一篇文章中,我们谈论了在Laravel应用程序中关联不同的模型和定义关系。最好的部分是,现在我们项目中的每个用户都可以拥有个性化的任务。

在这篇新文章中,我们将进一步迈出一步,并探索多一到多面的关系。我们将介绍一个名为标签的全新模型,该模型将使我们的应用程序中的每个任务都具有多个标签。

但是,还有更多!在本文中,我们还将创建一个出色的搜索选择刀片组件,旨在使我们的表格中的标签工作。所以准备好,因为我们要深入研究一些令人兴奋的东西!


在刀片中创建搜索选择输入

现在,让我们谈论一个我决定添加的新功能:一个搜索选择输入,用于在任务表格中添加和选择标签。可能有一些可用的软件包可以在应用程序中对此有所帮助,但是让我们首先使用纯JavaScript实施。

所以,让我们开始!

首先,我创建了一个简单的输入组件,用于搜索文本,位于刀片组件的共同目录中。然后,我将此输入组件添加到我们的创建任务视图中,如下图所示:

Image description

现在,如果我们想在共同输入中使用纯JavaScript,我们通常需要定义resources/js目录中的app.js中的所有JavaScript函数。但是,我更喜欢这样做的一种更方便的方法,那就是使用堆叠的脚本。为了实现这一目标,我们在称为push的刀片语法中具有另一个有用的功能。您可以这样使用:

@push('scripts')
<script type="module">
    // Your JavaScript code goes here
</script>
@endpush

通过使用此语法,您可以在@push('scripts')@endpush标签中定义脚本,并将其视为模块。这使得在您的应用程序中更容易管理和组织您的JavaScript代码。

但是,现在这些脚本将无法使用。为了使它们栩栩如生,我们需要在我们的主要布局中添加堆叠的脚本:

Image description

现在,如果您在浏览器内打开控制台,您会看到我们的脚本有效:

Image description

现在,第一步是聆听用户在输入字段中输入的每个更改和字符。为了实现这一目标,您需要将事件侦听器分配给输入元素。

要查看此操作,您可以将以下代码段添加到脚本中。当您运行它时,您会注意到您输入的每个字符都会登录到您的控制台:

@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

此模型很简单,因为它只有一个属性,即名称。因此,通过添加简单的名称列来更新迁移文件。同样,更新请求文件,如下所示:

Image description

最后,让我们回到控制器上,因为这是新事物发生的地方,我们需要对此进行工作。

首先,让我们以略有不同的方式利用我们的索引功能。我们不简单地列出视图中的所有标签,而是将其修改为返回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');

如下图所示,我只添加一些示例数据并提出一个请求:

Image description

好吧,让我们看看如何在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。

Image description

接下来,当用户单击列表中的标签项目时,我们要执行以下操作:

  1. 创建并附加一个代表所选标签的元素,此外,我们添加了一个关闭的按钮元素,我们稍后将实现其功能。
  2. 触发另一个函数,该功能通过添加选定标签的ID来更新标签 - 选择输入字段。
  3. 隐藏显示搜索结果的下拉列表。
  4. 最后,清除标签搜索输入元素以准备添加另一个标签。

这是处理每个列出的标签的点击事件的函数:

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);
}

惊人的让我们看看我们的出色结果:

Image description

最后一步是实现用户按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数组(我们不能合并,因为合并可能会导致冲突,因为任务模型中没有列的列列标记)。

现在,让我们使用两个给定标签来测试其功能。

Image description

是!
用给定标签存储一个任务

现在如何将此标签与我们的任务联系起来?

正如我之前提到的,我们需要一种方法来管理标签和任务之间的关系。在上一篇文章中,我们讨论了此主题,您可以在这里找到:

要定义标签和任务之间的关系,我们将需要另一个表,该表将记录这些模型之间的所有连接。该中间表将具有两个主要属性,这些属性是每个模型的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:

Image description

现在,如果我们添加其他任务并进行更新,您会注意到,不仅会添加新标签,而且最后一个附件的标签也将再次附加。

Image description

一种可能的解决方案是首先分离所有先前附加的标签,然后将它们全部连接在一起。但是,另一个选项是使用称为同步的辅助函数,该功能在一个步骤中处理整个过程:

// 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'));

让我们看到结果:

Image description

我们刚刚完成了一篇令人难以置信且广泛的文章,其中涵盖了广泛的主题,包括多到许多关系的迷人领域。此外,我们引入了一个名为“ Tag”的全新模型,并在用户任务和关联标签之间建立了关系。为了增强用户体验,我们甚至开发了专门设计用于管理我们表格中标签的刀片组件。

这是我们项目的repo。在整个材料中,我们将采取逐步步骤将其转换为功能齐全的应用。

快乐编码!