1. Getting Started
      1. Video Quick-Start Series
      2. Server Requirements
        1. MySQL 5.0.51 Issues
      3. Installation
        1. Basic Installation
          1. MODx Revolution on Debian
          2. Problems with WAMPServer 2.0i
          3. Lighttpd Guide
          4. Installation on a server running ModSecurity
          5. MODX and Suhosin
          6. Nginx Server Config
        2. Successful Installation, Now What Do I Do?
        3. Successful Installation, Now What Do I Do?
        4. Advanced Installation
        5. Git Installation
        6. Command Line Installation
          1. The Setup Config Xml File
        7. Troubleshooting Installation
        8. Using MODx Revolution from SVN
      4. An Overview of MODX
        1. Glossary of Revolution Terms
          1. Explanation of Directory Structure
        2. Roadmap
        3. MODX Revolution Framework Structure Ideology
        4. What's New in 2.3
    2. FAQs & Troubleshooting
      1. CMP Development FAQs & Troubleshooting
    3. Making Sites with MODX
      1. Structuring Your Site
        1. Resources
          1. Content Types
          2. Named Anchor
          3. Static Resource
          4. Symlink
            1. Using Resource Symlinks
          5. Weblink
        2. Templates
        3. Chunks
        4. Using Snippets
      2. Tag Syntax
      3. Customizing Content
        1. Template Variables
          1. Creating a Template Variable
          2. Adding a Custom TV Type - MODX 2.2
          3. Bindings
            1. CHUNK Binding
            2. DIRECTORY Binding
            3. EVAL Binding
            4. FILE Binding
            5. INHERIT Binding
            6. RESOURCE Binding
            7. SELECT Binding
          4. Template Variable Input Types
          5. Template Variable Output Types
            1. Date TV Output Type
            2. Delimiter TV Output Type
            3. HTML Tag TV Output Type
            4. Image TV Output Type
            5. URL TV Output Type
          6. Adding a Custom TV Input Type
          7. Adding a Custom TV Output Type
          8. Creating a multi-select box for related pages in your template
          9. Accessing Template Variable Values via the API
        2. Properties and Property Sets
        3. Input and Output Filters (Output Modifiers)
          1. Custom Output Filter Examples
      4. Commonly Used Template Tags
        1. Date Formats
    4. Administering Your Site
      1. Settings
        1. System Settings
          1. access_category_enabled
          2. date_timezone
          3. access_context_enabled
          4. access_resource_group_enabled
          5. allow_duplicate_alias
          6. allow_forward_across_contexts
          7. allow_multiple_emails
          8. allow_tags_in_post
          9. archive_with
          10. automatic_alias
          11. auto_check_pkg_updates
          12. auto_check_pkg_updates_cache_expire
          13. auto_menuindex
          14. base_help_url
          15. blocked_minutes
          16. cache_action_map
          17. cache_context_settings
          18. cache_db
          19. cache_db_expires
          20. cache_db_session
          21. cache_default
          22. cache_disabled
          23. cache_format
          24. cache_handler
          25. cache_json
          26. cache_json_expires
          27. cache_lang_js
          28. cache_lexicon_topics
          29. cache_noncore_lexicon_topics
          30. cache_resource
          31. cache_resource_expires
          32. cache_scripts
          33. cache_system_settings
          34. clear_cache_refresh_trees
          35. compress_css
          36. compress_js
          37. concat_js
          38. container_suffix
          39. cultureKey
          40. custom_resource_classes
          41. default_per_page
          42. default_template
          43. editor_css_path
          44. editor_css_selectors
          45. emailsender
          46. emailsubject
          47. enable_dragdrop
          48. error_page
          49. extension_packages
          50. failed_login_attempts
          51. feed_modx_news
          52. feed_modx_news_enabled
          53. feed_modx_security
          54. feed_modx_security_enabled
          55. fe_editor_lang
          56. filemanager_path
          57. filemanager_path_relative
          58. filemanager_url
          59. filemanager_url_relative
          60. forgot_login_email
          61. friendly_alias_lowercase_only
          62. forward_merge_excludes
          63. friendly_alias_max_length
          64. friendly_alias_restrict_chars
          65. friendly_alias_restrict_chars_pattern
          66. friendly_alias_strip_element_tags
          67. friendly_alias_translit
          68. friendly_alias_translit_class
          69. friendly_alias_translit_class_path
          70. friendly_alias_trim_chars
          71. friendly_alias_urls
          72. friendly_alias_word_delimiter
          73. friendly_alias_word_delimiters
          74. friendly_urls
          75. friendly_url_prefix
          76. friendly_url_suffix
          77. global_duplicate_uri_check
          78. hidemenu_default
          79. link_tag_scheme
          80. mail_charset
          81. mail_encoding
          82. mail_smtp_auth
          83. mail_smtp_helo
          84. mail_smtp_hosts
          85. mail_smtp_keepalive
          86. mail_smtp_pass
          87. mail_smtp_port
          88. mail_smtp_prefix
          89. mail_smtp_single_to
          90. mail_smtp_timeout
          91. mail_smtp_user
          92. mail_use_smtp
          93. manager_date_format
          94. manager_direction
          95. manager_favicon_url
          96. manager_language
          97. manager_lang_attribute
          98. manager_theme
          99. manager_time_format
          100. context_tree_sort
          101. context_tree_sortby
          102. context_tree_sortdir
          103. session_enabled
          104. upload_files
          105. modx_charset
          106. new_file_permissions
          107. new_folder_permissions
          108. password_generated_length
          109. password_min_length
          110. phpthumb_allow_src_above_docroot
          111. phpthumb_cache_maxage
          112. phpthumb_cache_maxfiles
          113. phpthumb_cache_maxsize
          114. phpthumb_cache_source_enabled
          115. phpthumb_document_root
          116. phpthumb_error_bgcolor
          117. phpthumb_error_fontsize
          118. phpthumb_error_textcolor
          119. phpthumb_far
          120. phpthumb_imagemagick_path
          121. phpthumb_nohotlink_enabled
          122. phpthumb_nohotlink_erase_image
          123. phpthumb_nohotlink_text_message
          124. phpthumb_nohotlink_valid_domains
          125. phpthumb_nooffsitelink_enabled
          126. phpthumb_nooffsitelink_erase_image
          127. phpthumb_nooffsitelink_require_refer
          128. phpthumb_nooffsitelink_text_message
          129. phpthumb_nooffsitelink_valid_domains
          130. phpthumb_nooffsitelink_watermark_src
          131. phpthumb_zoomcrop
          132. principal_targets
          133. proxy_auth_type
          134. proxy_host
          135. proxy_password
          136. proxy_port
          137. proxy_username
          138. publish_default
          139. rb_base_dir
          140. rb_base_url
          141. request_controller
          142. request_param_alias
          143. request_param_id
          144. resource_tree_node_name
          145. resource_tree_node_tooltip
          146. richtext_default
          147. search_default
          148. server_offset_time
          149. server_protocol
          150. session_cookie_domain
          151. session_cookie_lifetime
          152. session_cookie_path
          153. session_cookie_secure
          154. session_handler_class
          155. session_name
          156. settings_version
          157. signupemail_message
          158. site_name
          159. site_start
          160. site_status
          161. site_unavailable_message
          162. site_unavailable_page
          163. strip_image_paths
          164. symlink_merge_fields
          165. tree_default_sort
          166. tree_root_id
          167. tvs_below_content
          168. udperms_allowroot
          169. ui_debug_mode
          170. unauthorized_page
          171. upload_maxsize
          172. use_alias_path
          173. use_browser
          174. use_editor
          175. use_multibyte
          176. welcome_screen
          177. which_editor
          178. which_element_editor
          179. xhtml_urls
      2. Using Friendly URLs
      3. Contexts
        1. Creating a Subdomain from a Folder using Virtual Hosts
        2. Using One Gateway Plugin to Manage Multiple Domains
      4. Customizing the Manager
        1. Customizing the Manager via Plugins
        2. Form Customization Profiles
        3. Form Customization Sets
          1. Customizing Tabs via Form Customization
          2. MODX GitHub Contributor's Guide
        4. Manager Templates and Themes
      5. MODX GitHub Integrator's Guide
      6. Security
        1. Hardening MODX Revolution
        2. Policies
          1. ACLs
          2. Permissions
            1. Permissions - Administrator Policy
            2. Permissions - Resource Policy
          3. PolicyTemplates
        3. Resource Groups
        4. Roles
        5. Security Standards
        6. Security Tutorials
          1. More on the Anonymous User Group
          2. Creating a Second Super Admin User
          3. Giving a User Manager Access
          4. Making Member-Only Pages
          5. Restricting an Element from Users
        7. Troubleshooting Security
          1. Resetting a User Password Manually
        8. User Groups
        9. Users
      7. Installing a Package
        1. Troubleshooting Package Management
      8. Upgrading MODX
        1. Upgrading to Revolution 2.0.5
        2. Upgrading from 2.0.x to 2.1.x
        3. Upgrading from Versions Earlier than 2.0.5
        4. Upgrading to 2.2.x
        5. Upgrading to Revolution 2.0.0-rc-2
        6. Troubleshooting Upgrades
        7. Upgrading from MODx Evolution
          1. Functional Changes from Evolution
      9. Moving Your Site to a New Server, or to Root from Subfolder
      10. Media Sources
        1. Adding a Media Source
        2. Assigning Media Sources to TVs
        3. Media Source Types
          1. Media Source Type - File System
          2. Media Source Type - S3
        4. Securing a Media Source
          1. Creating a Media Source for Clients Tutorial
      11. Dashboards
        1. Assigning a Dashboard to a User Group
        2. Creating a Dashboard Widget
        3. Dashboard Widget Types
          1. Dashboard Widget Type - File
          2. Dashboard Widget Type - HTML
          3. Dashboard Widget Type - Inline PHP
          4. Dashboard Widget Type - Snippet
        4. Managing Your Dashboard
    5. Developing in MODX
      1. Code Standards
      2. Overview of MODX Development
        1. Developer Introduction
          1. Getting Started Developing
        2. Extras Directories
        3. Setting up a Development Environment
      3. Basic Development
        1. Static Elements
        2. Plugins
          1. System Events
            1. OnMODXInit
            2. OnFileManagerBeforeUpload
            3. OnFileManagerDirCreate
            4. OnFileManagerDirRemove
            5. OnFileManagerDirRename
            6. OnFileManagerFileCreate
            7. OnFileManagerFileRemove
            8. OnFileManagerFileRename
            9. OnFileManagerFileUpdate
            10. OnPackageInstall
            11. OnPackageUninstall
            12. OnPackageRemove
            13. OnBeforeCacheUpdate
            14. OnBeforeChunkFormDelete
            15. OnBeforeChunkFormSave
            16. OnBeforeDocFormDelete
            17. OnBeforeDocFormSave
            18. OnBeforeEmptyTrash
            19. OnBeforeManagerLogin
            20. OnBeforeManagerLogout
            21. OnBeforeManagerPageInit
            22. OnBeforePluginFormDelete
            23. OnBeforePluginFormSave
            24. OnBeforeSaveWebPageCache
            25. OnBeforeSnipFormDelete
            26. OnBeforeSnipFormSave
            27. OnBeforeTempFormDelete
            28. OnBeforeTempFormSave
            29. OnBeforeTVFormDelete
            30. OnBeforeTVFormSave
            31. OnBeforeUserActivate
            32. OnBeforeUserFormDelete
            33. OnBeforeUserFormSave
            34. OnBeforeWebLogin
            35. OnBeforeWebLogout
            36. OnCacheUpdate
            37. OnCategoryBeforeRemove
            38. OnCategoryBeforeSave
            39. OnCategoryRemove
            40. OnCategorySave
            41. OnChunkBeforeRemove
            42. OnChunkBeforeSave
            43. OnChunkFormDelete
            44. OnChunkFormPrerender
            45. OnChunkFormRender
            46. OnChunkFormSave
            47. OnChunkRemove
            48. OnChunkSave
            49. OnContextBeforeRemove
            50. OnContextBeforeSave
            51. OnContextFormPrerender
            52. OnContextFormRender
            53. OnContextRemove
            54. OnContextSave
            55. OnDocFormDelete
            56. OnDocFormPrerender
            57. OnDocFormRender
            58. OnDocFormSave
            59. OnDocPublished
            60. OnDocUnPublished
            61. OnEmptyTrash
            62. OnFileManagerUpload
            63. OnHandleRequest
            64. OnInitCulture
            65. OnLoadWebDocument
            66. OnLoadWebPageCache
            67. OnManagerAuthentication
            68. OnManagerLogin
            69. OnManagerLoginFormPrerender
            70. OnManagerLoginFormRender
            71. OnManagerLogout
            72. OnManagerPageAfterRender
            73. OnManagerPageBeforeRender
            74. OnManagerPageInit
            75. OnPageNotFound
            76. OnPageUnauthorized
            77. OnParseDocument
            78. OnPluginBeforeRemove
            79. OnPluginBeforeSave
            80. OnPluginEventRemove
            81. OnPluginFormDelete
            82. OnPluginFormPrerender
            83. OnPluginFormRender
            84. OnPluginFormSave
            85. OnPluginRemove
            86. OnPluginSave
            87. OnPropertySetBeforeRemove
            88. OnPropertySetBeforeSave
            89. OnPropertySetRemove
            90. OnPropertySetSave
            91. OnResourceGroupBeforeRemove
            92. OnResourceGroupBeforeSave
            93. OnResourceGroupRemove
            94. OnResourceGroupSave
            95. OnRichTextBrowserInit
            96. OnRichTextEditorInit
            97. OnRichTextEditorRegister
            98. OnSiteRefresh
            99. OnSiteSettingsRender
            100. OnTemplateVarBeforeRemove
            101. OnTemplateVarBeforeSave
            102. OnTemplateVarRemove
            103. OnTemplateVarSave
            104. OnUserActivate
            105. OnUserBeforeRemove
            106. OnUserBeforeSave
            107. OnUserChangePassword
            108. OnUserFormDelete
            109. OnUserFormSave
            110. OnUserNotFound
            111. OnUserRemove
            112. OnUserSave
            113. OnWebAuthentication
            114. OnWebLogin
            115. OnWebLogout
            116. OnWebPageComplete
            117. OnWebPageInit
            118. OnWebPagePrerender
        3. Snippets
          1. Adding CSS and JS to Your Pages Through Snippets
          2. How to Write a Good Chunk
          3. How to Write a Good Snippet
          4. Templating Your Snippets
        4. xPDO
      4. Advanced Development
        1. Caching
          1. Setting up Memcache in MODX
        2. Custom Manager Pages
          1. Custom Manager Pages in 2.3
          2. Actions and Menus
            1. Action List
          3. Custom Manager Pages Tutorial
          4. MODExt
            1. MODx.combo.ComboBox
            2. MODx.Console
            3. MODx.FormPanel
            4. MODx.grid.Grid
            5. MODx.grid.LocalGrid
            6. MODx.msg
            7. MODx.tree.Tree
            8. MODx.Window
            9. MODExt Tutorials
              1. 1. Ext JS Tutorial - Message Boxes
              2. 2. Ext JS Tutorial - Ajax Include
              3. 3. Ext JS Tutorial - Animation
              4. 4. Ext JS Tutorial - Manipulating Nodes
              5. 5. Ext JS Tutorial - Panels
              6. 7. Ext JS Tutoral - Advanced Grid
              7. 8. Ext JS Tutorial - Inside a CMP
            10. MODExt MODx Object
        3. Custom Resource Classes
          1. Creating a Resource Class
            1. Creating a Resource Class - Step 2
            2. Creating a Resource Class - Step 3
            3. Creating a Resource Class - Step 4
        4. Extending modUser
        5. From the Command Line (CLI)
        6. Internationalization
          1. Adding a Translation
        7. MODX Services
          1. modFileHandler
          2. modMail
          3. modRegistry
        8. Namespaces
        9. Package Management
          1. Package Dependencies
          2. Creating a 3rd Party Component Build Script
          3. Providers
          4. Transport Packages
        10. Using runProcessor
        11. Validating Requests: Tokens and Nonces
        12. Developing RESTful APIs
      5. Other Development Resources
        1. Summary of Legacy Code Removed in 2.1
        2. API Reference
        3. Class Reference
          1. modResource
            1. modResource.isMember
          2. modChunk
            1. modChunk.getContent
            2. modChunk.setContent
          3. modUser
            1. modUser.addSessionContext
            2. modUser.changePassword
            3. modUser.endSession
            4. modUser.getSessionContexts
            5. modUser.getSettings
            6. modUser.hasSessionContext
            7. modUser.isAuthenticated
            8. modUser.isMember
            9. modUser.loadAttributes
            10. modUser.removeSessionContext
            11. modUser.removeSessionContextVars
            12. modUser.removeSessionCookie
          4. modX
            1. modX.addEventListener
            2. modX.checkForLocks
            3. modX.checkSession
            4. modX.executeProcessor
            5. modX.getAuthenticatedUser
            6. modX.getCacheManager
            7. modX.getChildIds
            8. modX.getChunk
            9. modX.getConfig
            10. modX.getContext
            11. modX.getEventMap
            12. modX.getLoginUserID
            13. modX.getLoginUserName
            14. modX.getParentIds
            15. modX.getParser
            16. modX.getPlaceholder
            17. modX.getRegisteredClientScripts
            18. modX.getRegisteredClientStartupScripts
            19. modX.getRequest
            20. modX.getResponse
            21. modX.getService
            22. modX.getSessionState
            23. modX.getTree
            24. modX.getUser
            25. modX.getVersionData
            26. modX.handleRequest
            27. modX.hasPermission
            28. modX.initialize
            29. modX.invokeEvent
            30. modX.lexicon
            31. modX.makeUrl
            32. modX.parseChunk
            33. modX.regClientCSS
            34. modX.regClientHTMLBlock
            35. modX.regClientScript
            36. modX.regClientStartupHTMLBlock
            37. modX.regClientStartupScript
            38. modX.reloadConfig
            39. modX.removeAllEventListener
            40. modX.removeEventListener
            41. modX.runProcessor
            42. modX.runSnippet
            43. modX.sendError
            44. modX.sendErrorPage
            45. modX.sendForward
            46. modX.sendRedirect
            47. modX.sendUnauthorizedPage
            48. modX.setDebug
            49. modX.setPlaceholder
            50. modX.setPlaceholders
            51. modX.switchContext
            52. modX.toPlaceholder
            53. modX.toPlaceholders
            54. modX.unsetPlaceholder
            55. modX.unsetPlaceholders
        4. Loading MODX Externally
        5. Reserved Parameters
    6. Case Studies and Tutorials
      1. Developing an Extra in MODX Revolution
        1. Developing an Extra in MODX Revolution, Part II
        2. Developing an Extra in MODX Revolution, Part III
      2. Developing an Extra in MODX Revolution - MODX 2.1 and Earlier
        1. Developing an Extra in MODX Revolution, Part II - MODX 2.1 and Earlier
        2. Developing an Extra in MODX Revolution, Part III - MODX 2.1 and Earlier
      3. PHP Coding in MODx Revolution, Pt. I
        1. PHP Coding in MODx Revolution, Pt. II
        2. PHP Coding in MODx Revolution, Pt. III
      4. Using Custom Database Tables in your 3rd Party Components
      5. Creating a Blog in MODx Revolution
      6. Loading Pages in the Front-End via AJAX and jQuery Tabs
      7. Reverse Engineer xPDO Classes from Existing Database Table
      8. Integrating a Template into MODX Tutorial
      9. Quick and Easy MODX Tutorials
        1. Automated Server-Side Image Editing
      10. Adding Custom Fields to Manager Forms
      11. Create a Multilingual Website with migxMultiLang
      12. Managing Resources and Elements via SVN
    7. MODX Community Information
      1. Becoming a Core Contributor
      2. Filing Bug Reports
      3. Getting a MODx Account
      4. Using GitHub

