Form Theming

Improvements?

Let us know by posting here.

Custom Template for Form Block in Concrete CMS

The form block is powered by Express Forms, allowing more flexibility and the addition of new attribute types. However, this introduces challenges in customizing the markup, especially for sites not using Twitter Bootstrap, as shown in the default and custom theme examples:

Default Theme: Default Theme Form

Custom Theme: Custom Theme Form

Challenges with Forking Attribute Views

Creating a custom template isn't straightforward due to the spread of logic across attribute types. Forking core attribute views like application/attributes/text/controller.php affects the display globally, including the Dashboard, which relies on Bootstrap styling.

Solution: Contexts

Contexts resolve this by allowing specification of rendering contexts. This feature lets you differentiate between front-end and dashboard displays, ensuring customization applies only where intended.

Next, we'll explore using a Form custom template and then delve into contexts for more nuanced control over form customization.

Express Form Custom Templating

Creating a custom template for the Express Form block in Concrete CMS involves understanding the block view template's markup. Key elements of this template include:

<?php defined('C5_EXECUTE') or die("Access Denied."); ?>
<div class="ccm-block-express-form">
    <!-- Renderer and Form Elements -->
    <form enctype="multipart/form-data" class="form-stacked" method="post" action="<?=$view->action('submit')?>#form<?=$bID?>">
        <!-- Form Rendering and Captcha -->
    </form>
</div>

The $renderer object, an instance of Concrete\Core\Express\Form\Renderer, encapsulates the form's view logic. Customizing the output of $renderer->render(); is challenging due to its encapsulation.

Customizable elements with this template include:

  • Success and error message outer markup.
  • Captcha outer markup.
  • Markup before/after the form.
  • Form container markup.
  • Submit button.

To modify form elements themselves, particularly for non-Bootstrap themes, a new custom context object is necessary. This object directs Express where to load site-specific form templates. Understanding this requires a deeper dive into Express form rendering.

Programmatically Rendering an Express Form

Rendering an Express form involves a few straightforward steps. First, retrieve the Express object:

$express = app('express');
$entity = $express->getObjectByHandle('student');

Select the desired form. For multiple forms, iterate to find the specific one:

$forms = $entity->getForms();
foreach($forms as $expressForm) {
    if ($expressForm->getName() == 'Frontend') {
        $form = $expressForm;
        break;
    }
}

With the Concrete\Core\Entity\Express\Form object in $form, create the context for form rendering. For front-end rendering, use the FrontendFormContext:

$context = new FrontendFormContext();

Then, prepare the renderer:

use Concrete\Core\Express\Form\Renderer;
$renderer = new Renderer($context, $form);
print $renderer->render();

Built-In Context Reference

Core contexts for rendering Express forms include:

  • Concrete\Core\Express\Form\Context\DashboardFormContext
  • Concrete\Core\Express\Form\Context\DashboardViewContext
  • Concrete\Core\Express\Form\Context\FormContext.php
  • Concrete\Core\Express\Form\Context\FrontendFormContext
  • Concrete\Core\Express\Form\Context\FrontendViewContext
  • Concrete\Core\Express\Form\Context\ViewContext

Use Dashboard contexts for the Dashboard and Frontend contexts for the site's front end.

Dynamically Retrieving the Appropriate Context

Dynamically binding custom contexts is possible for more tailored form rendering. Retrieve the controller and context like this:

$controller = $express->getEntityController($entity);
$factory = new ContextFactory($controller);
$context = $factory->getContext(new FrontendFormContext());

This method allows using a custom subclass of FrontendFormContext at runtime, offering more control and customization options.

Express Form Rendering Flow

Implemented form rendering code:

$controller = $express->getEntityController($entity);
$context = new FrontendFormContext();
$renderer = new Renderer($context, $form);

Rendered in page:

print $renderer->render();

form

Form\Express\Renderer::render()

Concrete\Core\Form\Express\Renderer::render() retrieves a Control View (Concrete\Core\Express\Form\Control\View\FormView), implementing Concrete\Core\Form\Control\ViewInterface, calling getControlRenderer().

