phplaravellaravel-8

In Laravel, how can I keep the selected options of a multiselect on an invalid form?


I am working on a blogging application in Laravel 8.

There is the option to add tags to articles. There is a many-to-many relationship between articles and tags. I have an article_tag pivot table.

In the Tag model I have:

class Tag extends Model
{
   use HasFactory;

   protected $fillable = ['name'];

   public function articles()
   {
       return $this->belongsToMany(Article::class);
   } 
}

To the Article model I have added the tags() method

public function tags()
{
    return $this->belongsToMany(Tag::class)->as('tags');
}

I the ArticleController controller, I have two 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->tags[] = $request->get('tags[]');
    $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 (isset($request->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 edit-article.blade.php, I use a multiselect element to assign tags to articles:

<div class="row mb-2">
    <label for="tags" class="col-md-12">{{ __('Tags') }}</label>

    <select name="tags[]" id="tags" class="form-control" multiple="multiple">
        @foreach ($tags as $tag)
            <option value="{{ $tag->id }}"
                {{ in_array($tag->id, $attached_tags) ? 'selected' : '' }}>{{ $tag->name }}</option>
        @endforeach
    </select>
</div>

The problem I am faced with is that when I edit the tags list, the <select> element above does not keep the selected tags if the form is invalid because of validation errors on other form fields.

Where is my mistake?


Solution

  • There are few errors in this (optimizations too)

    1. Remove this line

      $article->tags[] = $request->get('tags[]'); // $article->tags()->sync will do the job
      
    2. Change this

       if ($request->has('tags')) { # changed
           $article->tags()->sync($request->tags);
       } else {
           $article->tags()->sync([]);
       }
      
    3. In view

      @php
          // Use old input if present, otherwise use the attached tags from the article.
          $selectedTags = old('tags', $attached_tags);
      @endphp
      
      <div class="row mb-2">
          <label for="tags" class="col-md-12">{{ __('Tags') }}</label>
          <select name="tags[]" id="tags" class="form-control" multiple="multiple">
              @foreach ($tags as $tag)
                  <option value="{{ $tag->id }}" {{ in_array($tag->id, $selectedTags) ? 'selected' : '' }}>
                      {{ $tag->name }}
                  </option>
              @endforeach
          </select>
      </div>