Default and extra classes in custom form templates

Hello.

I’m trying to implement mdb into my app.

So far I’ve created the following form templates:

//config/custom_forms.php

<?php
//Sobrecribe los templates usados por FormHelper
return [
    // Used for checkboxes in checkbox() and multiCheckbox().
    'checkbox' => '<input type="checkbox" class="form-check-input" name="{{name}}" value="{{value}}"{{attrs}}>',
    // Input group wrapper for checkboxes created via control().
    'checkboxFormGroup' => '{{label}}',
    // Wrapper container for checkboxes.
    'checkboxWrapper' => '<div class="checkbox">{{label}}</div>',
    // Error message wrapper elements.
    'error' => '<div class="invalid-feedback">{{content}}</div>',
    // General grouping container for control(). Defines input/label ordering.
    'formGroup' => '{{prepend}}{{label}}{{input}}',
    // Container element used by control().
    'inputContainer' => '<div class="md-form{{required}}">{{content}}</div>',
    // Container element used by control() when a field has an error.
    'inputContainerError' => '<div class="md-form">{{content}}{{error}}</div>',
    // Label element when inputs are not nested inside the label.
    'label' => '<label class="mdb-main-label" {{attrs}}>{{text}}</label>',
    // Label element used for radio and multi-checkbox inputs.
    'nestingLabel' => '{{hidden}}{{input}}<label{{attrs}}>{{text}}</label>',
    // Multi-select element,
    'selectMultiple' => '<select name="{{name}}[]" class="mdb-select colorful-select dropdown-primary md-form" multiple {{attrs}} searchable="Filtrar...">{{content}}</select>',
];

The problem is that I need to add default classes while still beign able of passing extra classes when using the Helper.

For example:

//template
'<input type="checkbox" class="form-check-input" name="{{name}}" value="{{value}}"{{attrs}}>'
//view
echo $this->Form->control('is_active', ['class' => 'extra-class']);
//desired output
'<input type="checkbox" class="form-check-input extra-class" name="is_active" value="0">'
//actual output
'<input type="checkbox" class="form-check-input" name="is_active" value="0">'
//duplicated attr key "class" is removed/omitted and attr value is not added to class

Is there a way to achieve this in CakePHP 4.x? Thanks


EDIT:
An option would be to use templateVars, but I’d like to write the less code possible:

//view
echo $this->Form->control('is_active', ['templateVars' => ['extraclasses' => 'extra-class']]);
//template
'<input type="checkbox" class="form-check-input {{extraclasses}}" name="{{name}}" value="{{value}}"{{attrs}}>'

Any class that’s added as in your example should be included in the {{attrs}}, and hence in the output from your template. How are you checking your output? If it’s with something like the developer tools in a browser, then it may be missing because the result will be a second class attribute in the same tag, which is invalid, so they just drop the second one. If you look at raw HTML, it may be there? (I’m testing this with Cake 3, so it might have changed in 4.x.)

I might not explained myself well (not english speaker).

That’s exactly what is happening: it creates a duplicated class attribute, hence the browser removes it.

What I need is to have a default class already added in the template while still being able to add extra classes via Helper.

I reviewed the code of FriendsOfCake/bootstrap-ui and they use some method called injectClasses but it’s too advanced for me to understand.

By now I have two options:

  • Do not create class atribute in templates and always pass the classes via Helper
  • Pass a templateVars and add it to class attribute on template.

I don’t like neither. But maybe it’s something i have to live with

Thanks.

A third option (if you can’t find the ideal fix) is:

echo $this->YourHelper->control('is_active', ['class' => 'extra-class']);

YourHelper implements this:

public function control($name, $options) {
    $options['templateVars' => ['extraclasses' => 'extra-class']];
    echo $this->Form->control($name, ['templateVars' => ['extraclasses' => 'extra-class']]);
}

In this way, when you do find the correct solution, you will simply have to change the helper called for control when the first arg is is_active and your code will be fix.

1 Like

You just gave me a simple idea. It may not be the best workaround, but it’s simple:

//view
echo $this->Form->control('is_active', ['class' => 'extra-class']);
//template
'<input type="checkbox" class="form-check-input{{extraClasses}}" name="{{name}}" value="{{value}}"{{attrs}}>'
//Custom FormHelper that extends Cake's FormHelper
<?php
declare(strict_types=1);

namespace App\View\Helper;

use Cake\View\Helper\FormHelper as Helper;

class FormHelper extends Helper
{
    /**
     * Generates a form input element complete with label and wrapper div.
     *
     * @param string $fieldName This should be "Modelname.fieldname".
     * @param array $options Each type of input takes different options.
     * @return string Completed form widget.
     */
    public function control(string $fieldName, array $options = []): string
    {
        if (!empty($options['class'])) {
            $options['templateVars']['extraClasses'] = ' ' . $options['class'];
            unset($options['class']);
        }

        return parent::control($fieldName, $options);
    }
}

May not be the best approach because now I have to create all templates but it’s a beginning.
I can add an input type switch to filter, etc…

Thanks!

I took your code and added to the origional control function with 1 other change and it works great.

public function control(string $fieldName, array $options = []): string
{
    $options += [
        'type' => null,
        'label' => null,
        'error' => null,
        'required' => null,
        'options' => null,
        'templates' => [],
        'templateVars' => [],
        'labelOptions' => true,
    ];
    $options = $this->_parseOptions($fieldName, $options);
    $options += ['id' => $this->_domId($fieldName)];

    if (!empty($options['class'])) {
        $options['templateVars']['extraClasses'] = ' ' . $options['class'];
        unset($options['class']);
    }

    $templater = $this->templater();
    $newTemplates = $options['templates'];

    if ($newTemplates) {
        $templater->push();
        $templateMethod = is_string($options['templates']) ? 'load' : 'add';
        $templater->{$templateMethod}($options['templates']);
    }
    unset($options['templates']);