getControlRenderer() returns Concrete\Core\Form\Control\RendererInterface, taking parameters:

  • Concrete\Core\Form\Control\View\FormView (View object)
  • FrontendFormContext (Context)

Concrete\Core\Form\Control\Renderer renders control, either the entire form or a specific control in the Express Form.

Form\Control\Renderer::render()

Renderer::render() uses Concrete\Core\Filesystem\TemplateLocator from Concrete\Core\Form\Control\ViewInterface (FormView) to locate templates.

TemplateLocator Object

TemplateLocator searches for Concrete files in various locations, replacing complex file existence checks.

createTemplateLocator() method in FormView:

public function createTemplateLocator()
{
    $locator = new TemplateLocator('form');
    return $locator;
}

TemplateLocator initialized with "form" template handle.

setLocation() method in Concrete\Core\Form\Context\ContextInterface (FrontendFormContext) modifies TemplateLocator.

setLocation() in FormContext:

public function setLocation(TemplateLocator $locator)
{
    $locator = parent::setLocation($locator);
    $locator->prependLocation(DIRNAME_ELEMENTS . DIRECTORY_SEPARATOR . DIRNAME_EXPRESS . DIRECTORY_SEPARATOR . DIRNAME_EXPRESS_FORM_CONTROLS . DIRECTORY_SEPARATOR . DIRNAME_EXPRESS_FORM_CONTROLS);
    return $locator;
}

Looks for 'elements/express/form/form/form.php'.

concrete/elements/express/form/form/form.php

Template contents:

<input type="hidden" name="express_form_id" value="<?=$form->getID()?>">
<?=$token->output('express_form')?>
<div class="ccm-dashboard-express-form">
    <?php foreach ($form->getFieldSets() as $fieldSet) { ?>
        <fieldset>
            <?php if ($fieldSet->getTitle()) { ?>
                <legend><?= $fieldSet->getTitle() ?></legend>
            <?php } ?>
            <?php foreach($fieldSet->getControls() as $setControl) {
                $controlView = $setControl->getControlView($context);
                if (is_object($controlView)) {
                    $renderer = $controlView->getControlRenderer();
                    print $renderer->render();
                }
            } ?>
        </fieldset>
    <?php } ?>
</div>

Looping through field sets and controls, rendering each using ControlInterface's getControlView and getControlRenderer.

Rendering the Name Control

getControlView for an attribute key control returns Concrete\Core\Express\Form\Control\View\AttributeKeyFormView.

Attribute context separated from express form context. Attribute's control view retrieved from within AttributeKeyFormView.

Running render()

Final rendering steps:

  • createTemplateLocator() in AttributeKeyFormView delegates to attribute's control view.
  • setLocation($locator) uses Concrete\Core\Attribute\Context\BasicFormContext, locating wrapper templates.

Wrapper template concrete/elements/form/bootstrap3.php:

<div class="form-group">
    <?php if ($view->supportsLabel()) { ?>
        <label class="control-label"><?=$view->getLabel()?></label>
    <?php } ?>
    <?php if ($view->isRequired()) { ?>
        <span class="text-muted small"><?=t('Required')?></span>
    <?php } ?>
    <?php $view->renderControl()?>
</div>

Rendering the Binder (Association) Control

Similar process for association control, determining view class based on context.

Customizing Form and Attribute Markup with Contexts

Express Forms in Concrete CMS have default styles that suit the Dashboard and Twitter Bootstrap themes, but they might not align well with themes using different markup. This guide shows how to make a form with default styling:

Broken form appearance

And make it into a properly styled version:

Fixed form appearance

Creating a Custom Form Controller

First, a custom form controller is needed. This is done by adding PHP script in a package that's autoloaded. For example, in a package my_site, include the following in the package controller:

protected $pkgAutoloaderRegistries = array(
    'src/MySite' => '\MySite',
);

Create FormController.php here:

packages/my_site/src/MySite/Express/Controller/FormController.php

The file starts as:

namespace MySite\Express\Controller;
use Concrete\Core\Express\Controller\StandardController;

class FormController extends StandardController
{

}

In the on_start() method of the package, specify this controller for Express Forms:

public function on_start()
{
    $this->app->make('Concrete\Core\Express\Controller\Manager')
    ->setStandardController('MySite\Express\Controller\FormController');
}

Single Form Customization

To apply the controller to a specific Express entity, use the extend() method with the entity's handle.

public function on_start()
{
    $this->app->make('Concrete\Core\Express\Controller\Manager')
    ->extend('document', function() {
        return new MySite\Express\Controller\DocumentFormController();
    });
}

Implementing a Context Registry

With the custom form controller, different functionalities like custom validation, notification, response handling, and context registry can be implemented. For custom context registry:

Creating the Custom Context Class

Create FrontendFormContext.php at:

packages/my_site/src/MySite/Express/Form/Context/FrontendFormContext.php

Extend the core's front-end form context:

namespace MySite\Express\Form\Context;

use Concrete\Core\Express\Form\Context\FrontendFormContext as CoreFrontendFormContext;

class FrontendFormContext extends CoreFrontendFormContext
{

}

In your custom controller, add a getContextRegistry method with a custom context registry object:

namespace MySite\Express\Controller;
use Concrete\Core\Express\Controller\StandardController;
use Concrete\Core\Express\Form\Context\FrontendFormContext as CoreFrontendFormContext;
use MySite\Express\Form\Context\FrontendFormContext;
use Concrete\Core\Form\Context\Registry\ContextRegistry;

class FormController extends StandardController
{
    public function getContextRegistry()
    {
        return new ContextRegistry([
            CoreFrontendFormContext::class => new FrontendFormContext()
        ]);
    }
}

Customizing the Custom Context

Edit FrontendFormContext.php to customize template loading:

use Concrete\Core\Filesystem\TemplateLocator;

public function setLocation(TemplateLocator $locator)
{
    $locator = parent::setLocation($locator);
    $locator->prependLocation([DIRNAME_ELEMENTS .
        DIRECTORY_SEPARATOR .
        DIRNAME_EXPRESS .
        DIRECTORY_SEPARATOR .
        DIRNAME_EXPRESS_FORM_CONTROLS .
        DIRECTORY_SEPARATOR .
        DIRNAME_EXPRESS_FORM_CONTROLS // not a typo
    , 'my_site']);
    return $locator;
}

Copying and Customizing Control Templates

Copy express custom control templates from concrete/elements/express/form/form to packages/my_site/elements/express/form/form and customize as needed.

Customizing Attributes

Implement custom attribute form context by creating FrontendFormContext.php in:

packages/my_site/src/MySite/Attribute/Context/FrontendFormContext.php

Extend the core class and add customization:

namespace MySite\Attribute\Context;

use Concrete\Core\Attribute\Context\FrontendFormContext as BaseFrontendFormContext;

class FrontendFormContext extends BaseFrontendFormContext
{

}

In MySite/Express/Form/Context/FrontendFormContext.php, add:

public function getAttributeContext()
{
    return new \MySite\Attribute\Context\FrontendFormContext();
}

Customize attribute context in MySite/Attibute/Context/FrontendFormContext.php:

public function __construct()
{
    parent::__construct();
    $this->preferTemplateIfAvailable('site', 'my_site');
}

public function setLocation(TemplateLocator $locator)
{
    $locator->setTemplate(['site', 'my_site']);
    return $locator;
}

Modifying Outer Template Files

Change outer templates by copying from

  • concrete/elements/form/bootstrap3.php to packages/my_site/elements/form/site.php
  • and concrete/elements/form/grouped/bootstrap3.php to packages/my_site/elements/form/grouped/site.php

Adjust as needed.

Creating Attribute Form Templates

