This guide will demonstrate step-by-step creating how to create simple plugin making use of most of the features provided. For this guide, you'll be shown how to create a plugin which displays a message at the top of every page on the community.
Step 1: Creating the Plugin
To begin, you'll need to have a test installation in developer mode. Once developer mode is enabled, a "Create Plugin" button will appear in the Admin CP under System --> Site Features --> Plugin. Use this tool to create your plugin, after which you'll be taken into the Plugin Developer Center.
Step 2: Creating the Theme Hook
The easiest way to display a message at the top of every page is to create a theme hook. Theme hooks allow you to modify the content of a template. The template we'll be modifying is "globalTemplate" which is in the "global" template in the "front" location in the "core" application.
Under the "Hooks" tab, create a new hook and choose "core --> front: global" as the template group. Once it has been created, click the edit button, then choose "globalTemplate" from the menu on the left.
Choose "Select Element" to bring up the contents of the template and choose the point you want to hook on - a good place is to select <div id="ipsLayout_mainArea"> and then for the content position choose "Insert content inside the chosen element(s), at the start."
For now, specify the contents manually:
<div class="ipsMessage ipsMessage_information">This is the global message.</div>
After saving you can go to the homepage and you will immediately see the message you've just created.
Congratulations, you've just created a simple Plugin! The remaining steps will show how to expand this functionality.
Step 3: Using Templates
It's good practice to keep all HTML in templates. While the current approach works fine, the HTML content is buried within the code of your plugin, so if someone installing the plugin wanted to modify it in some way (perhaps change the CSS classes on it), it would be difficult to do so.
Fortunately, creating a HTML template is really easy. If you look into the directory on your computer/server where IPS Community Suite is installed, you'll notice a "plugins" directory - inside this you'll find a directory has been created for your plugin with the name you specified in Step 1. Inside this is a folder navigate to dev/html. Within this folder, files you create will become available as a template.
Create a file called globalMessage.phtml and set the following as it's contents:
<ips:template parameters="" /> <div class="ipsMessage ipsMessage_information"> Now using a template! </div>
The first line is just to name any parameters that will be passed into the template - which is not needed in this case.
Once the file has been created, edit your theme hook and change the contents it inserts to use this template by using this code:
{template="globalMessage" group="plugins" location="global" app="core"}
This is a template tag which pulls in the contents of a template. All templates created by plugins are created in the "plugins" group in the "global" location in the "core" application.
Once this is done, you should see the message has changed to "Now using a template!"
As a side-note, if you wanted to add CSS code, you can just add css files into the dev/css folder and they will automatically be included. Plugin CSS files are bundled together with IPS4's own CSS so that they are available on every page.
Step 4: Settings & Language Strings
Now you have a global message - but currently there's no way to customize what it says. It would be handy if the plugin had a simple setting in the Admin CP that the admin could use to change the contents.
To create a setting, go to the "Settings" tab in the developer center for your plugin and add a setting. For the key, use globalMessage_content, and set the default value to whatever you like. It's important to make sure that your plugin starts working straight after it installs so the admin knows it's working properly, so don't leave the default value blank.
Creating the setting here allocates space for your setting in the database, but you still need to create a form where the admin can edit it. To do this, again look in the directory for your plugin on the filesystem; you'll see a file called settings.rename.php. First rename this to settings.php then open it up. It already contains example code to get you going. Change the first line of code (the $form->add(...) call) to this:
$form->add( new \IPS\Helpers\Form\Editor( 'globalMessage_content', \IPS\Settings::i()->globalMessage_content, FALSE, array( 'app' => 'core', 'key' => 'Admin', 'autoSaveKey' => 'globalMessage_content' ) ) );
This code is using the form helper.
Now when you go to the Plugins area of the Admin CP, you'll see a new "edit" button next to your plugin, when clicked, this brings up a form with a place where users can fill in a message.
There is however, a problem with your form. The label for the setting just says "globalMessage_content" - obviously this needs to be changed to something more useful, for which you'll need a language string. Language strings in IPS4 are simple key/value pairs, though the language strings can use more advanced features such as string replacements and pluralization.
To create one, look in the directory for your plugin again and open up the dev/lang.php file. It will contain just an empty array. Add an element to this array like so:
$lang = array( 'globalMessage_content' => "Message", );
The label will now say "Message".
Finally, you need to actually make the user-defined message show. To do this, open up the globalMessage.phtml file you created in step 3 and change it's contents to:
<ips:template parameters="" /> {{if settings.globalMessage_content}} <div class="ipsMessage ipsMessage_information"> {setting="globalMessage_content"} </div> {{endif}}
That adds some template logic which detects if there is a value for our setting and only displays the message if there is, and another template tag which gets the value of our setting.
Step 5: Making Database Changes
Note:
If you are planning on submitting your plugin to the Marketplace, changes to stock Invision Community tables are not allowed. You may want to consider creating an application instead which offers much more robust management of database tables.
If we wanted to take this plugin even further, you could add a "close" button to the message allowing users to dismiss the message once they have read it. Whether or not any given user has dismissed the message is information can be stored in the database.
Open up the dev/setup folder of your plugin directory. In here you'll find a file called install.php. this file is run when your plugin is installed. If you need to make subsequent database changes in future versions of your plugin, you can create new versions in the developer center, and a file will be created for each version you create. You need to make sure you add whatever changes you need both to the install.php file and the appropriate upgrade file.
Open up install.php and add this code inside the step1 method:
\IPS\Db::i()->addColumn( 'core_members', array( 'name' => 'globalMessage_dismissed', 'type' => 'BIT', 'length' => 1, 'null' => FALSE, 'default' => 0, 'comment' => 'If 1, the user has dismissed the global message' ) ); return TRUE;
This code adds a new column to the core_members table in the database, which is the table which contains information on all the members. The column created is a BIT(1) column (which means it can store only 1 or 0) - 0 will indicate the user has not dismissed the message (so it should show) and 1 will indicate they have - the default value is set to 0.
Though this code will make sure when your plugin gets installed the column will be added, you'll need to run a query on your local database so that you have it during development. Run this SQL query using whatever your preferred method is for managing your database:
ALTER TABLE core_members ADD COLUMN globalMessage_dismissed BIT(1) NOT NULL DEFAULT 0 COMMENT 'If 1, the user has dismissed the global message';
After the user dismisses the message, their preference will need to be reset if the admin changes the contents of the message. To handle this, add this line to your settings.php file in your plugin directory, just after the $form->saveAsSettings(); call.
\IPS\Db::i()->update( 'core_members', array( 'globalMessage_dismissed' => 0 ) );
Step 6: Creating a Code Hook
Now you've allocated space in the database where the flag will be stored, you need to write code to actually set that value, and link it up with the template.
You'll need to add a method to a controller which will handle the user's click. A generic controller intended for this sort of thing is available - \IPS\core\modules\front\system\plugins - though theoretically, you could add the method to any controller.
In the developer center, create a code hook on that class and open it up (either in the Admin CP or by opening the file that has been created in the hooks directory of your plugin directory) and add the following code inside the class:
public function dismissGlobalMessage() { \IPS\Session::i()->csrfCheck(); if ( \IPS\Member::loggedIn()->member_id ) { \IPS\Member::loggedIn()->globalMessage_dismissed = TRUE; \IPS\Member::loggedIn()->save(); } else { \IPS\Request::i()->setCookie( 'globalMessage_dismissed', TRUE ); } \IPS\Output::i()->redirect( isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : \IPS\Http\Url::internal( '' ) ); }
It's important to understand thoroughly what this function is doing:
- It first performs a CSRF check. Because this is a controller method, it is executed automatically on accessing the appropriate URL. Because it does something which affects the user (it modifies a preference) it is essential that it have a CSRF check. If the check fails, execution will be halted automatically.
- It checks if the current user is a registered member. \IPS\Member::loggedIn() returns an \IPS\Member object for the current user - if the user is a guest (not logged in) the member_id property will be 0. If the user is logged in, it sets the globalMessage_dismissed property to TRUE and saves the member. Since \IPS\Member is an Active Record, this will edit the column you created in step 5 for the appropriate row in the database. If the user is not logged in, it sets a cookie so that even users who are not logged in can dismiss the message.
- It then redirects the user back to the page they were viewing, or if the server cannot provide the HTTP Referer, the home page.
Now that the action has been created, you need to adjust the template to honor the preference, and add a button that links to the dismiss action.
Modify the template you created in step 3 to:
<ips:template parameters="" /> {{if settings.globalMessage_content and !member.globalMessage_dismissed and !isset( cookie.globalMessage_dismissed )}} <div class="ipsMessage ipsMessage_information"> <a href="{url="app=core&module=system§ion=plugins&do=dismissGlobalMessage" csrf="1"}" class="ipsMessage_code ipsType_blendlinks ipsPos_right"><i class="fa fa-times"></i></a></span> {setting="globalMessage_content"} </div> {{endif}}
Step 7: Adding Javascript
You now have a completely functional plugin which displays a message to all users that they can dismiss. For a little bit of added flair, you can make it so when the user dismisses the message, rather than reloading the page, it performs the action with an AJAX request and then the message fades out. It's really important that you only think about JavaScript enhancements after the core functionality has been written, so that users who don't have JavaScript enabled can still use your plugin, and search engines can access any content your plugin provides.
To do this, you need to create a JavaScript controller. In the dev/js directory of your plugin directory, create a file named globalMessageDismiss.js with the following contents:
;( function($, _, undefined){ "use strict"; ips.controller.register('plugins.globalMessageDismiss', { initialize: function () { this.on( document, 'click', '[data-action="dismiss"]', this.dismiss ); }, dismiss: function (e) { e.preventDefault(); var url = $( e.currentTarget ).attr('href'); var message = $(this.scope); ips.getAjax()(url).done(function(){ ips.utils.anim.go( 'fadeOut', message ); }).fail(function(){ window.location = url; }); } }); }(jQuery, _));
To explain what this is doing:
- Everything other than the contents of the initialize and dismiss functions is required code for JavaScript controllers. The ips.controller.register line specifies the name of the controller.
- When an element with a JavaScript controller attached to it is loaded (you'll attach it to the global message in a moment) the initialize function is run. The best practice is to only set up event handlers here and handle events in other functions. An event is being set up here to fire when any elements matching the selector [data-action="dismiss"] (you'll add that attribute to the close button in a moment) are clicked, and it calls the dismiss function when this happens.
- This dismiss function first prevents the event from performing it's default action (as a click will of course take the user to the target URL), then sets the variables needed (the URL that the button points to and the message box. It then sends an AJAX request to the URL that the button points to - if it succeeds, it fades out the box, and if it fails, it redirects the user to it just as if there was no JavaScript functionality. By redirecting to the original URL on fail, the user will be able to see the actual error that occurred.
To make it actually work, you need to specify that you want your message to use this controller. In your template, add data-controller="plugins.globalMessageDismiss" to the div element, and data-action="dismiss" to the a element.
For completeness, we should also adjust our action a little so that is aware of AJAX requests. While this is not strictly necessary, if it is not done, it is possible that if the redirect has to redirect to a screen which will display an error message, that the AJAX request will see this response and assume the request failed. Open up the action.php file you created in step 6 and change the redirect line to:
if ( \IPS\Request::i()->isAjax() ) { \IPS\Output::i()->sendOutput( NULL, 200 ); } else { \IPS\Output::i()->redirect( isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : \IPS\Http\Url::internal( '' ) ); }
Once this is done, closing your message will now fade out without a page reload.
Step 8: Downloading Your Plugin
Congratulations, you've just created your first plugin! You can download so that you can install it on other sites or distribute it through the IPS Marketplace from the developer center.
For reference, a downloaded version of the hook, as well as a zip of the plugin directory for development are attached:
Report Document