Build a module : Backend (2)

In the part 1 & 2 of this tutorial, we created one module which manage "authors".

Now, we want to link these authors to article.

To do that, we need one window to display the author list when we're editing one article.

From this window, we will be able to drag authors from the authors list and drop them to the article.

We will display the list of authors and one button to add authors in the options panel of the article.

Activate the "addons" placeholderTop of Page

Module placeholders & placeholders views

In the Ionize back-office, articles and pages have placeholders for modules, which can display views from modules.

These placeholders are named, and loads the modules corresponding views if they exists.

Modules placeholders are available for panels, and each panel is considered like a "parent" for the module :

  • Article edition panel. Parent code : 'article',
  • Page edition panel. Parent code : 'page'
  • Media edition panel. Parent code : 'media'

The module placeholder views are called "add ons".

Ionize Module : Placeholders

On article and page edition panels, they are 3 available module placeholders :

  • main_top : Displays the view after the page or article title
  • options_top : Displays the module view before the options accordion
  • options_bottom : Displays the module view after the options accordion

Activate the "addon" placeholder

One module addon is the view which will be displayed in one placeholder.

In the part 1 of this tutorial, we created the module default admin controller, /modules/Demo/controllers/admin/demo.php.

This controller currently just outputs the "admin panel" of the module.

By adding one method to this controller, we will be able to load module's placeholder views.

First, let be organized and create one view folder for the addons :

modules /
 Demo/
 views/
 admin/
 addons/
 article/


This folder organization is not needed, but is a good practice to organize views more logically and will help other developers to faster understand your module.

Edit the file /modules/Demo/controllers/admin/demo.php and add the following method :

/**
 * Adds "Addons" to core panels
 * @param Array The current edited object (page, article, ...)
 *
 */
public function _addons($object = array())
{
    $CI =& get_instance();

    // Send the article to the view
    $data['article'] = $object;

    // Options panel Top Addon
    $CI->load_addon_view(
        'demo', // Module folder
        'article', // Parent panel code
        'options_top', // Placehoder code
        'admin/addons/article/options', // View to display in the placeholder
        $data // Data send to the view
    );
}


Some explanation :

This method receives the current edited object as an array. If you're editing one page, it receive the page array, if you're editing one article, it receive the article array.

The call to $CI->load_addon_view() loads the asked view in the wished placeholder.
In this example, we tell the loader that the module is 'demo', that the parent panel is 'article' and that the view 'admin/addons/article/options' must be displayed in the placeholder 'options_top' of the parent panel.

From article, display the authorsTop of Page

To do that, we will open one window which will display the authors list.

We will now need :

  • One view to display in the article panel placeholder.
  • One controller method to display the list of authors who are linked to the edited article
  • One model method to get the list of linked authors from the database.

Modify the controller

Modify the controller /modules/Demo/controllers/admin/author.php and add the methods :

/**
 * Displays the list of linked authors
 *
 */
public function get_linked_authors()
{
    $parent = $this->input->post('parent');
    $id_parent = $this->input->post('id_parent');

    $this->template['authors'] = array();
    $this->template['parent'] = $parent;
    $this->template['id_parent'] = $id_parent;

    if ($parent && $id_parent)
    {
        $this->template['authors'] = $this->author_model->get_linked_author($parent, $id_parent);
    }
    $this->output('admin/addons/article/authors');
}

Modify the model

Modify the model /modules/Demo/models/demo_author_model.php and add the method :

