Skip to content

Latest commit

 

History

History
391 lines (325 loc) · 16.3 KB

File metadata and controls

391 lines (325 loc) · 16.3 KB

#Asynchronous gallery

To provide asynchronous functionality we have to reload separate parts of gallery/index.php view with out reloading whole page. So first of all we have to change view structure.

##Creating wright views

Definitely we have to reload our list of images, so let's create new view gallery/list.php:

<ul class="gallery">
    <?php iv('items')?>
</ul>

And of course we have to change the state of sorter links asynchronously so let's create gallery/sorter.php view:

<a href="<?php url_base('gallery', 'form')?>"><?php t('Upload photo')?></a>
<?php t('Sort by:')?>
<a class="sorter" href="<?php url_base('gallery', 'list', 'Loaded', 'ASC', 'current_page')?>"> <?php t('Date')?> ↗</a>
<a class="sorter" href="<?php url_base('gallery', 'list', 'Loaded', 'DESC', 'current_page')?>"> <?php t('Date')?> ↘</a>
<a class="sorter" href="<?php url_base('gallery', 'list', 'size', 'ASC', 'current_page')?>"> <?php t('Size')?> ↗</a>
<a class="sorter" href="<?php url_base('gallery', 'list', 'size', 'DESC', 'current_page')?>"> <?php t('Size')?> ↘</a>

Now we have to change our main view gallery/index.php in order to load all this separate views at one page.

<div class="top_menu">
   <div id="line1">
   <!--        Load sorter menu-->
       <?php iv('gallery_sorter')?>
   </div>
    <div id="line2">
        <!--        Load pager-->
        <ul id="pager"><?php iv('pager_html')?></ul>
        <!--        Load language switcher-->
        <?php m('i18n')->render('list')?>
    </div>
</div>
<div class="gallery-container">
    <!-- Load list of images-->
    <?php iv('gallery_list')?>
</div>

##Creating asynchronous controller

There is a build in ability to create special type of controllers that are executed separately in SamsonPHP core, not like standard controllers we call them asynchronous controllers. This controller must have async prefix. The main idea of async controller is that it must return asynchronous response associative array which must have one required key defined status, there are two possible values array('status' => '1') or array('status' => '0'). All asynchronous controllers are accessible via HTTP GET/POST/DELETE/PUT/UPDATE requests without _async_ prefix but must have special HTTP request header set or POST/GET field SJSAsync:true. If we have asynchronous controller action function gallery_async_list() then it will be accessible via HTTP GET /gallery/list. If asynchronous controller returns status == '1' then system automatically encodes it response status array into JSON using json_encode().

###Create asynchronous gallery_list controller

In our controllers/gallery.php we have to build gallery_async_list() function, which will provide all the same functionality as gallery_list, and then call this asynchronous function in our main listing function to provide initial load of the gallery.

/** Gallery images list asynchronous controller action */
function gallery_async_list($sorter = null, $direction = 'ASC', $currentPage = 1, $pageSize=4)
{
    // Set the $result['status'] to 1 to provide asynchronous functionality
    $result = array('status' => 1);

    // If no sorter is passed
    if (!isset($sorter)) {
        // Load sorter from session if it is there
        $sorter = isset($_SESSION['sorter']) ? $_SESSION['sorter'] : null;
        $direction = isset($_SESSION['direction']) ? $_SESSION['direction'] : null;
    }

    if (!isset($currentPage)) {
        // Load current page from session if it is there
        $currentPage = isset($_SESSION['SamsonPager_current_page']) ? $_SESSION['SamsonPager_current_page'] : 1;
    }

    // Prepare db query object
    $query = dbQuery('gallery');

    // Set the prefix for pager
    $urlPrefix = "gallery/list/".$sorter."/".$direction."/";
    // Count the number of images in query
    $rowsCount = $query->count();

    // Create a new instance of Pager
    $pager = new \samson\pager\Pager($currentPage, $pageSize, $urlPrefix, $rowsCount);

    // Set the limit condition to db request
    $query->limit($pager->start, $pager->end);

    if (isset($sorter) && in_array($sorter, array('Loaded', 'size'))) {
        // Add sorting condition to db request
        $query->order_by($sorter, $direction);

        // Store sorting in a session
        $_SESSION['sorter'] = $sorter;
        $_SESSION['direction'] = $direction;
    }

    // Iterate all records from "gallery" table
    $items = '';
    foreach ($query->exec() as $dbItem) {
        /**@var \samson\activerecord\gallery $dbItem``` */

        /*
         * Render view(output method) and pass object received fron DB and
         * prefix all its fields with "image_", return and gather this outputs
         * in $items
         */
        $items .= m()->view('gallery/item')->image($dbItem)->output();
    }

    // Include the data about images in the result array
    $result['list'] = m()->view('gallery/list')->items($items)->output();
    // Include the data about Pager state in the result array
    $result['pager'] = $pager->toHTML();
    // Include the data about sorter links state in the result array
    $result['sorter'] = m()->view('gallery/sorter')->current_page($currentPage)->output();

    return $result;
}

