⚠ In case you've missed it, we have migrated to our new website, with a brand new forum. For more details about the migration you can read our blog post for website migration. This is an archived forum. ⚠

  •     

profile picture

Multiple parallel edits using flexigrid



DrPaul
  • profile picture
  • Member

Posted 24 July 2014 - 14:10 PM

I've found GC an excellent framework for building a number of systems, and after producing 3 or 4 of them, there are some user requests which seem to be common across a range of applications. The biggest of these for me was a request to be able to edit more than one table row at a time - at the "bottom" of a typical database hierarchy, people often end up wanting to edit a small group of related rows at the same time.

 

In a "standard" GC implementation, you render a list view of the different rows, then "dip in" to edit one, "back out" to the list to select another, then edit that and so on. That has been a big usability issue for a couple of clients.

 

I had thought that I would need to break out into some custom code to achieve this, but finally worked out how to achieve it completely within GC - no mods required. In this specific instance, I'm modelling an event management scenario - where a given event will have an arbitrary number of staff positions, and each staff position will have a separate booking for each day of the event. So from the "staff position" list view we want to be able to edit every daily booking for a given position on a single edit screen.

 

You will need to create two controllers and two views - one pair will be used to provide the outer view, the other will provide the content of the inner multiple edit views.

 

In the outer view you will need some code like this, where I read in the primary keys for the bookings for a given position primary key (passed into the view in the controller code as detailed elsewhere):

 

  $bookings = get_pos_bookings_as_string($pwkdata['pid']);
  $book_array = array();
  $book_array = explode(',', $bookings);
  $iframes = '<table><tr>';
  
  foreach($book_array as $bid) {
  $iframes .= '<td><iframe src="'.site_url('/booking/bookings_multi_item/edit/'.$bid).'" width="560" height="1" name="frame_'.$bid.'" id="frame_'.$bid.'" scrolling="auto" frameborder="1"></iframe></td>';
  }
  
  $iframes .= '</tr></table>';
 

 

(I set the iframe height to 1 as I resize it dynamically later on in the code)

 

Then later in the outer view template you just replace echo $output with echo $iframes and that part is done. For a given position id you will get an edit screen with however many iframes are needed for every booking related to that position. Now we just need to fill the iframes...

 

The key code for the inner controller is as follows, based around the core $crud->render() call:

 

$crud->unset_back_to_list();
        $output = $crud->render();
$state = $crud->getState();
 
if($state == 'edit') {
    $state_info = $crud->getStateInfo();
        $primary_key = $state_info->primary_key;
    } else {
        $primary_key = 0;
    }
$pwkdata = array();
$pwkdata['bid'] = $primary_key;
$output->pwkdata = $pwkdata;
 
Unset back-to-list, we don't want that within an iframe. I've also included how I pass the booking primary key into the inner view from the controller - I use this for some JavaScript trickery later on.

 

All that is left is the inner view template, used within each iframe. This is essentially an "empty" template - we don't want any of the outer template appearing more than once. Here is the entire body of my inner template (you keep all of the stuff in the head, that's where all the jQuery trickery resides):

 

<body>
    <div id="fairway_empty">
       <?php echo $output; ?>
    </div>
</body>
 

An entry for the div id in flexigrid.css sorts out odds and ends like the top margin.

 

The JavaScript trickery with that primary key? A combination of JS in both inner and outer views (constructed via PHP) means that all of the iframes scroll simultaneously when any one of them is scrolled.

 

If any of this is of interest to others, I'm happy to try and flesh it out further.


Amit Shah
  • profile picture
  • Member

Posted 25 July 2014 - 15:52 PM

Interesting..

 

Thank you Paul.. will surely like to look it in implementation!!


DrPaul
  • profile picture
  • Member

Posted 27 July 2014 - 18:55 PM

OK, here are a couple of screen grabs to give a flavour of what it looks like, along with some of the other tweaks I've incorporated.

 

First off, the list view of staff positions for a given event:

 

screengrab1a.png

 

1. Note search key passed in as a GET variable at the end of the URL.

2. Search panel and controls at the top of the list - where IMHO they should be as standard!

3. Search panel open by default (quick CSS tweak)

4. Actions on the left of the grid so they are always in view.

5. Action opens in new tab automatically.

 

Clicking on the action brings up this screen in a new tab (and when that tab is closed the first tab will automatically reload to reflect any updates)

 

screengrab2a.png

 

1. "Outer" view is passed the position primary key as a GET variable and generates the iframes.

2. Each iframe uses the "inner" controller and view.

3. Synchronised scrolling of iframes!

4. Integration of the jQuery timepicker control.

5. Varied size for text inputs by "injecting" extra styling into the tag.

 

Here's the core code for the "outer" controller (note the use of a session variable to maintain state information - to be avoided in the "inner" controller):

 

