Strategies for printing a page in a CMP?

When the content of the CMP is all that is wanted, do you use print styles to hide and adjust all the many elements of the manager? Or send the user to a page outside of the manager where you can better control the markup? Something else?

Also, when using print styles, is it safe to rely on IDs of the form ext-gennn? They appear to be generated by ExtJS and I have no idea when or whether they might be regenerated.

Dave

It is way more effective to write i.e. a PDF export for the table data than creating a nice print CSS for the CMP.

1 Like

In the tbar of the grid you can insert the following:

        {
            xtype: 'button',
            itemId: 'export-items',
            text: '<i class="icon icon-file-pdf-o">',
            handler: this.exportItems
        },

A simple exportItems method can look like this:

    exportItems: function () {
        var dataStore = this.getStore();
        var params = {...dataStore.baseParams};
        var requestParams = Ext.apply(params, {
            action: 'mgr/items/export',
            limit: 0,
            HTTP_MODAUTH: MODx.siteId,
        });
        window.open(Packagename.options.connectorUrl + '?' + Ext.urlEncode(requestParams), '_blank');
    },

Then you have to duplicate the getlist processor and replace/add the outputArray method inside with a PDF generation (in this case with mPDF)

    public function outputArray(array $array, $count = false)
    {
        $output = $this->createOutput($array, $count);

        @session_write_close();

        $mpdf = new Mpdf();
        $mpdf->WriteHTML($output);
        $mpdf->output('item-export.pdf', Destination::INLINE);
    }

I have used an extjs plugin for this previously.

Thank you for your help! I’ve taken your advice and gone the mPDF route. I don’t know how to work with ExtJS so I’ve been trying to adapt it to use vanilla JS. I have created a processor and I think it’s mostly working but I can’t get it to download the actual file. Here’s what my processor looks like:

<?php
namespace FBECEvents\Processors;

use MODX\Revolution\Processors\Processor;

class PrintDetail extends Processor {
	public function process() {
        require_once MODX_CORE_PATH . 'components/fbecevents/vendor/autoload.php';
        
        try {
			$event = $this->getEvent();
			$html = $this->generateHtml($event);
            $mpdf = new \Mpdf\Mpdf([
                'margin_left' => 20,
                'margin_right' => 20,
                'margin_top' => 20,
                'margin_bottom' => 20,
            ]);
            
			@session_write_close();

            $mpdf->WriteHTML($html);
            $mpdf->OutputHttpDownload('event.pdf');

            $output = json_encode([
							'success' => true,
							'total' => 1,
							'message' => $html
						], JSON_INVALID_UTF8_SUBSTITUTE);
			return $output;
            
        } catch (Exception $e) {
            return $this->failure($e->getMessage());
        }
    }
    
	private function getEvent() {
		try {
			$namespace = "FBECEvents\\Model\\";
			$id = $this->getProperty('id', null);
			if ($id === null) throw new \Exception('No ID provided');

			$event = $this->modx->getObject($namespace.'fbecEvent', $id);
			if (!$event) throw new \Exception('Event not found');

			return $event;

		} catch (\Exception $e) {
			throw $e;
		}
	}

    private function generateHtml($event) {
        if (!$event) throw new \Exception('Event not found');
        
        $html = '
        <html>
            <head>
                <style>
                    body { font-family: Arial, sans-serif; }
                    .header { font-size: 24px; font-weight: bold; }
                    /* Add more styles as needed */
                </style>
            </head>
            <body>
                <div class="header">' . $event->get('title') . '</div>
                <div class="date">' . $event->get('date') . '</div>
            </body>
        </html>';
        
        return $html;
    }
}

If I use OutputFile() it successfully creates a file on the server but how can I get the processor to send the file to the client for download? I’ve tried exit()ing after OutputHttpDownload() but it didn’t help. I tried OutputBinaryData() and OutputHttpInline() as well but they didn’t help either.

Adding the inline destination creates the download. Maybe Destination::DOWNLOAD will work too. If you use Destination::STRING_RETURN, you have to set the download headers on your own.

Please don’t use anything after $mpdf->Output(). The code works well on several custom extras.

