Skip to content
Browse files

API CHANGE: Allow for the creation of custom GridField fragments. (#6…

…911)
  • Loading branch information...
1 parent c80e86f commit 5800db0239f1c7c3e7a5ae639d3315799d606be7 @sminnee sminnee committed Mar 8, 2012
Showing with 209 additions and 14 deletions.
  1. +36 −0 docs/en/topics/grid-field.md
  2. +68 −12 forms/gridfield/GridField.php
  3. +11 −2 forms/gridfield/GridFieldComponent.php
  4. +94 −0 tests/forms/GridFieldTest.php
View
36 docs/en/topics/grid-field.md
@@ -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
80 forms/gridfield/GridField.php
@@ -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) {
@@ -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(
@@ -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']
);
}
View
13 forms/gridfield/GridFieldComponent.php
@@ -12,8 +12,17 @@
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
94 tests/forms/GridFieldTest.php
@@ -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{
@@ -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.
Something went wrong with that request. Please try again.