public function bookings_multi()
{
$crud = new Extension_grocery_CRUD();
$crud->basic_booking_config('bookings', 'Position Bookings', 'flexigrid');
 
if (isset($_GET['pid'])) {
$pid = $_GET['pid'];
$this->session->set_userdata('pwkpid', $pid);
} else {
$pid = $this->session->userdata('pwkpid');
}
 
$crud->where('bookings.PositionID', $pid)
->order_by('bookings.WorkDate')
->columns('StaffID', 'WorkDate', 'StartTimePlanned', 'EndTimePlanned', 'Status')
->extra_styling('HourlyBillRate', 'style = "width:80px;"')
->extra_styling('HourlyPayRate', 'style = "width:80px;"')
->unset_fields('PositionID');
$output = $crud->render();
$pwkdata = array();
$pwkdata['pid'] = $pid;
$output->pwkdata = $pwkdata;
$this->_bookings_output('coda_multi_template.php', $output);
}
 

And for the iframe controller:

 

public function bookings_multi_item() // only edit is used within iframes for multiple edits
{
$crud = new Extension_grocery_CRUD();
$crud->basic_booking_config('bookings', 'Booking', 'flexigrid');
$crud->columns('StaffID', 'WorkDate', 'StartTimePlanned', 'EndTimePlanned', 'Status')
->extra_styling('HourlyBillRate', 'style = "width:80px;"')
->extra_styling('HourlyPayRate', 'style = "width:80px;"')
->unset_back_to_list();
$output = $crud->render();
$state = $crud->getState();
 
if($state == 'edit') {
    $state_info = $crud->getStateInfo();
        $primary_key = $state_info->primary_key;
    } else {
        $primary_key = 0;
    }
 
$pwkdata = array();
$pwkdata['bid'] = $primary_key;
$output->pwkdata = $pwkdata;
$this->_bookings_output('coda_empty_template.php', $output);
}
 

Synchronised scrolling of iframes turned out to be a non-trivial exercise (as the saying goes) - I can go into a bit more detail about that in another post if it's of interest.

 

Each iframe is effectively a separate web page as far as the server is concerned, but they are viewed in a format where the user can edit them in parallel.


Amit Shah
  • profile picture
  • Member

Posted 28 July 2014 - 16:24 PM

Woo .. thats nice.. interesting stuff!!

lets c how i can put it to action!


DrPaul
  • profile picture
  • Member

Posted 28 July 2014 - 20:23 PM

Just to fill in the gaps, here's the JS code which will do the synchronised scrolling of iframes.

 

The key issue is that iframes don't have an onScroll event, so you have to work round that limitation. The trick is to define a set of JS functions in the "outer" view, and then to call these functions from within the relevant iframe view using the 'parent.' syntax.

 

So in the outer view I add in this code, using the array loaded earlier in the code:

 

<?php 
foreach($book_array as $bid) {
    $js = "function scroll_$bid(topp) {\n";
 
    foreach($book_array as $bid2) {
 
        if ($bid2 != $bid) {
            $js .= "document.getElementById(\"frame_".$bid2."\").contentWindow.scrollTo(0,topp);";
        }
 
    }
 
    $js .= "}\n\n";
    echo $js;
}
?>
 

In my example with 3 iframes this produces the following JS in the head of the outer view - a function for each iframe which scrolls the other iframes:

 

function scroll_13930(topp) {
    document.getElementById("frame_13931").contentWindow.scrollTo(0,topp);
    document.getElementById("frame_13932").contentWindow.scrollTo(0,topp);
}
 
function scroll_13931(topp) {
    document.getElementById("frame_13930").contentWindow.scrollTo(0,topp);
    document.getElementById("frame_13932").contentWindow.scrollTo(0,topp);
}
 
function scroll_13932(topp) {
    document.getElementById("frame_13930").contentWindow.scrollTo(0,topp);
    document.getElementById("frame_13931").contentWindow.scrollTo(0,topp);
}
 

The final piece of the jigsaw is the JS in the "inner" view used within each iframe, which will call these functions in the outer view - PHP is used to construct the name of the function called from the parent (outer) page using the data passed from the controller by adding it to the $output variable:

 

window.onscroll = scroll_iframes;
function scroll_iframes() {
    var topp = (document.documentElement && document.documentElement.scrollTop) ?
       document.documentElement.scrollTop : document.body.scrollTop;
    parent.scroll_<?php echo $pwkdata['bid']; ?>(topp);
}
 

And there you have it! :-)


DrPaul
  • profile picture
  • Member

Posted 05 August 2014 - 15:50 PM

Quick follow-up.

 

Having extended the idea to other parts of the db, of course the code which produces the iframes and the JS functions should be abstracted into the relevant controller, and the resulting values passed into the view.