Developing an Extra in MODX Revolution, Part II

Last edited by Mark Hamstra on Jan 15, 2018.

This tutorial is part of a Series:

This section will cover creating the Custom Manager Page (CMP) for our Doodles Extra we created in step 1. This includes explaining controllers/connectors/processors, making our Namespace, Action and Menu item, and working with ExtJS to create the UI.

First Setup Steps

We've got our snippet and our basic directory structure. Now we need to setup a few things before we can start developing our Custom Manager Page. The first is our Namespace.

Namespaces

Namespaces in MODX Revolution are basically silos of development in the system. They load the base path for your CMP, and allow MODX to know where to grab the files for it and for Lexicon files (i18n). They allow you to develop and run your Extras without having to modify core MODX files or get in the way of Git/SVN deployments of MODX.

We'll want to create ours. Go ahead and click on the Namespaces submenu item in the System menu represented by the “cog” icon:

From there, go ahead and click the 'Create New' button above the grid to load a Create Namespace window. Input these values into the form:

  • Name - doodles
  • Core Path - the full absolute path to your core/components/doodles/ directory. If you placed your files within the MODX root, you can use {core_path}components/doodles/ as a shortcut.
  • Assets Path - the full absolute path to your assets/components/doodles/ directory, or {assets_path}components/doodles/ if you placed that within the MODX root.

