Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Try Vanilla Forums Cloud product
Vanilla 2.6 is here! It includes security fixes and requires PHP 7.0. We have therefore ALSO released Vanilla 2.5.2 with security patches if you are still on PHP 5.6 to give you additional time to upgrade.

How do I write a Plugin with a Module?

hgtonighthgtonight ∞ · New Moderator
This discussion was created from comments split from: Template Errors.

Search first

Check out the Documentation! We are always looking for new content and pull requests.

Click on insightful, awesome, and funny reactions to thank community volunteers for their valuable posts.

peregrineilovetechSudoCat

Comments

  • Oh boy, I wish I could write my own Vanilla plugins! I wanted to learn how to do it for quite some time now, but never got around it.

    I had to implement a countdown button in the side panel for our forum just recently, which was when I discovered that I can't just insert PHP code into the default master template file.

    My search for more info brought me to this discussion, and further on how to implement a custom Smarty hook. So I wrote a function.countdown.php and put it into the library/vendors/SmartyPlugins folder. That solution is working just fine right now on my production site, but of course I know it's just not the proper way to do. If I could convert it to a plugin, I would be able to manage the countdown from the dashboard instead of having to enter the data directly as parameters in the Smarty hook in the template file. Hopefully at one point I find the time to play around with that idea...

  • R_JR_J Cheerleader & Troubleshooter Munich Moderator

    Just start, it's easy! And if you have written the logic and encapsulated it in smarty, you can transform it easily into a module. Look at that tutorial: http://vanillawiki.homebrewforums.net/index.php/How_to_Build_a_Simple_Module_and_Integrate_it_as_a_Plugin

    Come back at any time and ask if you get stuck

    MasterOnehgtonightSudoCat
  • @R_J, cool, a tutorial is exactly what I was looking for next. Will give it a read in my spare time, thanks. :)

    hgtonight
  • R_JR_J Cheerleader & Troubleshooter Munich Moderator
    edited June 2014

    Well, I'm in teaching mode, so here is step2: class.countdownmodule.php

    A module should do/show something and you have already written some code. Mind sharing it with us? We could implement it in the skeleton I'll construct in this post.

    Here we go. As I've said above, we need to create our own class and there is a naming convention we should take into account. Garden, the framework behind Vanilla, has some "base" classes that we normally extend when we are writing a plugin (no one has to start at zero, here). In step 1 we have used Gdn_Plugin and now that we are writing our module, we do this:

    class CountdownModule extends Gdn_Module {
    I guess there's nothing left to say...

    A module should have 2 functions "AssetTarget" that will hold the information where we want our module to show up and "ToString" which handles the output of the module.

    public function AssetTarget() {
       return 'Panel';
    }
    

    That was all we have to do to tell Vanilla that we want our module to show up in the panel. Not much functionality, not much code. I like that.

    public function ToString() {
       $CountdownModuleContent = <_<_<_EOS
    <_div id="MasterOnesCountdown" class="Box CountDownBox">
       <_h4>Fancy countdown timer!<_/h4>
       <_ul class="PanelInfo PanelCountdown">
          <_li>5<_/li>
          <_li>4<_/li>
          <_li>3<_/li>
          <_li>2<_/li>
          <_li>1<_/li>
       <_/ul>
    <_/div>
    EOS;
       echo $CountdownModuleContent;
    }
    

    So what have we done here? We've created the function ToString that I've mentioned above and let it echo some html. Beware! You do not have to return the string, you have to echo it! But maybe it works with a return also - it's up to you to do some testing again...
    Modules headings are h4 and they have the div.Box container so you should simply copy the styling of the standard modules in order to have a nice looking forum.

    Well, that's it. Not very spectacular, so let me simply repeat what I've said before:
    1. extend class Gdn_Module
    2. create a function "AssetTarget" that defines where this module should be shown
    3. create function "ToString" that echos out the html you want to include in your panel

    Not much more to say. It's really that simple. Here's the code for class.countdownmodule.php

    <_?php if (!defined('APPLICATION')) exit();
    class CountdownModule extends Gdn_Module {
       public function AssetTarget() {
          return 'Panel';
       }
    
       public function ToString() {
          $CountdownModuleContent = <_<_<_EOS
    <_div id="MasterOnesCountdown" class="Box CountDownBox">
       <_h4>Fancy countdown timer!<_/h4>
       <_ul class="PanelInfo PanelCountdown">
          <_li>5<_/li>
          <_li>4<_/li>
          <_li>3<_/li>
          <_li>2<_/li>
          <_li>1<_/li>
       <_/ul>
    <_/div>
    EOS;
          echo $CountdownModuleContent;
       }   
    }
    

    You can now try to implement your markup inside of the ToString function already :)
    By the way: modules are very flexible and there are lot of ways to achieve the same. You don't really need a dedicated AssetTarget or ToString function, but it is far more easier to teach it that way.

    Step 3 will be the config screen, but I'll wait until you tell us what should be in there.

    I think we'll do a step 4 which will include localization.

    Edit: sorry, markup is fucked up - just search for "<_" and replace by "<"

    MasterOnehgtonightperegrine
  • Wow, you are amazing, @R_J, this will be really a great help not only for me. Right now I really can't start playing around with this (have to work), but in the meantime I can let you know how exactly I have implemented that countdown functionality with my "hacky" style.

    library/vendors/SmartyPlugins/function.countdown.php

    <?php if (!defined('APPLICATION')) exit();
    /**
     * Writes the countdown to the page.
     *
     * @param array The parameters passed into the function. This currently takes no parameters.
     * @param Smarty The smarty object rendering the template.
     * @return The url.
     */
    function smarty_function_countdown($Params, &$Smarty) {
       $url = ArrayValue('url', $Params);
       $title = ArrayValue('title', $Params);
       $days = ArrayValue('days', $Params);
       $date = strtotime(ArrayValue('date', $Params));
       $remaining = $date - time();
       $days_remaining = floor($remaining / 86400) + 1;
       if ($days_remaining <= $days && $days_remaining >= 0) {
         $Result = '<a class="CountDown Day'.$days_remaining.'" href="'.$url.'" title="'.$title.'"></a>';
       } else {
         $Result = '';
       }
       return $Result;
    }
    

    The folder themes/name_of_theme/design contains 31 PNGs named from 00_days.png to 31_days.png and the following addition in custom.css:

    .CountDown {
        display: block;
        width: 200px;
        height: 118px;
        background-repeat: no-repeat;
        margin: 0 0 10px 0;
    }
    
    .Day31 {
        background-image: url("31_days.png");
    }
    
    .Day30 {
        background-image: url("30_days.png");
    }
    
    .
    .
    .
    
    .Day00 {
        background-image: url("00_days.png");
    }
    

    Then in themes/name_of_theme/views/default.master.tpl it was just placing the new Smarty hook at the right place:

          <div class="Row">
            <div id="Panel" class="Column PanelColumn">
              <a href="{link path="/"}">{logo}</a>
              {countdown date="June 28, 2014 2:00 AM" days="31" url="/discussion/617/a-little-something-new" title="Something NEW is coming!"}
    

    Here you can also see the parameters I'd like to have configurable in the dashboard.

    hgtonightperegrine
  • R_JR_J Cheerleader & Troubleshooter Munich Moderator

    Since you do not know how to use the configuration by now, use something like that in the ToString function:

    public function ToString() {
       $CountdownDate = "June 28, 2014 2:00 AM";
       $CountdownDays = "31";
       $CountdownUrl = "/discussion/617/a-little-something-new"; // I'd prefer "Target" instead of "Url"
       $CountdownTitle = "Something NEW is coming!";
       ...
    

    We'll take care for those vars in the next step.

    MasterOne
  • Very well, @R_J, the two files are in place now, I'm fine with $CountdownTarget, so let's stick with that one.

    If possible, can you please also show how to set permissions for accessing and configuring that plugin? I'd like users of a certain role to be able to set and start the countdown.

  • R_JR_J Cheerleader & Troubleshooter Munich Moderator

    We'll do that later on. Let me show you what is "normally" done and then we can tune one or two aspects to your needs.

    For example I wouldn't go with 31 pictures but either only one picture using css sprites (http://csssprites.com/) or using a nicely styled standard font.

    By the way: I will not write any "countdown" code - that's completely up to you. I show you how you could make your code into a module. It will be more satisfying seeing your own code running in your forum ;-)
    Moreover, I see one thing in your code that needs you to understand what happened in step 1&2 in order to implement in a clean way. So doing that by yourself will surely help you getting comfortable with coding for Vanilla.

    I start writing step3 down, but weather is great and I think I will be out most of the day...

    MasterOne
  • R_JR_J Cheerleader & Troubleshooter Munich Moderator

    Step 3 will be the hardest and longest part. Not because using the configuration is difficult, but because using forms include more steps by itself:
    1. design a form
    2. insert configuration values when the form is loaded
    3. save form values to configuration when a button is hit

    First let me begin with some general comments about configuration. You can store individual settings anywhere but there is a convention for plugin related settings. They should be saved inside conf/config.php. If you look at that file, you may seem some other plugins configurations. When we are finished, you will find your $CountdownTitle like that inside the config: $Configuration['Plugins']['Countdown']['Title'] = 'Something NEW is coming!';
    Afterwards you'll be able to do something like this echo C('Plugins.Countdown.Title', 'Optional default title if this setting is not yet made in the config');. That will either echo the config setting or - if that could not be found - the default we've specified.
    If you want to initially set a config value or change what is set in there, you can do it like that SaveToConfig('Plugins.Countdown.Title', 'Shiny new title');

    Knowing that, you can already make your plugin configurable! I've told you to set the $CountdownSomething variables in the ToString function. Instead of assigning string values to them, you can get the values from the config: $CountdownSomething = C('Plugins.Countdown.Something');. But you still have to change the config.php manually and we wanted to have a dedicated settings screen.

    Before we start with the form itself, let's take a look at the plugin. We have to add only one single line to the PluginInfo in order to get a (not-yet-functional) settings button to the plugin list where we can see our new plugin.

    $PluginInfo['Countdown'] = array(
       'Name' => 'Countdown',
       'Description' => "I don't know - it's your task to describe what we are doing here, MasterOne...",
       'Version' => '0.1',
       'Author' => 'MasterOne',
       'SettingsUrl' => '/dashboard/plugin/countdown'
    );
    

    Only the last line is new. Check the plugin list to see the new settings button. The url of this button is /dashboard/plugin/countdown and that could be read as "dashboard" = current application, "plugin" = controller and "countdown" = function in the plugin controller. Yes, we point our settings screen to a function inside of Garden controller. This might sound strange, but we have a way to define/create custom functions "inside" of controllers so that they will be treated as if they belong to them. Let's do exactly that:

    public function PluginController_Countdown_Create($Sender) {
       $Sender->Permission('Garden.Settings.Manage');
    
       $Sender->AddSideMenu('dashboard/settings/plugins');
    
       $Sender->Form = new Gdn_Form();
    
       $Validation = new Gdn_Validation();
       $ConfigurationModel = new Gdn_ConfigurationModel($Validation);
       $ConfigurationModel->SetField(array(
          'Plugins.Countdown.Title',
          'Plugins.Countdown.Target',
          'Plugins.Countdown.Date',
          'Plugins.Countdown.Days'
       ));
    
       $Sender->Form->SetModel($ConfigurationModel);
    
       if ($Sender->Form->AuthenticatedPostBack() === FALSE) {
          $Sender->Form->SetData($ConfigurationModel->Data);
       } else {
          if ($Sender->Form->Save() !== FALSE) {
             $Sender->StatusMessage = 'Your settings have been saved.';
          }
       }
    
       $Sender->Render($this->GetView('settings.php'));
    }
    

    public function PluginController_Countdown_Create($Sender) {
    We Create the function Countdown to be a part of the PluginController with that line. If you just put in the line echo 'Overwhelming!'; die; inside of this function and press the settings button in the plugins overview, you'll see how easy it is to create a setting screen! But that's not how we will continue...

    $Sender->Permission('Garden.Settings.Manage');
    By just inserting that line, the current users permissions are checked and if he hasn't the requested right, he'll see an error screen. You've asked for custom permissions and so we will come back later to this line. The requested permission here is a general permission.

    $Sender->AddSideMenu('dashboard/settings/plugins');
    That line will highlight the "Plugins" entry in the side menu, but you do not need to do it.

    $Sender->Form = new Gdn_Form();
    Garden has a lot of helpers that you can use when dealing with forms. A setting screen is a form and that's why we attach a form to our controller.

    $Validation = new Gdn_Validation();
    Validation is one other helper that we will use. You can do a lot with that, but we just need it for the ConfigurationModel...

    $ConfigurationModel = new Gdn_ConfigurationModel($Validation);
    There's magic in there. The "Model" in MVC is the part that stores and retrieves data. ConfigurationModel does exactly that and it's "backend" is the config file. We plan on getting information from the config and write to it and so having a model for that makes accessing the config quite easy. We have to give a special rule set to the ConfigurationModel and that rule set is defined in the Validation. That sounds quite complicated and I have to admit that I can not write such code down by heart. I always have to look at other plugins for code like that. But you do not have to understand each single step in order to start writing your first plugin.

    $ConfigurationModel->SetField(array(
       'Plugins.Countdown.Title',
       'Plugins.Countdown.Target',
       'Plugins.Countdown.Date',
       'Plugins.Countdown.Days'
    ));
    

    As I've said, the ConfigurationModel gives us access to the config.php but we certainly do not want to handle each value in there, but only the fields of your plugin. So we specify what those are.

    $Sender->Form->SetModel($ConfigurationModel);
    We can link a form and a model! Think about it: if there is a connection between a data source and input fields, it is obvious that we will not have much work to do. Displaying the stored values can be done by creating html elements with the correct ids (more on "correct" later) and the values of a form could be easily saved back. That's exactly what we've needed.

    if ($Sender->Form->AuthenticatedPostBack() === FALSE) {
    Here we check if the form already contains data that has been sent back to us or if we see the form for the first time.

    $Sender->Form->SetData($ConfigurationModel->Data);
    If the above check result was that we see the form for the first time, we want to see the current values of our defined fields in the form. I guess we can call that magic again: $ConfigurationModel->Data gets us an array of the desired config values. Form->SetData sets each form element that has an id which is included in the result array to the value of that array element! So that single line will prepopulate our form fields with the config values, great isn't it?

    } else {
       if ($Sender->Form->Save() !== FALSE) {
          $Sender->StatusMessage = 'Your settings have been saved.';
       }
    }
    

    If we do not see form for the first time, then the form has been submitted back to us and we now have to save it. We've linked the ConfigurationModel to the Form and so if we call the forms save function, the values will also be written to the config. A one-liner again! But we also check that the return process hasn't failed. If there was no error, we give a status message back to the user so that he knows that everything worked as expected.

    $Sender->Render($this->GetView('settings.php'));
    We've talked a lot about the form, defined the functionality behind it and now it is time to load and display it. The GetView function looks at several places for the specified file name. One of those places is the views subdirectory of the current files directory.

    That has been a lot of stuff although it has been not much more than a dozen lines of code. The form itself will be a piece of cake now that we have taken that step!

    MasterOne
  • R_JR_J Cheerleader & Troubleshooter Munich Moderator

    Here comes the form (/plugins/Countdown/views/settings.php):

    <?php defined('APPLICATION') or die();
    
    $Output = <<<EOS
    <_h1>Countdown Module<_/h1>
    <_div class="Info">Here you can configure all the countdown settings... (ugly description, but it's your job to think of something nice)<_/div>
    
    {$this->Form->Open()}
    {$this->Form->Errors()}
    
    <_ul>
       <_li>
          {$this->Form->Label('Countdown Title', 'Plugins.Countdown.Title')}
          {$this->Form->TextBox('Plugins.Countdown.Title')}
       <_/li>
       <_li>
          {$this->Form->Label('Countdown Target', 'Plugins.Countdown.Target')}
          {$this->Form->TextBox('Plugins.Countdown.Target')}
       <_/li>
       <_li>
          {$this->Form->Label('Countdown Date', 'Plugins.Countdown.Date')}
          {$this->Form->TextBox('Plugins.Countdown.Date')}
       <_/li>
       <_li>
          {$this->Form->Label('Countdown Days', 'Plugins.Countdown.Days')}
          {$this->Form->Date('Plugins.Countdown.Days')}
       <_/li>
    <_/ul>
    
    {$this->Form->Button('Save')}
    {$this->Form->Close()}
    
    EOS;
    
    echo $Output;
    

    (and I had to replace "<" by "<_" again)

    I will not explain every line since there's a lot of repetition and some trivial html in there. If you've read my previous post thoroughly, you'll be able to understand every single line immediately.

    {$this->Form->Open()}
    This will write an opening html form tag with the correct action (the current forms url) and method ("post") for us.

    {$this->Form->Errors()}
    I've spoken about "validation" before and if some of the input fields would be sent with content that doesn't adhere to the validation rules, the error message will be written back here with this line.

    {$this->Form->Label('Countdown Title', 'Plugins.Countdown.Title')} and
    {$this->Form->TextBox('Plugins.Countdown.Title')}
    There are some predefined form elements that we are using here. The Form->Label creates a label html element with the text "Countdown Title" and a for reference to "Plugins.Countdown.Title".
    The above Form->TextBox creates an input type text with the name "Plugins.Countdown.Title". Naming the input field like that is a requisite! Only if the html input elements have the same names as the config settings, the ConfigModel and the Form can work hand in hand.
    (The name "Plugins.Countdown.Title" gets converted to "Plugins-dot-Countdown-dot-Title" for the html element, by the way, but you don't have to care for that)

    We enclose our form elements in an ordered list. Why? Simply because the wise creators of Vanilla have decided to use it like that and so we have a predefined style for that. For this example I started by enclosing the form elements in paragraphs, but ended up looking in official plugins what markup has been used there. Copy whenever you can copy ;)

    {$this->Form->Label('Countdown Days', 'Plugins.Countdown.Days')}
    {$this->Form->Date('Plugins.Countdown.Days')}
    If you look close you'll see that we are using a form element called "Date" that you do not know from html. Correct. Get an inspiration of what form elements are accessible by looking at /library/core/class.form.php. There is some fine tuning possible for the Date "element" that we'll look at later on. What is important for you is to know that it is saved as "YYYY-MM-DD" inside of the config. In your example you've given an exact time. Think if this is really necessary.

    There's a lot more that you can know about each single step, but I think I've told you everything you have to knw to create a plugin that shows a module and has a configuration screen. I'll go through the code and give additional information to a few points. If something is not working as expected, give a short feedback and we'll get it up and running!

    MasterOne
  • R_JR_J Cheerleader & Troubleshooter Munich Moderator

    We've started with PluginInfo and I want to elaborate just a little bit on it again. There are quite a lot of options that you can specify here and we started with barebone, but there are some more that I would recommend to you:

    'MobileFriendly' => TRUE
    That decides if your plugin will be loaded for the mobile theme also. Default seems to be FALSE

    `'License' => 'Whatever your preferred license is'
    That will make it easier for other, honest authors to work with your code

    'AuthorEmail' => '...',
    'AuthorUrl' => '...',
    Just in case someone still has a question

    'HasLocale' => TRUE
    Make your plugin translatable! And perhaps include a translation file. We will do this for your plugin, too. But to be honest: that setting has no meaning at all. It is just a convention and not requested at any time ;-)

    'RequiredApplications' => array('Vanilla' => '2.1'),
    'RequiredTheme' => FALSE,
    'RequiredPlugins' => FALSE,
    Those are some other PluginInfo fields that might be of interest at some time. I would include the RequiredApplications in order to stress that you haven't tested you plugin with older versions.

    MasterOne
  • OK, I have put it all in place. Some things I'd like to comment on:

    @R_J said:
    For example I wouldn't go with 31 pictures but either only one picture using css sprites (http://csssprites.com/) or using a nicely styled standard font.

    I have implemented it with single pictures because it was the easiest that I can do on a short notice. Have a look at my test setup >>> here <<< to see my hacky-style implemention at work. That's exactly how I wanted it to look like, and I could not figure out a way to place the text in that button-link by just using the empty button as background and put the text in with some formating magic.

    Not sure about the advantage of converting it to a single CSS sprites picture.

    @R_J said:

     <li>
        {$this->Form->Label('Countdown Date', 'Plugins.Countdown.Date')}
        {$this->Form->TextBox('Plugins.Countdown.Date')}
     </li>
     <li>
        {$this->Form->Label('Countdown Days', 'Plugins.Countdown.Days')}
        {$this->Form->Date('Plugins.Countdown.Days')}
     </li>
    

    I think you meant the Date element the other way around:

       <li>
          {$this->Form->Label('Countdown Date', 'Plugins.Countdown.Date')}
          {$this->Form->Date('Plugins.Countdown.Date')}
       </li>
       <li>
          {$this->Form->Label('Countdown Days', 'Plugins.Countdown.Days')}
          {$this->Form->TextBox('Plugins.Countdown.Days')}
       </li>
    

    @R_J said:
    There is some fine tuning possible for the Date "element" that we'll look at later on. What is important for you is to know that it is saved as "YYYY-MM-DD" inside of the config. In your example you've given an exact time. Think if this is really necessary.

    Unfortunately it is necessary to have the time provided for the countdown calculation, because the day-switch has to happen at a certain daytime in a different time zone.

    Maybe the time should be put in its own variable and input field?

    @R_J said:
    'MobileFriendly' => TRUE

    Looks like this will be a little tricky, because in my desired end result the countdown is shown at the top of the side panel column below the site logo when in full mode, but put in the footer in line with the site logo when in mobile mode, because the mobile template has no side panel column (you can see the desired result in my above mentioned test setup).

    I really appreciate your outstanding help with this matter, it would have definitely taken a lot of time to figure this all out by myself if at all.

  • R_JR_J Cheerleader & Troubleshooter Munich Moderator

    CSS sprites only make sense for small graphics, so they wouldn't be the right solution in your case. Look at that short CSS hack: http://codepen.io/anon/pen/wEyLq
    Colors and size are not what you need, but I think it is a start to use pure CSS for what you are doing right now with a picture.

    Yes, I've mixed up Date and Day. If you need the time, you have a lot of options:
    1. use a normal text input and either trust the input or do some validations
    2. use the Date element and add another element: maybe a select field so that you can choose the time with a dropdown?
    3. search for a javascript date time picker like that: http://www.ama3.com/anytime/ It transforms a normal TextBox so that you can use all the advantages of Vanilla.

    I assume there is no problem with the mobile version. Try to set MobileFriendly to FALSE. It should be possible to include the module manually in your mobile themes default.master.tpl. Look at that code snippet: http://vanillaforums.org/discussion/comment/206387/#Comment_206387

    MasterOne
  • vrijvlindervrijvlinder Papillon-Sauvage MVP
  • MasterOneMasterOne ✭✭
    edited June 2014

    @R_J, a plethora of new information I have to process first, really cool stuff! Pure CSS really could be it, but is the shown CSS hack cross-browser compatible? With CSS I'm always afraid having to make specific adaptations for various browsers to have it look the same whichever browser the visitor has in use.

    @vrijvlinder‌, interesting idea, though the solution as it is implemented now and seen in the test installation mentioned above (without panel and logo links / countdown button at the bottom) pretty much already suits our needs. I guess you mean a side panel in a mobile theme that is hidden to have the full screen estate for the discussions and only gets visible on demand? I think that will confuse most users, because they are more used getting a menu when something opens, and not a whole panel. Is there any ready to test mobile theme which has it implemented that way?

  • vrijvlindervrijvlinder Papillon-Sauvage MVP
    edited June 2014

    No, I mean that if your mobile theme does not have the panel, simply add it to the tpl. then anything that is on the panel will also work for the mobile.

    Mobile themes by default don't contain the panel. I added it to my themes because I wanted to have the panel and it's contents also available for mobile.

    Adding the panel to the mobile will not make the page wider. It will simply appear above or bellow the content depending on where you add it in the master. Then add css for it to make it width 100% that will force it to the size of the mobile window.

    All my mobile themes have the panel . Just pick one and see how it's put together.

    Yellow-Mobile and Mobile are tpl based themes

    the others are php based mobile themes in case you want to compare those .

    MasterOne
  • R_JR_J Cheerleader & Troubleshooter Munich Moderator
    edited June 2014

    I wanted to go through the code and make some annotations.

    I've told you that your module needs two functions, but gave you a hint that they could be replaced. While I would always stick to the ToString function, I guess it would not be bad to replace AssetTarget.
    AssetTarget determines where your module should be rendered. Gave you following code:

    public function AssetTarget() {
       return 'Panel';
    }
    

    A function that returns only one value is no great function at all and so I think it is wise to think of other options to provide this information. Do you remember how we have added our module to the controller in the plugin code? $Sender->AddModule($CountdownModule); . So let's take a closer look at that "function AddModule". It is always helpful to have the source code at hand and do a search. We will find the following line public function AddModule($Module, $AssetTarget = '') {"> public function AddModule($Module, $AssetTarget = '') {

    Obviously we could specify the AssetTarget already when we add our module to the controller! That's why you could replace $Sender->AddModule($CountdownModule); with $Sender->AddModule($CountdownModule, 'Panel'); and don't need that small function AssetTarget inside the module any more!

    Maybe something general again. I've said several times in this posting, that we added the module to the controller while you may be convinced that we've added it to the output. Yes, you are certainly right. But in order to get a full understanding of Vanilla, you need to have at least a few insights of what a MVC framework is. In fact it is no true MVC framework but it is usually called that way. Since it is not really MVC, it should be better called CMV: the browser passes the request to the Controller, the controller gets data from the Model and then gets the appropriate View. Our function AddModule is a function of the controller object that tells the controller how (where) to render our module. The controller is the traffic policeman that tells which code has to go where...

    vrijvlinder already told you that you can add the Panel asset to your mobile theme and that's a good solution, but it might lead to more modules inside of your mobile theme than you wanted. If that is the case, create a new asset in your mobile theme. Something like {asset name="MobileFooter"} .
    Then you can do the following in your module or in the plugin (although I've told you to get rid of the function AssetTarget, I'll use it again for this example because it is a standard function and in this case there will be logic inside of it):

    public function AssetTarget() {
       if(IsMobile()) {
          return 'MobileFooter';
       } else {
          return 'Panel';
       }
    }
    

    edit: using a non standard asset name will make your plugin unusable for others!

    MasterOneSudoCat
  • R_JR_J Cheerleader & Troubleshooter Munich Moderator

    @MasterOne said:
    but is the shown CSS hack cross-browser compatible?

    Don't know much about CSS. Check cross compatibility here: http://caniuse.com/#cats=CSS

    MasterOne
  • R_JR_J Cheerleader & Troubleshooter Munich Moderator

    Speaking of CSS: when you want to style your module, you should create a css file and save it in the subfolder "design" of your plugin. Call it "countdown.css" or "custom.css". Now we want to add that css just like we have added our module with $Sender->AddModule, right? Here is how it is done: put that code below the $Sender->AddModule line $Sender->AddCssFile('countdown.css', 'plugins/Countdown');
    Just like we have added the module to the controller, we can add also our css file to it! We pass the name of the css file and the path to our plugin to it and the function will automatically guess that we have saved that file in our "design" folder.

    That's comfortable, isn't it?

    One possible solution for your timestamp problem was the usage of some Javascript - guess what our next step is ;)

    We have seen that there is a convenient function for adding a css file, so wouldn't it be nice if there is something similar for javascript? At first we need the file somewhere. Create the subfolder "js" in your plugins directory. And then, just from the code above, what would you guess is the right function and syntax? $Sender->AddJsFile('yourfile.js', 'plugins/Countdown'); is what you have guessed and you are right!

    But to put that line in the function where we attach the module to the controller doesn't make sense. We need the js file when our settings screen is loaded and that happened in our public function PluginController_Countdown_Create($Sender). So put the line inside of that function and when you take a look at the html source of your settings screen, you will see our file has been loaded.

    MasterOne
  • R_JR_J Cheerleader & Troubleshooter Munich Moderator

    You've asked for custom setting permissions and that's what we do next. We have started the plugin part of our settings code like that:

    public function PluginController_Countdown_Create($Sender) {
       $Sender->Permission('Garden.Settings.Manage');
    

    I've told you that we do a standard permission check. Try to access the setting screen when you are not logged in: http://www.example.com/dashboard/plugin/countdown. You will be prompted to log in and if you login with a standard user account you will see an error message. That's good.
    Let's doublecheck and comment out the above permission check. Try to access this side again when you are not logged in... and OH MY GOD! The settings page is now accessible to guests! Well that looks severe, but in fact they are not able to change anything. But nevertheless, that is critical.
    Now log in as a normal user and visit that settings page. Bingo. Normal users are allowed to change the config settings if we do not use any permission check for that. That's why you always have to check for proper permissions when you create a custom setting screen.

    Checking for a permission could also mean checking for a custom permission. Here we are...

    Let's make that permission check: $Sender->Permission('Plugins.Countdown.Manage');. Yes! Bye bye guest access, bye bye user access. Only admin will be able to see your setting screen now (permission check do not work for admins, they are allowed to access anything). Now that we are checking for the right permission we would like to grant that permission to someone, maybe to moderators. But when you look at the permissions of the moderator role, you will not find that permission. We will have to create a new permission first before we can use it. But that again is most easy.

    I have told you something about the possible entries in PluginInfo array, but I left this one out: "RegisterPermissions". That's our solution. Simply add the following line somewhere to the PluginInfo array at the top of your plugin file:
    'RegisterPermissions' => array('Plugins.Countdown.Manage'),
    (By the way: the order of the entries in that array doesn't matter, but you have to take care that you separate each of the entries with a comma)

    That may look promising but when we look again at the permissions of our moderators, we cannot see our new permission. We first have to disable and reenable our plugin. Do that and then return to the permissions and you will see our newly created permission. Now assign that permission and try it out. You'll see that it works perfectly!

    To sum up what we need for a custom permission:
    1. create the new permission

    $PluginInfo['Countdown'] = array(
       'RegisterPermissions' => array('Plugins.Countdown.Manage'),
       'SettingsUrl' => '/dashboard/plugin/countdown',
       ...
    
    1. assign the new permission to some roles

    2. check for the correct permission before you render a view

    public function PluginController_Countdown_Create($Sender) {
          $Sender->Permission('Plugins.Countdown.Manage');
    

    And that's it.

    Now that the right user roles have access to your setting screen, you have to take care that they are able to find it ;)
    I think that this great tutorial describes a nice solution for that problem: http://vanillaforums.org/discussion/27099/tutorial-how-to-add-an-option-to-the-flyout-menu-in-2-1-and-add-link-in-dashboard-as-well

    MasterOne
  • JS was exactly the kind of magic to handle the date/time issue.

    @R_J said:
    3. search for a javascript date time picker like that: http://www.ama3.com/anytime/ It transforms a normal TextBox so that you can use all the advantages of Vanilla.

    Wow, that's nice date/time picker, never seen such before. That's the one I want to use (can't play around with it right now, I am at work, but will definitely try to get that one going as soon as I have some time).

    This all is really insightful, though I have to process all the info and play around with it.

  • R_JR_J Cheerleader & Troubleshooter Munich Moderator

    Take your time. I will go down through the code without waiting for you, but if you have any questions, just point to what is not clear and I'll try to explain it better.
    I'll have to write it down in time in order not to loose track what I have already spoken about... ;)

    MasterOne
  • R_JR_J Cheerleader & Troubleshooter Munich Moderator

    When speaking about the settings view, I haven't said much about the validation - I just said "we need it". Well, if we have something like a validation, let's use it! By now we do not validate anything. I think what we need at least is that every field is filled. So let's validate that!

    // we know that code already
    $Validation = new Gdn_Validation();
    $ConfigurationModel = new Gdn_ConfigurationModel($Validation);
    $ConfigurationModel->SetField(array(
       'Plugins.Countdown.Title',
       'Plugins.Countdown.Target',
       'Plugins.Countdown.Date',
       'Plugins.Countdown.Days'
    ));
    // let's start the validation!
    $ConfigurationModel->Validation->ApplyRule('Plugins.Countdown.Title', 'Required');
    $ConfigurationModel->Validation->ApplyRule('Plugins.Countdown.Target', 'Required');
    $ConfigurationModel->Validation->ApplyRule('Plugins.Countdown.Date', 'Required', 'No date, no countdown...');
    $ConfigurationModel->Validation->ApplyRule('Plugins.Countdown.Days', 'Required');
    

    As you see, there is a possibility to apply a rule to the fields. I've also provided one custom error message so that you see that it is possible. If the function is called "ApplyRule" and we have a rule called "Require" there's a big probability that there are other existing rules. Take a look at the source: https://github.com/vanilla/vanilla/blob/2.1/library/core/class.validation.php#L104-128
    See that there is "UrlString", "Timestamp" and "Integer"? Looks like we could do some more validation... :)

    $ConfigurationModel->Validation->ApplyRule('Plugins.Countdown.Title', 'Required');
    $ConfigurationModel->Validation->ApplyRule('Plugins.Countdown.Target', array('Required', 'WebAddress')); // will only accept full addresses that include http:// and domain
    $ConfigurationModel->Validation->ApplyRule('Plugins.Countdown.Date', 'Required', 'No date, no countdown...');
    $ConfigurationModel->Validation->ApplyRule('Plugins.Countdown.Days', array('Required', 'Integer'));
    

    Wait, I've told you above that there is a validation called Timestamp but I didn't included it! That's because I was curious and looked at the source code (functions.validation.php) and here's what I have found:

    function ValidateTimestamp($Value, $Field) {
       // TODO: VALIDATE A TIMESTAMP
       return FALSE;
    

    I'm glad I've looked that up. So you will be on your own when you try to validate that. Vanilla is great but not perfect. But there's always more than one way to reach the goal. We "simply" have to implement our own check.

    $Sender->Validation->AddRule('RegexCountdownTimestamp', 'regex:/ don\'t know what you need... /');
    $Sender->Validation->ApplyRule('Plugins.Countdown.Date', 'RegexCountdownTimestamp', "That's no valid time stamp");   
    

    At first we add a rule. We give it a name so that we can reference it later on and then we define some regex magic.
    Afterwards, we assign our new rule to the field like we have done with all the prebuild rules.
    And that's the best thing about validation if you ask me: you can let the input be validated against any custom regex on the server. Validations are one of my favorites.

    MasterOne
  • R_JR_J Cheerleader & Troubleshooter Munich Moderator

    I wanted to go through the code from top to bottom and add information whenever I think it could be useful. Now we are at a point that was hard for me to leave out on the first run: translations. It is really easy to make your code translatable and you should always do.

    Look at that line: $Sender->StatusMessage = 'Your settings have been saved.';. Before we continue, I have to beg your pardon for showing you StatusMessage. That function is deprecated and replaced by InformMessage - or ErrorMessage but let's hope we never need that ;)

    I guess there is a translation for the above sentence in any language pack, but it could not be applied to that line because we passed the string directly to the function. We need a translation function that will take care of translations. The function in Vanilla for that is simply called T. If we change the above line to $Sender->InformMessage = T('Your settings have been saved.'); the function looks up for a translation in the current active language pack and returns that translation if there is one. If there is no translation for that string, it will be returned. Here is a comprehensive HowTo for language files: http://vanillawiki.homebrewforums.net/index.php/Internationalization_&_Localization

    If you want to use that translation code in your view, you'll see that you have to change something. You cannot use a function inside of the heredoc style string (which I prefer for longer strings). Here's a workaround for that...
    Each controller has a public array called $Data that you can use as a temporary storage. So if that array contains a value for 'Title' we would be able to access it like that $this->Data['Title'] and that could be enclosed in our heredoc string.
    That's the code I wanted to note down originally for the view:

    < h1>{$this->Data['Title']}< /h1>
    < div class="Info">{$this->Data['Description']} < /div>
    

    So that the translation part isn't done inside of the view but somewhere else. In fact we have to fill those variables and that is done in the function that renders the settings view.

    $Sender->Data['Title'] = T('Countdown Settings');
    $Sender->Data['Description'] = T('Countdown description', 'Write here the long description that should be presented in the settings view.');
    $Sender->Render($this->GetView('settings.php'));
    

    But that's just a matter of taste and since this is my tutorial, I show you mine ;)

    MasterOne
  • R_JR_J Cheerleader & Troubleshooter Munich Moderator

    I guess that's all I have to say right now... But one thing to the Date field, although you will not use it if I got you right.

    $this->Form->Date('Plugins.Countdown.Date')
    We can add some parameters to that date field. Maybe you want to have another order of those fields? I prefer day, month, year and that's how it could be done: $this->Form->Date('Plugins.Countdown.Date', array ('Fields' => array('day', 'month', 'year'))). Seen that it only provides the current year and a lot of long past years? Try that: $this->Form->Date('Plugins.Countdown.Date', array ('Fields' => array('day', 'month', 'year'), 'YearRange' => '2012 - 2016')).
    If you do not want abbreviations for the month names, include the following in your language file:

    $Definition['Jan'] = 'January';
    $Definition['Feb'] = 'February';
    $Definition['Mar'] = 'March';
    $Definition['Apr'] = 'April';
    $Definition['May'] = 'May';
    $Definition['Jun'] = 'June';
    $Definition['Jul'] = 'July';
    $Definition['Aug'] = 'August';
    $Definition['Sep'] = 'September';
    $Definition['Oct'] = 'October';
    $Definition['Nov'] = 'November';
    $Definition['Dec'] = 'December';
    

    That's it! It was a pleasure :)

    MasterOne
  • It all makes so much sense, and it's too bad I'm so overloaded with work right now, I could play with module creation all day long.

    I guess I should consider putting those logo links, which I placed manually into the two template files (normal and mobile) as well:

    Placing logo-links in a row above {asset name="Content"}

    Is every possible modification suitable to be made into a module, or are there things which you consider better be put into the template?

    What's the difference between a module and an application (like Yaga is, which I just installed)?

  • hgtonighthgtonight ∞ · New Moderator

    @MasterOne said:
    What's the difference between a module and an application (like Yaga is, which I just installed)?

    There are 4 different types of addons in Garden (the framework that Vanilla runs on).

    • Themes
    • Locales
    • Plugins
    • Applications

    Themes change the way your site looks. Locales change the wording of text. Plugins add functionality. Applications can do all three. The lines blur a little when you start talking about theme hooks and overriding classes. There is also a push over on GitHub to lose the distinction between Applications and Plugins.

    Modules aren't on the list because Modules can't attach themselves to controllers. They have to be bundled with a plugin or application to be used.

    Search first

    Check out the Documentation! We are always looking for new content and pull requests.

    Click on insightful, awesome, and funny reactions to thank community volunteers for their valuable posts.

    MasterOne
  • R_JR_J Cheerleader & Troubleshooter Munich Moderator

    A module is a convenient helper class. You could achieve anything a module can do with just a normal plugin. But read that comment why it is wise to stick to modules nevertheless: http://vanillaforums.org/discussion/comment/207569/#Comment_207569

    I understand modules as what is called a block or something similar in some CMS. And I would only use it that way.
    But sometimes you cannot insert code at places that you need to. That's when a custom theme is needed. And I think it is an elegant solution to insert an asset at the place where your additional content should be. Then you can render your code with a module and set the appropriate AssetTarget.

    But as I said before: using a custom AssetTarget makes your plugin unusable for others. But there is something like a "solution" for that. In the PluginInfo array, you could specify 'RequiredTheme' => 'YourTheme' to stress that your plugin only works with your theme.

    MasterOne
Sign In or Register to comment.