Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ csf: vendor
vendor/bin/codefixer src tests

phpstan: vendor
vendor/bin/phpstan analyse -l 9 -c phpstan.neon src
vendor/bin/phpstan analyse -l 10 -c phpstan.neon src

tests: vendor
vendor/bin/phpunit
Expand Down
18 changes: 5 additions & 13 deletions src/BootstrapForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,7 @@ public function __construct($container = null)
]);
$this->elementPrototype = $prototype;

/**
* @param BootstrapForm $form
*/
$this->onError[] = function ($form): void {
$this->onError[] = function (self $form): void {
$form->showValidation = $this->autoShowValidation;
};
}
Expand Down Expand Up @@ -161,20 +158,15 @@ public function setAjax(bool $isAjax = true)
{
$this->isAjax = $isAjax;

BootstrapUtils::standardizeClass($this->getElementPrototype());
$prototypeClass = $this->getElementPrototype()->class;
$prototype = $this->getElementPrototype();
$present = BootstrapUtils::hasClass($prototype, $this->ajaxClass);

$present = in_array($this->ajaxClass, $prototypeClass);
if ($present && !$isAjax) {
// remove the class
$prototypeClass = array_diff($prototypeClass, [$this->ajaxClass]);
BootstrapUtils::removeClass($prototype, $this->ajaxClass);
} elseif (!$present && $isAjax) {
// add class
$prototypeClass[] = $this->ajaxClass;
BootstrapUtils::addClass($prototype, $this->ajaxClass);
}

$this->getElementPrototype()->class = $prototypeClass;

return $this;
}

Expand Down
25 changes: 7 additions & 18 deletions src/BootstrapRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -325,11 +325,12 @@ public function renderBegin(): string
$el = $this->configElem('form', $this->form->getElementPrototype());

if ($this->form->isMethod('get')) {
$el->action = (string) $el->action;
/** @noinspection PhpUndefinedFieldInspection */
$query = parse_url($el->action, PHP_URL_QUERY);
/** @noinspection PhpUndefinedFieldInspection */
$el->action = str_replace('?' . $query, '', $el->action);
// the action may be set as anything printable, a Nette\Application\UI\Link among others
$action = $el->getAttribute('action');
$action = is_scalar($action) || $action instanceof Stringable ? (string) $action : '';

$query = parse_url($action, PHP_URL_QUERY);
$el->action = str_replace('?' . $query, '', $action);

$s = '';
$params = ($query === null || $query === false)
Expand Down Expand Up @@ -616,19 +617,7 @@ protected function fetchConfig(string $key): array
*/
protected function fetchClasses(Html $el): array
{
$class = $el->getAttribute('class');

if (is_array($class)) {
return $class;
}

// class is set, but not as an array
if (is_string($class)) {
return explode(' ', $class);
}

// class is not set
return [];
return BootstrapUtils::fetchClasses($el);
}

/**
Expand Down
52 changes: 52 additions & 0 deletions src/BootstrapUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,56 @@ public static function standardizeClass(Html $control): void
}
}

/**
* Reads element classes as a list, whichever way the class attribute was set
*
* @return mixed[]
*/
public static function fetchClasses(Html $control): array
{
$class = $control->getAttribute('class');

if (is_array($class)) {
return $class;
}

// class is set, but not as an array
if (is_string($class)) {
return explode(' ', $class);
}

// class is not set
return [];
}

/**
* Appends a class to the element
*/
public static function addClass(Html $control, string $class): void
{
$classes = self::fetchClasses($control);
$classes[] = $class;

$control->class = $classes;
}

/**
* Removes a class from the element, if it is there at all
*/
public static function removeClass(Html $control, string $class): void
{
$control->class = array_filter(
self::fetchClasses($control),
static fn ($presentClass): bool => $presentClass !== $class
);
}

/**
* Whether the element already carries the class
*/
public static function hasClass(Html $control, string $class): bool
{
return in_array($class, self::fetchClasses($control), true);
}

}
5 changes: 3 additions & 2 deletions src/Grid/BootstrapCell.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Contributte\FormsBootstrap\Grid;

use Contributte\FormsBootstrap\BootstrapRenderer;
use Contributte\FormsBootstrap\BootstrapUtils;
use Contributte\FormsBootstrap\Enums\RendererConfig;
use Contributte\FormsBootstrap\Traits\BootstrapContainerTrait;
use Nette\ComponentModel\IComponent;
Expand Down Expand Up @@ -94,7 +95,7 @@ public function render(): Html
$renderer = $this->row->getContainer()->getForm()->getRenderer();