Let's explain. This gives us a Namespace key of 'doodles', which we can reference our Namespace with. Secondly, we're pointing the path to our doodles core directory that we've been developing in. This tells MODX to load the controller files that load the CMP (more on that in a sec) from that directory, which is what we want. Because that's where we'll be installing the files with the Transport Package. However, setting it to an absolute path in our environment allows us to develop it outside of the MODX webroot.

The core path tells MODX where to look for the lexicon and controller files. The assets path is not currently used by the MODX core.

Now go to the System Settings, and edit the two settings you added for doodles.core_path and doodles.assets_url. Set their Namespace to 'doodles'.

Actions and Menus

Next, we'll need to create our menu item for the CMP; this includes creating an “Action”. 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.

Previous to MODX 2.3, actions were differentiated only by an integer and you would need to call that action number with a menu item to load your CMP. Now CMPs can be loaded with our Namespace and the name of our PHP controller file without needing to look up the action number in the database.

So if we name our controller index.class.php, our action can be called with index. Or if we name our controller home.class.php, our action can be called with home.

In our case, we've already created a namespace called “doodles” and later when we create our controller file, we'll call it index.class.php. So, in MODX 2.3 our CMP can be called with:

http://your-web-root/manager/ ?a=index&namespace=doodles

Before MODX 2.3, if we gave our CMP an Action number of 2, then our CMP would be called with:

http://your-web-root/manager/ ?a=2

More information on the new way of creating CMPs in 2.3 can be found here:

http://rtfm.modx.com/revolution/2.x/developing-in-modx/advanced-development/custom-manager-pages/custom-manager-pages-in-2.3

We will now go ahead and create the menu item that creates the URL to load our CMP.

Go back to the System menu and select the menu item called Menus.

You'll be presented with the following screen. Click on the Create Menu button above the menu tree.

In the Create Menu window that appears, there are a few fields to fill out.

  • Parent - Extras
  • Lexicon Key - Doodles
  • Description - The Doodles Extra
  • Handler -
  • Permission -
  • Action - index
  • Parameters -
  • Namespace - doodles
  • Icon -

And let's explain each field:

Parent: The menu item we want our Extra to be listed under. The standard is under "Extras".

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. For now we'll just directly give it a name but you can change it to 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.

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.

Action - This tells MODX what action to load when the Menu item is clicked. This is the name of our controller file. Call it 'index'.

Parameters - This allows you to attach GET parameters to the menu item when clicked. We don't need this, so we'll skip it.

Namespace - We will use the doodles namespace we set up earlier.

Icon - If you wish, you can add an icon here to be used by your menu item.

Click Save and you'll see your menu item added to the tree under 'Extras'.

