diff --git a/src/Former/Form/Fields/Switchbox.php b/src/Former/Form/Fields/Switchbox.php new file mode 100644 index 00000000..570460f0 --- /dev/null +++ b/src/Former/Form/Fields/Switchbox.php @@ -0,0 +1,23 @@ +isGrouped()) { + // Remove any possible items added by the Populator. + $this->items = array(); + } + $this->items(func_get_args()); + + return $this; + } +} diff --git a/src/Former/Form/Group.php b/src/Former/Form/Group.php index 12b1f72a..6877547e 100644 --- a/src/Former/Form/Group.php +++ b/src/Former/Form/Group.php @@ -3,6 +3,7 @@ use BadMethodCallException; use Former\Helpers; +use HtmlObject\Input; use HtmlObject\Element; use HtmlObject\Traits\Tag; use Illuminate\Container\Container; @@ -176,17 +177,30 @@ public function wrapField($field) { $label = $this->getLabel($field); $help = $this->getHelp(); - if ($field->isCheckable() && $this->app['former']->framework() == 'TwitterBootstrap4') { - $wrapperClass = $field->isInline() ? 'form-check form-check-inline' : 'form-check'; + if ($field->isCheckable() && + in_array($this->app['former']->framework(), ['TwitterBootstrap4', 'TwitterBootstrap5']) + ) { + $wrapperClass = null; + if ($this->app['former']->framework() === 'TwitterBootstrap4') { + $wrapperClass = $field->isInline() ? 'form-check form-check-inline' : 'form-check'; + } if ($this->app['former']->getErrors($field->getName())) { - $hiddenInput = Element::create('input', null, ['type' => 'hidden'])->class('form-check-input is-invalid'); + $hiddenInput = Input::create('hidden')->addClass('form-check-input is-invalid'); $help = $hiddenInput.$help; } - $help = Element::create('div', $help)->class($wrapperClass); + $help = $help ? Element::create('div', $help)->addClass($wrapperClass) : ''; } + $withFloatingLabel = $field->withFloatingLabel(); + $field = $this->prependAppend($field); $field .= $help; + if ($withFloatingLabel && + $this->app['former']->framework() === 'TwitterBootstrap5' + ) { + return $this->wrapWithFloatingLabel($field, $label); + } + return $this->wrap($field, $label); } @@ -210,17 +224,27 @@ public function state($state) /** * Set a class on the Group * - * @param string $class The class to add + * @param string $class The class(es) to add on the Group */ public function addGroupClass($class) { $this->addClass($class); } + /** + * Remove one or more classes on the Group + * + * @param string $class The class(es) to remove on the Group + */ + public function removeGroupClass($class) + { + $this->removeClass($class); + } + /** * Set a class on the Label * - * @param string $class The class to add on the Label + * @param string $class The class(es) to add on the Label */ public function addLabelClass($class) { @@ -234,6 +258,23 @@ public function addLabelClass($class) return $this; } + /** + * Remove one or more classes on the Label + * + * @param string $class The class(es) to remove on the Label + */ + public function removeLabelClass($class) + { + // Don't remove a label class if it isn't an Element instance + if (!$this->label instanceof Element) { + return $this; + } + + $this->label->removeClass($class); + + return $this; + } + /** * Adds a label to the group * @@ -322,8 +363,9 @@ public function blockHelp($help, $attributes = array()) { // Reserved method if ($this->app['former.framework']->isnt('TwitterBootstrap') && - $this->app['former.framework']->isnt('TwitterBootstrap3') && - $this->app['former.framework']->isnt('TwitterBootstrap4') + $this->app['former.framework']->isnt('TwitterBootstrap3') && + $this->app['former.framework']->isnt('TwitterBootstrap4') && + $this->app['former.framework']->isnt('TwitterBootstrap5') ) { throw new BadMethodCallException('This method is only available on the Bootstrap framework'); } @@ -428,6 +470,23 @@ public function wrap($contents, $label = null) return $group; } + /** + * Wraps content in a group with floating label + * + * @param string $contents The content + * @param string $label The label to add + * + * @return string A group + */ + public function wrapWithFloatingLabel($contents, $label = null) + { + $floatingLabelClass = $this->app['former.framework']->getFloatingLabelClass(); + if ($floatingLabelClass) { + $this->addClass($floatingLabelClass); + } + return $this->wrap($label, $contents); + } + /** * Prints out the current label * @@ -445,7 +504,7 @@ protected function getLabel($field = null) // Wrap label in framework classes $labelClasses = $this->app['former.framework']->getLabelClasses(); if ($field->isCheckable() && - $this->app['former']->framework() == 'TwitterBootstrap4' && + in_array($this->app['former']->framework(), ['TwitterBootstrap4', 'TwitterBootstrap5']) && $this->app['former.form']->isOfType('horizontal') ) { $labelClasses = array_merge($labelClasses, array('pt-0')); diff --git a/src/Former/Framework/TwitterBootstrap5.php b/src/Former/Framework/TwitterBootstrap5.php new file mode 100644 index 00000000..ebda9d62 --- /dev/null +++ b/src/Former/Framework/TwitterBootstrap5.php @@ -0,0 +1,496 @@ +app = $app; + $this->setFrameworkDefaults(); + } + + //////////////////////////////////////////////////////////////////// + /////////////////////////// FILTER ARRAYS ////////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Filter buttons classes + * + * @param array $classes An array of classes + * + * @return string[] A filtered array + */ + public function filterButtonClasses($classes) + { + // Filter classes + // $classes = array_intersect($classes, $this->buttons); + + // Prepend button type + $classes = $this->prependWith($classes, 'btn-'); + $classes[] = 'btn'; + + return $classes; + } + + /** + * Filter field classes + * + * @param array $classes An array of classes + * + * @return array A filtered array + */ + public function filterFieldClasses($classes) + { + // Filter classes + $classes = array_intersect($classes, $this->fields); + + // Prepend field type + $classes = array_map(function ($class) { + return Str::startsWith($class, 'col') ? $class : 'input-'.$class; + }, $classes); + + return $classes; + } + + //////////////////////////////////////////////////////////////////// + ///////////////////// EXPOSE FRAMEWORK SPECIFICS /////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Framework error state + * + * @return string + */ + public function errorState() + { + return 'is-invalid'; + } + + /** + * Returns corresponding inline class of a field + * + * @param Field $field + * + * @return string + */ + public function getInlineLabelClass($field) + { + $inlineClass = parent::getInlineLabelClass($field); + if ($field->isOfType('checkbox', 'checkboxes', 'radio', 'radios')) { + $inlineClass = 'form-check-label'; + } + + return $inlineClass; + } + + /** + * Set the fields width from a label width + * + * @param array $labelWidths + */ + protected function setFieldWidths($labelWidths) + { + $labelWidthClass = $fieldWidthClass = $fieldOffsetClass = ''; + + $viewports = $this->getFrameworkOption('viewports'); + foreach ($labelWidths as $viewport => $columns) { + if ($viewport) { + $labelWidthClass .= " col-$viewports[$viewport]-$columns"; + $fieldWidthClass .= " col-$viewports[$viewport]-".(12 - $columns); + $fieldOffsetClass .= " offset-$viewports[$viewport]-$columns"; + } + } + + $this->labelWidth = ltrim($labelWidthClass); + $this->fieldWidth = ltrim($fieldWidthClass); + $this->fieldOffset = ltrim($fieldOffsetClass); + } + + //////////////////////////////////////////////////////////////////// + ///////////////////////////// ADD CLASSES ////////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Add classes to a field + * + * @param Field $field + * @param array $classes The possible classes to add + * + * @return Field + */ + public function getFieldClasses(Field $field, $classes) + { + // Add inline class for checkables + if ($field->isCheckable()) { + // Adds correct checkbox input class when is a checkbox (or radio) + $field->addClass('form-check-input'); + $classes[] = 'form-check'; + + if (in_array('inline', $classes)) { + $field->inline(); + } + } + + // Filter classes according to field type + if ($field->isButton()) { + $classes = $this->filterButtonClasses($classes); + } else { + $classes = $this->filterFieldClasses($classes); + } + + // Add form-control class for text-type, textarea and file fields + // As text-type is open-ended we instead exclude those that shouldn't receive the class + if (!$field->isCheckable() && !$field->isButton() && !in_array($field->getType(), [ + 'plaintext', + 'select', + ]) && !in_array('form-control', $classes) + ) { + $classes[] = 'form-control'; + } + + // Add form-select class for select fields + if ($field->getType() === 'select' && !in_array('form-select', $classes)) { + $classes[] = 'form-select'; + } + + if ($this->app['former']->getErrors($field->getName())) { + $classes[] = $this->errorState(); + } + + return $this->addClassesToField($field, $classes); + } + + /** + * Add group classes + * + * @return string A list of group classes + */ + public function getGroupClasses() + { + if ($this->app['former.form']->isOfType('horizontal')) { + return 'mb-3 row'; + } else { + return 'mb-3'; + } + } + + /** + * Add label classes + * + * @return string[] An array of attributes with the label class + */ + public function getLabelClasses() + { + if ($this->app['former.form']->isOfType('horizontal')) { + return array('col-form-label', $this->labelWidth); + } elseif ($this->app['former.form']->isOfType('inline')) { + return array('visually-hidden'); + } else { + return array('form-label'); + } + } + + /** + * Add uneditable field classes + * + * @return string An array of attributes with the uneditable class + */ + public function getUneditableClasses() + { + return ''; + } + + /** + * Add plain text field classes + * + * @return string An array of attributes with the plain text class + */ + public function getPlainTextClasses() + { + return 'form-control-plaintext'; + } + + /** + * Add form class + * + * @param string $type The type of form to add + * + * @return string|null + */ + public function getFormClasses($type) + { + return $type ? 'form-'.$type : null; + } + + /** + * Add actions block class + * + * @return string|null + */ + public function getActionClasses() + { + if ($this->app['former.form']->isOfType('horizontal') || $this->app['former.form']->isOfType('inline')) { + return 'mb-3 row'; + } + + return null; + } + + /** + * Add floating label class + * + * @return string Get the floating label class + */ + public function getFloatingLabelClass() + { + return 'form-floating'; + } + + //////////////////////////////////////////////////////////////////// + //////////////////////////// RENDER BLOCKS ///////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Render an help text + * + * @param string $text + * @param array $attributes + * + * @return Element + */ + public function createHelp($text, $attributes = array()) + { + return Element::create('span', $text, $attributes)->addClass('form-text'); + } + + /** + * Render an validation error text + * + * @param string $text + * @param array $attributes + * + * @return string + */ + public function createValidationError($text, $attributes = array()) + { + return Element::create('div', $text, $attributes)->addClass('invalid-feedback'); + } + + /** + * Render an help text + * + * @param string $text + * @param array $attributes + * + * @return Element + */ + public function createBlockHelp($text, $attributes = array()) + { + return Element::create('div', $text, $attributes)->addClass('form-text'); + } + + /** + * Render a disabled field + * + * @param Field $field + * + * @return Element + */ + public function createDisabledField(Field $field) + { + return Element::create('span', $field->getValue(), $field->getAttributes()); + } + + /** + * Render a plain text field + * + * @param Field $field + * + * @return Element + */ + public function createPlainTextField(Field $field) + { + $label = $field->getLabel(); + if ($label) { + $label->for(''); + } + + return Element::create('div', $field->getValue(), $field->getAttributes()); + } + + //////////////////////////////////////////////////////////////////// + //////////////////////////// WRAP BLOCKS /////////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Wrap an item to be prepended or appended to the current field + * + * @return Element A wrapped item + */ + public function placeAround($item, $place = null) + { + // Render object + if (is_object($item) and method_exists($item, '__toString')) { + $item = $item->__toString(); + } + + $items = (array) $item; + $element = ''; + foreach ($items as $item) { + $hasButtonTag = strpos(ltrim($item), 'addClass($class); + } + + return $element; + } + + /** + * Wrap a field with prepended and appended items + * + * @param Field $field + * @param array $prepend + * @param array $append + * + * @return string A field concatented with prepended and/or appended items + */ + public function prependAppend($field, $prepend, $append) + { + $return = '
'; + $return .= join(null, $prepend); + $return .= $field->render(); + $return .= join(null, $append); + $return .= '
'; + + return $return; + } + + /** + * Wrap a field with potential additional tags + * + * @param Field $field + * + * @return Element A wrapped field + */ + public function wrapField($field) + { + if ($this->app['former.form']->isOfType('horizontal')) { + return Element::create('div', $field)->addClass($this->fieldWidth); + } + + return $field; + } + + /** + * Wrap actions block with potential additional tags + * + * @param Actions $actions + * + * @return string A wrapped actions block + */ + public function wrapActions($actions) + { + // For horizontal forms, we wrap the actions in a div + if ($this->app['former.form']->isOfType('horizontal')) { + return Element::create('div', $actions)->addClass(array($this->fieldOffset, $this->fieldWidth)); + } + + return $actions; + } +} diff --git a/src/Former/MethodDispatcher.php b/src/Former/MethodDispatcher.php index c80f50fc..17ca8043 100644 --- a/src/Former/MethodDispatcher.php +++ b/src/Former/MethodDispatcher.php @@ -230,6 +230,10 @@ protected function getClassFromMethod($method) // Else convert known fields to their classes switch ($method) { + case 'switch': + case 'switches': + $class = Former::FIELDSPACE.'Switchbox'; + break; case 'submit': case 'link': case 'reset': diff --git a/src/Former/Traits/Checkable.php b/src/Former/Traits/Checkable.php index e79cb55f..93fac5a0 100644 --- a/src/Former/Traits/Checkable.php +++ b/src/Former/Traits/Checkable.php @@ -363,10 +363,9 @@ protected function createCheckable($item, $fallbackValue = 1) // If inline items, add class $isInline = $this->inline ? ' '.$this->app['former.framework']->getInlineLabelClass($this) : null; - // In Bootsrap 3 or 4, don't append the the checkable type (radio/checkbox) as a class if + // In Bootsrap 3 or 4 or 5, don't append the the checkable type (radio/checkbox) as a class if // rendering inline. - $class = ($this->app['former']->framework() == 'TwitterBootstrap3' || - $this->app['former']->framework() == 'TwitterBootstrap4') ? trim($isInline) : $this->checkable.$isInline; + $class = in_array($this->app['former']->framework(), ['TwitterBootstrap3', 'TwitterBootstrap4', 'TwitterBootstrap5']) ? trim($isInline) : $this->checkable.$isInline; // Merge custom attributes with global attributes $attributes = array_merge($this->attributes, $attributes); @@ -381,7 +380,7 @@ protected function createCheckable($item, $fallbackValue = 1) } // Add hidden checkbox if requested - if ($this->isOfType('checkbox', 'checkboxes')) { + if ($this->isOfType('checkbox', 'checkboxes', 'switch', 'switches')) { if ($this->isPushed or ($this->app['former']->getOption('push_checkboxes') and $this->isPushed !== false)) { $field = $this->app['former']->hidden($name)->forceValue($this->app['former']->getOption('unchecked_value')).$field->render(); @@ -392,16 +391,23 @@ protected function createCheckable($item, $fallbackValue = 1) } // If no label to wrap, return plain checkable - if (!$label) { + if (!$label && !$this->isOfType('switch', 'switches')) { $element = (is_object($field)) ? $field->render() : $field; - } elseif ($this->app['former']->framework() == 'TwitterBootstrap4') { - // Revised for Bootstrap 4, move the 'input' outside of the 'label' - $labelClass = 'form-check-label'; - $element = $field . Element::create('label', $label)->for($attributes['id'])->class($labelClass)->render(); - - $wrapper_class = $this->inline ? 'form-check form-check-inline' : 'form-check'; + } elseif (in_array($this->app['former']->framework(), ['TwitterBootstrap4', 'TwitterBootstrap5'])) { + $element = $field; + if ($label) { + // Revised for Bootstrap 4 and 5, move the 'input' outside of the 'label' + $labelClass = 'form-check-label'; + $element .= Element::create('label', $label)->for($attributes['id'])->class($labelClass)->render(); + } - $element = Element::create('div', $element)->class($wrapper_class)->render(); + $wrapperClass = $this->inline ? 'form-check form-check-inline' : 'form-check'; + if ($this->app['former']->framework() === 'TwitterBootstrap5' && + $this->isOfType('switch', 'switches') + ) { + $wrapperClass.= ' form-switch'; + } + $element = Element::create('div', $element)->class($wrapperClass)->render(); } else { // Original way is to add the 'input' inside the 'label' $element = Element::create('label', $field.$label)->for($attributes['id'])->class($class)->render(); diff --git a/src/Former/Traits/Field.php b/src/Former/Traits/Field.php index 11e3a8de..fb8c0b2e 100644 --- a/src/Former/Traits/Field.php +++ b/src/Former/Traits/Field.php @@ -65,6 +65,13 @@ abstract class Field extends FormerObject implements FieldInterface */ protected $bind; + /** + * Renders with floating label + * + * @var boolean + */ + protected $floatingLabel = false; + /** * Get the current framework instance * @@ -230,7 +237,7 @@ public function isUnwrappable() */ public function isCheckable() { - return $this->isOfType('checkbox', 'checkboxes', 'radio', 'radios'); + return $this->isOfType('checkbox', 'checkboxes', 'radio', 'radios', 'switch', 'switches'); } /** @@ -243,6 +250,16 @@ public function isButton() return false; } + /** + * Check if the field get a floating label + * + * @return boolean + */ + public function withFloatingLabel() + { + return $this->floatingLabel; + } + /** * Get the rules applied to the current field * @@ -386,6 +403,16 @@ public function name($name) return $this; } + /** + * Set the field as floating label + */ + public function floatingLabel($isFloatingLabel = true) + { + $this->floatingLabel = $isFloatingLabel; + + return $this; + } + /** * Get the field's labels * diff --git a/src/config/former.php b/src/config/former.php index b7bbac6e..c37fe795 100644 --- a/src/config/former.php +++ b/src/config/former.php @@ -77,6 +77,29 @@ // The framework to be used by Former 'framework' => 'TwitterBootstrap3', + 'TwitterBootstrap5' => array( + + // Map Former-supported viewports to Bootstrap 5 equivalents + 'viewports' => array( + 'large' => 'lg', + 'medium' => 'md', + 'small' => 'sm', + 'mini' => 'xs', + ), + // Width of labels for horizontal forms expressed as viewport => grid columns + 'labelWidths' => array( + 'large' => 2, + 'small' => 4, + ), + // HTML markup and classes used by Bootstrap 5 for icons + 'icon' => array( + 'tag' => 'i', + 'set' => 'fa', + 'prefix' => 'fa', + ), + + ), + 'TwitterBootstrap4' => array( // Map Former-supported viewports to Bootstrap 4 equivalents @@ -91,7 +114,7 @@ 'large' => 2, 'small' => 4, ), - // HTML markup and classes used by Bootstrap 5 for icons + // HTML markup and classes used by Bootstrap 4 for icons 'icon' => array( 'tag' => 'i', 'set' => 'fa', diff --git a/tests/Fields/FloatingLabelTest.php b/tests/Fields/FloatingLabelTest.php new file mode 100644 index 00000000..7104af31 --- /dev/null +++ b/tests/Fields/FloatingLabelTest.php @@ -0,0 +1,173 @@ +former->framework('TwitterBootstrap5'); + $this->former->vertical_open()->__toString(); + } + + //////////////////////////////////////////////////////////////////// + ////////////////////////////// MATCHERS //////////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Matches a floating label + * + * @return array + */ + public function matchFloatingLabel() + { + return array( + 'tag' => 'label', + ); + } + + /** + * Matches an input text tag + * + * @return array + */ + public function matchFloatingInputText() + { + return array( + 'tag' => 'input', + 'attributes' => array( + 'id' => 'foo', + 'class' => 'form-control', + 'placeholder' => 'Dummy placeholder', + 'type' => 'text', + 'name' => 'foo', + ), + ); + } + + /** + * Matches a textarea tag + * + * @return array + */ + public function matchFloatingTextarea() + { + return array( + 'tag' => 'textarea', + 'attributes' => array( + 'id' => 'foo', + 'class' => 'form-control', + 'placeholder' => 'Dummy placeholder', + 'name' => 'foo', + ), + ); + } + + /** + * Matches a select tag + * + * @return array + */ + public function matchFloatingSelect() + { + return array( + 'tag' => 'select', + 'children' => array( + 'count' => 4, + 'only' => array('tag' => 'option'), + ), + 'attributes' => array( + 'id' => 'foo', + 'class' => 'form-select', + 'name' => 'foo', + ), + ); + } + + //////////////////////////////////////////////////////////////////// + ////////////////////////////// ASSERTIONS ////////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Matches a Form Floating Label Group + * + * @param string $input + * @param string $label + * + * @return boolean + */ + protected function formFloatingLabelGroup( + $input = '', + $label = '' + ) { + return '
'.$input.$label.'
'; + } + + //////////////////////////////////////////////////////////////////// + //////////////////////////////// TESTS ///////////////////////////// + //////////////////////////////////////////////////////////////////// + + public function testCanCreateFloatingLabelInputText() + { + $input = $this->former + ->text('foo') + ->placeholder('Dummy placeholder') + ->floatingLabel() + ->__toString(); + + $this->assertHTML($this->matchFloatingLabel(), $input); + $this->assertHTML($this->matchFloatingInputText(), $input); + + $matcher = $this->formFloatingLabelGroup(); + $this->assertEquals($matcher, $input); + } + + public function testCanCreateFloatingLabelTextarea() + { + $textarea = $this->former + ->textarea('foo') + ->placeholder('Dummy placeholder') + ->floatingLabel() + ->__toString(); + + $this->assertHTML($this->matchFloatingLabel(), $textarea); + $this->assertHTML($this->matchFloatingTextarea(), $textarea); + + $matcher = $this->formFloatingLabelGroup(''); + $this->assertEquals($matcher, $textarea); + } + + public function testCanCreateFloatingLabelSelect() + { + $selectElement = $this->former + ->select('foo') + ->options($this->options) + ->placeholder('Choose an option') + ->floatingLabel(); + + $select = $selectElement->__toString(); + + $this->assertHTML($this->matchFloatingLabel(), $select); + $this->assertHTML($this->matchFloatingSelect(), $select); + + $placeholderOption = Element::create('option', 'Choose an option', array('value' => '', 'disabled' => 'disabled', 'selected' => 'selected')); + + $options = array($placeholderOption); + foreach ($this->options as $key => $option) { + $options[] = Element::create('option', $option, array('value' => $key)); + } + $this->assertEquals($selectElement->getOptions(), $options); + + $matcher = $this->formFloatingLabelGroup(''); + $this->assertEquals($matcher, $select); + } +} diff --git a/tests/Fields/SwitchTest.php b/tests/Fields/SwitchTest.php new file mode 100644 index 00000000..67895536 --- /dev/null +++ b/tests/Fields/SwitchTest.php @@ -0,0 +1,566 @@ +former->framework('TwitterBootstrap5'); + $this->former->horizontal_open()->__toString(); + } + + //////////////////////////////////////////////////////////////////// + ////////////////////////////// MATCHERS //////////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Matches a switch + * + * @param string $name + * @param string $label + * @param integer $value + * @param boolean $inline + * @param boolean $checked + * @param mixed $disabled If 'disabled', rendered as `disabled="disabled"`. If true, then rendered as `disabled`. + * @param boolean $raw + * + * @return string + */ + private function matchSwitch( + $name = 'foo', + $label = null, + $value = 1, + $inline = false, + $checked = false, + $disabled = false, + $raw = false + ) { + $checkAttr = array( + 'class' => 'form-check-input', + 'disabled' => $disabled === 'disabled' ? 'disabled' : null, + 'id' => $name, + 'type' => 'checkbox', + 'name' => $name, + 'checked' => 'checked', + 'value' => $value, + ); + $labelAttr = array( + 'for' => $name, + 'class' => 'form-check-label', + ); + if (!$checked) { + unset($checkAttr['checked']); + } + if (!$disabled) { + unset($checkAttr['disabled']); + } + + $switch = 'attributes($checkAttr).'>'; + + $controlClasses = 'form-check'; + if ($inline) { + $controlClasses .= ' form-check-inline'; + } + $controlClasses .= ' form-switch'; + + if ($label) { + $switch .= 'attributes($labelAttr).'>'.$label.''; + } + + if ($raw) { + return $switch; + } + + return '
'.$switch.'
'; + } + + /** + * Matches a checked switch + * + * @param string $name + * @param string $label + * @param integer $value + * @param boolean $inline + * + * @return string + */ + private function matchCheckedSwitch($name = 'foo', $label = null, $value = 1, $inline = false) + { + return $this->matchSwitch($name, $label, $value, $inline, true); + } + + /** + * Matches a checked switch + * + * @param string $name + * @param string $label + * @param integer $value + * @param boolean $inline + * @param boolean $checked + * @param mixed $disabled If 'disabled', rendered as `disabled="disabled"`. If true, then rendered as `disabled`. + * + * @return string + */ + private function matchRawSwitch( + $name = 'foo', + $label = null, + $value = 1, + $inline = false, + $checked = false, + $disabled = false + ) { + return $this->matchSwitch($name, $label, $value, $inline, $checked, $disabled, true); + } + + + //////////////////////////////////////////////////////////////////// + //////////////////////////////// TESTS ///////////////////////////// + //////////////////////////////////////////////////////////////////// + + public function testCanCreateASingleCheckedSwitch() + { + $switch = $this->former->switch('foo')->__toString(); + $matcher = $this->formGroupWithBS5($this->matchSwitch('foo')); + + $this->assertEquals($matcher, $switch); + } + + public function testCanCreateASwitchWithALabel() + { + $switch = $this->former->switch('foo')->text('bar')->__toString(); + $matcher = $this->formGroupWithBS5($this->matchSwitch('foo', 'Bar')); + + $this->assertEquals($matcher, $switch); + } + + public function testCanSetValueOfASingleSwitch() + { + $switch = $this->former->switch('foo')->text('bar')->value('foo')->__toString(); + $matcher = $this->formGroupWithBS5($this->matchSwitch('foo', 'Bar', 'foo')); + + $this->assertEquals($matcher, $switch); + } + + public function testCanCreateMultipleSwitches() + { + $switches = $this->former->switches('foo')->switches('foo', 'bar')->__toString(); + $matcher = $this->formGroupWithBS5($this->matchSwitch('foo_0', 'Foo').$this->matchSwitch('foo_1', 'Bar')); + + $this->assertEquals($matcher, $switches); + } + + public function testCanFocusOnASwitch() + { + $switches = $this->former->switches('foo') + ->switches('foo', 'bar') + ->on(0)->text('first')->on(1)->text('second')->__toString(); + + $matcher = $this->formGroupWithBS5($this->matchSwitch('foo_0', 'First').$this->matchSwitch('foo_1', 'Second')); + + $this->assertEquals($matcher, $switches); + } + + public function testCanCreateInlineSwitches() + { + $switches1 = $this->former->inline_switches('foo')->switches('foo', 'bar')->__toString(); + $this->resetLabels(); + $switches2 = $this->former->switches('foo')->inline()->switches('foo', 'bar')->__toString(); + $matcher = $this->formGroupWithBS5($this->matchSwitch('foo_0', 'Foo', 1, true).$this->matchSwitch('foo_1', 'Bar', 1, true)); + + $this->assertEquals($matcher, $switches1); + $this->assertEquals($matcher, $switches2); + } + + public function testCanCreateStackedSwitches() + { + $switches1 = $this->former->stacked_switches('foo')->switches('foo', 'bar')->__toString(); + $this->resetLabels(); + $switches2 = $this->former->switches('foo')->stacked()->switches('foo', 'bar')->__toString(); + $matcher = $this->formGroupWithBS5($this->matchSwitch('foo_0', 'Foo', 1).$this->matchSwitch('foo_1', 'Bar', 1)); + + $this->assertEquals($matcher, $switches1); + $this->assertEquals($matcher, $switches2); + } + + public function testCanCreateMultipleSwitchesViaAnArray() + { + $this->resetLabels(); + $switches = $this->former->switches('foo')->switches(array('Foo' => 'foo', 'Bar' => 'bar'))->__toString(); + $matcher = $this->formGroupWithBS5($this->matchSwitch('foo', 'Foo').$this->matchSwitch('bar', 'Bar')); + + $this->assertEquals($matcher, $switches); + } + + public function testCanCustomizeSwitchesViaAnArray() + { + $switches = $this->former->switches('foo')->switches($this->checkables)->__toString(); + $matcher = $this->formGroupWithBS5( + '
'. + ''. + ''. + '
'. + '
'. + ''. + ''. + '
'); + + $this->assertEquals($matcher, $switches); + } + + public function testCanCreateMultipleAnonymousSwitches() + { + $checkables = $this->checkables; + unset($checkables['Foo']['name']); + unset($checkables['Bar']['name']); + + $switches = $this->former->switches('foo')->switches($checkables)->__toString(); + $matcher = $this->formGroupWithBS5( + '
'. + ''. + ''. + '
'. + '
'. + ''. + ''. + '
' + ); + + $this->assertEquals($matcher, $switches); + } + + public function testCanCheckASingleSwitch() + { + $switch = $this->former->switch('foo')->check()->__toString(); + $matcher = $this->formGroupWithBS5($this->matchCheckedSwitch('foo')); + + $this->assertEquals($matcher, $switch); + } + + public function testCanCheckOneInSeveralSwitches() + { + $switches = $this->former->switches('foo')->switches('foo', 'bar')->check('foo_1')->__toString(); + $matcher = $this->formGroupWithBS5($this->matchSwitch('foo_0', 'Foo').$this->matchCheckedSwitch('foo_1', 'Bar')); + + $this->assertEquals($matcher, $switches); + } + + public function testCanCheckMultipleSwitchesAtOnce() + { + $switches = $this->former->switches('foo')->switches('foo', 'bar')->check(array( + 'foo_0' => false, + 'foo_1' => true, + ))->__toString(); + $matcher = $this->formGroupWithBS5($this->matchSwitch('foo_0', 'Foo').$this->matchCheckedSwitch('foo_1', 'Bar', 1)); + + $this->assertEquals($matcher, $switches); + } + + public function testCanRepopulateSwitchesFromPost() + { + $this->request->shouldReceive('input')->with('_token', '', true)->andReturn(''); + $this->request->shouldReceive('input')->with('foo', '', true)->andReturn(''); + $this->request->shouldReceive('input')->with('foo_0', '', true)->andReturn(true); + $this->request->shouldReceive('input')->with('foo_1', '', true)->andReturn(false); + + $switches = $this->former->switches('foo')->switches('foo', 'bar')->__toString(); + $matcher = $this->formGroupWithBS5($this->matchCheckedSwitch('foo_0', 'Foo').$this->matchSwitch('foo_1', 'Bar')); + + $this->assertEquals($matcher, $switches); + } + + public function testCanPopulateSwitchesFromAnObject() + { + $this->former->populate((object) array('foo_0' => true)); + + $switches = $this->former->switches('foo')->switches('foo', 'bar')->__toString(); + $matcher = $this->formGroupWithBS5($this->matchCheckedSwitch('foo_0', 'Foo').$this->matchSwitch('foo_1', 'Bar')); + + $this->assertEquals($matcher, $switches); + } + + public function testCanPopulateSwitchesWithRelations() + { + $eloquent = new DummyEloquent(array('id' => 1, 'name' => 3)); + + $this->former->populate($eloquent); + $switches = $this->former->switches('roles')->__toString(); + $matcher = $this->formGroupWithBS5( + $this->matchSwitch('1', 'Foo').$this->matchSwitch('3', 'Bar'), + ''); + + $this->assertEquals($matcher, $switches); + } + + public function testCanRepopulateSwitchesWithRelations() + { + $eloquent = new DummyEloquent; + + $roles = array( + 'Value 01' => array( + 'name' => 'rolesAsCollection[1]', + 'value' => '1', + ), + 'Value 02' => array( + 'name' => 'rolesAsCollection[2]', + 'value' => '2', + ), + ); + + $this->former->populate($eloquent); + $switches = $this->former->switches('rolesAsCollection[]')->switches($roles)->__toString(); + + $this->assertEquals($this->formGroupWithBS5( + '
'. + ''. + ''. + '
'. + '
'. + ''. + ''. + '
', + ''), $switches); + } + + public function testCanDecodeCorrectlySwitches() + { + $switch = $this->former->switch('foo')->__toString(); + + $content = html_entity_decode($switch, ENT_QUOTES, 'UTF-8'); + + $this->assertEquals($content, $this->former->switch('foo')->__toString()); + } + + public function testCanPushUncheckedSwitches() + { + $switch = $this->former->switch('foo')->text('foo')->push(true); + $matcher = $this->formGroupWithBS5( + '
'. + ''. + $this->matchRawSwitch('foo'). + ''. + '
'); + + $this->assertEquals($matcher, $switch->wrapAndRender()); + } + + public function testCanOverrideGloballyPushedSwitches() + { + $this->mockConfig(array('push_checkboxes' => true)); + $switch = $this->former->switch('foo')->text('foo')->push(false); + + $matcher = $this->formGroupWithBS5( + '
'. + $this->matchRawSwitch('foo'). + ''. + '
'); + + $this->assertEquals($matcher, $switch->wrapAndRender()); + } + + public function testCanPushASingleSwitch() + { + $this->mockConfig(array('push_checkboxes' => true)); + + $switch = $this->former->switch('foo')->text('foo')->__toString(); + $matcher = $this->formGroupWithBS5( + '
'. + ''. + $this->matchRawSwitch('foo'). + ''. + '
'); + + $this->assertEquals($matcher, $switch); + } + + public function testCanRepopulateSwitchesOnSubmit() + { + $this->mockConfig(array('push_checkboxes' => true)); + $this->request->shouldReceive('input')->andReturn(''); + + $switch = $this->former->switch('foo')->text('foo')->__toString(); + $matcher = $this->formGroupWithBS5( + '
'. + ''. + $this->matchRawSwitch('foo'). + ''. + '
'); + + $this->assertEquals($matcher, $switch); + } + + + public function testCanCustomizeTheUncheckedValue() + { + $this->mockConfig(array('unchecked_value' => 'unchecked', 'push_checkboxes' => true)); + + $switch = $this->former->switch('foo')->text('foo')->__toString(); + $matcher = $this->formGroupWithBS5( + '
'. + ''. + $this->matchRawSwitch('foo'). + ''. + '
'); + + $this->assertEquals($matcher, $switch); + } + + public function testCanRecognizeGroupedSwitchesValidationErrors() + { + $this->mockSession(array('foo' => 'bar', 'bar' => 'baz')); + $this->former->withErrors(); + + $auto = $this->former->switches('foo[]', '')->switches('Value 01', 'Value 02')->__toString(); + $chain = $this->former->switches('foo', '')->grouped()->switches('Value 01', 'Value 02')->__toString(); + + $matcher = + '
'. + '
'. + '
'. + ''. + ''. + '
'. + '
'. + ''. + ''. + '
'. + '
'. + ''. + '
bar
'. + '
'. + '
'. + '
'; + + $this->assertEquals($matcher, $auto); + $this->assertEquals($matcher, $chain); + } + + public function testCanHandleAZeroUncheckedValue() + { + $this->mockConfig(array('unchecked_value' => 0)); + $switches = $this->former->switches('foo')->value('bar')->__toString(); + $matcher = $this->formGroupWithBS5($this->matchSwitch('foo', null, 'bar')); + + $this->assertEquals($matcher, $switches); + } + + public function testRepopulatedValueDoesntChangeOriginalValue() + { + $this->markTestSkipped('Test reformulated proves opposite of that stated'); + + $this->former->populate(array('foo' => 'bar')); + $switchTrue = $this->former->switch('foo')->__toString(); + $matcherTrue = $this->formGroupWithBS5($this->matchCheckedSwitch()); + + $this->assertEquals($matcherTrue, $switchTrue); + + $this->former->populate(array('foo' => 'baz')); + $switchFalse = $this->former->switch('foo')->__toString(); + $matcherFalse = $this->formGroupWithBS5($this->matchSwitch()); + + $this->assertEquals($matcherFalse, $switchFalse); + } + + public function testCanPushSwitchesWithoutLabels() + { + $this->mockConfig(array('automatic_label' => false, 'push_checkboxes' => true)); + + $html = $this->former->label('Views per Page')->render(); + $html .= $this->former->switch('per_page')->class('input')->render(); + + $this->assertIsString($html); + } + + public function testDisabled() + { + $switch = $this->former->switch('foo')->disabled()->__toString(); + $matcher = $this->formGroupWithBS5($this->matchSwitch('foo', null, 1, false, false, true)); + $this->assertEquals($matcher, $switch); + } + + public function testToStringMagicMethodShouldOnlyReturnString() + { + $this->former->group(); + $output = $this->former->switch('foo')->text('bar').''; + $this->former->closeGroup(); + + $this->assertIsString($output); + } + + public function testCanBeManualyDefinied() + { + $switch = $this->former->switch('foo', null, 'foo', ['value' => 'bar'])->__toString(); + $matcher = $this->formGroupWithBS5('
'); + + $this->assertEquals($matcher, $switch); + } + + public function testCanBeManualyDefiniedAndRepopulated() + { + $this->former->populate(array('foo' => 'bar')); + $switch = $this->former->switch('foo', null, 'foo', ['value' => 'bar'])->__toString(); + $matcher = $this->formGroupWithBS5( + '
'. + ''. + '
'); + + $this->assertEquals($matcher, $switch); + } + + public function testShouldNotBeCheckedIfHisValueIsManualyChanged() + { + $this->former->populate(array('foo' => 'foo')); + $switch = $this->former->switch('foo', null, 'foo', ['value' => 'bar'])->__toString(); + $matcher = $this->formGroupWithBS5( + '
'. + ''. + '
'); + + $this->assertEquals($matcher, $switch); + } + + public function testNestedCanSetNonOverriddenConstructorParameters() + { + $switch = $this->former->switch('foo[bar]', 'Foo Bar Label', 'bis', ['class' => 'ter'])->__toString(); + $matcher = $this->formGroupWithBS5( + '
'. + ''. + '
', + '' + ); + + $this->assertEquals($matcher, $switch); + } + + public function testNestedCanSetNonOverriddenConstructorParametersAndBeRepopulated() + { + $this->former->populate(array('foo' => array('bar' => 'bis'))); + $switch = $this->former->switch('foo[bar]', 'Foo Bar Label', 'bis', ['class' => 'ter'])->__toString(); + $matcher = $this->formGroupWithBS5( + '
'. + ''. + '
', + '' + ); + + $this->assertEquals($matcher, $switch); + } + + public function testNestedShouldNotBeCheckedIfContstructorValueParameterIsOverridden() + { + $this->former->populate(array('foo' => array('bar' => 'bis'))); + $switch = $this->former->switch('foo[bar]', null, 'bis', ['value' => 'ter'])->__toString(); + $matcher = $this->formGroupWithBS5( + '
'. + ''. + '
', + '' + ); + + $this->assertEquals($matcher, $switch); + } +} diff --git a/tests/Framework/TwitterBootstrap5Test.php b/tests/Framework/TwitterBootstrap5Test.php new file mode 100644 index 00000000..041b34a6 --- /dev/null +++ b/tests/Framework/TwitterBootstrap5Test.php @@ -0,0 +1,212 @@ +former->framework('TwitterBootstrap5'); + $this->former->horizontal_open()->__toString(); + } + + //////////////////////////////////////////////////////////////////// + ////////////////////////////// MATCHERS //////////////////////////// + //////////////////////////////////////////////////////////////////// + + public function hmatch($label, $field) + { + return '
'.$label.'
'.$field.'
'; + } + + public function vmatch($label, $field) + { + return '
'.$label.$field.'
'; + } + + //////////////////////////////////////////////////////////////////// + //////////////////////////////// TESTS ///////////////////////////// + //////////////////////////////////////////////////////////////////// + + public function testFrameworkIsRecognized() + { + $this->assertNotEquals('TwitterBootstrap', $this->former->framework()); + $this->assertEquals('TwitterBootstrap5', $this->former->framework()); + } + + public function testVerticalFormFieldsDontInheritHorizontalMarkup() + { + $this->former->open_vertical(); + $field = $this->former->text('foo')->__toString(); + $this->former->close(); + + $match = $this->vmatch('', + ''); + + $this->assertEquals($match, $field); + } + + public function testHorizontalFormWithDefaultLabelWidths() + { + $field = $this->former->text('foo')->__toString(); + $match = $this->hmatch('', + ''); + + $this->assertEquals($match, $field); + } + + public function testPrependIcon() + { + $this->former->open_vertical(); + $icon = $this->former->text('foo')->prependIcon('thumbs-up')->__toString(); + $match = $this->vmatch('', + '
'. + ''. + ''. + '
'); + + $this->assertEquals($match, $icon); + } + + public function testAppendIcon() + { + $this->former->open_vertical(); + $icon = $this->former->text('foo')->appendIcon('thumbs-up')->__toString(); + $match = $this->vmatch('', + '
'. + ''. + ''. + '
'); + $this->assertEquals($match, $icon); + } + + public function testTextFieldsGetControlClass() + { + $this->former->open_vertical(); + $field = $this->former->text('foo')->__toString(); + $match = $this->vmatch('', + ''); + + $this->assertEquals($match, $field); + } + + public function testButtonSizes() + { + $this->former->open_vertical(); + $buttons = $this->former->actions()->lg_submit('Submit')->submit('Submit')->sm_submit('Submit')->xs_submit('Submit')->__toString(); + $match = '
'. + ''. + ' '. + ' '. + ' '. + '
'; + + $this->assertEquals($match, $buttons); + } + + public function testCanOverrideFrameworkIconSettings() + { + // e.g. using other Glyphicon sets + $icon1 = $this->app['former.framework']->createIcon('facebook', null, array( + 'tag' => 'span', + 'set' => 'social', + 'prefix' => 'glyphicon', + ))->__toString(); + $match1 = ''; + + $this->assertEquals($match1, $icon1); + + // e.g using Font-Awesome circ v3.2.1 + $icon2 = $this->app['former.framework']->createIcon('flag', null, array( + 'tag' => 'i', + 'set' => '', + 'prefix' => 'icon', + ))->__toString(); + $match2 = ''; + + $this->assertEquals($match2, $icon2); + } + + public function testCanCreateWithErrors() + { + $this->former->open_vertical(); + $this->former->withErrors($this->validator); + + $required = $this->former->text('required')->__toString(); + $matcher = + '
'. + ''. + ''. + '
The required field is required.
'. + '
'; + + $this->assertEquals($matcher, $required); + } + + public function testAddScreenReaderClassToInlineFormLabels() + { + $this->former->open_inline(); + + $field = $this->former->text('foo')->__toString(); + + $match = + '
'. + ''. + ''. + '
'; + + $this->assertEquals($match, $field); + $this->assertEquals($match, $field); + + $this->former->close(); + } + + public function testHeightSettingForFields() + { + $this->former->open_vertical(); + + $field = $this->former->lg_text('foo')->__toString(); + $match = + '
'. + ''. + ''. + '
'; + $this->assertEquals($match, $field); + + $this->resetLabels(); + $field = $this->former->sm_select('foo')->__toString(); + $match = + '
'. + ''. + ''. + '
'; + $this->assertEquals($match, $field); + + $this->former->close(); + } + + public function testAddFormControlClassToInlineActionsBlock() + { + $this->former->open_inline(); + $buttons = $this->former->actions()->submit('Foo')->__toString(); + $match = '
'. + ''. + '
'; + + $this->assertEquals($match, $buttons); + + $this->former->close(); + } + + public function testButtonsAreNotWrapped() + { + $button = $this->former->text('foo')->append($this->former->button('Search'))->wrapAndRender(); + $matcher = ''; + + $this->assertStringContainsString($matcher, $button); + } +} diff --git a/tests/GroupTest.php b/tests/GroupTest.php index 993d61e0..63bc08d6 100644 --- a/tests/GroupTest.php +++ b/tests/GroupTest.php @@ -304,6 +304,21 @@ public function testCanAddClassToGroup() $this->assertEquals($matcher, $control); } + public function testCanRemoveClassToGroup() + { + $element = $this->former->text('foo')->addGroupClass('foo'); + $control = $element->__toString(); + $matcher = $this->createMatcher(); + $matcher = str_replace('group"', 'group foo"', $matcher); + + $this->assertEquals($matcher, $control); + + $controlWithoutClass = $element->removeGroupClass('foo')->__toString(); + $matcherWithoutClass = str_replace('group foo"', 'group"', $matcher); + + $this->assertEquals($matcherWithoutClass, $controlWithoutClass); + } + public function testCanAddClassToLabel() { $control = $this->former->text('foo')->addLabelClass('foo-label')->__toString(); @@ -313,6 +328,21 @@ public function testCanAddClassToLabel() $this->assertEquals($matcher, $control); } + public function testCanRemoveClassToLabel() + { + $element = $this->former->text('foo')->addLabelClass('foo-label'); + $control = $element->__toString(); + $matcher = $this->createMatcher(); + $matcher = str_replace('control-label"', 'foo-label control-label"', $matcher); + + $this->assertEquals($matcher, $control); + + $controlWithoutClass = $element->removeLabelClass('foo-label')->__toString(); + $matcherWithoutClass = str_replace('foo-label control-label"', 'control-label"', $matcher); + + $this->assertEquals($matcherWithoutClass, $controlWithoutClass); + } + public function testCanRecognizeGroupValidationErrors() { $this->mockSession(array('foo' => 'bar', 'bar' => 'baz')); diff --git a/tests/TestCases/ContainerTestCase.php b/tests/TestCases/ContainerTestCase.php index 864bf645..a815918a 100644 --- a/tests/TestCases/ContainerTestCase.php +++ b/tests/TestCases/ContainerTestCase.php @@ -165,6 +165,16 @@ protected function mockConfig(array $options = array()) 'small' => 'sm', 'mini' => 'xs', ), + 'TwitterBootstrap5.icon.prefix' => 'fa', + 'TwitterBootstrap5.icon.set' => 'fa', + 'TwitterBootstrap5.icon.tag' => 'i', + 'TwitterBootstrap5.labelWidths' => array('large' => 2, 'small' => 4), + 'TwitterBootstrap5.viewports' => array( + 'large' => 'lg', + 'medium' => 'md', + 'small' => 'sm', + 'mini' => 'xs', + ), 'ZurbFoundation.icon.prefix' => 'fi', 'ZurbFoundation.icon.set' => null, 'ZurbFoundation.icon.tag' => 'i', diff --git a/tests/TestCases/FormerTests.php b/tests/TestCases/FormerTests.php index 1d325f5e..7b276a7a 100644 --- a/tests/TestCases/FormerTests.php +++ b/tests/TestCases/FormerTests.php @@ -303,7 +303,7 @@ protected function formGroup( } /** - * Matches a Form Group + * Matches a Form Group for Bootstrap 4 * * @param string $input * @param string $label @@ -317,6 +317,21 @@ protected function formGroupWithBS4( return '
'.$label.'
'.$input.'
'; } + /** + * Matches a Form Group for Bootstrap 5 + * + * @param string $input + * @param string $label + * + * @return boolean + */ + protected function formGroupWithBS5( + $input = '', + $label = '' + ) { + return '
'.$label.'
'.$input.'
'; + } + /** * Matches a required Control Group *