F
Filament11mo ago
John

Using form schema for PDF export

I'm building an application with a LOT of forms. I also need a full PDF export containing multiple forms. The form definition is in a Filament form schema, obviously. The PDF export is built separately. Can I extend the schema or form components to include PDF render functionality? That way I could just throw the same schema at it, and it can return the html needed for PDF print. I'd only need one source of the form schema, which is faster to build and schema changes wouldn't have to be applied twice. Maybe some plugin does a similar extension?
Solution:
I've ultimately created quite a hacky solution for this. Overruling ViewComponent.php via composer. If anyone would like more details, let me know.
Jump to solution
4 Replies
John
JohnOP11mo ago
I guess I'd have to mimic the way Filament renders it forms, but offer it a different set of blade files. That way I can reuse layout components, conditional fields, etc. Anyone who has experience with something like this?
Solution
John
John10mo ago
I've ultimately created quite a hacky solution for this. Overruling ViewComponent.php via composer. If anyone would like more details, let me know.
Felipe Castro
Felipe Castro9mo ago
Hi. would you please share how you solved it?
John
JohnOP9mo ago
Sure. In composer.json, override ViewComponent.php:
"autoload": {
"exclude-from-classmap": [
"Filament\\Support\\Components\\ViewComponent"
],
"files": [
"app/Classes/Filament/Support/Components/ViewComponent.php"
]
},
"autoload": {
"exclude-from-classmap": [
"Filament\\Support\\Components\\ViewComponent"
],
"files": [
"app/Classes/Filament/Support/Components/ViewComponent.php"
]
},
Copy the vendor ViewComponent.php to your custom location. And change this part:
public function getView(): string
{
if (! isset($this->view)) {
throw new Exception('Class [' . static::class . '] extends [' . ViewComponent::class . '] but does not have a [$view] property defined.');
}

/**
* <CUSTOM>
*/
if (isset($GLOBALS['pdf'])) {
return strtr(
$this->view,
[
'forms::' => 'forms.pdf.',
'forms.components' => 'forms.pdf.components'
]
);
}
/**
* </CUSTOM>
*/

return $this->view;
}
public function getView(): string
{
if (! isset($this->view)) {
throw new Exception('Class [' . static::class . '] extends [' . ViewComponent::class . '] but does not have a [$view] property defined.');
}

/**
* <CUSTOM>
*/
if (isset($GLOBALS['pdf'])) {
return strtr(
$this->view,
[
'forms::' => 'forms.pdf.',
'forms.components' => 'forms.pdf.components'
]
);
}
/**
* </CUSTOM>
*/

return $this->view;
}
Then provide a custom blade for every component you use. E.g. fieldset.blade.php:
<fieldset class="component">
@if (filled($label = $getLabel()))
<legend>
{{ $label }}
</legend>
@endif

{{ $getChildComponentContainer() }}
</fieldset>
<fieldset class="component">
@if (filled($label = $getLabel()))
<legend>
{{ $label }}
</legend>
@endif

{{ $getChildComponentContainer() }}
</fieldset>
text-input.blade.php:
<div class="component">
<div class="label">{{ $getLabel() }}</div>
<div class="value">
@if ($getType() == 'date')
{{ $getState() ? \Carbon\Carbon::parse($getState())->isoFormat('D MMMM YYYY') : '-' }}
@else
{{ $getState() ?: '-' }}
@endif
</div>
</div>
<div class="component">
<div class="label">{{ $getLabel() }}</div>
<div class="value">
@if ($getType() == 'date')
{{ $getState() ? \Carbon\Carbon::parse($getState())->isoFormat('D MMMM YYYY') : '-' }}
@else
{{ $getState() ?: '-' }}
@endif
</div>
</div>
textarea.blade.php:
<div class="component textarea">
<div class="label">{{ $getLabel() }}</div>
<div class="value">
{!! nl2br(e($getState() ?? '-')) !!}
</div>
</div>
<div class="component textarea">
<div class="label">{{ $getLabel() }}</div>
<div class="value">
{!! nl2br(e($getState() ?? '-')) !!}
</div>
</div>
checkbox.blade.php:
<div class="component">
<div class="value">
<div class="symbol-and-text">
@if ($getState())
<div class="symbol">&#xf1fe;</div>
@else
<div class="symbol">&#xe835;</div>
@endif
{{ $getLabel() }}
</div>
</div>
</div>
<div class="component">
<div class="value">
<div class="symbol-and-text">
@if ($getState())
<div class="symbol">&#xf1fe;</div>
@else
<div class="symbol">&#xe835;</div>
@endif
{{ $getLabel() }}
</div>
</div>
</div>
(I use this css for checkbox:)
.form .symbol {
font-family: MaterialSymbolsOutlined, sans-serif;
font-size: 1.1rem;
}
.form .symbol {
font-family: MaterialSymbolsOutlined, sans-serif;
font-size: 1.1rem;
}
Let me know if you need more explanation. Oh, and you'll need something like this to generate the HTML. It has some stuff specific for my multi-form workflow, which you should change obviously:
<?php

namespace App\Http\Livewire;

use App\Models\BaseRequest;
use App\Models\Request;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Livewire\Component;
use Str;

class RequestPdf extends Component implements HasForms
{
use InteractsWithForms;

public function getHtml(BaseRequest|Request $request): string
{
$GLOBALS['pdf'] = true;

$html = '';

$first = true;
foreach (requestConfig(get_class($request))->getSteps($request) as $key => $form) {
$requestClass = Str::kebab(class_basename($request));
$title = $form->getTitle() ?: __("{$requestClass}.step.{$key}.title");
$html .= '<h1' . ($first ? '' : ' class="page-break"') . '>' . $title . '</h1>';
$html .= $this->makeForm()
->schema($form->getSchema())
->model($request)
->fill($request->toArray())
->render();
$first = false;
}

return $html;
}
}
<?php

namespace App\Http\Livewire;

use App\Models\BaseRequest;
use App\Models\Request;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Livewire\Component;
use Str;

class RequestPdf extends Component implements HasForms
{
use InteractsWithForms;

public function getHtml(BaseRequest|Request $request): string
{
$GLOBALS['pdf'] = true;

$html = '';

$first = true;
foreach (requestConfig(get_class($request))->getSteps($request) as $key => $form) {
$requestClass = Str::kebab(class_basename($request));
$title = $form->getTitle() ?: __("{$requestClass}.step.{$key}.title");
$html .= '<h1' . ($first ? '' : ' class="page-break"') . '>' . $title . '</h1>';
$html .= $this->makeForm()
->schema($form->getSchema())
->model($request)
->fill($request->toArray())
->render();
$first = false;
}

return $html;
}
}
This approach saves me SO much time vs manually building and keeping track of a PDF html next to the Filament definition.
Want results from more Discord servers?
Add your server