From 0250089233c23cc8db17760c2c12564b76308330 Mon Sep 17 00:00:00 2001 From: Tortue Torche Date: Fri, 22 Jan 2021 15:13:55 +0100 Subject: [PATCH 01/10] Initial Bootstrap 5 support Alpha support, need more work! --- src/Former/Form/Group.php | 9 +- src/Former/Framework/TwitterBootstrap5.php | 486 +++++++++++++++++++++ src/Former/Traits/Checkable.php | 7 +- src/config/former.php | 25 +- tests/Framework/TwitterBootstrap5Test.php | 212 +++++++++ tests/TestCases/ContainerTestCase.php | 10 + 6 files changed, 741 insertions(+), 8 deletions(-) create mode 100644 src/Former/Framework/TwitterBootstrap5.php create mode 100644 tests/Framework/TwitterBootstrap5Test.php diff --git a/src/Former/Form/Group.php b/src/Former/Form/Group.php index 12b1f72a..71e189c2 100644 --- a/src/Former/Form/Group.php +++ b/src/Former/Form/Group.php @@ -176,7 +176,9 @@ public function wrapField($field) { $label = $this->getLabel($field); $help = $this->getHelp(); - if ($field->isCheckable() && $this->app['former']->framework() == 'TwitterBootstrap4') { + if ($field->isCheckable() && + in_array($this->app['former']->framework(), ['TwitterBootstrap4', 'TwitterBootstrap5']) + ) { $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'); @@ -323,7 +325,8 @@ 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('TwitterBootstrap4') && + $this->app['former.framework']->isnt('TwitterBootstrap5') ) { throw new BadMethodCallException('This method is only available on the Bootstrap framework'); } @@ -445,7 +448,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..c60d7977 --- /dev/null +++ b/src/Former/Framework/TwitterBootstrap5.php @@ -0,0 +1,486 @@ +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 .= " col-$viewports[$viewport]-offset-$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; + } + + //////////////////////////////////////////////////////////////////// + //////////////////////////// RENDER BLOCKS ///////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Render an help text + * + * @param string $text + * @param array $attributes + * + * @return Element + */ + public function createHelp($text, $attributes = array()) + { + return Element::create('small', $text, $attributes)->addClass('text-muted'); + } + + /** + * 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('small', $text, $attributes)->addClass('form-text text-muted'); + } + + /** + * 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/Traits/Checkable.php b/src/Former/Traits/Checkable.php index e79cb55f..028ece26 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); @@ -394,7 +393,7 @@ protected function createCheckable($item, $fallbackValue = 1) // If no label to wrap, return plain checkable if (!$label) { $element = (is_object($field)) ? $field->render() : $field; - } elseif ($this->app['former']->framework() == 'TwitterBootstrap4') { + } elseif (in_array($this->app['former']->framework(), ['TwitterBootstrap4', 'TwitterBootstrap5'])) { // 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(); 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/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/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', From 83c098fd7845556b4cb8e90e620dd7b01037a251 Mon Sep 17 00:00:00 2001 From: Tortue Torche Date: Thu, 11 Feb 2021 10:40:17 +0100 Subject: [PATCH 02/10] Fix indentation --- src/Former/Form/Group.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Former/Form/Group.php b/src/Former/Form/Group.php index 71e189c2..05dc813d 100644 --- a/src/Former/Form/Group.php +++ b/src/Former/Form/Group.php @@ -324,9 +324,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('TwitterBootstrap3') && $this->app['former.framework']->isnt('TwitterBootstrap4') && - $this->app['former.framework']->isnt('TwitterBootstrap5') + $this->app['former.framework']->isnt('TwitterBootstrap5') ) { throw new BadMethodCallException('This method is only available on the Bootstrap framework'); } From 9a2a793a2d865579ce3fd2e602f108361ebd918f Mon Sep 17 00:00:00 2001 From: Tortue Torche Date: Thu, 11 Feb 2021 10:41:33 +0100 Subject: [PATCH 03/10] Fix Former::actions() for Bootstrap 5 --- src/Former/Framework/TwitterBootstrap5.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Former/Framework/TwitterBootstrap5.php b/src/Former/Framework/TwitterBootstrap5.php index c60d7977..33c3e55f 100644 --- a/src/Former/Framework/TwitterBootstrap5.php +++ b/src/Former/Framework/TwitterBootstrap5.php @@ -190,7 +190,7 @@ protected function setFieldWidths($labelWidths) if ($viewport) { $labelWidthClass .= " col-$viewports[$viewport]-$columns"; $fieldWidthClass .= " col-$viewports[$viewport]-".(12 - $columns); - $fieldOffsetClass .= " col-$viewports[$viewport]-offset-$columns"; + $fieldOffsetClass .= " offset-$viewports[$viewport]-$columns"; } } From 494309e822549cc116f2d9259b74bf40aed7b26a Mon Sep 17 00:00:00 2001 From: Tortue Torche Date: Thu, 4 Mar 2021 18:40:46 +0100 Subject: [PATCH 04/10] Update the help() and blockHelp() methods for BS5 Since Bootstrap v5.0.0-alpha1, '.form-text' no longer sets 'display', but does set 'color' and 'font-size'. So instead of you should now use
or . Reference: https://getbootstrap.com/docs/5.0/migration/#forms-2 --- src/Former/Form/Group.php | 2 +- src/Former/Framework/TwitterBootstrap5.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Former/Form/Group.php b/src/Former/Form/Group.php index 05dc813d..3c7e0a6c 100644 --- a/src/Former/Form/Group.php +++ b/src/Former/Form/Group.php @@ -177,7 +177,7 @@ public function wrapField($field) $label = $this->getLabel($field); $help = $this->getHelp(); if ($field->isCheckable() && - in_array($this->app['former']->framework(), ['TwitterBootstrap4', 'TwitterBootstrap5']) + $this->app['former']->framework() === 'TwitterBootstrap4' ) { $wrapperClass = $field->isInline() ? 'form-check form-check-inline' : 'form-check'; if ($this->app['former']->getErrors($field->getName())) { diff --git a/src/Former/Framework/TwitterBootstrap5.php b/src/Former/Framework/TwitterBootstrap5.php index 33c3e55f..6431b0a6 100644 --- a/src/Former/Framework/TwitterBootstrap5.php +++ b/src/Former/Framework/TwitterBootstrap5.php @@ -343,7 +343,7 @@ public function getActionClasses() */ public function createHelp($text, $attributes = array()) { - return Element::create('small', $text, $attributes)->addClass('text-muted'); + return Element::create('span', $text, $attributes)->addClass('form-text'); } /** @@ -369,7 +369,7 @@ public function createValidationError($text, $attributes = array()) */ public function createBlockHelp($text, $attributes = array()) { - return Element::create('small', $text, $attributes)->addClass('form-text text-muted'); + return Element::create('div', $text, $attributes)->addClass('form-text'); } /** From 70b8a5802ce43b43446363f839cbbcf7a1547273 Mon Sep 17 00:00:00 2001 From: Tortue Torche Date: Fri, 7 May 2021 15:01:27 +0200 Subject: [PATCH 05/10] Add the switch markup which is a custom checkbox for BS5 Some use cases with Laravel: {!! Former::switch('valid_switch_ok') ->text('Valid switch OK') !!} {!! Former::switches('valid_inline_switches_ok') ->switches('first', 'second', 'third', 'fourth') ->inline() !!} Reference: https://getbootstrap.com/docs/5.0/forms/checks-radios/#switches --- src/Former/Form/Fields/Switchbox.php | 23 +++++++++++++++++++++++ src/Former/MethodDispatcher.php | 4 ++++ src/Former/Traits/Checkable.php | 12 ++++++++---- src/Former/Traits/Field.php | 2 +- 4 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 src/Former/Form/Fields/Switchbox.php 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/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 028ece26..2fc4f713 100644 --- a/src/Former/Traits/Checkable.php +++ b/src/Former/Traits/Checkable.php @@ -380,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(); @@ -398,9 +398,13 @@ protected function createCheckable($item, $fallbackValue = 1) $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'; - - $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..fe079b3d 100644 --- a/src/Former/Traits/Field.php +++ b/src/Former/Traits/Field.php @@ -230,7 +230,7 @@ public function isUnwrappable() */ public function isCheckable() { - return $this->isOfType('checkbox', 'checkboxes', 'radio', 'radios'); + return $this->isOfType('checkbox', 'checkboxes', 'radio', 'radios', 'switch', 'switches'); } /** From c4fca7a7dfdda714ecee71b0e7eb4749db155bcb Mon Sep 17 00:00:00 2001 From: Tortue Torche Date: Fri, 7 May 2021 17:36:18 +0200 Subject: [PATCH 06/10] Add removeGroupClass() and removeLabelClass() methods and tests Usage with Laravel: {!! Former::text('test') ->removeGroupClass('row') ->removeLabelClass('foo') !!} Warning: there is a bug in the HTMLObject package! See my patch for more info: https://github.com/tortuetorche/html-object/commit/b3c5601e5a6bb06c20df10827b9292d800261c1e --- src/Former/Form/Group.php | 31 +++++++++++++++++++++++++++++-- tests/GroupTest.php | 30 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/Former/Form/Group.php b/src/Former/Form/Group.php index 3c7e0a6c..e504b520 100644 --- a/src/Former/Form/Group.php +++ b/src/Former/Form/Group.php @@ -212,17 +212,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) { @@ -236,6 +246,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 * 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')); From 21b30f78c1a25c40acc33877173c22445a2190bf Mon Sep 17 00:00:00 2001 From: Tortue Torche Date: Fri, 7 May 2021 17:47:11 +0200 Subject: [PATCH 07/10] Support BS5 floating labels for , '); + $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); + } +}