Create site.php in packages/my_site/attributes/<attribute> for each attribute. Any time an attribute in this context is rendered, site.php for the attribute in question will be loaded from packages/my_site/attributes/<attribute>/site.php, if it exists, where <attribute> is the attribute handle in question such as address, email, number, select, text, textarea, or url.

For example, modifying the default attribute form.php, copied to packages/my_site/attributes/text/site.php, to include a <div class="field"></div> wrapper, the file might look like this:

<?php
defined('C5_EXECUTE') or die("Access Denied.");
?>
<div class="field">
    <?php
    print $form->text(
        $this->field('value'),
        $value,
        [
            'placeholder' => $this->akTextPlaceholder
        ]
    );

    ?>
</div>

Example: Modifying Express Form "Required" Fields Display

Learn how to change the display of required form elements in an Express form.

This screenshot shows the contact form in a standard Concrete install, with required elements labeled right of the label. We'll adjust their classes and placement.

Custom Templates

No need to modify block custom templates. We'll write code to impact all Express Forms in our theme's front-end.

Custom Front-end Form Context

Create FrontendFormContext.php in application/src/MySite/Express/Form/Context. This class determines where Express Form loads its templates and delivers a custom attribute context object for forms.

<?php

namespace MySite\Express\Form\Context;

use Concrete\Core\Express\Form\Context\FrontendFormContext as CoreFrontendFormContext;

class FrontendFormContext extends CoreFrontendFormContext
{

public function getAttributeContext()
{
    return new \MySite\Attribute\Context\FrontendFormContext();
}

}

Custom Attribute Context

This context dictates where Concrete loads forms for a specific attribute context. Create FrontendFormContext.php in src/MySite/Attribute/Context/.

<?php
namespace MySite\Attribute\Context;

use Concrete\Core\Attribute\Context\FrontendFormContext as BaseFrontendFormContext;
use Concrete\Core\Filesystem\TemplateLocator;

class FrontendFormContext extends BaseFrontendFormContext
{

    public function setLocation(TemplateLocator $locator)
    {
        $locator->setTemplate('frontend');
        return $locator;
    }

}

Copy and Edit Core Template

Copy concrete/elements/form/bootstrap3.php to application/elements/form/frontend.php. Edit frontend.php, to change the <span/> classes from text-muted small to label label-info.

Custom Form Controller

Create FormController.php in application/src/MySite/Express/Controller/. This controller uses our custom front-end form context.

<?php

namespace MySite\Express\Controller;
use Concrete\Core\Express\Controller\StandardController;
use Concrete\Core\Express\Form\Context\FrontendFormContext as CoreFrontendFormContext;
use MySite\Express\Form\Context\FrontendFormContext;

use Concrete\Core\Form\Context\Registry\ContextRegistry;

class FormController extends StandardController
{


    public function getContextRegistry()
    {
        return new ContextRegistry([
            CoreFrontendFormContext::class => new FrontendFormContext()
        ]);
    }
}

Enable Auto-Loading and Custom Controller

Add to application/bootstrap/app.php to set up namespace and controller defaults.

$strictLoader = new \Concrete\Core\Foundation\Psr4ClassLoader();
$strictLoader->addPrefix('\MySite', DIR_APPLICATION . '/src/MySite');
$strictLoader->register();

$app->make('Concrete\Core\Express\Controller\Manager')
    ->setStandardController(\MySite\Express\Controller\FormController::class);

Results

Our form now appears like this:

Conclusion and File List

You should have:

  • application/boostrap/app.php: Registers MySite\* namespace and custom form controller.
  • application/src/MySite/Express/Controller/FormController: Custom form controller for front-end context mapping.
  • application/src/Express/Form/Context/FrontendFormContext: Custom front-end form context object.
  • application/src/MySite/Attribute/Context/FrontendFormContext: Custom attribute context for overriding bootstrap3 template.
  • application/elements/form/frontend.php: Customized form control wrapper template.

Working with Packages

For package-level overrides, use the package's on_start() method and package autoloading, adjusting the setTemplate method with your package handle.