- Basic Concepts
- Introduction
- Processing user interactions
- Nesting leaves
- Data Binding
- Tutorial
- Building a blog
- View Bridges
- Introduction
- The model
- Re-rendering
- Events
- Handling Children
- Controls
- Extending View Bridges
- Control Components
- What is a control?
- Text Controls
- TextBox
- TextArea
- PasswordTextBox
- Selection Controls
- The selection control pattern
- Standard Variants
- DropDown
- CheckSet
- RadioButtons
- SearchControl
- ModelSearchControl
- Attaching additional data
- Dynamically driving the available items
- File Uploads
- SimpleFileUpload
- Html5FileUpload
- Other
- Buttons
- Building your own control
- Application Components
- What is an application component?
- Pager
- Table
- SearchPanel
- Tabs
- Advanced Topics
- View Indexes
Tabs
The Tabs application component lets you display a selection of tabs and have events raised when the user changes the selected tab.
The tab is changed by raising a server event then swapping CSS classes on the tabs to achieve the effect. The tab control is not by default re-rendered, however it is quite common that other components affected by the change of selected tab may re-render.
Creating the tab control
In your hosting page's View:
class MyPageView extends View
{
protected function createSubLeaves()
{
$this->registerSubLeaf(
$tabs = new Tabs()
);
$tabs->setTabDefinitions([
new TabDefinition('Incoming', ['Status' => 'Incoming']),
new TabDefinition('Outgoing', ['Status' => 'Outgoing']),
new TabDefinition('Stale', ['Status' => 'Stale'])
]);
}
}
This example creates a tab set with 3 tabs, 'Incoming', 'Outgoing' and 'Stale'. Each tab
is expressed using a TabDefinition
object and the base implementation allows for the
tab name and an array of 'data' to associate with the tab.
By default the initially selected tab will be the first in the array.
Using tabs to control content
When the user changes the selected tab the Tabs component will raise a selectedTabChangedEvent
.
You can handle this event and then update your model and cause other elements on the page to
change. A common use case is to change the filters on a collection passed to a Table component.
class MyPageView extends View
{
protected function createSubLeaves()
{
$this->registerSubLeaf(
$tabs = new Tabs(),
// Set up a table with a collection matching the filter of the first tab.
$table = new Table(Jobs::find(new Equals('Status', 'Incoming'))
);
$tabs->setTabDefinitions([
new TabDefinition('Incoming', ['Status' => 'Incoming']),
new TabDefinition('Outgoing', ['Status' => 'Outgoing']),
new TabDefinition('Stale', ['Status' => 'Stale'])
]);
$tabs->selectedTabChangedEvent->attachHandler(function(TabDefinition $selectedTab) use ($table){
$status = $selectedTab->data['Status'];
// Change the table's collection and re-render it
$table->getCollection()->replaceFilter(new Equals('Status', $status));
$table->reRender();
});
}
}
In a real application the View would of course raise an event to let the Leaf return a new collection to be given to the View, but hopefully you get the picture.
Notice the TabDefinition object is passed as an argument to the selectedTabChangedEvent. $label
, $data
and $selected
are all public properties you can access.
Displaying counts
Most tab interfaces display a count on the tab to indicate the number of records found within. To do this
you need to firstly set the $includeCountIfSupported
public property to true. You also need to
attach a handler to the getCollectionEvent
. In order to calculate the count your host needs to
be able to provide a stem Collection for the datasource that will drive the UI behind the tab.
This is simply counted and added to the label in the tab surrounded by brackets.
class MyPageView extends View
{
protected function createSubLeaves()
{
$this->registerSubLeaf(
$tabs = new Tabs(),
// Set up a table with a collection matching the filter of the first tab.
$table = new Table(Jobs::find(new Equals('Status', 'Incoming'))
);
$tabs->setTabDefinitions([
new TabDefinition('Incoming', ['Status' => 'Incoming']),
new TabDefinition('Outgoing', ['Status' => 'Outgoing']),
new TabDefinition('Stale', ['Status' => 'Stale'])
]);
$tabs->includeCountIfSupported = true;
$tabs->getCollectionEvent->attachHandler(function(TabDefinition $tabToCount){
return Jobs::find(new Equals('Status', $tabToCount->data['Status'));
});
}
}
Sub classing TabDefinition
If you are creating a Tabs experience with more than one 'type' of tab it makes sense to create your
own extensions of TabDefinition and then use instanceof
in your handler to understand how best
to respond to the change of tab.
In addition a sub class can customise the HTML for the tab itself by overriding the getLabel($count)
function.
FilterTabs
FilterTabs
provides a simple way to filter other components that raise the getFilterEvent
.
Components raising getFilterEvent
do so just before rendering and expect a stem Filter object
to be returned (this can be an AndGroup or an OrGroup of course).
The filters are defined explicitly when setting up the tabs using FilterTabDefinition
objects instead of TabDefinition
.
This allows our example above to be written much more succinctly:
class MyPageView extends View
{
protected function createSubLeaves()
{
$this->registerSubLeaf(
$tabs = new FilterTabs(),
// Set up a table with an unfiltered collection - on render the filter for the
// first tab will be applied.
$table = new Table(Jobs::all())
);
$tabs->setTabDefinitions([
new FilterTabDefinition('Incoming', new Equals('Status', 'Incoming'),
new FilterTabDefinition('Outgoing', new Equals('Status', 'Outgoing'),
new FilterTabDefinition('Stale', new Equals('Status', 'Stale')
]);
$tabs->bindEventsWith($table);
}
}
Notice the call to
bindEventsWith
to ensure that both components discover their support for each other's events.
SearchPanelTabs
SearchPanelTabs
extends FilterTabs
to allow the tab control to interoperate with
a SearchPanel.
SearchPanelTabs
is used in conjunction with SearchResultsTabDefinition
classes. These
contain an array of key value pairs mapping the name of control in the SearchPanel
to
a set value.
When the tab is selected the SearchPanel
is updated to have those control values set.
In turn the SearchPanel
then raises it's own event to say that the search values have
changed which can cause other connected controls to update, e.g. a Table
.
The pattern provides a set of tabs that are essentially 'defaults' which configure a
SearchPanel
but the user can continue to search for whatever they want.
If the search values don't match any tab the tab control will in return create a new temporary tab called "Search Results".
class MyPageView extends View
{
protected function createSubLeaves()
{
$this->registerSubLeaf(
$this->search = new JobSearchPanel(),
$this->tabs = new SearchPanelTabs("Search"),
// Set up a table with an unfiltered collection - on render the filter for the
// first tab will be applied.
$this->table = new Table(Job::all())
);
$this->tabs->setTabDefinitions([
new SearchPanelTabDefinition('Unsent', []),
new SearchPanelTabDefinition('Incoming', ['Status' => 'Incoming']),
new SearchPanelTabDefinition('Outgoing', ['Status' => 'Outgoing']),
new SearchPanelTabDefinition('Sent', [
'Status' => 'Outgoing',
'Sent' => true
]),
]);
$this->table->columns = [
"JobID",
"JobTitle",
"Status",
"Sent"
];
$this->search->bindEventsWith($this->table);
$this->search->bindEventsWith($this->tabs);
$this->tabs->bindEventsWith($this->table);
}
protected function printViewContent()
{
print $this->search;
print $this->tabs;
print $this->table;
}
}
<?php
namespace Rhubarb\Leaf\Tabs\Examples\SearchPanelTabsExample;
use Rhubarb\Stem\Models\Model;
use Rhubarb\Stem\Schema\Columns\AutoIncrementColumn;
use Rhubarb\Stem\Schema\Columns\BooleanColumn;
use Rhubarb\Stem\Schema\Columns\StringColumn;
use Rhubarb\Stem\Schema\ModelSchema;
class Job extends Model
{
/**
* Returns the schema for this data object.
*
* @return \Rhubarb\Stem\Schema\ModelSchema
*/
protected function createSchema()
{
$schema = new ModelSchema("Job");
$schema->addColumn(
new AutoIncrementColumn("JobID"),
new StringColumn("JobTitle", 100),
new StringColumn("Status", 100),
new BooleanColumn("Sent"));
return $schema;
}
}
<?php
namespace Rhubarb\Leaf\Tabs\Examples\SearchPanelTabsExample;
use Rhubarb\Leaf\Controls\Common\Checkbox\Checkbox;
use Rhubarb\Leaf\Controls\Common\SelectionControls\DropDown\DropDown;
use Rhubarb\Leaf\SearchPanel\Leaves\SearchPanel;
use Rhubarb\Stem\Filters\Equals;
use Rhubarb\Stem\Filters\Group;
class JobSearchPanel extends SearchPanel
{
protected function createSearchControls()
{
$status = new DropDown("Status");
$status->setSelectionItems([
["", "Any Status"],
["Incoming"],
["Outgoing"],
["Stale"]
]);
return [
$status,
new Checkbox("Sent")
];
}
public function populateFilterGroup(Group $filterGroup)
{
$searchValues = $this->getSearchControlValues();
if ($searchValues["Status"]) {
$filterGroup->addFilters(new Equals("Status", $searchValues["Status"]));
}
$filterGroup->addFilters(new Equals("Sent", (bool)$searchValues["Sent"]));
}
}
<?php
namespace Rhubarb\Leaf\Tabs\Examples\SearchPanelTabsExample;
use Rhubarb\Leaf\Leaves\Leaf;
use Rhubarb\Leaf\Leaves\LeafModel;
class SearchPanelTabsExample extends Leaf
{
/**
* Returns the name of the standard view used for this leaf.
*
* @return string
*/
protected function getViewClass()
{
return SearchPanelTabsExampleView::class;
}
/**
* Should return a class that derives from LeafModel
*
* @return LeafModel
*/
protected function createModel()
{
return new SearchPanelTabsExampleModel();
}
}
<?php
namespace Rhubarb\Leaf\Tabs\Examples\SearchPanelTabsExample;
use Rhubarb\Leaf\Leaves\LeafModel;
class SearchPanelTabsExampleModel extends LeafModel
{
}
<?php
namespace Rhubarb\Leaf\Tabs\Examples\SearchPanelTabsExample;
use Rhubarb\Crown\Deployment\DeploymentPackage;
use Rhubarb\Crown\Deployment\ResourceDeploymentPackage;
use Rhubarb\Leaf\Table\Leaves\Table;
use Rhubarb\Leaf\Tabs\Leaves\SearchPanelTabDefinition;
use Rhubarb\Leaf\Tabs\Leaves\SearchPanelTabs;
use Rhubarb\Leaf\Views\View;
class SearchPanelTabsExampleView extends View
{
protected function createSubLeaves()
{
$this->registerSubLeaf(
$this->search = new JobSearchPanel(),
$this->tabs = new SearchPanelTabs("Search"),
// Set up a table with an unfiltered collection - on render the filter for the
// first tab will be applied.
$this->table = new Table(Job::all())
);
$this->tabs->setTabDefinitions([
new SearchPanelTabDefinition('Unsent', []),
new SearchPanelTabDefinition('Incoming', ['Status' => 'Incoming']),
new SearchPanelTabDefinition('Outgoing', ['Status' => 'Outgoing']),
new SearchPanelTabDefinition('Sent', [
'Status' => 'Outgoing',
'Sent' => true
]),
]);
$this->table->columns = [
"JobID",
"JobTitle",
"Status",
"Sent"
];
$this->search->bindEventsWith($this->table);
$this->search->bindEventsWith($this->tabs);
$this->tabs->bindEventsWith($this->table);
}
protected function printViewContent()
{
print $this->search;
print $this->tabs;
print $this->table;
}
public function getDeploymentPackage()
{
$package = new ResourceDeploymentPackage();
$package->resourcesToDeploy[] = __DIR__.'/Tabs.css';
return $package;
}
}
.search-panel {
border: 1px solid #CCC;
padding: 10px;
}
ul.tabs {
margin-left: 0;
margin-top: 5px;
margin-bottom: 0;
list-style: none;
}
ul.tabs li {
display: inline-block;
padding: 10px;
border: 1px solid #999;
border-right: 0;
border-bottom: 0;
margin: 0;
background: #EEE;
cursor: pointer;
}
ul.tabs li:last-child {
border-right: 1px solid #999;
}
ul.tabs li:hover, ul.tabs li.selected {
background: #FFF;
margin-top: 1px;
border-bottom: 1px solid #FFF;
}
.list {
border: 1px solid #999;
padding: 10px;
margin-top: -1px;
}