admin管理员组文章数量:1392098
I am working on a blogging application (link to GitHub repo) in Laravel 8.
I have put together a way to add and edit tags to articles.
I ran into a bug whose cause and solution I have been unable to find: updating the article's list of tags fails after one or more (but not all) tags are deleted. By contrast, if I only select new tags, the update operation works fine.
In the ArticleController controller, I have the below methods for editing and updating an article:
public function edit($id)
{
$article = Article::find($id);
$attached_tags = $article->tags()->get()->pluck('id')->toArray();
return view(
'dashboard/edit-article',
[
'categories' => $this->categories(),
'tags' => $this->tags(),
'attached_tags' => $attached_tags,
'article' => $article
]
);
}
public function update(Request $request, $id)
{
$validator = Validator::make($request->all(), $this->rules, $this->messages);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator->errors())->withInput();
}
$fields = $validator->validated();
$article = Article::find($id);
// If a new image is uploaded, set it as the article image
// Otherwise, set the old image...
if (isset($request->image)) {
$imageName = md5(time()) . Auth::user()->id . '.' . $request->image->extension();
$request->image->move(public_path('images/articles'), $imageName);
} else {
$imageName = $article->image;
}
$article->title = $request->get('title');
$article->short_description = $request->get('short_description');
$article->category_id = $request->get('category_id');
$article->featured = $request->has('featured');
$article->image = $request->get('image') == 'default.jpg' ? 'default.jpg' : $imageName;
$article->content = $request->get('content');
// Save changes to the article
$article->save();
//Attach tags to article
if ($request->has('tags')) {
$article->tags()->sync($request->tags);
} else {
$article->tags()->sync([]);
}
return redirect()->route('dashboard.articles')->with('success', 'The article titled "' . $article->title . '" was updated');
}
In views\dashboard\edit-article.blade.php
I have this code got the pat of the interface that deals with the tags:
<div class="row mb-2">
<label for="tags" class="col-md-12">{{ __('Tags') }}</label>
<div class="position-relative">
<span id="tagSelectorToggler" class="tag-toggler" onclick="toggleTagSelector(event)">
<i class="fas fa-chevron-up"></i>
</span>
<ul id="tagsList" class="form-control tags-list mb-1" onclick="toggleTagSelector(event)">
<li class="text-muted">
Use [Ctrl] + [Click] to select one or more tags from the list
</li>
</ul>
</div>
<div id="tagActions" class="tag-actions">
<input oninput="filterTags(event)" type="search" class="form-control mb-1"
placeholder="Filter available tags" />
@php $selectedTags = old('tags', $attached_tags) @endphp
<select name="tags[]" id="tags" class="form-control tag-select" multiple>
@foreach ($tags as $tag)
<option value="{{ $tag->id }}"
{{ in_array($tag->id, $selectedTags) ? 'selected' : '' }}>
{{ $tag->name }}
</option>
@endforeach
</select>
</div>
</div>
In resources\js\tags.js
, I have:
const tagsList = document.querySelector(".tags-list")
const tagActions = document.getElementById("tagActions")
const tagSelector = document.getElementById("tags")
const tagToggler = document.getElementById("tagSelectorToggler")
if (tagSelector) {
var preSelectedTags = Array.from(tagSelector.options)
.filter((option) => option.selected)
.map((option) => option.text)
var selectedTags = new Set()
}
window.filterTags = (event) => {
var query = event.target.value
var availableTags = Array.from(tagSelector.options)
availableTags.forEach(function (option) {
if (!option.text.toLowerCase().includes(query.toLowerCase())) {
option.classList.add("d-none")
} else {
option.classList.remove("d-none")
}
})
}
window.toggleTagSelector = (event) => {
let tagActionsVisibility = tagActions.checkVisibility()
if (event.target.tagName !== "BUTTON" && event.target.tagName !== "SPAN") {
if (tagActionsVisibility) {
tagActions.style.display = "none"
tagToggler.classList.add("active")
} else {
tagActions.style.display = "block"
tagToggler.classList.remove("active")
}
}
}
window.renderTags = () => {
tagsList.innerHTML =
[...selectedTags]
.sort()
.map(
(tag) =>
`<li class="tag"
><span class="value">${tag}</span>
<button>×</button>
</li>`,
)
.join("") ||
`<li class="text-muted">Use [Ctrl] + [Click] to select one or more tags from the list</li>`
for (const option of tagSelector.options) {
option.selected = selectedTags.has(option.textContent)
}
}
if (preSelectedTags) {
window.addPreselectedTags = () => {
preSelectedTags.forEach(selectedTags.add.bind(selectedTags))
renderTags()
}
}
if (tagsList) {
tagsList.addEventListener("click", function (event) {
if (event.target.tagName !== "BUTTON") return
let tagToRemove = event.target.closest("LI").children[0].textContent
let optionToDeselect = Array.from(tagSelector.options).find((option) => {
return option.innerText == tagToRemove
})
optionToDeselect.removeAttribute('selected')
selectedTags.delete(tagToRemove)
console.log(selectedTags);
renderTags()
})
}
if (tagSelector) {
tagSelector.addEventListener("change", function () {
selectedTags = new Set(
Array.from(tagSelector.options)
.filter((option) => option.selected)
.map((option) => option.textContent),
)
renderTags()
console.log(selectedTags);
})
}
if (tagSelector) {
window.addPreselectedTags();
}
If I click a tag's close button and then update the article, it loses all the tags (not only the deleted one).
There is a working fiddle HERE, with JavaScript and HTML.
Where is my mistake?
I am working on a blogging application (link to GitHub repo) in Laravel 8.
I have put together a way to add and edit tags to articles.
I ran into a bug whose cause and solution I have been unable to find: updating the article's list of tags fails after one or more (but not all) tags are deleted. By contrast, if I only select new tags, the update operation works fine.
In the ArticleController controller, I have the below methods for editing and updating an article:
public function edit($id)
{
$article = Article::find($id);
$attached_tags = $article->tags()->get()->pluck('id')->toArray();
return view(
'dashboard/edit-article',
[
'categories' => $this->categories(),
'tags' => $this->tags(),
'attached_tags' => $attached_tags,
'article' => $article
]
);
}
public function update(Request $request, $id)
{
$validator = Validator::make($request->all(), $this->rules, $this->messages);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator->errors())->withInput();
}
$fields = $validator->validated();
$article = Article::find($id);
// If a new image is uploaded, set it as the article image
// Otherwise, set the old image...
if (isset($request->image)) {
$imageName = md5(time()) . Auth::user()->id . '.' . $request->image->extension();
$request->image->move(public_path('images/articles'), $imageName);
} else {
$imageName = $article->image;
}
$article->title = $request->get('title');
$article->short_description = $request->get('short_description');
$article->category_id = $request->get('category_id');
$article->featured = $request->has('featured');
$article->image = $request->get('image') == 'default.jpg' ? 'default.jpg' : $imageName;
$article->content = $request->get('content');
// Save changes to the article
$article->save();
//Attach tags to article
if ($request->has('tags')) {
$article->tags()->sync($request->tags);
} else {
$article->tags()->sync([]);
}
return redirect()->route('dashboard.articles')->with('success', 'The article titled "' . $article->title . '" was updated');
}
In views\dashboard\edit-article.blade.php
I have this code got the pat of the interface that deals with the tags:
<div class="row mb-2">
<label for="tags" class="col-md-12">{{ __('Tags') }}</label>
<div class="position-relative">
<span id="tagSelectorToggler" class="tag-toggler" onclick="toggleTagSelector(event)">
<i class="fas fa-chevron-up"></i>
</span>
<ul id="tagsList" class="form-control tags-list mb-1" onclick="toggleTagSelector(event)">
<li class="text-muted">
Use [Ctrl] + [Click] to select one or more tags from the list
</li>
</ul>
</div>
<div id="tagActions" class="tag-actions">
<input oninput="filterTags(event)" type="search" class="form-control mb-1"
placeholder="Filter available tags" />
@php $selectedTags = old('tags', $attached_tags) @endphp
<select name="tags[]" id="tags" class="form-control tag-select" multiple>
@foreach ($tags as $tag)
<option value="{{ $tag->id }}"
{{ in_array($tag->id, $selectedTags) ? 'selected' : '' }}>
{{ $tag->name }}
</option>
@endforeach
</select>
</div>
</div>
In resources\js\tags.js
, I have:
const tagsList = document.querySelector(".tags-list")
const tagActions = document.getElementById("tagActions")
const tagSelector = document.getElementById("tags")
const tagToggler = document.getElementById("tagSelectorToggler")
if (tagSelector) {
var preSelectedTags = Array.from(tagSelector.options)
.filter((option) => option.selected)
.map((option) => option.text)
var selectedTags = new Set()
}
window.filterTags = (event) => {
var query = event.target.value
var availableTags = Array.from(tagSelector.options)
availableTags.forEach(function (option) {
if (!option.text.toLowerCase().includes(query.toLowerCase())) {
option.classList.add("d-none")
} else {
option.classList.remove("d-none")
}
})
}
window.toggleTagSelector = (event) => {
let tagActionsVisibility = tagActions.checkVisibility()
if (event.target.tagName !== "BUTTON" && event.target.tagName !== "SPAN") {
if (tagActionsVisibility) {
tagActions.style.display = "none"
tagToggler.classList.add("active")
} else {
tagActions.style.display = "block"
tagToggler.classList.remove("active")
}
}
}
window.renderTags = () => {
tagsList.innerHTML =
[...selectedTags]
.sort()
.map(
(tag) =>
`<li class="tag"
><span class="value">${tag}</span>
<button>×</button>
</li>`,
)
.join("") ||
`<li class="text-muted">Use [Ctrl] + [Click] to select one or more tags from the list</li>`
for (const option of tagSelector.options) {
option.selected = selectedTags.has(option.textContent)
}
}
if (preSelectedTags) {
window.addPreselectedTags = () => {
preSelectedTags.forEach(selectedTags.add.bind(selectedTags))
renderTags()
}
}
if (tagsList) {
tagsList.addEventListener("click", function (event) {
if (event.target.tagName !== "BUTTON") return
let tagToRemove = event.target.closest("LI").children[0].textContent
let optionToDeselect = Array.from(tagSelector.options).find((option) => {
return option.innerText == tagToRemove
})
optionToDeselect.removeAttribute('selected')
selectedTags.delete(tagToRemove)
console.log(selectedTags);
renderTags()
})
}
if (tagSelector) {
tagSelector.addEventListener("change", function () {
selectedTags = new Set(
Array.from(tagSelector.options)
.filter((option) => option.selected)
.map((option) => option.textContent),
)
renderTags()
console.log(selectedTags);
})
}
if (tagSelector) {
window.addPreselectedTags();
}
If I click a tag's close button and then update the article, it loses all the tags (not only the deleted one).
There is a working fiddle HERE, with JavaScript and HTML.
Where is my mistake?
Share Improve this question edited Mar 22 at 8:23 Razvan Zamfir asked Mar 11 at 17:10 Razvan ZamfirRazvan Zamfir 4,7107 gold badges49 silver badges285 bronze badges 5 |2 Answers
Reset to default 2 +50Ok, I traced the issue. The issue is in the expression option.textContent
, which is used two times in the tags.js file. When you retrieve option.textContent
it returns entire text content which includes the extra spaces before and after the actual tag text. So your script is not properly pre-selecting the tags, specifically at this line:
option.selected = selectedTags.has(option.textContent)
Solution:
In place of the expression option.textContent
, either use option.textContent.trim()
or option.text
. You need to replace this in all the places.
I cloned your Brave-2.0 CMS and tried it out locally.
The problem:
The <select name="tags[]" id="tags" class="form-control tag-select" multiple>
is not maintaining its previous select value when accessing the edit article page.
If you resubmit without changing anything, your code will not detect data coming from tags
array (the $request->has('tags')
is always false
), thus deleting ALL of your previously input tag.
The solution:
I can't give you a specific code yet but the easiest fix I can think of is to do the following:
- Prepare a variable that stores your #tags select value. When the user access the edit article page, make sure the backend values are stored into the javascript variables. (through API or through element embedding)
- Before submitting the form, force the javascript to prevent the form submission, add the
tags[]
array data, then submit the form.
Alternatively, you can try using Vue script just for the multi-select of your tags.
Hope this helps.
Regards,
版权声明:本文标题:php - Laravel and Javascript: strange bug while updating article-tags many-to-many relationship - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1744780258a2624666.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
console.log(selectedTags)
output the set slightly different, but by all appearances ok. – Razvan Zamfir Commented Mar 11 at 21:27dd( $request->tags );
in the first line of the update method and modify your question to show the dump in the case of a deletion and in the case of an addition. – HEXALIX Commented Mar 12 at 7:16