Skip to content

Commit

Permalink
API CHANGE: Allow for the creation of custom GridField fragments. (#6911
Browse files Browse the repository at this point in the history
)
  • Loading branch information
Sam Minnee committed Mar 8, 2012
1 parent c80e86f commit 5800db0
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 14 deletions.
36 changes: 36 additions & 0 deletions docs/en/topics/grid-field.md
Expand Up @@ -191,6 +191,42 @@ For example, this components will add a footer row to the grid field, thanking t

If you wish to add CSS or JavaScript for your component, you may also make `Requirements` calls in this method.

### Defining new fragments

Sometimes it is helpful to have one component write HTML into another component. For example, you might have an action header row at the top of your GridField that several different components may define actions for.

To do this, you can put the following code into one of the HTML fragments returned by an HTML provider.

$DefineFragment(fragment-name)

Other `GridField_HTMLProvider` components can now write to `fragment-name` just as they would write to footer, etc. Fragments can be nested.

For example, this component creates a `header-actions` fragment name that can be populated by other components:

:::php
class HeaderActionComponent implements GridField_HTMLProvider {
public function getHTMLFragments($gridField) {
$colSpan = $gridField->getColumnCount();
array(
"header" => "<tr><td colspan=\"$colspan\">\$DefineFragment(header-actions)</td></tr>"
);
}
}

This is a simple example of how you might populate that new fragment:

:::php
class AddNewActionComponent implements GridField_HTMLProvider {
public function getHTMLFragments($gridField) {
$colSpan = $gridField->getColumnCount();
array(
"header-actions" => "<button>Add new</button>"
);
}
}

If you write to a fragment that isn't defined anywhere, or you create a circular dependency within fragments, an exception will be thrown.

### GridField_ColumnProvider