Lexicons

Lexicons 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'. 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:

{namespace_path}/lexicon/{language}/{topic}.inc.php

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:

<?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';

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.

Setting up the Controller with MODExt

CMPs in MODX are generated with ExtJS, 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. 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 a basic controller first before we can proceed with development.

The Base Controller

Let's create our controller at: /www/doodles/core/components/doodles/controllers/index.class.php. And put this in it:

<?php
require_once dirname(dirname(__FILE__)) . '/model/doodles/doodles.class.php';
class DoodlesIndexManagerController 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;}
	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';
	}
}

A bit of explanation here. What we're doing is creating a Controller Class (DoodlesIndexManagerController) for our Extra that extends modExtraManagerController, a special class for developing Extras. MODX 2.3 does request routing via Controller classes, which are all sorts of powerful. But in our Controller 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 Controller 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. We then define checkPermissions(), which basically says if it doesn't return true, deny access to this controller page.

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 via 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.

At the bottom, 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.

Create your template file at /www/doodles/core/components/doodles/templates/home.tpl and fill it with this:

<div id="doodles-panel-home-div"></div>

Note that in the initialize() function of our controller, 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:

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();

So, basically, we're loading a Doodles object which extends the Ext.Component class. This also gives us a nice JavaScript namespace of 'Doodles'.

Our Doodles CMP Page

The Section JS File

Let's first create the index.js file, at /www/doodles/assets/components/doodles/js/mgr/sections/index.js:

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);

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 controllers/index.class.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.

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:

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
                }*/]
            }]
            // only to redo the grid layout after the content is rendered
            // to fix overflow components' panels, especially when scroll bar is shown up
            ,listeners: {
                'afterrender': function(tabPanel) {
                    tabPanel.doLayout();
                }
            }
        }]
    });
    Doodles.panel.Home.superclass.constructor.call(this,config);
};
Ext.extend(Doodles.panel.Home,MODx.Panel);
Ext.reg('doodles-panel-home',Doodles.panel.Home);

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:

{
    html: '<h2>'+_('doodles.management')+'</h2>'
    ,border: false
    ,cls: 'modx-page-header'
}

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:

,{
   xtype: 'modx-tabs'
   ,defaults: { border: false ,autoHeight: true }
   ,border: true
   ,items: /* ... */
}

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:

{
   title: _('doodles')
   ,defaults: { autoHeight: true }
   ,items: [{
      html: '<p>'+_('doodles.management_desc')+'</p>'
      ,border: false
      ,bodyCssClass: 'panel-desc'
   }]
}

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.

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.

The Doodles Grid

First off, go ahead and uncomment this line in your index.class.php controller:

$this->addJavascript($doodles->config['jsUrl'].'mgr/widgets/doodles.grid.js');

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:

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);

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 :

[{
   html: '<p>'+_('doodles.management_desc')+'</p>'
   ,border: false
},{
   xtype: 'doodles-grid-doodles'
   ,cls: 'main-wrapper'
   ,preventRender: true
}]

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.

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:

<?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' => '',
));

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.

On different circumstances, you can create a file at your different folder, eg: /www/doodles/config.core.php and put this in it:

<?php
define('MODX_CORE_PATH', '/www/modx/core/');
define('MODX_CONFIG_KEY', 'config');

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:



{"success":false,"message":"Access denied.","total":0,"data":[],"object":[]}

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.

Don't ever paste or share with anyone your $modx->siteId or HTTP_MODAUTH key. It keeps your site secure.

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:



/www/doodles/core/components/doodles/processors/mgr/doodle/getlist.class.php

So let's go ahead and make that file to give our grid some data:

<?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';

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'] 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:

Great! We've got a working grid. Now, let's add some functionality to it, since right now all it does is list Doodles.

Adding Search

Add this bit of code to your grid panel in widgets/doodles.grid.js, right after the columns: definition at line 29:

,tbar:[{
    xtype: 'textfield'
    ,id: 'doodles-search-filter'
    ,emptyText: _('doodles.search...')
    ,listeners: {
        'change': {fn:this.search,scope:this}
        ,'render': {fn: function(cmp) {
            new Ext.KeyMap(cmp.getEl(), {
                key: Ext.EventObject.ENTER
                ,fn: function() {
                    this.fireEvent('change',this);
                    this.blur();
                    return true;
                }
                ,scope: cmp
            });
        },scope:this}
    }
}]

We just added a textfield to the top bar of our grid, and we gave it some 'emptyText', meaning that when empty, display this text. Also, we gave it a DOM ID of 'doodles-search-filter', and told it to run the 'this.search' method when it changes. Also, the code in the 'render' listener means to fire the change event when someone hits ENTER on their keyboard when editing it.

So let's define the 'this.search' method - since our Panel is OOP, this means that this.search can be defined in our grid object. To do that, find this code, now at line 52:

Ext.extend(Doodles.grid.Doodles,MODx.grid.Grid);

And replace it with this:

Ext.extend(Doodles.grid.Doodles,MODx.grid.Grid,{
    search: function(tf,nv,ov) {
        var s = this.getStore();
        s.baseParams.query = tf.getValue();
        this.getBottomToolbar().changePage(1);
        this.refresh();
    }
});

What we're telling it to do here is to extend the MODx.grid.Grid class, and then add another method called 'search'. In that method, we're getting the grid's "Store", which is where the data for the grid is stored and what determines where the data comes from. Then, we are adding a 'query' parameter to our baseParams (remember that earlier?), changing the current page of the grid back to 1, and refreshing it.

This will pass a 'query' REQUEST parameter to our getList Processor in the getlist.class.php file. Since we're not doing anything to handle that yet, let's go open it up. Add this method to the class after line 7:

    public function prepareQueryBeforeCount(xPDOQuery $c) {
        $query = $this->getProperty('query');
        if (!empty($query)) {
            $c->where(array(
                'name:LIKE' => '%'.$query.'%',
                'OR:description:LIKE' => '%'.$query.'%',
            ));
        }
        return $c;
    }

The modObjectGetListProcessor assistance class lets us extend the prepareQueryBeforeCount() method to modify the xPDOQuery object before it's passed to the getCount() method. All we have to do is return our modified query object. We're going to extend it here to add searchability to our grid via the "query" parameter. Note we can get whatever that parameter is via the neat ->getProperty() method.

Now load your grid, and you'll get:

And there's our searchable grid. Now let's work on updating records.

Adding an Update Window

First off, MODX grids usually have context menus when you click them. Ours doesn't, and that's because we haven't defined it yet. Let's go ahead and define it. Add a 'getMenu' method to your Doodles.grid.Grid definition, right below your search: method we just added, line 48:

,getMenu: function() {
    return [{
        text: _('doodles.doodle_update')
        ,handler: this.updateDoodle
    },'-',{
        text: _('doodles.doodle_remove')
        ,handler: this.removeDoodle
    }];
}

MODX looks for a getMenu method on grids that extend it, and if it finds it, it runs it. It adds then any menu items you return. Here we've added 2 menu items for our context menu, one that runs a this.updateDoodle method, and the other that runs a this.removeDoodle method. We'll get to the removeDoodle method here in a bit. For now, let's add another JS method below the getMenu call, line 58, and call it updateDoodle:

,updateDoodle: function(btn,e) {
    e.preventDefault();
    if (!this.updateDoodleWindow) {
        this.updateDoodleWindow = MODx.load({
            xtype: 'doodles-window-doodle-update'
            ,record: this.menu.record
            ,listeners: {
                'success': {fn:this.refresh,scope:this}
            }
        });
    }
    this.updateDoodleWindow.setValues(this.menu.record);
    this.updateDoodleWindow.show(e.target);
}