$element = $renderer->configElem(RendererConfig::GRID_CELL, $element);
$element->class[] = $this->createClass();
BootstrapUtils::addClass($element, $this->createClass());

foreach ($this->childControls as $childControl) {
$pairHtml = $renderer->renderPair($childControl);
Expand Down Expand Up @@ -133,7 +134,7 @@ public function setCurrentGroup(?ControlGroup $currentGroup): self
*/
public function addHtmlClass(string $class)
{
$this->elementPrototype->class[] = $class;
BootstrapUtils::addClass($this->elementPrototype, $class);

return $this;
}
Expand Down
4 changes: 1 addition & 3 deletions src/Inputs/ColorPicker.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ class ColorPicker extends \Nette\Forms\Controls\ColorPicker implements IValidati
public function getControl(): Html
{
$control = parent::getControl();
BootstrapUtils::standardizeClass($control);

$control->class[] = 'form-control';
BootstrapUtils::addClass($control, 'form-control');

return $control;
}
Expand Down
3 changes: 2 additions & 1 deletion src/Inputs/DateInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Contributte\FormsBootstrap\Inputs;

use Contributte\FormsBootstrap\BootstrapUtils;
use Contributte\FormsBootstrap\Enums\DateTimeFormat;
use DateTime;
use DateTimeInterface;
Expand Down Expand Up @@ -160,7 +161,7 @@ public function setValue($value)
public function getControl(): Html
{
$control = parent::getControl();
$control->class[] = implode(' ', static::$additionalHtmlClasses);
BootstrapUtils::addClass($control, implode(' ', static::$additionalHtmlClasses));

return $control;
}
Expand Down
4 changes: 1 addition & 3 deletions src/Inputs/DateTimeControl.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ class DateTimeControl extends \Nette\Forms\Controls\DateTimeControl implements I
public function getControl(): Html
{
$control = parent::getControl();
BootstrapUtils::standardizeClass($control);

$control->class[] = 'form-control';
BootstrapUtils::addClass($control, 'form-control');

return $control;
}
Expand Down
4 changes: 1 addition & 3 deletions src/Inputs/TextAreaInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,7 @@ public function setAutocomplete(?bool $bool)
public function getControl(): Html
{
$control = parent::getControl();
BootstrapUtils::standardizeClass($control);

$control->class[] = 'form-control';
BootstrapUtils::addClass($control, 'form-control');
if ($this->autocomplete !== null) {
$control->setAttribute('autocomplete', $this->autocomplete ? 'on' : 'off');
}
Expand Down
4 changes: 1 addition & 3 deletions src/Inputs/TextInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,7 @@ public function setAutocomplete(?bool $bool)
public function getControl(): Html
{
$control = parent::getControl();
BootstrapUtils::standardizeClass($control);

$control->class[] = 'form-control';
BootstrapUtils::addClass($control, 'form-control');
if (!empty($this->placeholder)) {
$control->setAttribute('placeholder', $this->placeholder);
}
Expand Down
6 changes: 5 additions & 1 deletion src/Inputs/UploadInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Contributte\FormsBootstrap\BootstrapForm;
use Contributte\FormsBootstrap\BootstrapRenderer;
use Contributte\FormsBootstrap\BootstrapUtils;
use Contributte\FormsBootstrap\Enums\BootstrapVersion;
use Contributte\FormsBootstrap\Enums\RendererConfig;
use Nette\Application\UI\Form;
Expand Down Expand Up @@ -49,7 +50,10 @@ public function getControl()
{
/** @var Html $control */
$control = parent::getControl();
$control->class = trim($control->class .= BootstrapForm::getBootstrapVersion() === BootstrapVersion::V5 ? ' form-control' : ' custom-file-input');
BootstrapUtils::addClass(
$control,
BootstrapForm::getBootstrapVersion() === BootstrapVersion::V5 ? 'form-control' : 'custom-file-input'
);

$el = Html::el('div', ['class' => [BootstrapForm::getBootstrapVersion() === BootstrapVersion::V5 ? null : 'custom-file']]);
$el->addHtml($control);
Expand Down
3 changes: 1 addition & 2 deletions src/Traits/BootstrapButtonTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@ public function setAllignWithInputControls(bool $allignWithControls = true): voi

protected function addBtnClass(Html $element): void
{
BootstrapUtils::standardizeClass($element);
$element->class[] = 'btn ' . $this->getBtnClass();
BootstrapUtils::addClass($element, 'btn ' . $this->getBtnClass());
}

}
11 changes: 10 additions & 1 deletion src/Traits/BootstrapContainerTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use Nette\Forms\Controls\TextInput as NetteTextInput;
use Nette\Forms\Controls\UploadControl;
use Nette\Forms\Form;
use Nette\InvalidArgumentException;
use Nette\Utils\Html;

/**
Expand Down Expand Up @@ -201,7 +202,15 @@ public function addEmail(
*/
public function addInputError(string $componentName, string $message): void
{
$this->getComponent($componentName)->addError($message);
$component = $this->getComponent($componentName);

if (!$component instanceof BaseControl) {
throw new InvalidArgumentException(
sprintf('Component "%s" is not a form control, so it cannot carry an error.', $componentName)
);
}

$component->addError($message);
}

/**
Expand Down
10 changes: 10 additions & 0 deletions tests/BootstrapFormTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ public function testAjaxClassIsAddedAndRemoved(): void
$this->assertNotContains('ajax', $form->getElementPrototype()->class);
}

public function testAjaxClassIsNotAddedTwice(): void
{
$form = new BootstrapForm();

$form->setAjax(true);
$form->setAjax(true);

$this->assertSame(['ajax'], $form->getElementPrototype()->class);
}

public function testGetRendererReturnsTheBootstrapRenderer(): void
{
$form = new BootstrapForm();
Expand Down
51 changes: 51 additions & 0 deletions tests/BootstrapUtilsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,55 @@ public function testStandardizeClass(): void
$this->assertEquals(['c1', 'c2'], $html->class);
}

public function testFetchClassesReadsEitherShapeOfTheAttribute(): void
{
$this->assertSame(['c1', 'c2'], BootstrapUtils::fetchClasses(Html::el('div', ['class' => 'c1 c2'])));
$this->assertSame(['c1', 'c2'], BootstrapUtils::fetchClasses(Html::el('div', ['class' => ['c1', 'c2']])));
$this->assertSame([], BootstrapUtils::fetchClasses(Html::el('div')));
}

public function testAddClassKeepsWhateverWasThere(): void
{
$html = Html::el('div', ['class' => 'c1 c2']);

BootstrapUtils::addClass($html, 'c3');

$this->assertSame('<div class="c1 c2 c3"></div>', (string) $html);
}

public function testAddClassOnElementWithoutOne(): void
{
$html = Html::el('div');

BootstrapUtils::addClass($html, 'only');

$this->assertSame('<div class="only"></div>', (string) $html);
}

public function testRemoveClassDropsOnlyTheNamedOne(): void
{
$html = Html::el('div', ['class' => 'keep drop']);

BootstrapUtils::removeClass($html, 'drop');

$this->assertSame('<div class="keep"></div>', (string) $html);
}

public function testRemoveClassThatIsNotThereChangesNothing(): void
{
$html = Html::el('div', ['class' => 'keep']);

BootstrapUtils::removeClass($html, 'absent');

$this->assertSame('<div class="keep"></div>', (string) $html);
}

public function testHasClass(): void
{
$html = Html::el('div', ['class' => 'c1 c2']);

$this->assertTrue(BootstrapUtils::hasClass($html, 'c1'));
$this->assertFalse(BootstrapUtils::hasClass($html, 'c3'));
}

}
11 changes: 11 additions & 0 deletions tests/Traits/BootstrapContainerTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Contributte\FormsBootstrap\Inputs\DateTimeControl;
use Contributte\FormsBootstrap\Inputs\TextInput;
use Contributte\FormsBootstrap\Inputs\UploadInput;
use Nette\InvalidArgumentException;
use Tests\BaseTestCase;

/**
Expand Down Expand Up @@ -152,6 +153,16 @@ public function testAddInputErrorAttachesTheErrorToTheNamedControl(): void
$this->assertTrue($form->hasErrors());
}

public function testAddInputErrorRejectsComponentThatIsNotControl(): void
{
$form = new BootstrapForm();
$form->addContainer('sub');

$this->expectException(InvalidArgumentException::class);

$form->addInputError('sub', 'nowhere to put this');
}

public function testAddColorProducesColorInput(): void
{
$form = new BootstrapForm();
Expand Down
Loading