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>&times;</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>&times;</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
  • 1 Have you tried debugging the variables in a console log before submission and after submission, via dump(), in the update() method ? – HEXALIX Commented Mar 11 at 20:36
  • @HEXALIX The multiple select works fine. It is when a tag is deleted that the bug happens. The 2 instances of console.log(selectedTags) output the set slightly different, but by all appearances ok. – Razvan Zamfir Commented Mar 11 at 21:27
  • Can you put this dd( $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
  • 1 If you want better/more answers, you should really debug the issue first. Figure out whether the backend or frontend is causing the issue. Then focus your question around the actual issue. – naamhierzo Commented Mar 14 at 8:02
  • 1 First we need to figure out whether this is a client-side issue, a server-side issue or there is an issue at both. In order to differentiate where your problem is, the following questions need to be answered: 1. Is your view displaying properly whatever you already have in the database? 2. When you send a request (insert/update/delete items) to the server, is the request and its payload correct? 3. If the server gets proper, correct input, does it respond properly? You need to address these questions as well as provide detailed information of your experiments, including sample data and result – Lajos Arpad Commented Mar 14 at 18:48
Add a comment  | 

2 Answers 2

Reset to default 2 +50

Ok, 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,

本文标签: phpLaravel and Javascript strange bug while updating articletags manytomany relationshipStack Overflow