public function get_linked_author($parent, $id_parent)
{
    // Returned data
    $data = array();

    // Conditions
    $where = array(
        'parent' => $parent,
        'id_parent' => $id_parent,
        $this->_author_lang_table.'.lang' => Settings::get_lang('default')
    );

    $query = $this->{$this->db_group}
        ->where($where)
        ->order_by('ordering ASC')
        ->join(
         $this->_author_table,
         $this->_author_table.'.id_author = ' . $this->_link_table.'.id_author',
         'left'
     )
        ->join(
        $this->_author_lang_table,
        $this->_author_lang_table.'.id_author = ' . $this->_author_table.'.id_author',
        'left'
     )
        ->get($this->_link_table)
    ;

    if ( $query->num_rows() > 0 )
        $data = $query->result_array();

    return $data;


Create the view to display linked authors

Create the view /modules/Demo/views/admin/addons/article/authors.php :

This view will display the authors who are linked to the current edited article.

<?php if ( ! empty($authors)) :?>

<ul id="demoAuthorsList">

    <?php foreach($authors as $author) :?>

    <li class="sortme">
        <a class="title"><?php echo $author['name'] ;?></a>
    </li>

    <?php endforeach; ?>

</ul>

<?php endif; ?>

Create the view /modules/Demo/views/admin/addons/article/options.php :

This view contains the button "Add authors" and the container for the authors list.

<h3 class="toggler toggler-options"><?php echo lang('module_demo_title_authors'); ?></h3>

<div class="element element-options">

    <div class="element-content">

        <!-- Linked Authors container -->
        <?php if ($article['id_article'] != '') :?>

        <div id="demoAuthorsContainer"></div>

        <?php endif ;?>

        <!--
            Button : Link one author
        -->
        <a id="btnDemoLinkAuthor" class="button light plus">
            <i class="icon-plus"></i>
            <?php echo lang('module_demo_button_link_authors') ?>
        </a>

    </div>
</div>

<script type="text/javascript">

    // Opens the authors window
    $('btnDemoLinkAuthor').addEvent('click', function()
    {
        // See : /themes/admin/javascript/ionize/ionize_window.js
        ION.dataWindow(
            'demoAuthors', // ID of the window
            'module_demo_title_link_authors', // Lang term used for window title
            // URL to the content of the window
            ION.adminUrl + 'module/demo/author/get_list',
            // Window options
            {
                'width':400,
                'height':250
            },
            // Data to send by POST to the called URL
            {
                'id_article': '<?php echo $article['id_article'] ?>'
            }
        );
    });

    // Linked creators : Called when this view is loaded
    if ($('demoAuthorsContainer'))
    {
        ION.HTML(
            admin_url + 'module/demo/author/get_linked_authors',
            {
                'parent': 'article',
                'id_parent': '<?= $article['id_article'] ?>'
            },
            {'update': 'demoAuthorsContainer'}
        );
    }

</script>


Now, we have a nice "Add Authors" button in the article options panel.
One click on this button will open one window containing the list of authors.

The authors placeholder is from now also supposed to load the linked authors list.

To go further (optional) :

As you noticed it, we used the same controller to list the authors in this authors windows than to list the authors in the module's administration view. For this reason, when we click on one author, it opens the author edition window.

Unfortunately, editing or delete one author from this window will not update the window list, because this window does not contains the "author list container" with the ID "moduleDemoAuthorsList" (this container is in the view /modules/Demo/views/admin/demo.php).

We can modify the "save" and "delete" method of the author controller so that this list is also updated.

Using Firebug to have a look at the HTML DOM elements is strongly recommended to understand this step.

  1. Get the HTML selector element
    The ID of the window is "demoAuthors".
    Ionize automatically adds one "w" in front of this ID to identify the window.
    So we know that the window has the ID "wdemoAuthors"
    The content area of each Ionize window has the class ".mochaContent"
    The complete selector to the HTML container is then : "wdemoAuthors .mochaContent"
  2. Add the update callback to the controller:
    In the controller /modules/Demo/admin/controllers/author.php:
    Replace all occurrence of :
    $this->update[] = array(
        'element' => 'moduleDemoAuthorsList',
        'url' => admin_url() . 'module/demo/author/get_list'
    );

    by :

    $this->update[] = array(
        'element' => 'moduleDemoAuthorsList, #wdemoAuthors .mochaContent',
        'url' => admin_url() . 'module/demo/author/get_list'
    );

But for the moment, we don't have any behavior to link one author to the article.

Drag'n'drop authors to articleTop of Page

We will need :

  • Drag event on each author from the list,
  • One drop area to drop the dragged author on,
  • One controller method to link one author to the article (called when the author is dropped on the "drop area"
  • One corresponding model to create the link author / article in database.

Prepare the Drag'n'drop

Edit the file /modules/Demo/views/admin/author_list.php and add the following lines in the javascript block :

<script type="text/javascript">

    // Click Event to display the details of one creator
    $$('.authorPanelList li').each(function(item, idx)
    {
        /* Existing code … After a.addEvent('click', function(e) */

        // Adds Drag'n'Drop behavior on each author name
        ION.addDragDrop(
                a,                                  // DOM element to drag
                '.dropDemoAuthor',                  // Selector of the drop areas.
                'DEMO_MODULE.dropAuthorOnParent' // Method to execute when the dragged element is dropped
        );
    });

</script>



The DEMO_MODULE javascript object doesn't exists.
Let's create it !

Create the folder :

modules /
  Demo/
    assets/
      javascript/

Create the file /modules/Demo/assets/javascript/admin.js and add the code:

var DEMO_MODULE = (DEMO_MODULE || {});

DEMO_MODULE.append = function(hash){
    Object.append(DEMO_MODULE, hash);
}.bind(DEMO_MODULE);

DEMO_MODULE.append(
{
    baseUrl: base_url,
    adminUrl: admin_url,
    moduleUrl: admin_url + 'module/demo/',

    /**
     * Called when one author is dropped to one droppable element
     * This method receives :
     *
     * @param DOM element Dragged clone of the element
     * @param DOM element DOM Element on which the element is dropped
     * @param event The event
     *
     */
    dropAuthorOnParent: function(element, droppable, event)
    {
        ION.JSON(
                this.moduleUrl + 'author/add_link',
                {
                    'parent': droppable.getProperty('data-parent'),
                    'id_parent': droppable.getProperty('data-parent-id'),
                    'id_author': element.getProperty('data-id')
                }
        );
    }
});


Some explanation :

If it exists, the Javascript file /modules/Demo/assets/javascript/admin.js is auto loaded at Ionize backend satrtup.

In the /modules/Demo/views/admin/author_list.php view, we added drag'n'drop capabilities to the title of each author in the author list. This is done with the ION.addDragDrop() method which is waiting for:

  1. The element to drag
  2. The selector (CSS class) of droppable DOM elements
  3. One callback method name. This method will be executed when the element is dropped to one droppable DOM element.

The callback method DEMO_MODULE.dropAuthorOnParent() will automatically receive :

  1. The dropped element
  2. The droppable element
  3. The drag'n'drop event

With that, we will be able to get properties from the dragged element but also from the dropped element, exactly what is done by dropAuthorOnParent() which call the controller's method "add_link()" and send him the needed data.

We now need to add the droppable area in the module view which is displayed in the options panel.

Edit the file /modules/Demo/views/admin/addons/article/options.php :

Before :

<!-- Linked Authors container -->

Add :

<!-- Droppable area --> 
<div class="droppable dropDemoAuthor" data-parent="article" data-parent-id="<?php echo $article['id_article'];?>">
    <?php echo lang('module_demo_label_drop_author'); ?>
</div>

We see that the droppable area has the parent code ('article') and the parent ID as HTML attributes.

Modify the model

Edit the file /modules/Demo/models/demo_author_model.php and add the following methods:

    /**
     * Creates one link between one parent and one author
     *
     * @param string Parent code (article, page)
     * @param int Parent ID
     * @param int Author ID
     *
     * @return bool TRUE if inserted, FALSE if the link already exists
     *
     */
    public function link_author_to_parent($parent, $id_parent, $id_author)
    {
        $data = array(
            'parent' => $parent,
            'id_parent' => $id_parent,
            'id_author' => $id_author
        );
        $this->db->where($data);
    
        $query = $this->{$this->db_group}
            ->where($data)
            ->get($this->_link_table);
    
        if ($query->num_rows() == 0)
        {
            $this->{$this->db_group}->insert($this->_link_table, $data);
            return TRUE;
        }
        return FALSE;
    }

Modify the controller

Edit the file /modules/Demo/controllers/admin/author.php and add the following methods

    /**
     * Links one Author with one parent
     *
     */
    public function add_link()
    {
        $parent = $this->input->post('parent');
        $id_parent = $this->input->post('id_parent');
        $id_author = $this->input->post('id_author');
    
        if ($this->author_model->link_author_to_parent($parent, $id_parent, $id_author))
        {
            // Set the callbacks
            $this->update_dom_linked_authors($parent, $id_parent, $id_author);
    
            // Send the user a message
            $this->success(lang('ionize_message_operation_ok'));
        }
        else
        {
            // Send the user a message
            $this->error(lang('module_demo_message_author_already_linked'));
        }
    }
    
    /**
     * Send the callback to update the linked authors list
     *
     * @param string Parent code
     * @param int Parent ID
     *
     */
    protected function update_dom_linked_authors($parent, $id_parent)
    {
        $this->callback = array(
            array(
                'fn' => 'ION.HTML',
                'args' => array(
                    'module/demo/author/get_linked_authors',
                    array('parent' => $parent,'id_parent' => $id_parent),
                    array('update' => 'demoAuthorsContainer')
                )
            )
        );
    }

The add_link() method calls the model to create the link between the author and one article and then call the update_dom_linked_authors() method, which returns to the XHR request the name of the callback method to execute in order to refresh the authors list.

This refresh code is stored in a dedicated method because it will also be called when one author is unlinked from one article.

Unlink authors form articlesTop of Page

We will now add one "unlink" button to each author.

Modify the view

Modify the view /modules/Demo/views/admin/addons/article/authors.php :

<?php if ( ! empty($authors)) :?>

    <ul id="demoAuthorsList">
    
        <?php foreach($authors as $author) :?>
    
        <li class="sortme">
            <a class="title"><?php echo $author['name'] ;?></a>
            <!-- Unlink icon -->
            <a class="icon unlink right" data-id="<?php echo $author['id_author'] ;?>"></a>
        </li>
    
        <?php endforeach; ?>
    
    </ul>
    
    <script type="text/javascript">
    
        $$('#demoAuthorsList li').each(function(item)
        {
            var unlinkIcon = item.getElement('.unlink');
    
            ION.initRequestEvent(
                    // Element to add the request on
                    unlinkIcon,
                    // URL to send the data
                    '<?= admin_url() ?>/module/demo/author/unlink',
                    // Data send by POST to the URL
                    {
                        'parent': '<?php echo $parent ?>',
                        'id_parent': '<?= $id_parent ?>',
                        'id_author': unlinkIcon.getProperty('data-id')
                    }
            );
        });
    
    </script>

<?php endif; ?>    



Here, one JS XHR request event is added to each "unlink" icon of the author list.

This request will call the method unlink() from the author controller.

Modify the controller

Modify the controller /modules/Demo/controllers/admin/author.php and add the "unlink()" method :

    public function unlink()
    {
        $parent = $this->input->post('parent');
        $id_parent = $this->input->post('id_parent');
        $id_author = $this->input->post('id_author');
    
        $this->author_model->unlink_author_from_parent($parent, $id_parent, $id_author);
    
        // Set the callbacks
        $this->update_dom_linked_authors($parent, $id_parent, $id_author);
    
        // Direct output, without message
        $this->response();
    }


Modify the model

Edit the file /modules/Demo/models/demo_author_model.php and add the following methods:

    /**
     * Deletes on link between one author and one parent
     *
     * @param string Parent code (article, page)
     * @param int Parent ID
     * @param int Author ID
     *
     * @return int Number of affected rows (1 or 0)
     *
     */
    public function unlink_author_from_parent($parent, $id_parent, $id_author)
    {
        $where = array(
            'parent' => $parent,
            'id_parent' => $id_parent,
            'id_author' => $id_author
        );
    
        return $this->{$this->db_group}->delete($this->_link_table, $where);
    }


Conclusion

That's it!

We now have a module which manage authors but which also interact with articles!

In the next part, we will create some tags to display the authors of each article on the website.