Garden Preview XV: Modules

Views are used to define the main content of a page (ie. a list of discussions, a user profile, etc), but what about all of the other supplementary elements on a page? How does Garden handle adding buttons, menus, and all of the sundry elements that make any page complete? Modules.

Modules are extremely simple classes that have one purpose: return a string. That string can really be anything you want, but typically it will be a string of xhtml that is to be added to any of the asset containers defined in the master view.

Garden comes with a number of predefined modules, such as:

HeadModule: Allows the addition of tags to the head of the document, like link (css) tags, script tags, meta tags, etc.
MenuModule: Used to build a hierarchical menu that can then be manipulated to do a number of different tasks. For example, it is used for the main menu of Garden that includes hover-driven dropdown items, and it also is used for the main settings page as a sidebar menu.
EditInPlaceModule: Can be added to any page in the sidebars, header, or footer and allows administrators to put anything they want into that page by editing the content block in-place.

Vanilla comes with a bunch of different modules as well, like:

BookmarkModule: contains a sidebar-sized list of discussions that the user has bookmarked.
CategoriesModule: contains a sidebar-sized list of categories, with the currently-viewed category highlighted.

There are actually a lot more modules than that, but I don’t want to give too much away about the new functionality in Vanilla.

How Modules Work

As previously stated, modules really only have one purpose, and that is to return a string which can then be added to an asset container and placed in the page. All modules are extended from the base Module class, which is an implementation of the IModule interface. The interface is quite simple:

interface IModule {

   /// <summary>
   /// Class constructor, requires the object that is constructing the module.
   /// </summary>
   /// <param name="Sender" type="object">
   /// The controller that is building the module.
   /// </summary>
   public function __construct($Sender);

   /// <summary>
   /// Returns the name of the asset where this component should be rendered.
   /// </summary>
   public function AssetTarget();

   /// <summary>
   /// Returns the xhtml for this module as a fully parsed and rendered string.
   /// </summary>
   public function FetchView();

   /// <summary>
   /// Returns the location of the view for this module in the filesystem.
   /// </summary>
   /// <param name="View" type="string" required="false" default="The name of the module">
   /// The name of the view to search for. If not provided, the name of the module (minus "module") will be used.
   /// </summary>
   /// <param name="ApplicationFolder" type="string" required="false" default="The application folder of the controller that built this module">
   /// The application folder that the view should be found within. If not provided, the application folder of the controller that constructed this module will be used.
   /// </summary>
   public function FetchViewLocation($View = '', $ApplicationFolder = '');

   /// <summary>
   /// Returns the name of the module.
   /// </summary>
   public function Name();

   /// <summary>
   /// Returns the module as a string to be rendered to the screen.
   /// </summary>
   public function ToString();
}

Modules can be added to the page in a number of different ways, but the easiest way is to use the base controller class’ AddModule method. Modules do not even need to be instantiated before being added with this method. If the name of a module is provided, the method will handle creating an instance of the module class and calling it’s ToString() method in order to get the module’s xhtml and add it to the appropriate asset container (as defined by IModule::AssetTarget()). The Controller::AddModule() method also gives you some extra control by allowing you to override the default asset target of the module. That method is defined as:

/// <summary>
/// Adds the specified module to the specified asset target. If no asset
/// target is defined, it will use the asset target defined by the module's
/// AssetTarget method.
/// </summary>
/// <param name="Modules" type="mixed">
/// An instance of a module or the name of a module to add to the page.
/// </param>
public function AddModule($Module, $AssetTarget = '') {}

Some modules might be as simple as returning a static string of xhtml (like a “New Discussion” button for the sidebar in Vanilla, for example), and others might need a lot more information or have a number of different methods which allow developers to manipulate them before returning their xhtml (like the MenuModule, which allows you to add different menu options based on a myriad of criteria, like permissions, session state, etc). So, in some cases you may want to instantiate a module and then fire an event so plugin authors can manipulate it before rendering, and in other cases you may just want to call the controller’s AddModule method with the module name as a parameter.

