API CHANGE: Allow for the creation of custom GridField fragments. (#6911)

This commit is contained in:
Sam Minnee 2012-03-08 19:04:07 +13:00
parent c80e86f430
commit 5800db0239
4 changed files with 209 additions and 14 deletions

View File

@ -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.

View File

@ -314,22 +314,79 @@ class GridField extends FormField {
// 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) {
@ -364,13 +421,12 @@ class GridField extends FormField {
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(
@ -386,9 +442,9 @@ class GridField extends FormField {
);
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']
);
}

View File

@ -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);

View File

@ -333,7 +333,90 @@ class GridFieldTest extends SapphireTest {
$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{
@ -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;
}
}