Now let's call this function and use its results to load main view.

/** Gallery images list controller action */
function gallery_list($sorter = null, $direction = 'ASC', $currentPage = 1, $pageSize=4)
{

    $gallery = gallery_async_list($sorter, $direction, $currentPage, $pageSize);

    /* Set window title and view to render, pass items variable to view, pass the Pager and current page to view*/
    m()->view('gallery/index')->title('My gallery')->gallery_list($gallery['list'])->gallery_sorter($gallery['sorter'])->pager_html($gallery['pager']);
}

###Building javascript

In yours project root folder you can find www/js/index.js file where you can write all necessary javascript code. There is a build in js library with a syntax similar jQuery, so it would be easy to work with this library. Just to understand how it works, let's add an update button to our gallery/index view.

      <a class="btn_update" href="<?php url_base('gallery', 'list')?>">Update</a>

Now in our www/js/index.js we will create an ajax request to reload list of images without reloading whole page.

s(document.body).pageInit(function(body){
    s('.btn_update').ajaxClick(function(response){
           console.log(response.status);
           console.log(response.list);
        });
});

In this example s(document.body).pageInit(function(body){ stands for $(document).ready(function(){ in jQuery. So in the console we would find an 1 and an html view of images. ajaxClick send the json encoded information and we can use the response as an object to load separate pars of our view. Now lets create the function to work with our page asynchronously.

var load = function(response)
{
    if (response && response.list) {
        s('.gallery-container').html(response.list);
        s('#pager').html(response.pager);
        s('#line1'.html(response.sorter);
    }
    
    // Function suppose to call itself after every asynchronous action in the page
    s('li a', pager).ajaxClick(load);
    s('.sorter').ajaxClick(load);
}
//Call this function when page is loaded for the firs time 
s('#pager').pageInit(load);

##Asynchronous delete function

For now we provided listing of images asynchronously. But what happens if we delete an image? The page will reload, so now we have to rebuilt function gallery_delete() controller in order to make this action asynchronous. When the image will be deleted we want to reload list of items in the page, to make it smother we will use the asynchronous controller chain. To use it we just form HTTP request consisted of both controllers calls GET /gallery/delete/[id]/list. System automatically parses URL and match every asynchronous controller action call and executes each controller one by one. If one of the controllers returns status == '0' chain fails and response is generated returning HTTP 404 status. All of the asynchronous response status arrays returned by each executed controllers are merged into one single array. So let's replace function gallery_delete() with asynchronous function gallery_async_delete().

function gallery_async_delete($id)
{
    // Set the result status to 0 by default 
    $result = array('status' => 0);

    /** @var \samson\activerecord\gallery $dbItem */
    $dbItem = null;
    if (dbQuery('gallery')->id($id)->first($dbItem)) {
        // Delete uploaded file
        unlink($dbItem->Src);
        // Delete DB record about this file
        $dbItem->delete();
        // If deleted change the result status to 1
        $result['status'] = 1;
    }

    return $result;

}

And now as we want to use asynchronous controller chain we have to change delete button request in the gallery/item.php view.

<a class="btn delete" title="<?php t('Delete')?>" href="<?php url_base('gallery', 'delete', 'image_PhotoID', 'list', 'sorter',  'direction', 'current_page')?>">X</a>

Now when we create a list of items in our gallery_async_list() controller we have to send 'sorter', 'direction', 'current_page' variables to the gallery/item.php view. So let's modify this controller.

foreach ($query->exec() as $dbItem) {
        /**@var \samson\activerecord\gallery $dbItem``` */

        /*
         * Render view(output method) and pass object received fron DB and
         * prefix all its fields with "image_", return and gather this outputs
         * in $items
         */
        $items .= m()->view('gallery/item')->image($dbItem)->sorter($sorter)->direction($direction)->current_page($currentPage)->output();
    }

We have to modify our www/js/index.js to provide this functionality. All we need to do is to include another ajaxClick action into our load function.

var load = function(response)
{
    if (response && response.list) {
        s('.gallery-container').html(response.list);
        s('#pager').html(response.pager);
        s('#line1'.html(response.sorter);
    }

    s('li a', pager).ajaxClick(load);
    s('.sorter').ajaxClick(load);
    s('.delete').ajaxClick(load, function(btn){
        return confirm(s('.delete_message', btn.parent()).val());
    });
}

As a second parameter ajaxClick() takes beforeHandler function which returns true or false depending on confirmation of the user. If true is returned action will be executed. Now let's add a confirm message in gallery/item.php view.

    <a class="btn delete" title="<?php t('Delete')?>" href="<?php url_base('gallery', 'delete', 'image_PhotoID', 'list', 'sorter',  'direction', 'current_page')?>">X</a>
    <input class="delete_message" type="hidden" value="<?php t('Delete img')?>:<?php iv('image_Name')?>">

We added new piece of text that suppose to be translated by our i18n module. Now we have to add the translation into app/i18n/dictionary.php. Just add one more element to the array.

function dictionary()
{			
return array(	
		"ru"	=>array(
    "Delete img" => "Удалить картинку",
    ...

##Asynchronous upload form using js_tinybox module

First of all we have to include "samsonos/js_tinybox":"*", to the composer.json file and make a composer update. Now we can use tinyboxAjax() function which is created to work with forms. Before we go to the javascript we have to rebuild our controllers connected with upload form into asynchronous controllers.

/**
 * Gallery form controller action
 * @var string $id Item identifier
 */
function gallery_async_form($id = null)
{
    $result = array('status' => 1);

    /**@var \samson\activerecord\gallery $dbItem */
    $dbItem = null;
    /*
     * Try to recieve one first record from DB by identifier,
     * if $id == null the request will fail anyway, and in case
     * of success store record into $dbItem variable
     */
    if (dbQuery('gallery')->id($id)->first($dbItem)) {
        $form = m()->view('gallery/form/newfile')->image($dbItem)->output();
        // Render the form to redact item
        $result['form'] = m()->view('gallery/form/index')->title('Redact form')->image($dbItem)->form($form)->output();
    } elseif (isset($id)) {
        // File with passed ID wasn't find in DB
        $result['form'] = m()->view('gallery/form/notfoundID')->title('Not Found')->output();
    } else {
        // No ID was passed
        $result['form'] = m()->view('gallery/form/newfile')->title('New Photo')->output();
    }
    return $result;
}

/**
 * Gallery form controller action
 * @var string $id Item identifier
 */
function gallery_async_save()
{
    $result = array('status' => 0);
    // If we have really received form data
    if (isset($_POST)) {

        /** @var \samson\activerecord\gallery $dbItem */
        $dbItem = null;

        // Clear received variable
        $id = isset($_POST['id']) ? filter_var($_POST['id']) : null;

        /*
         * Try to receive one first record from DB by identifier,
         * in case of success store record into $dbItem variable,
         * otherwise create new gallery item
         */
        if (!dbQuery('gallery')->id($id)->first($dbItem)) {
            // Create new instance but without creating a db record
            $dbItem = new \samson\activerecord\gallery(false);
        }


        // Save image name
        if (isset($_POST['name'])) {
            $dbItem->Name = filter_var($_POST['name']);
            $dbItem->save();
            $result = array('status' => 1);
        }

        // At this point we can guarantee that $dbItem is not empty
        if (isset($_FILES['file']['tmp_name']) && $_FILES['file']['tmp_name'] != null) {
            $tmp_name = $_FILES["file"]["tmp_name"];
            $name = $_FILES["file"]["name"];

            // Create upload dir with correct rights
            if (!file_exists('upload')) {
                mkdir('upload', 0775);
            }

            $src = 'upload/' . md5(time() . $name);

            // If file has been created
            if (move_uploaded_file($tmp_name, $src)) {
                // Store file in upload dir
                $dbItem->Src = $src;
                $dbItem->size = $_FILES["file"]["size"];
                $dbItem->Name = $name;
                // Save image
                $dbItem->save();
                $result = array('status' => 1);
            }

        }

    }

    return $result;
}

Now in ```www/js/index.js``` we have to create javascript function to show our ```gallery/form``` view using tinybox. To make this function work asynchronously we have to call it within our ```load()``` function.
```javascript
function edit(btn){
    btn.tinyboxAjax({
        // Set the response container name 
        html : 'form',
        // Close tinybox on click elsewhere besides the box
        oneClickClose : true,
        renderedHandler : function(form, tb) {
            var uploadForm = s('form', form);
            uploadForm.ajaxSubmit(function(response){
                // Call load function after uploading the file
                load(response);
                // Close tinybox
                tb.close();
            });
        }
    });

}

var load = function(response)
{
    container = s('.gallery-container');
    pager = s('#pager');
    sorter = s('#line1');
    form = s('.tinybox');

    if (response && response.list) {
        container.html(response.list);
        pager.html(response.pager);
        sorter.html(response.sorter);
    }

    s('li a', pager).ajaxClick(load);
    s('.sorter').ajaxClick(load);
    s('.delete').ajaxClick(load, function(btn){
        return confirm(s('.delete_message', btn.parent()).val());
    });
    edit(s('.upload_btn'));
    edit(s('.edit'));
}

s('#pager').pageInit(load);

As we want to show uploaded picture at once after uploading we have to add asynchronous controller chain call when we submit upload form. So we have to change form action in the gallery/form/newfile.php view.

<form action="<?php url_base('gallery', 'save', 'list', 'Loaded', 'DESC', '1')?>/" method="post" enctype="multipart/form-data">