Any module you create can do anything you want, and you can add as many different properties and methods to it as you like to suit your purposes. As long as it is extended from the Module class and contains the methods defined in the IModule interface, it will function properly.

Theme-ing Modules

Of course it may be necessary for theme authors to edit the xhtml of a module. Depending on the complexity of the xhtml returned by their module, module authors have the choice of creating a view and placing it in their application’s view folder (in a “modules” directory), or not using a view file at all, and just returning the xhtml directly from the ToString method of their module. For example, here is the list of modules and their related views in Vanilla 2 (I’ve blurred the names of some modules in order to keep some of the new features in Vanilla 2 secret):

In the above image, the two short-blurred modules do not have associated views, but all of the other modules do.

Organizing Modules

Okay, so we’ve now got a way to add just about anything, anywhere, on any screen we want. How can we organize those modules on the page? For example, if I’m adding a bunch of different modules to the sidebar of the discussion page in Vanilla, how can I make sure that, for example, the “Start a New Discussion” button is at the top of that sidebar? Obviously, I could make sure that I add that module to the page *first* when I am coding the application, but what if you don’t want that button to be at the top of the sidebar in *your*installation?

I’ve created a modules.php file in the conf folder that handles organizing the order that modules appear in their various asset containers on specific pages. It is a simple associative array that appears as:

$Modules['ModuleSortContainerName']['AssetContainerName'] = 'ModuleName';

Every Controller class has a “ModuleSortContainer” property. This property allows a controller (or collection of controllers) to represent the same “container” for modules. For example, the main settings page for Garden allows you to see all of the various configuration options that are available: Application management, role management, user management, etc. Vanilla also has it’s own settings pages which appear in the sidebar menu on the garden settings pages (and vice versa). To the end user, it appears as though they are always in a “Settings” area whether they are looking at one of the garden settings pages (/garden/settings/applications, for example) or one of the vanilla settings pages (/vanilla/settings/categories, for example). So, both Garden’s settings controller and Vanilla’s settings controller use the same ModuleSortContainer name.

The AssetContainerName is really just the name of the asset that the module has been added to.

So, for example, if I want to sort the modules that appear in the panel of the discussions page in Vanilla, here is how they would appear in my conf/modules.php file:

// Discussions Panel Order
$Modules['Discussions']['Panel'][] = 'NewDiscussionModule';
$Modules['Discussions']['Panel'][] = 'CategoriesModule';
$Modules['Discussions']['Panel'][] = 'BookmarkedModule';
$Modules['Discussions']['Panel'][] = 'ThisModuleDoesntExist';

So, in order for this code to work, I would have had to define my DiscussionController’s ModuleSortContainer property as: “Discussions”. I did this in my DiscussionController’s class constructor.

With this code in place, it doesn’t matter what order the modules were actually added to the page programmatically. Furthermore, it doesn’t matter if the list contains a module that doesn’t exist (like the “ThisModuleDoesntExist” item in the list). If a module doesn’t exist, it will simply be ignored. So, this can be handy for making sure that modules which were added via plugins can be ordered regardless of whether the plugin is enabled or not.

Conclusion

My goal is to eventually have a page in the Garden settings area where you can drag & drop your modules to order them on the various pages they appear. I don’t think I will get that far for launch, but you never know. It will definitely come later if it doesn’t make it into the launch code.

Furthermore, I’d like it to go so far as allowing administrators to grab modules from a toolbox and drag/drop them to different pages so they can do things like: add an editinplace module to the header of a page and throw a banner ad in there, or add a “WhosOnline” module to their main homepage, etc.

One final note: I briefly considered calling them “controls”, but I didn’t want people to confuse them with controls from Vanilla 1, as they are different beasts entirely. In the end, “module” was the first word that came to mind.

Leave a comment