A few things. What this little bit of code does is checks for a class variable named 'updateDoodleWindow'. If it doesn't find it, it creates it. This prevents us from having to have ExtJS create a new window every time (it's faster and better to prevent DOM ID conflicts). Also, it passes in a few values:

  • xtype - This is obviously our unique xtype for the window, which is 'doodles-window-doodle-update'. We'll get to that soon.
  • record - MODx.Window objects will automatically fill their fields with whatever is passed into the 'record' configuration param. Also, MODx.grid.Grid objects always have the current row values stored in the 'this.menu.record' object. So we'll just pass that right in to our Window.
  • listeners - These execute on different events in the window. For now, we just want to refresh the grid (via the 'this.refresh' method for the MODx.grid.Grid) whenever the window succeeds; meaning whenever the window's form successfully submits and returns a 'success' response.

After we create the window, we'll run the show() method on it to show it. The 'e.target' just tells it to animate the opening from wherever the mouse was. If we already had a window object, before that we call setValues on it, which sets the values of the Window's form to the passed in value (similar to the record: param). This allows us to re-use windows.

Now let's actually define the window with this code at the end of the file:

Doodles.window.UpdateDoodle = function(config) {
    config = config || {};
    Ext.applyIf(config,{
        title: _('doodles.doodle_update')
        ,url: Doodles.config.connectorUrl
        ,baseParams: {
            action: 'mgr/doodle/update'
        }
        ,fields: [{
            xtype: 'hidden'
            ,name: 'id'
        },{
            xtype: 'textfield'
            ,fieldLabel: _('doodles.name')
            ,name: 'name'
            ,anchor: '100%'
        },{
            xtype: 'textarea'
            ,fieldLabel: _('doodles.description')
            ,name: 'description'
            ,anchor: '100%'
        }]
    });
    Doodles.window.UpdateDoodle.superclass.constructor.call(this,config);
};
Ext.extend(Doodles.window.UpdateDoodle,MODx.Window);
Ext.reg('doodles-window-doodle-update',Doodles.window.UpdateDoodle);

Similar to what you've seen in the grids, except this time we have 'fields' as the fields for the Window's form. We've provided some fields to edit - and remember, since this is an "Update" form, we need to provide the ID of the Doodle, passed in as a hidden field.

MODx.Window wraps Ext.Window, but provides a form inside that will automatically try and connect to the url: param with the baseParams: parameters, as well as the fields' values. It also automatically provides OK/Cancel buttons. Right-click on a record in the grid, and your window should look like this now:

Excellent! We've got a nice little update window. Now as you probably noticed in our baseParams, we're looking now for the 'mgr/doodle/update' processor. So let's create a file at: /www/doodles/core/components/doodles/processors/mgr/doodle/update.class.php:

<?php
class DoodleUpdateProcessor extends modObjectUpdateProcessor {
    public $classKey = 'Doodle';
    public $languageTopics = array('doodles:default');
    public $objectType = 'doodles.doodle';
}
return 'DoodleUpdateProcessor';

Again, we're going to create our Processor class by extending an assistance Processor class that MODX provides. This one automatically does all the dirty work for saving an object. All we have to do is specify the classKey and a few other parameters. It handles all the saving and the response automatically. Now we have a working Update form!

Adding a Remove Context Menu Option

Let's finish off the remove part of our UI. We've already got the context menu showing up, so we just need to add the JS method and the processor. After our updateDoodle method in our JS grid, add this at line 70:

,removeDoodle: function() {
    MODx.msg.confirm({
        title: _('doodles.doodle_remove')
        ,text: _('doodles.doodle_remove_confirm')
        ,url: this.config.url
        ,params: {
            action: 'mgr/doodle/remove'
            ,id: this.menu.record.id
        }
        ,listeners: {
            'success': {fn:this.refresh,scope:this}
        }
    });
}

MODx.msg.confirm pops up a confirmation dialog, and if confirmed, runs a processor via a connector. Let's take a look at each parameter:

  • title - This is the title of the confirmation dialog.
  • text - The text of the dialog. Here it basically asks if we really want to remove a Doodle.
  • url - The URL to the connector.
  • params - Any REQUEST parameters to send to the processor. We're going to send the processor path, and the ID of the Doodle to remove.
  • listeners - Similar to our update form's listeners, whenever this succeeds, refresh the grid.

Now let's create our remove processor at /www/doodles/core/components/doodles/processors/mgr/doodle/remove.class.php:

<?php
class DoodleRemoveProcessor extends modObjectRemoveProcessor {
    public $classKey = 'Doodle';
    public $languageTopics = array('doodles:default');
    public $objectType = 'doodles.doodle';
}
return 'DoodleRemoveProcessor';

Pretty similar to the update processor, except this time, we extend modObjectRemoveProcessor, which handles deleting the Doodle from the database. That's it! We can now remove Doodles.

Creating the Create Form

So we've got R, U and D of our CRUD interface. What about C? Let's work on a create form. Let's add a button to the top toolbar of the grid to load the create window. Add this to the tbar: property on the grid config in doodles.grid.js, right after our search textfield at line 48. Be careful to insert it between the closing curly bracket of the search textfield and the closing square bracket of the tbar:

,{
   text: _('doodles.doodle_create')
   ,handler: { xtype: 'doodles-window-doodle-create' ,blankValues: true }
}

