mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
Merge pull request #8460 from open-sausages/pulls/4/lazy-loadable-gidfield
API Add a new GridFieldLazyLoader component
This commit is contained in:
commit
7215637673
@ -6,6 +6,7 @@
|
||||
- `DataList`, `ArrayList` and `UnsavedRalationList` all have `columnUnique()` method for fetching distinct column values
|
||||
- Take care with `stageChildren()` overrides. `Hierarchy::numChildren() ` results will only make use of `stageChildren()` customisations that are applied to the base class and don't include record-specific behaviour.
|
||||
- New React-based search UI for the CMS, Asset-Admin, GridFields and ModelAdmins.
|
||||
- A new `GridFieldLazyLoader` component can be added to `GridField`. This will delay the fetching of data until the user access the container Tab of the GridField.
|
||||
|
||||
## Upgrading {#upgrading}
|
||||
|
||||
|
@ -53,16 +53,15 @@ class CompositeField extends FormField
|
||||
|
||||
public function __construct($children = null)
|
||||
{
|
||||
if ($children instanceof FieldList) {
|
||||
$this->children = $children;
|
||||
} elseif (is_array($children)) {
|
||||
$this->children = new FieldList($children);
|
||||
} else {
|
||||
//filter out null/empty items
|
||||
$children = array_filter(func_get_args());
|
||||
$this->children = new FieldList($children);
|
||||
// Normalise $children to a FieldList
|
||||
if (!$children instanceof FieldList) {
|
||||
if (!is_array($children)) {
|
||||
// Fields are provided as a list of arguments
|
||||
$children = array_filter(func_get_args());
|
||||
}
|
||||
$children = new FieldList($children);
|
||||
}
|
||||
$this->children->setContainerField($this);
|
||||
$this->setChildren($children);
|
||||
|
||||
parent::__construct(null, false);
|
||||
}
|
||||
@ -155,6 +154,7 @@ class CompositeField extends FormField
|
||||
public function setChildren($children)
|
||||
{
|
||||
$this->children = $children;
|
||||
$children->setContainerField($this);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ class FieldList extends ArrayList
|
||||
* If this fieldlist is owned by a parent field (e.g. CompositeField)
|
||||
* this is the parent field.
|
||||
*
|
||||
* @var FieldList|FormField
|
||||
* @var CompositeField
|
||||
*/
|
||||
protected $containerField;
|
||||
|
||||
@ -790,7 +790,15 @@ class FieldList extends ArrayList
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $field
|
||||
* @return CompositeField|null
|
||||
*/
|
||||
public function getContainerField()
|
||||
{
|
||||
return $this->containerField;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CompositeField|null $field
|
||||
* @return $this
|
||||
*/
|
||||
public function setContainerField($field)
|
||||
|
@ -110,18 +110,19 @@ class GridField extends FormField
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $readonlyComponents = array(
|
||||
protected $readonlyComponents = [
|
||||
GridField_ActionMenu::class,
|
||||
GridState_Component::class,
|
||||
GridFieldConfig_RecordViewer::class,
|
||||
GridFieldDetailForm::class,
|
||||
GridFieldDataColumns::class,
|
||||
GridFieldDetailForm::class,
|
||||
GridFieldLazyLoader::class,
|
||||
GridFieldPageCount::class,
|
||||
GridFieldPaginator::class,
|
||||
GridFieldSortableHeader::class,
|
||||
GridFieldToolbarHeader::class,
|
||||
GridFieldViewButton::class,
|
||||
);
|
||||
GridState_Component::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Pattern used for looking up
|
||||
|
84
src/Forms/GridField/GridFieldLazyLoader.php
Executable file
84
src/Forms/GridField/GridFieldLazyLoader.php
Executable file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Forms\GridField;
|
||||
|
||||
use SilverStripe\Forms\FormField;
|
||||
use SilverStripe\Forms\TabSet;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\Filterable;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
|
||||
/**
|
||||
* GridFieldLazyLoader alters the {@link GridField} behavior to delay rendering of rows until the tab containing the
|
||||
* GridField is selected by the user.
|
||||
*
|
||||
* @see GridField
|
||||
*/
|
||||
class GridFieldLazyLoader implements GridField_DataManipulator, GridField_HTMLProvider
|
||||
{
|
||||
|
||||
/**
|
||||
* Empty $datalist if the current request should be lazy loadable.
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param SS_List $dataList
|
||||
* @return SS_List
|
||||
*/
|
||||
public function getManipulatedData(GridField $gridField, SS_List $dataList)
|
||||
{
|
||||
// If we are lazy loading an empty the list
|
||||
if ($this->isLazy($gridField)) {
|
||||
if ($dataList instanceof Filterable) {
|
||||
// If our original list can be filtered, filter out all results.
|
||||
$dataList = $dataList->byIDs([-1]);
|
||||
} else {
|
||||
// If not, create an empty list instead.
|
||||
$dataList = ArrayList::create([]);
|
||||
}
|
||||
}
|
||||
return $dataList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply an appropriate CSS class to `$gridField` based on whatever the current request is lazy loadable or not.
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @return array
|
||||
*/
|
||||
public function getHTMLFragments($gridField)
|
||||
{
|
||||
$gridField->addExtraClass($this->isLazy($gridField) ?
|
||||
'grid-field--lazy-loadable' :
|
||||
'grid-field--lazy-loaded');
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if the current request should include results.
|
||||
* @param GridField $gridField
|
||||
* @return bool
|
||||
*/
|
||||
private function isLazy(GridField $gridField)
|
||||
{
|
||||
return
|
||||
$gridField->getRequest()->getHeader('X-Pjax') !== 'CurrentField' &&
|
||||
$this->isInTabSet($gridField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively check if $field is inside a TabSet.
|
||||
* @param FormField $field
|
||||
* @return bool
|
||||
*/
|
||||
private function isInTabSet(FormField $field)
|
||||
{
|
||||
$list = $field->getContainerFieldList();
|
||||
if ($list && $containerField = $list->getContainerField()) {
|
||||
// Classes that extends TabSet might not have the expected JS to lazy load.
|
||||
return get_class($containerField) === TabSet::class
|
||||
?: $this->isInTabSet($containerField);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace SilverStripe\Forms\GridField;
|
||||
|
||||
use SilverStripe\ORM\DataList;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
|
||||
/**
|
||||
@ -12,17 +11,17 @@ use SilverStripe\ORM\SS_List;
|
||||
* component can apply a sort.
|
||||
*
|
||||
* Generally, the data manipulator will make use of to {@link GridState}
|
||||
* variables to decide how to modify the {@link DataList}.
|
||||
* variables to decide how to modify the {@link SS_List}.
|
||||
*/
|
||||
interface GridField_DataManipulator extends GridFieldComponent
|
||||
{
|
||||
|
||||
/**
|
||||
* Manipulate the {@link DataList} as needed by this grid modifier.
|
||||
* Manipulate the {@link SS_List} as needed by this grid modifier.
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param SS_List $dataList
|
||||
* @return DataList
|
||||
* @return SS_List
|
||||
*/
|
||||
public function getManipulatedData(GridField $gridField, SS_List $dataList);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ namespace SilverStripe\Forms\Tests;
|
||||
|
||||
use SilverStripe\Dev\CSSContentParser;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\TextField;
|
||||
use SilverStripe\Forms\CompositeField;
|
||||
use SilverStripe\Forms\DropdownField;
|
||||
@ -107,4 +108,20 @@ class CompositeFieldTest extends SapphireTest
|
||||
"Validates when children are valid"
|
||||
);
|
||||
}
|
||||
|
||||
public function testChildren()
|
||||
{
|
||||
$field = CompositeField::create();
|
||||
|
||||
$this->assertInstanceOf(FieldList::class, $field->getChildren());
|
||||
$this->assertEquals($field, $field->getChildren()->getContainerField());
|
||||
|
||||
$expectedChildren = FieldList::create(
|
||||
$fieldOne = DropdownField::create('A', '', [ 'value' => 'value' ]),
|
||||
$fieldTwo = TextField::create('B')
|
||||
);
|
||||
$field->setChildren($expectedChildren);
|
||||
$this->assertEquals($expectedChildren, $field->getChildren());
|
||||
$this->assertEquals($field, $expectedChildren->getContainerField());
|
||||
}
|
||||
}
|
||||
|
@ -1176,4 +1176,18 @@ class FieldListTest extends SapphireTest
|
||||
// put all your HiddenFields at the top level.
|
||||
$this->assertNotNull($visible->dataFieldByName('D2'));
|
||||
}
|
||||
|
||||
public function testContainerField()
|
||||
{
|
||||
$fieldlist = new FieldList();
|
||||
$container = CompositeField::create();
|
||||
|
||||
$this->assertNull($fieldlist->getContainerField());
|
||||
|
||||
$fieldlist->setContainerField($container);
|
||||
$this->assertEquals($container, $fieldlist->getContainerField());
|
||||
|
||||
$fieldlist->setContainerField(null);
|
||||
$this->assertNull($fieldlist->getContainerField());
|
||||
}
|
||||
}
|
||||
|
235
tests/php/Forms/GridField/GridFieldLazyLoaderTest.php
Normal file
235
tests/php/Forms/GridField/GridFieldLazyLoaderTest.php
Normal file
@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Forms\Tests\GridField;
|
||||
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor;
|
||||
use SilverStripe\Forms\GridField\GridFieldLazyLoader;
|
||||
use SilverStripe\Forms\Tab;
|
||||
use SilverStripe\Forms\TabSet;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Team;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Permissions;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Cheerleader;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Player;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\DataList;
|
||||
|
||||
class GridFieldLazyLoaderTest extends SapphireTest
|
||||
{
|
||||
/**
|
||||
* @var ArrayList
|
||||
*/
|
||||
protected $list;
|
||||
|
||||
/**
|
||||
* @var GridField
|
||||
*/
|
||||
protected $gridField;
|
||||
|
||||
/**
|
||||
* @var GridFieldLazyLoader
|
||||
*/
|
||||
protected $component;
|
||||
|
||||
protected static $fixture_file = 'GridFieldTest.yml';
|
||||
|
||||
protected static $extra_dataobjects = [
|
||||
Permissions::class,
|
||||
Cheerleader::class,
|
||||
Player::class,
|
||||
Team::class,
|
||||
];
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
$this->list = DataList::create(Team::class);
|
||||
$this->component = new GridFieldLazyLoader();
|
||||
$config = GridFieldConfig_RecordEditor::create()->addComponent($this->component);
|
||||
$this->gridField = new GridField('testfield', 'testfield', $this->list, $config);
|
||||
}
|
||||
|
||||
public function testGetManipulatedDataWithoutHeader()
|
||||
{
|
||||
$gridField = $this->getHeaderlessGridField();
|
||||
$this->assertCount(
|
||||
0,
|
||||
$this->component->getManipulatedData($gridField, $this->list)->toArray(),
|
||||
'GridFieldLazyLoader::getManipulatedData should return an empty list if the X-Pjax is unset'
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetManipulatedDataWithoutTabSet()
|
||||
{
|
||||
$gridField = $this->getOutOfTabSetGridField();
|
||||
$this->assertSameSize(
|
||||
$this->list,
|
||||
$this->component->getManipulatedData($gridField, $this->list)->toArray(),
|
||||
'GridFieldLazyLoader::getManipulatedData should return a proper list if GridField is not in a tab'
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetManipulatedDataNonLazy()
|
||||
{
|
||||
$gridField = $this->getNonLazyGridField();
|
||||
$this->assertSameSize(
|
||||
$this->list,
|
||||
$this->component->getManipulatedData($gridField, $this->list),
|
||||
'GridFieldLazyLoader::getManipulatedData should return a proper list if GridField'
|
||||
. ' is in a tab with the pajax header'
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetHTMLFragmentsWithoutHeader()
|
||||
{
|
||||
$gridField = $this->getHeaderlessGridField();
|
||||
$actual = $this->component->getHTMLFragments($gridField);
|
||||
$this->assertEmpty($actual, 'getHTMLFragments should always return an array');
|
||||
$this->assertContains('grid-field--lazy-loadable', $gridField->extraClass());
|
||||
$this->assertNotContains('grid-field--lazy-loaded', $gridField->extraClass());
|
||||
}
|
||||
|
||||
public function testGetHTMLFragmentsWithoutTabSet()
|
||||
{
|
||||
$gridField = $this->getOutOfTabSetGridField();
|
||||
$actual = $this->component->getHTMLFragments($gridField);
|
||||
$this->assertEmpty($actual, 'getHTMLFragments should always return an array');
|
||||
$this->assertContains('grid-field--lazy-loaded', $gridField->extraClass());
|
||||
$this->assertNotContains('grid-field--lazy-loadable', $gridField->extraClass());
|
||||
}
|
||||
|
||||
public function testGetHTMLFragmentsNonLazy()
|
||||
{
|
||||
$gridField = $this->getNonLazyGridField();
|
||||
$actual = $this->component->getHTMLFragments($gridField);
|
||||
$this->assertEmpty($actual, 'getHTMLFragments should always return an array');
|
||||
$this->assertContains('grid-field--lazy-loaded', $gridField->extraClass());
|
||||
$this->assertNotContains('grid-field--lazy-loadable', $gridField->extraClass());
|
||||
}
|
||||
|
||||
|
||||
public function testReadOnlyGetManipulatedDataWithoutHeader()
|
||||
{
|
||||
$gridField = $this->makeGridFieldReadonly($this->getHeaderlessGridField());
|
||||
$this->assertCount(
|
||||
0,
|
||||
$this->component->getManipulatedData($gridField, $this->list)->toArray(),
|
||||
'Readonly GridFieldLazyLoader::getManipulatedData should return an empty list if the X-Pjax'
|
||||
. ' is unset'
|
||||
);
|
||||
}
|
||||
|
||||
public function testReadOnlyGetManipulatedDataWithoutTabSet()
|
||||
{
|
||||
$gridField = $this->makeGridFieldReadonly($this->getOutOfTabSetGridField());
|
||||
$this->assertSameSize(
|
||||
$this->list,
|
||||
$this->component->getManipulatedData($gridField, $this->list)->toArray(),
|
||||
'Readonly GridFieldLazyLoader::getManipulatedData should return a proper list if GridField is'
|
||||
. ' not in a tab'
|
||||
);
|
||||
}
|
||||
|
||||
public function testReadOnlyGetManipulatedDataNonLazy()
|
||||
{
|
||||
$gridField = $this->makeGridFieldReadonly($this->getNonLazyGridField());
|
||||
$this->assertSameSize(
|
||||
$this->list,
|
||||
$this->component->getManipulatedData($gridField, $this->list),
|
||||
'Readonly GridFieldLazyLoader::getManipulatedData should return a proper list if GridField is in'
|
||||
. ' a tab with the pajax header'
|
||||
);
|
||||
}
|
||||
|
||||
public function testReadOnlyGetHTMLFragmentsWithoutHeader()
|
||||
{
|
||||
$gridField = $this->makeGridFieldReadonly($this->getHeaderlessGridField());
|
||||
$actual = $this->component->getHTMLFragments($gridField);
|
||||
$this->assertEmpty($actual, 'getHTMLFragments should always return an array');
|
||||
$this->assertContains('grid-field--lazy-loadable', $gridField->extraClass());
|
||||
$this->assertNotContains('grid-field--lazy-loaded', $gridField->extraClass());
|
||||
}
|
||||
|
||||
public function testReadOnlyGetHTMLFragmentsWithoutTabSet()
|
||||
{
|
||||
$gridField = $this->makeGridFieldReadonly($this->getOutOfTabSetGridField());
|
||||
$actual = $this->component->getHTMLFragments($gridField);
|
||||
$this->assertEmpty($actual, 'getHTMLFragments should always return an array');
|
||||
$this->assertContains('grid-field--lazy-loaded', $gridField->extraClass());
|
||||
$this->assertNotContains('grid-field--lazy-loadable', $gridField->extraClass());
|
||||
}
|
||||
|
||||
public function testReadOnlyGetHTMLFragmentsNonLazy()
|
||||
{
|
||||
$gridField = $this->makeGridFieldReadonly($this->getNonLazyGridField());
|
||||
$actual = $this->component->getHTMLFragments($gridField);
|
||||
$this->assertEmpty($actual, 'getHTMLFragments should always return an array');
|
||||
$this->assertContains('grid-field--lazy-loaded', $gridField->extraClass());
|
||||
$this->assertNotContains('grid-field--lazy-loadable', $gridField->extraClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* This GridField will be lazy because it doesn't have a `X-Pjax` header.
|
||||
* @return GridField
|
||||
*/
|
||||
private function getHeaderlessGridField()
|
||||
{
|
||||
$this->gridField->setRequest(new HTTPRequest('GET', 'admin/pages/edit/show/9999'));
|
||||
$fieldList = FieldList::create(new TabSet('Root', new Tab('Main')));
|
||||
$fieldList->addFieldToTab('Root.GridField', $this->gridField);
|
||||
Form::create(null, 'Form', $fieldList, FieldList::create());
|
||||
return $this->gridField;
|
||||
}
|
||||
|
||||
/**
|
||||
* This GridField will not be lazy because it's in not in a tab set.
|
||||
* @return GridField
|
||||
*/
|
||||
private function getOutOfTabSetGridField()
|
||||
{
|
||||
$r = new HTTPRequest('POST', 'admin/pages/edit/EditForm/9999/field/testfield');
|
||||
$r->addHeader('X-Pjax', 'CurrentField');
|
||||
$this->gridField->setRequest($r);
|
||||
$fieldList = new FieldList($this->gridField);
|
||||
Form::create(null, 'Form', $fieldList, FieldList::create());
|
||||
return $this->gridField;
|
||||
}
|
||||
|
||||
/**
|
||||
* This gridfield will not be lazy, because it has `X-Pjax` header equal to `CurrentField`
|
||||
* @return GridField
|
||||
*/
|
||||
private function getNonLazyGridField()
|
||||
{
|
||||
$r = new HTTPRequest('POST', 'admin/pages/edit/EditForm/9999/field/testfield');
|
||||
$r->addHeader('X-Pjax', 'CurrentField');
|
||||
$this->gridField->setRequest($r);
|
||||
$fieldList = new FieldList(new TabSet('Root', new Tab('Main')));
|
||||
$fieldList->addFieldToTab('Root', $this->gridField);
|
||||
Form::create(null, 'Form', $fieldList, FieldList::create());
|
||||
return $this->gridField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a readonly transformation on our GridField's Form and return the ReadOnly GridField.
|
||||
*
|
||||
* We need to make sure the LazyLoader component still works after our GridField has been made readonly.
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @return GridField
|
||||
*/
|
||||
private function makeGridFieldReadonly(GridField $gridField)
|
||||
{
|
||||
$form = $gridField->getForm()->makeReadonly();
|
||||
$fields = $form->Fields()->dataFields();
|
||||
foreach ($fields as $field) {
|
||||
if ($field->getName() === 'testfield') {
|
||||
return $field;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user