<?php /** * @link http://www.yiiframework.com/ * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\bootstrap; use yii\helpers\Html; use yii\helpers\ArrayHelper; /** * A Bootstrap 3 enhanced version of [[\yii\widgets\ActiveField]]. * * This class adds some useful features to [[\yii\widgets\ActiveField|ActiveField]] to render all * sorts of Bootstrap 3 form fields in different form layouts: * * - [[inputTemplate]] is an optional template to render complex inputs, for example input groups * - [[horizontalCssClasses]] defines the CSS grid classes to add to label, wrapper, error and hint * in horizontal forms * - [[inline]]/[[inline()]] is used to render inline [[checkboxList()]] and [[radioList()]] * - [[enableError]] can be set to `false` to disable to the error * - [[enableLabel]] can be set to `false` to disable to the label * - [[label()]] can be used with a `boolean` argument to enable/disable the label * * There are also some new placeholders that you can use in the [[template]] configuration: * * - `{beginLabel}`: the opening label tag * - `{labelTitle}`: the label title for use with `{beginLabel}`/`{endLabel}` * - `{endLabel}`: the closing label tag * - `{beginWrapper}`: the opening wrapper tag * - `{endWrapper}`: the closing wrapper tag * * The wrapper tag is only used for some layouts and form elements. * * Note that some elements use slightly different defaults for [[template]] and other options. * You may want to override those predefined templates for checkboxes, radio buttons, checkboxLists * and radioLists in the [[\yii\widgets\ActiveForm::fieldConfig|fieldConfig]] of the * [[\yii\widgets\ActiveForm]]: * * - [[checkboxTemplate]] the template for checkboxes in default layout * - [[radioTemplate]] the template for radio buttons in default layout * - [[horizontalCheckboxTemplate]] the template for checkboxes in horizontal layout * - [[horizontalRadioTemplate]] the template for radio buttons in horizontal layout * - [[inlineCheckboxListTemplate]] the template for inline checkboxLists * - [[inlineRadioListTemplate]] the template for inline radioLists * * Example: * * ```php * use yii\bootstrap\ActiveForm; * * $form = ActiveForm::begin(['layout' => 'horizontal']); * * // Form field without label * echo $form->field($model, 'demo', [ * 'inputOptions' => [ * 'placeholder' => $model->getAttributeLabel('demo'), * ], * ])->label(false); * * // Inline radio list * echo $form->field($model, 'demo')->inline()->radioList($items); * * // Control sizing in horizontal mode * echo $form->field($model, 'demo', [ * 'horizontalCssClasses' => [ * 'wrapper' => 'col-sm-2', * ] * ]); * * // With 'default' layout you would use 'template' to size a specific field: * // echo $form->field($model, 'demo', [ * // 'template' => '{label} <div class="row"><div class="col-sm-4">{input}{error}{hint}</div></div>' * // ]); * * // Input group * echo $form->field($model, 'demo', [ * 'inputTemplate' => '<div class="input-group"><span class="input-group-addon">@</span>{input}</div>', * ]); * * ActiveForm::end(); * ``` * * @see \yii\bootstrap\ActiveForm * @see http://getbootstrap.com/css/#forms * * @author Michael Härtl <haertl.mike@gmail.com> * @since 2.0 */ class ActiveField extends \yii\widgets\ActiveField { /** * @var boolean whether to render [[checkboxList()]] and [[radioList()]] inline. */ public $inline = false; /** * @var string|null optional template to render the `{input}` placeholder content */ public $inputTemplate; /** * @var array options for the wrapper tag, used in the `{beginWrapper}` placeholder */ public $wrapperOptions = []; /** * @var null|array CSS grid classes for horizontal layout. This must be an array with these keys: * - 'offset' the offset grid class to append to the wrapper if no label is rendered * - 'label' the label grid class * - 'wrapper' the wrapper grid class * - 'error' the error grid class * - 'hint' the hint grid class */ public $horizontalCssClasses; /** * @var string the template for checkboxes in default layout */ public $checkboxTemplate = "<div class=\"checkbox\">\n{beginLabel}\n{input}\n{labelTitle}\n{endLabel}\n{error}\n{hint}\n</div>"; /** * @var string the template for radios in default layout */ public $radioTemplate = "<div class=\"radio\">\n{beginLabel}\n{input}\n{labelTitle}\n{endLabel}\n{error}\n{hint}\n</div>"; /** * @var string the template for checkboxes in horizontal layout */ public $horizontalCheckboxTemplate = "{beginWrapper}\n<div class=\"checkbox\">\n{beginLabel}\n{input}\n{labelTitle}\n{endLabel}\n</div>\n{error}\n{endWrapper}\n{hint}"; /** * @var string the template for radio buttons in horizontal layout */ public $horizontalRadioTemplate = "{beginWrapper}\n<div class=\"radio\">\n{beginLabel}\n{input}\n{labelTitle}\n{endLabel}\n</div>\n{error}\n{endWrapper}\n{hint}"; /** * @var string the template for inline checkboxLists */ public $inlineCheckboxListTemplate = "{label}\n{beginWrapper}\n{input}\n{error}\n{endWrapper}\n{hint}"; /** * @var string the template for inline radioLists */ public $inlineRadioListTemplate = "{label}\n{beginWrapper}\n{input}\n{error}\n{endWrapper}\n{hint}"; /** * @var boolean whether to render the error. Default is `true` except for layout `inline`. */ public $enableError = true; /** * @var boolean whether to render the label. Default is `true`. */ public $enableLabel = true; /** * @inheritdoc */ public function __construct($config = []) { $layoutConfig = $this->createLayoutConfig($config); $config = ArrayHelper::merge($layoutConfig, $config); parent::__construct($config); } /** * @inheritdoc */ public function render($content = null) { if ($content === null) { if (!isset($this->parts['{beginWrapper}'])) { $options = $this->wrapperOptions; $tag = ArrayHelper::remove($options, 'tag', 'div'); $this->parts['{beginWrapper}'] = Html::beginTag($tag, $options); $this->parts['{endWrapper}'] = Html::endTag($tag); } if ($this->enableLabel === false) { $this->parts['{label}'] = ''; $this->parts['{beginLabel}'] = ''; $this->parts['{labelTitle}'] = ''; $this->parts['{endLabel}'] = ''; } elseif (!isset($this->parts['{beginLabel}'])) { $this->renderLabelParts(); } if ($this->enableError === false) { $this->parts['{error}'] = ''; } if ($this->inputTemplate) { $input = isset($this->parts['{input}']) ? $this->parts['{input}'] : Html::activeTextInput($this->model, $this->attribute, $this->inputOptions); $this->parts['{input}'] = strtr($this->inputTemplate, ['{input}' => $input]); } } return parent::render($content); } /** * @inheritdoc */ public function checkbox($options = [], $enclosedByLabel = true) { if ($enclosedByLabel) { if (!isset($options['template'])) { $this->template = $this->form->layout === 'horizontal' ? $this->horizontalCheckboxTemplate : $this->checkboxTemplate; } else { $this->template = $options['template']; unset($options['template']); } if ($this->form->layout === 'horizontal') { Html::addCssClass($this->wrapperOptions, $this->horizontalCssClasses['offset']); } $this->labelOptions['class'] = null; } return parent::checkbox($options, false); } /** * @inheritdoc */ public function radio($options = [], $enclosedByLabel = true) { if ($enclosedByLabel) { if (!isset($options['template'])) { $this->template = $this->form->layout === 'horizontal' ? $this->horizontalRadioTemplate : $this->radioTemplate; } else { $this->template = $options['template']; unset($options['template']); } if ($this->form->layout === 'horizontal') { Html::addCssClass($this->wrapperOptions, $this->horizontalCssClasses['offset']); } $this->labelOptions['class'] = null; } return parent::radio($options, false); } /** * @inheritdoc */ public function checkboxList($items, $options = []) { if ($this->inline) { if (!isset($options['template'])) { $this->template = $this->inlineCheckboxListTemplate; } else { $this->template = $options['template']; unset($options['template']); } if (!isset($options['itemOptions'])) { $options['itemOptions'] = [ 'labelOptions' => ['class' => 'checkbox-inline'], ]; } } elseif (!isset($options['item'])) { $options['item'] = function ($index, $label, $name, $checked, $value) { return '<div class="checkbox">' . Html::checkbox($name, $checked, ['label' => $label, 'value' => $value]) . '</div>'; }; } parent::checkboxList($items, $options); return $this; } /** * @inheritdoc */ public function radioList($items, $options = []) { if ($this->inline) { if (!isset($options['template'])) { $this->template = $this->inlineRadioListTemplate; } else { $this->template = $options['template']; unset($options['template']); } if (!isset($options['itemOptions'])) { $options['itemOptions'] = [ 'labelOptions' => ['class' => 'radio-inline'], ]; } } elseif (!isset($options['item'])) { $options['item'] = function ($index, $label, $name, $checked, $value) { return '<div class="radio">' . Html::radio($name, $checked, ['label' => $label, 'value' => $value]) . '</div>'; }; } parent::radioList($items, $options); return $this; } /** * @inheritdoc */ public function label($label = null, $options = []) { if (is_bool($label)) { $this->enableLabel = $label; if ($label === false && $this->form->layout === 'horizontal') { Html::addCssClass($this->wrapperOptions, $this->horizontalCssClasses['offset']); } } else { $this->enableLabel = true; $this->renderLabelParts($label, $options); parent::label($label, $options); } return $this; } /** * @param boolean $value whether to render a inline list * @return static the field object itself * Make sure you call this method before [[checkboxList()]] or [[radioList()]] to have any effect. */ public function inline($value = true) { $this->inline = (bool) $value; return $this; } /** * @param array $instanceConfig the configuration passed to this instance's constructor * @return array the layout specific default configuration for this instance */ protected function createLayoutConfig($instanceConfig) { $config = [ 'hintOptions' => [ 'tag' => 'p', 'class' => 'help-block', ], 'errorOptions' => [ 'tag' => 'p', 'class' => 'help-block help-block-error', ], 'inputOptions' => [ 'class' => 'form-control', ], ]; $layout = $instanceConfig['form']->layout; if ($layout === 'horizontal') { $config['template'] = "{label}\n{beginWrapper}\n{input}\n{error}\n{endWrapper}\n{hint}"; $cssClasses = [ 'offset' => 'col-sm-offset-3', 'label' => 'col-sm-3', 'wrapper' => 'col-sm-6', 'error' => '', 'hint' => 'col-sm-3', ]; if (isset($instanceConfig['horizontalCssClasses'])) { $cssClasses = ArrayHelper::merge($cssClasses, $instanceConfig['horizontalCssClasses']); } $config['horizontalCssClasses'] = $cssClasses; $config['wrapperOptions'] = ['class' => $cssClasses['wrapper']]; $config['labelOptions'] = ['class' => 'control-label ' . $cssClasses['label']]; $config['errorOptions'] = ['class' => 'help-block help-block-error ' . $cssClasses['error']]; $config['hintOptions'] = ['class' => 'help-block ' . $cssClasses['hint']]; } elseif ($layout === 'inline') { $config['labelOptions'] = ['class' => 'sr-only']; $config['enableError'] = false; } return $config; } /** * @param string|null $label the label or null to use model label * @param array $options the tag options */ protected function renderLabelParts($label = null, $options = []) { $options = array_merge($this->labelOptions, $options); if ($label === null) { if (isset($options['label'])) { $label = $options['label']; unset($options['label']); } else { $attribute = Html::getAttributeName($this->attribute); $label = Html::encode($this->model->getAttributeLabel($attribute)); } } $this->parts['{beginLabel}'] = Html::beginTag('label', $options); $this->parts['{endLabel}'] = Html::endTag('label'); $this->parts['{labelTitle}'] = $label; } }