MODExt allows you to pass JSON objects into the handler: method on toolbars. What this does is loads the Window with the xtype 'doodles-window-doodle-create', makes sure its values are blanked on load, and runs this.success on a successful window form submit (basically shortcuts the stuff we've been doing). That's what we want, so let's now define the window at the end of our file:

Doodles.window.CreateDoodle = function(config) {
    config = config || {};
    Ext.applyIf(config,{
        title: _('doodles.doodle_create')
        ,url: Doodles.config.connectorUrl
        ,baseParams: {
            action: 'mgr/doodle/create'
        }
        ,fields: [{
            xtype: 'textfield'
            ,fieldLabel: _('doodles.name')
            ,name: 'name'
            ,anchor: '100%'
        },{
            xtype: 'textarea'
            ,fieldLabel: _('doodles.description')
            ,name: 'description'
            ,anchor: '100%'
        }]
    });
    Doodles.window.CreateDoodle.superclass.constructor.call(this,config);
};
Ext.extend(Doodles.window.CreateDoodle,MODx.Window);
Ext.reg('doodles-window-doodle-create',Doodles.window.CreateDoodle);

This is very similar to our Update window, except this one doesn't have an ID field, and passes 'create' as the processor. So, on to the processor at: /www/doodles/core/components/doodles/processors/mgr/doodle/create.class.php:

<?php
class DoodleCreateProcessor extends modObjectCreateProcessor {
    public $classKey = 'Doodle';
    public $languageTopics = array('doodles:default');
    public $objectType = 'doodles.doodle';
    public function beforeSave() {
        $name = $this->getProperty('name');
        if (empty($name)) {
            $this->addFieldError('name',$this->modx->lexicon('doodles.doodle_err_ns_name'));
        } else if ($this->doesAlreadyExist(array('name' => $name))) {
            $this->addFieldError('name',$this->modx->lexicon('doodles.doodle_err_ae'));
        }
        return parent::beforeSave();
    }
}
return 'DoodleCreateProcessor';

Similar to the update and remove methods, again, except we aren't grabbing the object, but rather creating it. We're extending the modObjectCreateProcessor assistance class now.

Also, we have a bit of form validation beforehand - we're going to make sure the name isn't blank, and if it is, return a custom error message on that specific field. If it's not blank, then we'll make sure there's no other Doodles with that name. Note we extended the beforeSave() method to do this, and then return the parent class's running of that same method. Then, inside of it, we do two things:

  1. Check to see if the name is empty and if so, add a field-specific error to the "name" field saying so
  2. Otherwise, see if a Doodle already exists (using the helper doesAlreadyExist method, which takes in a criteria array to search for), and if so, return a field-specific error to the "name" field saying so

Pretty neat, huh? Field-specific validation, built right in. And we've got a working create form!

Adding Inline-Editing

MODExt also has automatic inline editing built right into its grids. Simply add this to your Doodles.grid.Grid config object, right below the 'autoExpandColumn' property:

,save_action: 'mgr/doodle/updateFromGrid'
,autosave: true

That tells the grid to turn on inline editing and saving; and also to send any saves to the processor at mgr/doodle/updateFromGrid. So let's create it, at: /www/doodles/core/components/doodles/processors/mgr/doodle/updatefromgrid.class.php:

require_once (dirname(__FILE__).'/update.class.php');
class DoodleUpdateFromGridProcessor extends DoodleUpdateProcessor {
    public function initialize() {
        $data = $this->getProperty('data');
        if (empty($data)) return $this->modx->lexicon('invalid_data');
        $data = $this->modx->fromJSON($data);
        if (empty($data)) return $this->modx->lexicon('invalid_data');
        $this->setProperties($data);
        $this->unsetProperty('data');
        return parent::initialize();
    }
}
return 'DoodleUpdateFromGridProcessor';

Note how we're just extending our Update processor class (after including it), and in the initialize() method, parsing our "data" property from JSON (which our grid sends with the updated record) and setting that as the properties for the Processor. Then the Update processor handles the rest. Simple, eh?

Summary

We've got ourselves a nice CRUD user interface now, with creating, updating, removing, searching, pagination, and sorting. And all pretty easily, too.

Next, in Part III, we'll explore creating a Transport Package for our Doodles Extra so that we can distribute it on modxcms.com and via Revolution's Package Management system.

This tutorial is part of a Series:

The $objectType shown in all processors is not necessary to set.

I have made a couple of custom components and I have setupped a "afterSaveEvent" and "afterRemoveEvent" etc. But then there isn't a name like "object" passed to your plugin (containing your object). The name passed is based on the $objectType value. So when you have Doodles.. you will get a "doodles.doodle" passed into your plugin. With 10 components, 10 different types, this is very un-wanted.

So I have figured out that this $objectType is not required to let the processors work properly. When you leave that, MODX falls back on "object" so you will have $scriptProperties['object'] in your plugin, instead of many different type names.

Note: saves about 30 bytes disk space per processor :-)

Comment Policy

These comments are not for usage questions or support. See below for support options.

Comments intended add specific value for other users with this document are welcome! Have a suggestion to improve this document? Suggest an edit (requires GitHub account). Have suggestions for code changes or feature requests? Submit to the Revolution project at GitHub..

Support Options

If you need assistance, you can get it via the MODX Forums or the MODX Community Slack.

Suggest an edit to this page on GitHub (Requires GitHub account. Opens a new window/tab) or become an editor of the MODX Documentation.