Craftable

Relations

Craftable supports all Eloquent Relationships defined in Laravel documentation.

Code from example sections is placed in Craftable demo repository.


One to many example

Let's imagine you have articles with authors and you want to implement this behavior:

  • show authors information in listing,
  • authors filter to search articles of selected authors in listing,
  • choose author from multiselect when creating/editing article.

Migration

Our example migration contain author_id column which references to authors table.

Schema::create('articles_with_relationships', function (Blueprint $table) {
    $table->increments('id');
    $table->string('title');
    $table->text('perex')->nullable();
    $table->date('published_at')->nullable();
    $table->boolean('enabled')->default(false);
    $table->integer('author_id')->nullable();
    $table->foreign('author_id')->references('id')->on('authors')->onDelete('cascade');
    $table->timestamps();
});

Model

In Author model add hasMany relation to ArticlesWithRelationship::class.

public function articlesWithRelationships()
{
    return $this->hasMany(ArticlesWithRelationship::class);
}

In ArticlesWithRelationship model add belongsTo relation to Author::class.

public function author() {
    return $this->belongsTo(Author::class);
}

Controller

Controller methods should look like:

Index method

{info} Note, that we have to load the relation using $query->with(['author']);.

public function index(IndexArticlesWithRelationship $request)
{
    // create and AdminListing instance for a specific model and
    $data = AdminListing::create(ArticlesWithRelationship::class)->processRequestAndGet(
        // pass the request with params
        $request,

        // set columns to query
        ['id', 'title', 'published_at', 'enabled', 'author_id'],

        // set columns to searchIn
        ['id', 'title', 'perex'],

        function ($query) use ($request) {
            $query->with(['author']);
            if($request->has('authors')){
                $query->whereIn('author_id', $request->get('authors'));
            }
        }
    );

    if ($request->ajax()) {
        return ['data' => $data];
    }

    return view('admin.articles-with-relationship.index', [
        'data' => $data,
        'authors' => Author::all()
    ]);
}

Create method

public function create()
{
    $this->authorize('admin.articles-with-relationship.create');

    return view('admin.articles-with-relationship.create', [
        'authors' => Author::all(),
    ]);
}

Store method

public function store(StoreArticlesWithRelationship $request)
{
    // Sanitize input
    $sanitized = $request->validated();
    $sanitized['author_id'] = $request->getAuthorId();
    // Store the ArticlesWithRelationship
    $articlesWithRelationship = ArticlesWithRelationship::create($sanitized);

    if ($request->ajax()) {
        return [
            'redirect' => url('admin/articles-with-relationships'),
            'message' => trans('brackets/admin-ui::admin.operation.succeeded')
        ];
    }

    return redirect('admin/articles-with-relationships');
}

Edit method

public function edit(ArticlesWithRelationship $articlesWithRelationship)
{
    $this->authorize('admin.articles-with-relationship.edit', $articlesWithRelationship);

    return view('admin.articles-with-relationship.edit', [
        'articlesWithRelationship' => $articlesWithRelationship,
        'authors' => Author::all(),
    ]);
}

Update method

public function update(UpdateArticlesWithRelationship $request, ArticlesWithRelationship $articlesWithRelationship)
{
    // Sanitize input
    $sanitized = $request->validated();
    $sanitized['author_id'] = $request->getAuthorId();
    // Update changed values ArticlesWithRelationship
    $articlesWithRelationship->update($sanitized);

    if ($request->ajax()) {
        return ['redirect' => url('admin/articles-with-relationships'), 'message' => trans('brackets/admin-ui::admin.operation.succeeded')];
    }

    return redirect('admin/articles-with-relationships');
}

Requests

Store request

Store request requires methods:

public function rules()
{
    return [
        'title' => ['required', 'string'],
        'perex' => ['nullable', 'string'],
        'published_at' => ['nullable', 'date'],
        'enabled' => ['required', 'boolean'],
        'author' => ['required'],

    ];
}
public function getAuthorId(){
    if ($this->has('author')){
        return $this->get('author')['id'];
    }
    return null;
}

Update request

Update request requires methods:

public function rules()
{
    return [
        'title' => ['sometimes', 'string'],
        'perex' => ['nullable', 'string'],
        'published_at' => ['nullable', 'date'],
        'enabled' => ['sometimes', 'boolean'],
        'author' => ['required'],

    ];
}
public function getAuthorId(){
    if ($this->has('author')){
        return $this->get('author')['id'];
    }
    return null;
}

