Creating Toolkit2 Containers

Table of Contents

Overview

A container is a component that contains other components. This is useful for layout, or making lists or tables, or perhaps for more exotic constructions. Containers are hierarchical, in that each container can contain other components, which themselves may be containers containing components or other containers and so forth. This helps to make TK flexible as a way of building up user interfaces.

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.

Slots

First, we must talk about slots. Each container has one or more slots. A slot is basically an array of components. Most containers simply have one slot, which has all of the components that are contained in that container. However, there may be times where it is useful for a container to have multiple slots so that components can be grouped.

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().

Child Parameters

When a component is added to a container with TKContainer::add() and friends, an optional array of additional parameters can be passed in. These parameters can specify addition styles and classes for the component, or other bits of information specific to the container. It is up to the container to interpret parameters given to a child, and these parameters should be documented by the container. The container can access parameters for a child with TKContainer::getChildParams().

Tutorial

This tutorial will walk you through recreating TKList. It is a simple container, logically, but makes use of some of the more interesting features of TKComponent.

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:

Note that we follow the convention of ending our properties with '_class' or '_style'. This is actually required when using autotranslation of fields (see TKComponent::enableTranslateFields()). The real TKList also supports setting with a ul or an ol should be used as the list container element. We will eschew dealing with that, but it's relatively trivial to implement (see the TKList implementation if you really want the juicy details).

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.

Advanced Considerations

Themes

You do not want to hardcode classes and styles in your container class if at all possible. Instead, make properties for all classes and styles and have defaults for these set in a TK theme. For containers that are going to live in common, you will want to add the defaults in tk2/themes/TKDefaultTheme.php5. Otherwise, you will want to add the defaults to a project or page specific theme class. In either case, the way to set a default is the same. For our TKList copy class above, we probably want to set pretty defaults for the list 'class' itself, as well as for the items and the even and odd rows. To do that, we would add the following in TKDefaultTheme.php5:

    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.

Parameters

When adding components to a container, it is possible to provide additional parameters for that component. The parameters can also be associated with a component later with internal methods. These parameters can be used to store information specific to a given component. For example, if just one component needs to have a special style applied to its list item, then that information can be stored as a parameter and retrieved in renderContainer().

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().


Generated on Wed Nov 24 02:01:30 2010 for Common by  doxygen 1.5.6