By default, a grid contains no columns. All the columns displayed in a grid will need to be added by an appropriate component.
Expand Down
80 changes: 68 additions & 12 deletions forms/gridfield/GridField.php
Expand Up @@ -314,22 +314,79 @@ public function FieldHolder() {

// Render headers, footers, etc
$content = array(
'header' => array(),
'body' => array(),
'footer' => array(),
'before' => array(),
'after' => array(),
"before" => "",
"after" => "",
"header" => "",
"footer" => "",
);

foreach($this->components as $item) {
if($item instanceof GridField_HTMLProvider) {
$fragments = $item->getHTMLFragments($this);
foreach($fragments as $k => $v) {
$content[$k][] = $v;
$k = strtolower($k);
if(!isset($content[$k])) $content[$k] = "";
$content[$k] .= $v . "\n";
}
}
}

foreach($content as $k => $v) {
$content[$k] = trim($v);
}

// Replace custom fragments and check which fragments are defined
// Nested dependencies are handled by deferring the rendering of any content item that
// Circular dependencies are detected by disallowing any item to be deferred more than 5 times
// It's a fairly crude algorithm but it works

$fragmentDefined = array('header' => true, 'footer' => true, 'before' => true, 'after' => true);
reset($content);
while(list($k,$v) = each($content)) {
if(preg_match_all('/\$DefineFragment\(([a-z0-9\-_]+)\)/i', $v, $matches)) {
foreach($matches[1] as $match) {
$fragmentName = strtolower($match);
$fragmentDefined[$fragmentName] = true;
$fragment = isset($content[$fragmentName]) ? $content[$fragmentName] : "";

// If the fragment still has a fragment definition in it, when we should defer this item until later.
if(preg_match('/\$DefineFragment\(([a-z0-9\-_]+)\)/i', $fragment, $matches)) {
// If we've already deferred this fragment, then we have a circular dependency
if(isset($fragmentDeferred[$k]) && $fragmentDeferred[$k] > 5) throw new LogicException("GridField HTML fragment '$fragmentName' and '$matches[1]' appear to have a circular dependency.");

// Otherwise we can push to the end of the content array
unset($content[$k]);
$content[$k] = $v;
if(!isset($fragmentDeferred[$k])) $fragmentDeferred[$k] = 1;
else $fragmentDeferred[$k]++;
break;
} else {
$content[$k] = preg_replace('/\$DefineFragment\(' . $fragmentName . '\)/i', $fragment, $content[$k]);
}
}
}
}

// Check for any undefined fragments, and if so throw an exception
// While we're at it, trim whitespace off the elements
foreach($content as $k => $v) {
if(empty($fragmentDefined[$k])) throw new LogicException("GridField HTML fragment '$k' was given content, " .
"but not defined. Perhaps there is a supporting GridField component you need to add?");
}

$rows = array();
foreach($list as $idx => $record) {
$rowContent = '';
foreach($columns as $column) {
$colContent = $this->getColumnContent($record, $column);
// A return value of null means this columns should be skipped altogether.
if($colContent === null) continue;
$colAttributes = $this->getColumnAttributes($record, $column);
$rowContent .= $this->createTag('td', $colAttributes, $colContent);
}
$rows[] = $row;
}
$content['body'] = implode("\n", $rows);

$total = $list->count();
if($total > 0) {
Expand Down Expand Up @@ -364,13 +421,12 @@ public function FieldHolder() {
array("class" => 'ss-gridfield-item ss-gridfield-no-items'),
$this->createTag('td', array('colspan' => count($columns)), _t('GridField.NoItemsFound', 'No items found'))
);
$content['body'][] = $row;
}

// Turn into the relevant parts of a table
$head = $content['header'] ? $this->createTag('thead', array(), implode("\n", $content['header'])) : '';
$body = $content['body'] ? $this->createTag('tbody', array('class' => 'ss-gridfield-items'), implode("\n", $content['body'])) : '';
$foot = $content['footer'] ? $this->createTag('tfoot', array(), implode("\n", $content['footer'])) : '';
$head = $content['header'] ? $this->createTag('thead', array(), $content['header']) : '';
$body = $content['body'] ? $this->createTag('tbody', array('class' => 'ss-gridfield-items'), $content['body']) : '';
$foot = $content['footer'] ? $this->createTag('tfoot', array(), $content['footer']) : '';

$this->addExtraClass('ss-gridfield field');
$attrs = array_diff_key(
Expand All @@ -386,9 +442,9 @@ public function FieldHolder() {
);
return
$this->createTag('fieldset', $attrs,
implode("\n", $content['before']) .
$content['before'] .
$this->createTag('table', $tableAttrs, $head."\n".$foot."\n".$body) .
implode("\n", $content['after'])
$content['after']
);
}

Expand Down
13 changes: 11 additions & 2 deletions forms/gridfield/GridFieldComponent.php
Expand Up @@ -12,8 +12,17 @@ interface GridFieldComponent {
interface GridField_HTMLProvider extends GridFieldComponent {

/**
* Returns a map with 4 keys 'header', 'footer', 'before', 'after'. Each of these can contain an
* HTML fragment and each of these are optional.
* Returns a map where the keys are fragment names and the values are pieces of HTML to add to these fragments.
*
* Here are 4 built-in fragments: 'header', 'footer', 'before', and 'after', but components may also specify
* fragments of their own.
*
* To specify a new fragment, specify a new fragment by including the text "$DefineFragment(fragmentname)" in the
* HTML that you return. Fragment names should only contain alphanumerics, -, and _.
*
* If you attempt to return HTML for a fragment that doesn't exist, an exception will be thrown when the GridField
* is rendered.
*
* @return Array
*/
function getHTMLFragments($gridField);
Expand Down
94 changes: 94 additions & 0 deletions tests/forms/GridFieldTest.php
Expand Up @@ -333,7 +333,90 @@ public function testGridFieldAlterAction() {
$request = new SS_HTTPRequest('POST', 'url');
$obj->gridFieldAlterAction(array('StateID'=>$id), $form, $request);
}

/**
* Test the interface for adding custom HTML fragment slots via a component
*/
public function testGridFieldCustomFragments() {

new GridFieldTest_HTMLFragments(array(
"header-left-actions" => "left\$DefineFragment(nested-left)",
"header-right-actions" => "right",
));

new GridFieldTest_HTMLFragments(array(
"nested-left" => "[inner]",
));


$config = GridFieldConfig::create()->addComponents(
new GridFieldTest_HTMLFragments(array(
"header" => "<tr><td><div class=\"right\">\$DefineFragment(header-right-actions)</div><div class=\"left\">\$DefineFragment(header-left-actions)</div></td></tr>",
)),
new GridFieldTest_HTMLFragments(array(
"header-left-actions" => "left",
"header-right-actions" => "rightone",
)),
new GridFieldTest_HTMLFragments(array(
"header-right-actions" => "righttwo",
))
);
$field = new GridField('testfield', 'testfield', ArrayList::create(), $config);
$form = new Form(new Controller(), 'testform', new FieldList(array($field)), new FieldList());

$this->assertContains("<div class=\"right\">rightone\nrighttwo</div><div class=\"left\">left</div>",
$field->FieldHolder());
}

/**
* Test the nesting of custom fragments
*/
public function testGridFieldCustomFragmentsNesting() {
$config = GridFieldConfig::create()->addComponents(
new GridFieldTest_HTMLFragments(array(
"level-one" => "first",
)),
new GridFieldTest_HTMLFragments(array(
"before" => "<div>\$DefineFragment(level-one)</div>",
)),
new GridFieldTest_HTMLFragments(array(
"level-one" => "<strong>\$DefineFragment(level-two)</strong>",
)),
new GridFieldTest_HTMLFragments(array(
"level-two" => "second",
))
);
$field = new GridField('testfield', 'testfield', ArrayList::create(), $config);
$form = new Form(new Controller(), 'testform', new FieldList(array($field)), new FieldList());

$this->assertContains("<div>first\n<strong>second</strong></div>",
$field->FieldHolder());
}

/**
* Test that circular dependencies throw an exception
*/
public function testGridFieldCustomFragmentsCircularDependencyThrowsException() {
$config = GridFieldConfig::create()->addComponents(
new GridFieldTest_HTMLFragments(array(
"level-one" => "first",
)),
new GridFieldTest_HTMLFragments(array(
"before" => "<div>\$DefineFragment(level-one)</div>",
)),
new GridFieldTest_HTMLFragments(array(
"level-one" => "<strong>\$DefineFragment(level-two)</strong>",
)),
new GridFieldTest_HTMLFragments(array(
"level-two" => "<blink>\$DefineFragment(level-one)</blink>",
))
);
$field = new GridField('testfield', 'testfield', ArrayList::create(), $config);
$form = new Form(new Controller(), 'testform', new FieldList(array($field)), new FieldList());

$this->setExpectedException('LogicException');
$field->FieldHolder();
}
}

class GridFieldTest_Component implements GridField_ColumnProvider, GridField_ActionProvider, TestOnly{
Expand Down Expand Up @@ -388,4 +471,15 @@ class GridFieldTest_Player extends DataObject implements TestOnly {
);

static $belongs_many_many = array('Teams' => 'GridFieldTest_Team');
}


class GridFieldTest_HTMLFragments implements GridField_HTMLProvider, TestOnly{
function __construct($fragments) {
$this->fragments = $fragments;
}

function getHTMLFragments($gridField) {
return $this->fragments;
}
}

0 comments on commit 5800db0

Please sign in to comment.