Form.js

Add authors to props.

props: [
    'authors'
]

Add author property in data form object.

author:  '' ,

Listing.js

Add following code to data() and watch.

data() {
    return {
        showAuthorsFilter: false,
        authorsMultiselect: {},

        filters: {
            authors: [],
        },
    }
},

watch: {
    showAuthorsFilter: function (newVal, oldVal) {
        this.authorsMultiselect = [];
    },
    authorsMultiselect: function(newVal, oldVal) {
        this.filters.authors = newVal.map(function(object) { return object['key']; });
        this.filter('authors', this.filters.authors);
    }
}

create.blade.php

Add authors prop to form component.

:authors="{{$authors->toJson()}}"

edit.blade.php

Add authors prop to form component.

:authors="{{$authors->toJson()}}"

index.blade.php

In place where you want to have authors filter add following code.

Also you can prepare :options value on backend with own properties.

<div class="row" v-if="showAuthorsFilter">
    <div class="col-sm-auto form-group">
        <p>{{ __('Select author/s') }}</p>
    </div>
    <div class="col col-lg-12 col-xl-12 form-group">
        <multiselect v-model="authorsMultiselect"
             :options="{{ $authors->map(function($author) { return ['key' => $author->id, 'label' =>  $author->title]; })->toJson() }}"
             label="label"
             track-by="key"
             placeholder="{{ __('Type to search a author/s') }}"
             :limit="2"
             :multiple="true">
        </multiselect>
    </div>
</div>

You can also use User detail tooltip in <tbody>.

<user-detail-tooltip :user="item.author" v-if="item.author">
</user-detail-tooltip>

form-elements.blade.php

In form-elements.blade.php add following code in place where you want to have option to choose author of article.

<div class="form-group row align-items-center"
     :class="{'has-danger': errors.has('author_id'), 'has-success': this.fields.author_id && this.fields.author_id.valid }">
    <label for="author_id"
           class="col-form-label text-center col-md-4 col-lg-3">{{ trans('admin.post.columns.author_id') }}</label>
    <div class="col-md-8 col-lg-9">

        <multiselect
            v-model="form.author"
            :options="authors"
            :multiple="false"
            track-by="id"
            label="full_name"
            tag-placeholder="{{ __('Select Author') }}"
            placeholder="{{ __('Author') }}">
        </multiselect>

        <div v-if="errors.has('author_id')" class="form-control-feedback form-text" v-cloak>@{{
            errors.first('author_id') }}
        </div>
    </div>
</div>

Many to many example

Let's imagine you have articles with tags and you want to implement this behavior:

  • show tags in listing,
  • choose tags from multiselect when creating/editing article.

Migrations

Our example migrations contain articles_with_relationships and tags tables.

Schema::create('articles_with_relationships', function (Blueprint $table) {
    $table->increments('id');
    ...
    $table->timestamps();
});
Schema::create('tags', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
    $table->timestamps();
});

Our example also contain pivot table articles_with_relationship_tag which contain articles_with_relationship_id column which references to articles_with_relationships table and tag_id column which references to tags table.

Schema::create('articles_with_relationship_tag', function (Blueprint $table) {
    $table->unsignedInteger('articles_with_relationship_id');
    $table->foreign('articles_with_relationship_id')
        ->references('id')
        ->on('articles_with_relationships')
        ->onDelete('cascade');
    $table->unsignedInteger('tag_id');
    $table->foreign('tag_id')
        ->references('id')
        ->on('tags')
        ->onDelete('cascade');
});

Model

In ArticlesWithRelationship model add belongsToMany relation to Tag::class.

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

In Tag model add belongsToMany relation to ArticlesWithRelationship::class.

public function articlesWithRelationships()
{
    $this->belongsToMany(ArticlesWithRelationship::class);
}

Controller

Controller methods should look like:

Index method

{info} Note, that we have to load the relation using $query->with(['tags']);.

public function index(IndexArticlesWithRelationship $request)
{
    // create and AdminListing instance for a specific model and
    $data = AdminListing::create(ArticlesWithRelationship::class)->processRequestAndGet(
        // pass the request with params
        $request,

        // set columns to query
        ['id', 'title', 'published_at', 'enabled', 'author_id'],

        // set columns to searchIn
        ['id', 'title', 'perex'],

        function ($query) use ($request) {
            $query->with(['tags']);
        }
    );

    if ($request->ajax()) {
        return ['data' => $data];
    }

    return view('admin.articles-with-relationship.index', [
        'data' => $data,
    ]);
}

