Jump to main content Jump to doc navigation

Here's where we make the jump from stand-alone Ext JS to MODX. Many things are the same, but the learning curve still goes up, unfortunately. Our first challenge in this new playing field will be something as simple as possible: we are going to create a Custom Manager Page that lists content types. Yes, this is something that MODX already does under the System -> Content Types menu, but that's the point: we want to try our hand at re-creating something that MODX already provides.

Here's a screenshot of the built-in grid displaying Content Types. This is what we are going to re-create in our own CMP:

Orienting Yourself

The previous tutorials here were simple in the sense that they put everything in front of you: you had to include the Ext JS Javascript and CSS files, but beyond that, it was just a matter of manipulating the Javascript. Once we go into the MODX manager, however, things can get more complex. Things are a bit buried in various folders and PHP class files, and we have the added challenge of dealing with user permissions. So let's take a deep breath and review what we're looking at inside the manager.

The Manager

The MODX manager acts as a self-contain application. Most of the files we'll be dealing with here are in the following locations:

  • manager/assets/modext : home to the various Ext JS configurations, panels, widgets, et al
  • manager/controllers/ : home to the PHP that loads up the Ext JS stuff. The JS points to a connector.
  • connectors/ : thankfully, this nest of files is going away in MODX 2.3, but in MODX 2.2 and before, the connectors simply point to a processor.
  • core/model/modx/processors/ : this is what should handle the fetching of the data
  • core/model/modx/modmanagerrequest.class.php : responsible for handling all the requests in the manager
  • core/model/modx/modprocessor.class.php : responsible for actually retrieving the data.

Are you confused yet? I am, and I'm writing this stupid thing. So manager controllers are confusing, and they are poorly documented, and they will be changing in the next version, so what I am about to propose is a bit unorthodox perhaps, but it's a hell of a lot easier to follow.

Hold onto Your Butts Because the MODX manager controllers add an extra layer of complexity and they are not well documented, I'm going to skip them for this demonstration.

Ajax Controller

Your Ajax controllers need to be accessible via HTTP, so the typical location for them would be inside the assets/ folder, more specifically, inside assets/components/<your_pkg>/ somewhere.

<?php
/**
 * Controller for Ajax requests.
 */
// Adjust the path appropriately
$docroot = dirname(dirname(dirname(dirname(__FILE__))));
include $docroot . '/config.core.php';
if (!defined('MODX_API_MODE')) {
    define('MODX_API_MODE', false);
}
include_once MODX_CORE_PATH . 'model/modx/modx.class.php';

$modx = new modX();
$modx->initialize('mgr');

if (!$modx->hasPermission('view_document')) {
    header('HTTP/1.0 401 Unauthorized');
    print 'Operation not allowed.';
    exit;
}
// These are the standard values that are posted to the Ajax URL by Ext JS
$start = (int) $modx->getOption('start',$_POST,0);
$limit = (int) $modx->getOption('limit',$_POST,20);
$sort = $modx->getOption('sort',$_POST);
$dir = $modx->getOption('dir',$_POST,'ASC');

// error_log(print_r($_POST,true));  // <--- uncomment this for debugging

$c = $modx->newQuery('modResource');
$count = $modx->getCount('modResource',$c);
$c->sortby($sort,$dir);
$c->limit($limit,$start);

//$c->prepare(); error_log($c->toSQL()); // <-- uncomment for debugging

$pages = $modx->getCollection('modResource',$c);

$list = array();
foreach ($pages as $p) {
    $array = $p->toArray();
    $list[] = $array;
}

// The format of the output is not well documented, but it requires a node for "total" and "rows"
print '{"total":"'.$count.'","results":'.$modx->toJSON($list).',"success":true,"msg":"Got our rows..."}';

//error_log(print_r($list,true));  // <-- another debugging point

@session_write_close();
exit();

Do you see that? After we instantiate MODX and check permissions, it's essentially the same thing you might see inside of a Snippet. Doing things this way has several important advantages:

Advantages of Simple Controllers

  1. You can debug output by visiting the page directly, e.g. http://yoursite.com/assets/components/your_pkg/my_ajax_controller.php – you should see JSON data or maybe some PHP errors.
  2. Permission Checking is simple, just update the $modx->hasPermission('permission_key_here')
  3. No extra documentation or complexity required