Please make sure that no error occurs when creating the PDF file. Sometimes the headers are already sent and the browser generates a strange error message without displaying the error message. To be on the safe side, you can generate the result with Destination::STRING_RETURN, set the headers afterwards and exit the result at the end of the try block.

There must be something wrong with my general setup somewhere. I get the same result no matter whether I use INLINE, DOWNLOAD or STRING_RETURN. My try block looks like this now:

$event = $this->getEvent();
$html = $this->generateHtml($event);
$mpdf = new \Mpdf\Mpdf([
                'margin_left' => 20,
                'margin_right' => 20,
                'margin_top' => 20,
                'margin_bottom' => 20,
            ]);
            
@session_write_close();

$mpdf->WriteHTML($html);
            
$mpdf->Output('item-export.pdf', \Mpdf\Output\Destination::INLINE);

Here are the response headers I get back:

cache-control:
public, must-revalidate, max-age=0
content-disposition:
inline; filename="item-export.pdf"
content-type:
application/pdf
date:
Sat, 02 Nov 2024 13:52:41 GMT
expires:
Sat, 26 Jul 1997 05:00:00 GMT
last-modified:
Sat, 02 Nov 2024 13:52:41 GMT
mc:
jN1vZ7L/YO1beXvqWrj8e/3WmyO8EpumV6AwFe0J8xo31fRMDlbfy41RusBViL6p
pragma:
public
server:
nginx
set-cookie:
PHPSESSID=gsaqpi8juc76o7abssh0cuqmbs; path=/; HttpOnly
x-generator:
mPDF 8.2.4

Could there be something wrong with the way I’m making the call? Here’s the relevant JS:

Ext.select('#fbec-print').elements.forEach(function(print){
		Ext.get(print).on('click', function(evt){
			let data = new FormData();
			data.append('action', 'FBECEvents\\Processors\\PrintDetail');
			data.append('HTTP_MODAUTH', MODx.siteId);
			data.append('id', Ext.select('#fbec-events article').elements[0].id);
			fetch(conn, {body: data, method: 'post'})
				.then(response => console.log(response))
				.catch(error => console.error('Error:', error));
		});
	});

I’m not sure if you can start a file download with an AJAX request.


They way I see this usually done in MODX is with 2 requests. The first request (AJAX-request) creates the file, then on success the second request (not AJAX, using location.href) sets the headers (header('Content-Disposition: attachment; ...); etc.) and returns the file content.

An example from the MODX code:

If you don’t want to work with:

window.open(Packagename.options.connectorUrl + '?' + Ext.urlEncode(requestParams), '_blank');

or the location.href solution of halftrainedharry, you can create a hidden iframe solution. The following code is part of Agenda:

It is called like this:

        Packagename.util.FileDownload({
            url: Packagename.config.connectorUrl,
            params: {
                action: 'mgr/whatever/export',
                limit: 0
            },
            success: function (response) {
                MODx.msg.status({
                    title: _('success'),
                    message: response.message || _('packagename.export_msg_success'),
                    delay: 2
                });
            },
            failure: function (response) {
                MODx.msg.alert(_('error'), response.message);
            }
        });

Thank you both for your help! I really appreciate it. I managed to get the file to download like this:

fetch(conn, {body: data, method: 'post'})
	.then(response => response.blob())
	.then(blob => {
		const url = URL.createObjectURL(blob);
		const a = document.createElement('a');
		a.style.display = 'none';
		a.href = url;
		a.download = name+'.pdf';
		document.body.appendChild(a);
		a.click();
		URL.revokeObjectURL(url);
	})
	.catch(error => console.error('Error:', error));

Just to say that what I have done in the past when I needed to print off the contents of grids is grab the inner html of the grid container and set that as the body of a newly opened window and called print on document load on that. It actually prints very cleanly and I don’t have to think about security issues with web accessible PDFs on the server.

I remember too that when I was looking for a solution for this, I did find a Extjs plugin for it. Can’t recall exactly why I didn’t end up using it.

1 Like

That’s a nice idea! I’ll try that.

I can’t use the Ext JS plugin because I can’t use Ext JS; it has rebuffed my attempts to learn it. So I bring my own tools.