Create method

public function create()
{
    $this->authorize('admin.articles-with-relationship.create');

    return view('admin.articles-with-relationship.create', [
        'tags' => Tag::all(),
    ]);
}

Store method

public function store(StoreArticlesWithRelationship $request)
{
    // Sanitize input
    $sanitized = $request->validated();
    $sanitized['tags'] = $request->getTags();

    DB::transaction(function () use ($sanitized) {
        // Store the ArticlesWithRelationship
        $articlesWithRelationship = ArticlesWithRelationship::create($sanitized);
        $articlesWithRelationship->tags()->sync($sanitized['tags']);
    });

    if ($request->ajax()) {
        return [
            'redirect' => url('admin/articles-with-relationships'),
            'message' => trans('brackets/admin-ui::admin.operation.succeeded')
        ];
    }

    return redirect('admin/articles-with-relationships');
}

Edit method

{info} Note, that we have to load the relation using $articlesWithRelationship->load('tags');.

public function edit(ArticlesWithRelationship $articlesWithRelationship)
{
    $this->authorize('admin.articles-with-relationship.edit', $articlesWithRelationship);

    $articlesWithRelationship->load('tags');

    return view('admin.articles-with-relationship.edit', [
        'articlesWithRelationship' => $articlesWithRelationship,
        'tags' => Tag::all(),
    ]);
}

Update method

public function update(UpdateArticlesWithRelationship $request, ArticlesWithRelationship $articlesWithRelationship)
{
    // Sanitize input
    $sanitized = $request->validated();
    $sanitized['tags'] = $request->getTags();

    DB::transaction(function () use ($articlesWithRelationship, $sanitized) {
        // Update changed values ArticlesWithRelationship
        $articlesWithRelationship->update($sanitized);
        $articlesWithRelationship->tags()->sync($sanitized['tags']);
    });

    if ($request->ajax()) {
        return [
            'redirect' => url('admin/articles-with-relationships'), 
            'message' => trans('brackets/admin-ui::admin.operation.succeeded')
        ];
    }

    return redirect('admin/articles-with-relationships');
}

Requests

Store request

Store request requires methods:

public function rules()
{
    return [
        'title' => ['required', 'string'],
        'perex' => ['nullable', 'string'],
        'published_at' => ['nullable', 'date'],
        'enabled' => ['required', 'boolean'],
        'tags' => ['required'],

    ];
}
public function getTags(): array
{
    if ($this->has('tags')) {
        $tags = $this->get('tags');
        return array_column($tags, 'id');
    }
    return [];
}

Update request

Update request requires methods:

public function rules()
{
    return [
        'title' => ['sometimes', 'string'],
        'perex' => ['nullable', 'string'],
        'published_at' => ['nullable', 'date'],
        'enabled' => ['sometimes', 'boolean'],
        'tags' => ['required'],

    ];
}
public function getTags(): array
{
    if ($this->has('tags')) {
        $tags = $this->get('tags');
        return array_column($tags, 'id');
    }
    return [];
}

Form.js

Add availableTags to props.

props: [
    'availableTags'
]

Add tags property in data form object.

tags:  '',

create.blade.php

Add available-tags prop to form component.

:available-tags="{{ $tags->toJson() }}"

edit.blade.php

Add available-tags prop to form component.

:available-tags="{{ $tags->toJson() }}"

index.blade.php

In place where you want to show tags add following code in <tbody>.

<div v-for="tag in item.tags">
    <span class="badge badge-success text-white">@{{ tag.name }}</span>
</div>

form-elements.blade.php

In form-elements.blade.php add following code in place where you want to have option to choose tags of article.

<div class="form-group row align-items-center"
     :class="{'has-danger': errors.has('tags'), 'has-success': this.fields.tags && this.fields.tags.valid }">
    <label for="author_id"
           class="col-form-label text-center col-md-4 col-lg-3">{{ trans('admin.articles-with-relationship.columns.tags') }}</label>
    <div class="col-md-8 col-lg-9">

        <multiselect
                v-model="form.tags"
                :options="availableTags"
                :multiple="true"
                track-by="id"
                label="name"
                tag-placeholder="{{ __('Select Tags') }}"
                placeholder="{{ __('Tags') }}">
        </multiselect>

        <div v-if="errors.has('tags')" class="form-control-feedback form-text" v-cloak>@{{
            errors.first('tags') }}
        </div>
    </div>
</div>

Summary