Form Theming
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:
Custom Theme:
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\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()
inAttributeKeyFormView
delegates to attribute's control view.setLocation($locator)
usesConcrete\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:
And make it into a properly styled version:
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
topackages/my_site/elements/form/site.php
- and
concrete/elements/form/grouped/bootstrap3.php
topackages/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
: RegistersMySite\*
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.