How to create a custom operation with a form

I needed a form that sends an email to each user, directly from the admin panel. I already had a Users CRUD, so here's how I added a fo...

Karan Datwani
Karan Datwani
Share:

I needed a form that sends an email to each user, directly from the admin panel. I already had a Users CRUD, so here's how I added a form that allows the admin to send a quick email to a user. I'll help you create the same EmailOperation I created, or any operation that needs a custom form, really.

What do we need, from an admin’s perspective?

  1. a button, that shows up next to each User in the table, to allow them to get to the form:

  2. a form, that allows them to type out their Subject and Message for that user:

Let’s go ahead and do that, using Backpack features. We can load all of those custom features in a separate file, by creating a new Backpack operation.

I recommend you read all the way to the end, to understand how and why everything was done. But if you just want the working files, you can find them in this gist.

Steps

Step 1. Create the operation file

First, we need to create a custom operation. If you've installed backpack/generators, you can generate an empty operation trait using:

php artisan backpack:crud-operation Email

This will generate app/Http/Controllers/Admin/Operations/EmailOperation.php , but inside it, we need to change a few things to get it working the way we want it.

Step 2. Change the route to treat a particular entry

  • Add {id} to get route to identify the entry and perform the operation:
protected function setupEmailRoutes($segment, $routeName, $controller){
- Route::get($segment . '/email', [
+ Route::get($segment . '/{id}/email', [
        'as'        => $routeName.'.email',
        'uses'      => $controller.'@email',
        'operation' => 'email',
    ]);
}
  • Inside the email() method, append entry data to the array that is being passed to the view:
public function email(){
    $this->crud->hasAccessOrFail('email');
    $this->data['crud'] = $this->crud;
-    $this->data['title'] = $this->crud->getTitle() ?? 'email '.$this->crud->entity_name;
+    $this->data['title'] = $this->crud->getTitle() ?? 'Email '.$this->crud->entity_name;
+   $this->data['entry'] = $this->crud->getCurrentEntry();
}

Step 3. Create the button

  • We need a button to display inside the list view for each user, so let's create it in resources/views/vendor/backpack/crud/buttons/email.blade.php:
@if ($crud->hasAccess('email'))
  <a href="{{ url($crud->route.'/'.$entry->getKey().'/email') }}" class="btn btn-sm btn-link"><i class="la la-envelope"></i> Email</a>
@endif
  • Now, add the button inside the operation's setupEmailDefaults() by simply uncommenting this line:
protected function setupEmailDefaults(){
        $this->crud->allowAccess('email');
        $this->crud->operation('email', function () {
            $this->crud->loadDefaultOperationSettingsFromConfig();
        });
        $this->crud->operation('list', function () {
            // $this->crud->addButton('top', 'email', 'view', 'crud::buttons.email');
-           // $this->crud->addButton('line', 'email', 'view', 'crud::buttons.email');
+           $this->crud->addButton('line', 'email', 'view', 'crud::buttons.email');
        });
}

Step 4. Create the blade view

Let’s look at the email() method – it loads an “email” blade file. I'd rather give it a new name:

public function email(){
-   return view("crud::operations.email", $this->data);
+   return view("crud::operations.email_form", $this->data);
}

Let’s create this blade file (resources/views/vendor/backpack/crud/operations/email_form.blade.php), where we are going to extend Backpack's blank layout, to keep the look-and-feel of the admin panel:

@extends(backpack_view('blank'))

@section('content')
    {{-- TODO: SHOW FORM HERE --}}
@endsection

Step 5. Add the operation to our CrudController

Let's add our brand-new operation to the CrudController:

class UserCrudController extends CrudController{
+ use \App\Http\Controllers\Admin\Operations\EmailOperation;
}

Boom! We have reached halfway with the operation: it's usable in the browser, but... it does nothing:

Step 6. Customize the HTML form to fit the business logic

Now, let's fill the content section of the email_form.php blade file with the actual HTML code that shows a form. Also, let's fill the header section with breadcrumbs:

@extends(backpack_view('blank'))

@php
$defaultBreadcrumbs = [
    trans('backpack::crud.admin') => url(config('backpack.base.route_prefix'), 'dashboard'),
    $crud->entity_name_plural => url($crud->route),
    'Email' => false,
];
// if breadcrumbs aren't defined in the CrudController, use the default breadcrumbs
$breadcrumbs = $breadcrumbs ?? $defaultBreadcrumbs;
@endphp

@section('header')
    <section class="container-fluid">
        <h2>
            <span class="text-capitalize">Send Email</span>
            <small>Sending email to {!! $entry->name !!}.</small>
            @if ($crud->hasAccess('list'))
                <small>
                    <a href="{{ url($crud->route) }}" class="d-print-none font-sm">
                        <i
                            class="la la-angle-double-{{ config('backpack.base.html_direction') == 'rtl' ? 'right' : 'left' }}"></i>
                        {{ trans('backpack::crud.back_to_all') }}
                        <span>{{ $crud->entity_name_plural }}</span>
                    </a>
                </small>
            @endif
        </h2>
    </section>
@endsection

@section('content')
    <div class="row">
        <div class="col-md-8 bold-labels">
            @if ($errors->any())
                <div class="alert alert-danger pb-0">
                    <ul class="list-unstyled">
                        @foreach ($errors->all() as $error)
                            <li><i class="la la-info-circle"></i> {{ $error }}</li>
                        @endforeach
                    </ul>
                </div>
            @endif
            <form method="post" action="">
                @csrf
                <div class="card">
                    <div class="card-body row">
                        <div class="form-group col-md-4">
                            <label>From</label>
                            <input type="text" name="from" value="{{ old('from', config('mail.from.address')) }}" class="form-control @error('from') is-invalid @enderror">
                                @error('from')
                                <div class="invalid-feedback d-block">{{ $message }}</div>
                                @enderror
                        </div>
                        <div class="form-group col-md-4">
                            <label>To</label>
                            <input type="text" name="to" value="{{ $entry->email }}" readonly="readonly" disabled="disabled" class="form-control">
                        </div>
                        <div class="form-group col-md-4">
                            <label>Reply To</label>
                            <input type="text" name="reply_to" value="{{ old('reply_to',backpack_user()->email) }}" class="form-control @error('reply_to') is-invalid @enderror">
                                @error('reply_to')
                                <div class="invalid-feedback d-block">{{ $message }}</div>
                                @enderror
                        </div>
                        <div class="form-group col-sm-12">
                            <label>Subject</label>
                            <input type="text" name="subject" value="{{ old('subject') }}" class="form-control @error('subject') is-invalid @enderror">
                                @error('subject')
                                <div class="invalid-feedback d-block">{{ $message }}</div>
                                @enderror
                        </div>
                        <div class="form-group col-sm-12">
                            <label>Message</label>
                            <textarea name="message" class="form-control @error('message') is-invalid @enderror">{{ old('message') }}</textarea>
                            @error('message')
                            <div class="invalid-feedback d-block">{{ $message }}</div>
                            @enderror
                        </div>
                    </div>
                </div>
                <div class="d-none" id="parentLoadedAssets">[]</div>
                <div id="saveActions" class="form-group">
                    <input type="hidden" name="_save_action" value="send_email">
                    <button type="submit" class="btn btn-success">
                        <span class="la la-save" role="presentation" aria-hidden="true"></span> &nbsp;
                        <span data-value="send_email">Send Email</span>
                    </button>
                    <div class="btn-group" role="group">
                    </div>
                    <a href="{{ url($crud->route) }}" class="btn btn-default"><span class="la la-ban"></span>
                        &nbsp;Cancel</a>
                </div>
            </form>
        </div>
    </div>
@endsection

Step 7. Process that form, once submitted

Now let's define a POST route, that leads to a postEmailForm() method inside EmailOperation.php, that validates and processes the form:

protected function setupEmailRoutes($segment, $routeName, $controller){
    Route::get($segment . '/{id}/email', [
        'as'        => $routeName.'.email',
        'uses'      => $controller.'@email',
        'operation' => 'email',
    ]);
		
+   Route::post($segment . '/{id}/email', [
+       'as'        => $routeName . '.email-send',
+       'uses'      => $controller . '@postEmailForm',
+       'operation' => 'email',
+   ]);

}

Let’s fill in the logic, so it actually processes an email form and sends that email:

use Illuminate\Support\Facades\Mail;
use Illuminate\Http\Request;
use Exception;
use Validator;
use Alert;

public function postEmailForm(Request $request){
    // run validation
    $validator = Validator::make($request->all(), [
        'from' => 'required|email',
        'reply_to' => 'nullable|email',
        'subject' => 'required',
        'message' => 'required'
    ]);
		
    if($validator->fails())
        return redirect()->back()->withErrors($validator)->withInput();
   
	 $entry = $this->crud->getCurrentEntry();
   
	 try {
        // send the actual email
        Mail::raw($request['message'], function ($message) use ($entry, $request) {                
            $message->from($request->from);
            $message->replyTo($request->reply_to);
            $message->to($entry->email, $entry->name);
            $message->subject($request['subject']);
        });
            
        Alert::success('Mail Sent')->flash();

        return redirect(url($this->crud->route));
    } catch (Exception $e) {
        // show a bubble with the error message
        Alert::error("Error, " . $e->getMessage())->flash();

        return redirect()->back()->withInput();
    }
}

Result

That's it, here's what we're getting from the above:

Again, if you just want to copy-paste, you can find the final code here 💻

Thanks for reading this far 😀 – I hope you learned something new. Let us know what you think in the comments below.

Want to receive more articles like this?

Subscribe to our "Article Digest". We'll send you a list of the new articles, every week, month or quarter - your choice.

Reactions & Comments

What do you think about this?

Thank you, Karan! I wanted this so much on Backpack's admin panel, I added it right away. Ended up recording myself while following your article, in an attempt to get more comfortable in front of the camera and start producing video content (even if mediocre). So if anybody wants to follow the article in video form, please watch the YouTube video here - https://youtu.be/AR2ZCo8ihd4 - feedback is very very welcome.
The video is really completing our goal. Thank you, Cristian.
Thank you for this Karan.. really appreciate you for sharing this..

Latest Articles

Wondering what our community has been up to?