Since containers are components, they retain all of the features of components and can be used wherever components can be used. But there are some extra features that containers have, and containers also have a slightly different mechanism for rendering.
One slot is considered the default slot, and when components are added without specifying a slot, they will be added to this slot. Slots can be marked as private, in which case only the container itself (or subclasses of it) can add components to the slot, using the special TKContainer::addPriv() protected method. A slot can also be marked as being a singleton, in which case it can contain only one component. This might be useful for containers like scrollviews which really only need to contain one component. These attributes of slots can be set when a slot is created with TKContainer::addSlot(), and can be changed with TKContainer::setSlotOption().
The first thing to do is create a skeleton class for a container. You can use the following when creating new containers:
<?php class TKMyList extends TKContainer { public function __construct() { parent::__construct(); # initialize properties and events here } protected function renderContainer($class, $style, $events, $id) { $html = ''; # generate HTML here return $html; } } ?>
We have a constructor here, in which we can set up properties and events, as well as anything specific to the container itself. There may be private variables that need to be initialized. And then we have renderContainer(), which is called when it's time to generate the HTML for the container. This method is more complex than what we would have for a component's renderComponent() method.
Let's first add some properties for our list. All components (and thus containers) have a 'class' and a 'style' property already, so we don't need to define those. But we might want to have classes and styles for list elements. In fact, we might even want to have a class and property for even vs. odd rows. To be flexible, we should have classes/styles for each item, and then a set for even items and a set for odd items. That gives us the following properties:
So, to add these properties to our object, we simply need to have a few addProperty() calls in our constructor, like so:
# initialize properties and events here $this->addProperty('item_class', self::PROP_CLASS); $this->addProperty('item_style', self::PROP_STYLE); $this->addProperty('even_class', self::PROP_CLASS); $this->addProperty('even_style', self::PROP_STYLE); $this->addProperty('odd_class', self::PROP_CLASS); $this->addProperty('odd_style', self::PROP_STYLE);
The types for the properties are PROP_CLASS and PROP_STYLE, instead of PROP_ARRAY, which used to be the standard for class and style properties. Auto-translation of fields requires that you set the types correctly.
You do not need to initialize these properties. The user of the container will add their own classes and styles. If there are any styles that need to be set by default, you should add those in the appropriate theme class (see Advanced Considerations for more on how to do that). In general, you shouldn't add anything to class and style properties, except in certain places while rendering, to be described below. Give the user the power to control look and feel.
So now it's time to implement the rendering functionality. Let us start with the very simple case of loading all the child components and then putting them in as li's inside a ul. We won't worry about list item styles or odd and even row styles for now. The way to get the list of components in a slot is, not surprisingly, TKContainer::getChildrenBySlot(). By default, components are added to a slot called "main", so we need to get our children from that slot (especially since we didn't define any other slots). We will then iterate through those child components and put them into our output HTML. The rough structure of this is below:
$html = '<ul>'; foreach($this->getChildrenBySlot('main') as $id => $child) { # generate HTML for each item here... } $html .= '</ul>'; return $html;
Each child can be a component, or it can just be a string of text (which comes about when the user calls the TKContainer::addText() method). If it is a component, then we need to tell that component to render itself and then use the rendered HTML in our list item. If we just have a string, we can plop that string in directly:
foreach($this->getChildrenBySlot('main') as $id => $child) { # generate HTML for each item here... if(is_object($child)) $childHtml = $child->render(); else $childHtml = $child; $html .= "<li>$childHtml</li>"; }
At this point, we will correctly generate a list with ul and li tags. The child object HTML will be correctly rendered and placed in the list items. But we don't yet support styles and classes. Let's start with the styles and classes for the ul. The relevant properties are 'class' and 'style'. We also want to put the container ID in the ul and any JavaScript events. This can be done rather easily because all that information is passed into us as parameters to renderContainer(). Let's look at each of these parameters in turn.
The first parameter is called $class and it contains one entry for each class property in our object. The name of the entry is the name of the property without the '_class' suffix. For the 'class' property, the name is simply 0. In our case, we have the following entries in the $class array: 0, 'item', 'even' and 'odd', corresponding to the properties 'class', 'item_class', 'even_class' and 'odd_class'. The value of each entry is a string of the form ' class="abc def"'. That is, the class array is translated for you into a string that can be plugged directly into the HTML. The $style array is exactly the same, except for styles instead of classes.
Next is the $events string. This contains all JavaScript events, such as onclick, that should apply to the container. You will not need to deal with this string further except to put it into your HTML. The same is true of the $id parameter, which contains a string of the form ' id="idXYZ"'. You can put that directly into your HTML as well.
So let us put those fields into the opening ul tag and also for each list item (ignoring, for now, even and odd rows):
protected function renderContainer($class, $style, $events, $id) { $html = "<ul$class[0]$style[0]$events$id>"; foreach($this->getChildrenBySlot('main') as $id => $child) { # if the child is an object, render it, otherwise, use it as is if(is_object($child)) $childHtml = $child->render(); else $childHtml = $child; # synthesize the final HTML for the list item $html .= "<li$class[item]$style[item]>$childHtml</li>"; } $html .= '</ul>'; return $html; }
Note that there is no space between the interpolated variables $class[0], $style[0], etc. This is because those variables include a leading space, or are empty if there is no class/style set for the property. That is, if the user hasn't set a class for the list, then $class[0] will be empty and will not contain ' class=""'. This reduces the chance of extraneous HTML being generated.
All that's left now is to implement even and odd rows. First things first, we need to keep track of which items are even and which are odd. We can keep a counter in our renderContainer() method that increments for ever child added and if the value is even, we should apply the even class to the list item (along with the item class) and if it's odd, we apply the odd class. The same should be true for styles as well. This requires, unfortunately, that we rebuild the class and style strings. Fortunately, modern TK2 provides methods for regenerating these strings. In this case, we will use TKComponent::mergeClasses() and TKComponent::mergeStyles(). But it is sometimes also useful to use TKComponent::translateClass() and TKComponent::translateStyle(). Below is the modified code:
protected function renderContainer($class, $style, $events, $id) { $rowCount = 0; $html = "<ul$class[0]$style[0]$events$id>"; foreach($this->getChildrenBySlot('main') as $id => $child) { # if the child is an object, render it, otherwise, use it as is if(is_object($child)) $childHtml = $child->render(); else $childHtml = $child; # build the item classes and styles depending on whether the row is even or odd $which = ($rowCount % 2 == 0 ? 'even' : 'odd'); $itemClass = $this->mergeClasses(array('item', $which)); $itemStyle = $this->mergeStyles(array('item', $which)); # synthesize the final HTML for the list item $html .= "<li$itemClass$itemStyle>$childHtml</li>"; $rowCount++; } $html .= '</ul>'; return $html; }
So that's about it for creating a simple list. The rest of the work of adding components and keeping track of them, as well as keeping track of properties is taken care of by the toolkit.
protected function initialize() { # ... $this->setThemeDefault("TKMyList", "class", array('tkMyListOuter')); $this->setThemeDefault("TKMyList", "item_class", array('tkMyListItem')); $this->setThemeDefault("TKMyList", "even_class", array('tkMyListItem_even')); $this->setThemeDefault("TKMyList", "odd_class", array('tkMyListItem_odd')); # ... }
In each setThemeDefault() call, we specify the name of the class that we are setting the default for, in this case, it's TKMyList. Then we specify which property we want to set the default for. It can actually be any property, not just class and style properties. But since a theme is about appearance, it should really only specify class and style properties. The final parameter is the value for that property, just as if you were calling set() on that property. And that's it!
If you want derived classes to use the same defaults, then you must call TKTheme::inherit(), like so:
$this->inherit('TKMyDerivedClass', 'TKMyBaseClass');
The CSS classes should live in either ui/tk/toolkit.css, or in a project-specific CSS file.
To provide parameters for a component, simply add an array with the parameters at the end of the add() or addXxx() method call. For example, if we create a list and want one of our elements to have a custom class, then we can do this in the calling code:
$list = new TKMyList(); $list->addText('First Item'); $list->addText('Second Item', array('class' => array('highlight')));
Now the second list item will have a parameter called 'class' with the value array('highlight'). We can use this information in our renderContainer() method to apply a custom class, like so:
protected function renderContainer($class, $style, $events, $id) { $rowCount = 0; $html = "<ul$class[0]$style[0]$events$id>"; foreach($this->getChildrenBySlot('main') as $id => $child) { # if the child is an object, render it, otherwise, use it as is if(is_object($child)) $childHtml = $child->render(); else $childHtml = $child; # gather class and style params from the child $params = $this->getChildParams('main', $id); if(isset($params['class'])) $extraClasses = $params['class']; else $extraClasses = array(); if(isset($params['style'])) $extraStyles = $params['style']; else $extraStyles = array(); # build the item classes and styles depending on whether the row is even or odd $which = ($rowCount % 2 == 0 ? 'even' : 'odd'); $itemClass = $this->mergeClasses(array('item', $which), $extraClasses); $itemStyle = $this->mergeStyles(array('item', $which), $extraStyles); # synthesize the final HTML for the list item $html .= "<li$itemClass$itemStyle>$childHtml</li>"; $rowCount++; } $html .= '</ul>'; return $html; }
Here, we check to see if there is a 'class' or a 'style' parameter, and if so, use to that information to fill arrays which are then passed as 2nd arguments to mergeClasses() and mergeStyles().