... Next, we'll need to create our Actions and Menus for the CMP. What is an "Action" in MODX? Well, it's basically an abstract representation of a Manager page. Each MODX manager page has an Action in the modx_actions table, which can be referenced. This allows you to create any number of "Actions" that can be used as CMPs in the manager. Load up the Actions page on System \-> Actions. This should load 2 trees: !doodles-actions.png! We'll be first focused on the left-hand tree, which is the tree of Actions. The first level of the tree is all our Namespaces, which show as little computer icons. Below that are all the Actions for that Namespace. Our 'doodles' Namespace shows up there, but it's empty and has no Actions inside it. Let's remedy that. Right-click on the 'doodles' Namespace icon, and click 'Create Action Here'. That should load a window, in which we'll want to input these values (really only the Namespace and Controller fields matter): * *Controller* \- index * *Namespace* \- doodles * *Parent Controller* \- _(select "no action")_ Let's explain what each of these fields mean. *Controller*: This tells MODX where the controller file is at for this Action, relative to the Namespace path, without the .php extension. Our file is going to be at /www/doodles/core/components/doodles/index.class.php. So, minus our Namespace path, that ends up being index.class.php. We drop the .class.php, and we get "index". *Namespace*: The reference to the Namespace this Action is a part of. This should have automatically been filled in for you. *Parent Controller*: For hierarchical display, Actions can be structured in tree form. This doesn't affect their behavior at all, and doesn't really concern us, so we'll select No Action. Great, we've now got ourselves an Action. Now we need to tie that Action to a Menu item, which will show up in our main menu at the top of the manager. 'Menu' objects in MODX allow you to completely rearrange (and hide) menu items in the manager interface, enabling you to completely customize navigation for your MODX installation. We're going to want to create our Doodles menu item under the 'Components' menu, which is where 3rd Party Extras usually go. We could place it anywhere, but for standards-sake, let's place it under Components. Right-click on the 'Components' tree node in the right-hand tree, and click 'Place Action Here'. That will load a window, which we can fill with these values: * *Lexicon Key* \- doodles * *Description* \- doodles.desc * *Action* \- doodles - index * *Icon* \- * *Parameters* \- * *Handler* \- * *Permissions* \- And let's explain each field: *Lexicon Key*: This is the lexicon key for the menu item. Since MODX allows you to view the manager in a multiple number of languages, MODX provides us the option to load a Lexicon string (in our lexicon topic we said to load for the action earlier, _doodles:default_) to translate this with. We'll put _doodles_ and provide that as a Lexicon string later. *Description*: Similar to the first field, this allows us to provide either a straight up description, or a lexicon key to be translated. We'll provide a lexicon key, because we want that to be translated. *Action* \- This tells MODX what action to load when the Menu item is clicked. *Icon* \- Currently not used in the default manager interface, this allows menu items to have an icon. We'll skip it. *Parameters* \- This allows you to attach GET parameters to the menu item when clicked. We don't need this, so we'll skip it. *Handler* \- This allows you to run JavaScript instead of loading a page when running a menu item. It's useful for menu items that don't actually load a page but do things, such as the "Clear Cache" menu item under the Site menu. We'll skip this. *Permissions* \- Here you can specify a MODX Permission that MODX will check to see if the user has when loading the menu. If the user doesn't have this Permission, this menu item won't load. We don't want to restrict our CMP, so we'll leave this blank. Great\! We've got an Action and Menu now. Let's go ahead and create our default Lexicon Topic. h3. Lexicons [Lexicons|Internationalization] in MODX Revolution allow you to provide MODX with translations for your Extra (and anything, really) in any language. We want our Extra to be i18n-compatible, so we want to utilize this feature. Each string (also called an Entry) has its own key, such as 'doodles.desc' shown above. The common practice for Lexicon keys for Extras is to prefix them with the Namespace path and a dot. This prevents collisions with other Extras. Lexicon strings are collected in files called 'Lexicon Topics'. This means your strings can be isolated by a specific area (similar to how the core/lexicon/ directory does it), and makes it so you don't have to load _all_ the strings for your Extra when you may only want to load a few. If you wanted to use your Lexicons in a Snippet, you could use $modx->lexicon->load('doodles:default'). This would load the 'default' topic from the 'doodles' Namespace. For CMPs, however, this is a little different; you load it in the Controller class via the getLanguageTopics method. This method expects an array to return that will specify the Lexicon Topics to load so we can easily access them. But we haven't actually _made_ that Lexicon Topic file, so let's go do this now. Lexicons on the filesystem are structured thus: {quote}\{namespace_path\}/lexicon/\{language\}/\{topic\}.inc.php{quote} So we'll go ahead and create our file here: /www/doodles/core/components/doodles/lexicon/en/default.inc.php and fill it with this: {code}<?php $_lang['doodle'] = 'Doodle'; $_lang['doodles'] = 'Doodles'; $_lang['doodles.desc'] = 'Manage your doodles here.'; $_lang['doodles.description'] = 'Description'; $_lang['doodles.doodle_err_ae'] = 'A doodle with that name already exists.'; $_lang['doodles.doodle_err_nf'] = 'Doodle not found.'; $_lang['doodles.doodle_err_ns'] = 'Doodle not specified.'; $_lang['doodles.doodle_err_ns_name'] = 'Please specify a name for the doodle.'; $_lang['doodles.doodle_err_remove'] = 'An error occurred while trying to remove the doodle.'; $_lang['doodles.doodle_err_save'] = 'An error occurred while trying to save the doodle.'; $_lang['doodles.doodle_create'] = 'Create New Doodle'; $_lang['doodles.doodle_remove'] = 'Remove Doodle'; $_lang['doodles.doodle_remove_confirm'] = 'Are you sure you want to remove this doodle?'; $_lang['doodles.doodle_update'] = 'Update Doodle'; $_lang['doodles.downloads'] = 'Downloads'; $_lang['doodles.location'] = 'Location'; $_lang['doodles.management'] = 'Doodles Management'; $_lang['doodles.management_desc'] = 'Manage your doodles here. You can edit them by either double-clicking on the grid or right-clicking on the respective row.'; $_lang['doodles.name'] = 'Name'; $_lang['doodles.search...'] = 'Search...'; $_lang['doodles.top_downloaded'] = 'Top Downloaded Doodles';{code} There's quite a few strings in there\! We'll use them, don't worry. Note that all we're doing is filling a PHP array called $_lang. That's it; MODX will do the rest. You can also see our 'doodles' and 'doodles.desc' strings we referenced earlier in here. Great\! We're all setup to start developing our CMP. h2. Setting up the Controllers with MODExt CMPs in MODX are generated with [ExtJS|http://sencha.com/], a JavaScript framework by Sencha that allows for rapid and powerful UI development. MODX adds functionality to a few of the ExtJS tools (and calls them MODExt). We're going to use those tools in our CMP. This tutorial is not meant to teach you ExtJS, as there are plenty of tutorials on that on the web, and on the [Sencha main site|http://sencha.com/]. But we will go over how to use them to create a neat grid that can do CRUD actions. We're going to need to setup some basic controllers first before we can proceed with development. h3. The Base Controller Let's create our controller at: /www/doodles/core/components/doodles/index.class.php. And put this in it: {code} <?php require_once dirname(__FILE__) . '/model/doodles/doodles.class.php'; abstract class DoodlesManagerController extends modExtraManagerController { /** @var Doodles $doodles */ public $doodles; public function initialize() { $this->doodles = new Doodles($this->modx); $this->addCss($this->doodles->config['cssUrl'].'mgr.css'); $this->addJavascript($this->doodles->config['jsUrl'].'mgr/doodles.js'); $this->addHtml('<script type="text/javascript"> Ext.onReady(function() { Doodles.config = '.$this->modx->toJSON($this->doodles->config).'; }); </script>'); return parent::initialize(); } public function getLanguageTopics() { return array('doodles:default'); } public function checkPermissions() { return true;} } class IndexManagerController extends DoodlesManagerController { public static function getDefaultController() { return 'home'; } } {code} A bit of explanation here. What we're doing is creating an abstract base Controller Class (DoodlesManagerController) for our Extra that extends modExtraManagerController, a special class for developing Extras. MODX 2.2 does request routing via Controller classes, which are all sorts of powerful. But in our Controllers for our Extra, we want to make sure to always append some CSS/JS (similar to MODX 2.1 and earlier's header.php file), and also give our Controllers access to the Doodles class object. So, we do that in the initialize() method, which is called when a controller is loaded. Secondly, we define the getLanguageTopics() method to tell MODX to load our lexicon file for the manager. Finally, we define checkPermissions(), which basically says if it doesn't return true, deny access to this controller page. So that's our abstract class. But we have to define an actual class to use it, so we made "IndexManagerController", which since our action is "index" (remember what we put in the Action dialog above?) MODX is going to look for (nameOfAction)ManagerController: so, ergo, IndexManagerController. We then tell MODX via the fancy "getDefaultController()" static method that we actually want our home controller to be "home". We'll make that controller file here soon. Note here we're also loading a common JS file, _mgr/doodles.js_, in our JS directory. Then it runs a JS method when ExtJS has loaded that loads the config vars for our $doodles->config in the 'Doodles.config' JS object (which we'll use for paths and such). In our doodles.js file (which is found at /www/doodles/assets/components/doodles/js/mgr/doodles.js), we have this: {code}var Doodles = function(config) { config = config || {}; Doodles.superclass.constructor.call(this,config); }; Ext.extend(Doodles,Ext.Component,{ page:{},window:{},grid:{},tree:{},panel:{},combo:{},config: {} }); Ext.reg('doodles',Doodles); Doodles = new Doodles();{code} So, basically, we're loading a Doodles object which extends the Ext.Component class. This also gives us a nice JavaScript namespace of 'Doodles'. We're done with the header stuff. Let's start on our home Controller. h2. Our Doodles CMP Page Create a file at /www/doodles/core/components/doodles/controllers/home.class.php (remember in our index.class.php base controller, we had the default controller set to "home"?) and fill it with this: {code} <?php class DoodlesHomeManagerController extends DoodlesManagerController { public function process(array $scriptProperties = array()) { } public function getPageTitle() { return $this->modx->lexicon('doodles'); } public function loadCustomCssJs() { //$this->addJavascript($this->doodles->config['jsUrl'].'mgr/widgets/doodles.grid.js'); $this->addJavascript($this->doodles->config['jsUrl'].'mgr/widgets/home.panel.js'); $this->addLastJavascript($this->doodles->config['jsUrl'].'mgr/sections/index.js'); } public function getTemplateFile() { return $this->doodles->config['templatesPath'].'home.tpl'; } }{code} Then create your template file at /www/doodles/core/components/doodles/templates/home.tpl and fill it with this: {code}<div id="doodles-panel-home-div"></div>{code} Great\! So we're doing a couple things here. We define the process() method, which is necessary to be defined for each manager controller. We're not using it for anything, so we're going to leave it empty. Next we tell MODX what we want the page title to be vai the getPageTitle() method. We'll set it to our translated version of "Doodles". We then define the loadCustomCssJs() method, which allows us to register whatever specific CSS/JS for this specific page we want. We're loading a few 'widgets', and then loading a 'section'. These terms are arbitrary, but we're using them here in the same way MODX uses them in MODExt to render the manager interface. Basically, a "widget" is something like a grid of objects (such as Doodles), or a tree, or a specialized panel. Putting them in separate files allows you to use them in different pages without having to duplicate code. A "section" is a piece of JS that actually _loads_ the widgets onto a page. Including a widget won't load and render it - a section will render it. We're going to load first the doodles.grid.js, which is a widget that displays a grid of Doodles. Secondly, we load the 'home' panel, which is our home page's main panel, that the grid will reside in. And finally, we load the 'index' section, which renders the UI. We're going to leave the grid commented out for now, but we'll come back to it. Promise. {note}We could have put all these JS files in one file, which would have loaded the page faster. For illustration purposes, we put them in 3 separate files, to make explaining this tutorial easier. Feel free to do whatever you want when developing your CMP.{note} Finally, we tell MODX where to find the Template file for this Controller. This is a Smarty template that MODX will use when rendering the controller. h3. The Section JS File Let's first create the index.js file, at /www/doodles/assets/components/doodles/js/mgr/sections/index.js: {code}Ext.onReady(function() { MODx.load({ xtype: 'doodles-page-home'}); }); Doodles.page.Home = function(config) { config = config || {}; Ext.applyIf(config,{ components: [{ xtype: 'doodles-panel-home' ,renderTo: 'doodles-panel-home-div' }] }); Doodles.page.Home.superclass.constructor.call(this,config); }; Ext.extend(Doodles.page.Home,MODx.Component); Ext.reg('doodles-page-home',Doodles.page.Home);{code} Okay, let's explain. The first thing that happens is that we tell ExtJS, when the page is nice and loaded, "load" the component (or widget/object/panel) with 'xtype' _doodles-page-home_. How ExtJS works is that it allows you to define components with an 'xtype', which is kind of like a unique identifier for a panel, tree, etc. Think of it like an ID for a class. MODx.load simply instantiates that object. Below that, we actually define the 'doodles-page-home' object, and make it extend MODx.Component. MODx.Component is basically an abstracted JS class that renders a page in the MODX manager interface. It provides a few helper methods that make quick generation of MODX pages smoother. All we have to pass into it is the components we want to load; currently, in this case, the 'doodles-panel-home' component (which we haven't defined yet; it'll be in the home.panel.js file mentioned earlier). We also want it to render to the DOM ID of 'doodles-panel-home-div', which, as you might remember, was the "div" we returned earlier in our mgr/index.php controller. Finally, we register this page to the 'doodles-page-home' xtype, which we are referencing in the MODx.load call earlier. Great\! On to the panel. h3. The Panel JS File We've got our page, but now we want to load a panel in it. Let's create a file at www/doodles/assets/components/doodles/js/mgr/widgets/home.panel.js and put this in it: {code}Doodles.panel.Home = function(config) { config = config || {}; Ext.apply(config,{ border: false ,baseCls: 'modx-formpanel' ,cls: 'container' ,items: [{ html: '<h2>'+_('doodles.management')+'</h2>' ,border: false ,cls: 'modx-page-header' },{ xtype: 'modx-tabs' ,defaults: { border: false ,autoHeight: true } ,border: true ,items: [{ title: _('doodles') ,defaults: { autoHeight: true } ,items: [{ html: '<p>'+_('doodles.management_desc')+'</p>' ,border: false ,bodyCssClass: 'panel-desc' }/*,{ xtype: 'doodles-grid-doodles' ,cls: 'main-wrapper' ,preventRender: true }*/] }] }] }); Doodles.panel.Home.superclass.constructor.call(this,config); }; Ext.extend(Doodles.panel.Home,MODx.Panel); Ext.reg('doodles-panel-home',Doodles.panel.Home); {code} So, first, at the bottom, note how we're registering this panel to 'doodles-panel-home', which we referenced in our section. Also note that this panel extends MODx.Panel, which in turn extends Ext.Panel. Why not just extend Ext.Panel? Well, extending MODx.Panel does the same, and adds a CSS class to the panel to give it the nice manager MODX styling. We're going to give this panel a baseCls of 'modx-formpanel', which lets our top part have a transparent background. And we want a class of 'container', which handles spacing. Then, we'll make sure it doesn't have a border. Next, we'll define the 'items' in the panel. First, we add a header: {code}{ html: '<h2>'+_('doodles.management')+'</h2>' ,border: false ,bodyCssClass: 'panel-desc' }{code} Basically this just inserts some HTML into the top of the panel with a class of 'modx-page-header', and puts a nice h2 tag up there. Note the \_() method. This is MODX's way of doing i18n (Lexicons) in the manager JS. This tells MODX to translate this key. If you remember, we defined the 'doodles.management' string earlier with: "Doodles Management". So this will render the translation of this key in the h2 tag. Next, we'll add a TabPanel. We could just load the panel straight without tabs, but what if down the line we wanted to add another tab? Let's define it: {code},{ xtype: 'modx-tabs' ,defaults: { border: false ,autoHeight: true } ,border: true ,items: /* ... */ }{code} Note we load our tabpanel with the xtype 'modx-tabs'. This loads a MODX-specific tabpanel, which has some MODX-specific configuration options. Then we give it some padding, a border, and make sure the defaults for its tabs have no border and an automatic height. Then, we add the tab itself: {code} { title: _('doodles') ,defaults: { autoHeight: true } ,items: [{ html: '<p>'+_('doodles.management_desc')+'</p><br />' ,border: false ,bodyCssClass: 'panel-desc' }] }{code} Okay, this is going to load our first tab with a tab title translated to 'Doodles'. Then, we'll put some stuff in the tab (which is an Ext.Panel, by the way). We'll first put a nice little description with our "doodles.management_desc" lexicon string. Let's load the page and take a look now. You may need to refresh the Manager page to get the Doodles component loaded into the Components menu. !doodles-panel1.png! Cool\! We've got a MODX-styled panel going. Unfortunately, it's pretty useless. We need to add a grid to manage our Doodles. Let's go ahead and do that now. h2. The Doodles Grid First off, go ahead and uncomment this line in your home.class.php controller: {code}$this->addJavascript($doodles->config['jsUrl'].'mgr/widgets/doodles.grid.js');{code} This tells MODX to load the grid widget file, which we'll now create at /www/doodles/assets/components/doodles/js/mgr/widgets/doodles.grid.js: {code}Doodles.grid.Doodles = function(config) { config = config || {}; Ext.applyIf(config,{ id: 'doodles-grid-doodles' ,url: Doodles.config.connectorUrl ,baseParams: { action: 'mgr/doodle/getList' } ,fields: ['id','name','description','menu'] ,paging: true ,remoteSort: true ,anchor: '97%' ,autoExpandColumn: 'name' ,columns: [{ header: _('id') ,dataIndex: 'id' ,sortable: true ,width: 60 },{ header: _('doodles.name') ,dataIndex: 'name' ,sortable: true ,width: 100 ,editor: { xtype: 'textfield' } },{ header: _('doodles.description') ,dataIndex: 'description' ,sortable: false ,width: 350 ,editor: { xtype: 'textfield' } }] }); Doodles.grid.Doodles.superclass.constructor.call(this,config) }; Ext.extend(Doodles.grid.Doodles,MODx.grid.Grid); Ext.reg('doodles-grid-doodles',Doodles.grid.Doodles);{code} Whew, a lot in there\! Let's start off with the configuration parameters we're setting. * *id*: We give this panel an ID of 'doodles-grid-doodles'. * *url*: We point it to the connector file at Doodles.config.connectorUrl (we'll get to connectors here in a second). * *baseParams*: We setup it's base parameters to send when getting records for the grid via REQUEST with a key of 'action' and a value of 'mgr/doodle/getList'. More on this in a second. * *fields*: We setup the fields we'll get from the AJAX request to populate the grid. Basically, these are the fields of our Doodle. * *paging*: We want pagination for our grid, so MODExt handles all of this just by setting 'paging: true' here. * *remoteSort*: We set this to true, and Ext will allow our grid columns to be sortable. * *anchor*: We want this grid to stretch the panel width, so we set it to 97% (3% less to account for padding). * *autoExpandColumn*: We want to stretch the 'name' column to dynamically be the biggest column on the grid. Then, we define some columns for our grid. We also allow 'name' and 'description' to be editable by attaching an editor to each column. More on this later. Note how the 'dataIndex' parameter matches the name of the Doodles field we want to display. Finally, let's add the grid to our panel. Remove the comment tags in the home.panel.js file at lines 22 and 26 : {code} [{ html: '<p>'+_('doodles.management_desc')+'</p>' ,border: false },{ xtype: 'doodles-grid-doodles' ,cls: 'main-wrapper' ,preventRender: true }]{code} That loads our grid right below the message we posted earlier in our panel, with some nice spacing via the class. The preventRender attribute tells Ext not to render the grid until the rest of the panel loads. If you tried to load the page now, the grid would show, but not load any data - we haven't made our Connector yet, and so the grid doesn't have anywhere to fetch its data. Let's do that. h3. Hooking Up via Connectors What is a Connector in MODX? A Connector is, technically, a file that 'connects' to the model layer of MODX, or the Processors. Processors are form-layer files that run DB queries and other things that modify the model and/or database. In laymen's terms, Processors are where you will do all your database modifying. Connectors are a 'gateway' to these processors. They restrict access, check access permissions, and 'route' requests to the appropriate processor. They also limit the access points to your model, further securing your app. Think of your model as a fortress, your DB as the palace in the center, the processors the roads in that fortress, and connectors as the gates in the walls around your fortress. You want those gates to be secure, and limited in number. Back to our Extra. Our ExtJS grid needs to load its data for its rows via AJAX by our connector. But we need to *create* our connector first. Let's make it at /www/doodles/assets/components/doodles/connector.php: {code} <?php require_once dirname(dirname(dirname(dirname(__FILE__)))).'/config.core.php'; require_once MODX_CORE_PATH.'config/'.MODX_CONFIG_KEY.'.inc.php'; require_once MODX_CONNECTORS_PATH.'index.php'; $corePath = $modx->getOption('doodles.core_path',null,$modx->getOption('core_path').'components/doodles/'); require_once $corePath.'model/doodles/doodles.class.php'; $modx->doodles = new Doodles($modx); $modx->lexicon->load('doodles:default'); /* handle request */ $path = $modx->getOption('processorsPath',$modx->doodles->config,$corePath.'processors/'); $modx->request->handleRequest(array( 'processors_path' => $path, 'location' => '', )); {code} That's it. We first load the config.core.php file. We'll go ahead and add it here in our development environment; in standard MODX installs, this will already exist. Create a file at /www/doodles/config.core.php and put this in it: {code}<?php define('MODX_CORE_PATH', '/www/modx/core/'); define('MODX_CONFIG_KEY', 'config'); {code} Obviously, you'll need to change those values to your MODx installation paths. And if you're using SVN or Git for your Extra, you'll want to add those to your ignore file (ie, .gitignore), since you don't want those in your source repository. Next in our connector, we load the config file, and the MODX connectors/index.php file. Then, we load our Doodles class (with our magic system settings\!), which will add our xPDO custom Doodles model into MODX, and then load our default doodles Lexicon Topic. Finally, we 'handle' the request using our custom Processors path we defined in our Doodles class, and tell MODX to load the processors. This file will do nothing on its own when access. Loading it directly will give you this: {quote}\{"success":false,"message":"Access denied.","total":0,"data":\[\],"object":\[\]\}{quote} There's a few reasons for this. One is that the connectors are locked down and don't allow anyone without a MODX manager session to access them. Secondly, all requests to connectors *must* pass a unique-to-your-site authorization key that prevents CRSF attacks. It can either be passed in the HTTP headers as 'modAuth', or in a REQUEST var as HTTP_MODAUTH. The value will be $modx->siteId, which is set on a new install, and loaded when MODX is loaded. {warning}Don't ever paste or share with anyone your $modx->siteId or HTTP_MODAUTH key. It keeps your site secure.{warning} The great thing, though, is you won't have to worry about this. MODX already handles this in MODExt - all HTTP requests made by ExtJS in MODX pass this variable in via their HTTP headers. The second reason loading the connector file directly won't work is that we didn't specify a routing path - remember baseParams in the grid? Remember how we set the 'action' param in it to 'mgr/doodle/getList'? That's our routing path. That tells the connector to load the file at: {quote}/www/doodles/core/components/doodles/processors/mgr/doodle/getlist.class.php{quote} So let's go ahead and make that file to give our grid some data: {code}<?php class DoodleGetListProcessor extends modObjectGetListProcessor { public $classKey = 'Doodle'; public $languageTopics = array('doodles:default'); public $defaultSortField = 'name'; public $defaultSortDirection = 'ASC'; public $objectType = 'doodles.doodle'; } return 'DoodleGetListProcessor';{code} Great. So a few things. You'll note that we're in a class again - MODX 2.2 has new shiny Processor classes, including an assistance class named modObjectGetListProcessor that we're extending here. This class automatically does all the basic logic for handling normal CRUD processor actions, such as this one. All we have to do is specify some class variables on the class - such as $classKey, $objectType, and more. Let's dig into those: * *$classKey* \- This tells the Processor what MODX Class to grab. We want to grab our Doodle objects. * *$languageTopics* \- An array of language topics to load for this processor. * *$defaultSortField* \- The default sort field to use when grabbing the data. * *$defaultSortDirection* \- The default sort direction to do when grabbing the data. * *$objectType* \- This is often used to determine what error lexicon strings to load when grabbing data. Since in our lexicon file we have all the strings as $_lang['doodles.doodle_blahblah'|'doodles.doodle_blahblah'] and such, we'll specify the prefix here of "doodles.doodle". MODX then will prefix standard error messages with that prefix. The assistance class handles the rest, so we don't have to worry about it\! All we have to do is "return" the name of the Processor class so MODX knows where to find it. That's it. Now let's load up our grid: !doodles-grid1.png! Great\! We've got a working grid. Now, let's add some functionality to it, since right now all it does is list Doodles.
|