The disadvantages are that this isn't the "official" way of doing it (not sure where the "official" way is documented), so you cannot avail yourself of the manager's routing, and if you wanted to override custom controller behavior via a MODX manager theme, you can't because this stuff gets hard-coded. Seeing as this way is much simpler and the other way is not documented much, I think this is a worthy trade-off.

Your CMP

The following code you can paste into your core/components/<your_pkg_name/ directory and reference it as an action when you create your CMP.

<?php
/**
 * Generic MODX CMP
 */
$url = MODX_ASSETS_URL.'components/your_pkg_name/'; // <-- update this
//------------------------------------------------------------------------------
//!Grid
//------------------------------------------------------------------------------
$modx->regClientStartupHTMLBlock("<script>
function myactions(val) {
    return '<a href=\"index.php?a=30&id='+val+'\">Edit</a>';
}


Ext.onReady(function(){

    // create the Data Store
    var store = new Ext.data.JsonStore({
        root: 'results',
        totalProperty: 'total',
        idProperty: 'id',
        remoteSort: true,

        fields: [
            'id',
            'createdon',
            'pagetitle',
            'action'
        ],

        // load using script tags for cross domain, if the data in on the same domain as
        // this page, an HttpProxy would be better
        proxy: new Ext.data.HttpProxy({
            url: '{$url}getpages.php'  // <------- set this to point to your Ajax Controller
        })
    });
    store.setDefaultSort('id', 'ASC');


    var grid = new Ext.grid.GridPanel({
        id: 'articlesGrid',
        width:700,
        height:500,
        store: store,
        trackMouseOver:true,  // will highlight rows on hover
        disableSelection:true, // will allow you to select row(s)
        loadMask: true,  // will generate a spinner icon

        // grid columns
        columns:[{
            header: 'Date',
            dataIndex: 'createdon',
            width: 150,
            sortable: true
        },{
            header: 'Page Title',
            dataIndex: 'pagetitle',
            width: 350,
            sortable: true
        },{
            header: '',
            dataIndex: 'id',
            width: 100,
            sortable: false,
            renderer : myactions,
        }],

        // paging bar on the bottom
        bbar: new Ext.PagingToolbar({
            pageSize: 25,
            store: store,
            displayInfo: true,
            displayMsg: 'Displaying Records {0} - {1} of {2}',
            emptyMsg: 'No Records to display'
        })
    });

    // render it
    grid.render('articles-grid');

    // trigger the data store load
    // NOTE: the parameter names here correspond to keys in _POST
    store.load({params:{start:0, limit:25}});
});
</script>");

// Be sure to print an HTML div that is ref'd by the grid.render() method
return '
<h2>Example</h2>
<div id="articles-grid"></div>';

You'll notice that we're printing our Javascript directly into the document head. Yes, this means we have to be extra careful about our quoting styles and our editor's syntax highlighting probably go out the window, but it does mean we can print a few PHP variables directly into the Javascript and we don't have to keep a half-dozen files open just to bootstrap this thing.

Normally having PHP generate Javascript is not a great idea – it's something that's usually frowned upon, but it is required at certain integration points. The "more correct" way of doing this is to print some configuration details as Javascript objects, and then reference the Javascript variables in your code. We'll demonstrate that later – for now, just try to wrap your head around what's going on here in the code.

Support the team building MODX with a monthly donation.

The budget raised through OpenCollective is transparent, including payouts, and any contributor can apply to be paid for their work on MODX.

Backers

  • modmore
  • STERC
  • Jens Wittmann – Gestaltung & Entwicklung
  • Fabian Christen
  • Digital Penguin
  • Dannevang Digital
  • Sepia River Studios
  • CrewMark
  • Chris Fickling
  • deJaya
  • Following Sea
  • Anton Tarasov
  • eydolan
  • Raffy
  • Lefthandmedia
  • Murray Wood
  • Snow Creative
  • Nick Clark
  • Helen
  • JT Skaggs
  • krisznet
  • YJ
  • Yanni
  • Richard

Budget

$366 per month—let's make that $500!

Learn more