diff --git a/.gitignore b/.gitignore index 3171e94..56e9489 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ vendor/ composer.lock -.idea/ \ No newline at end of file +.idea/ +.phpunit.result.cache \ No newline at end of file diff --git a/src/BootstrapRenderer.php b/src/BootstrapRenderer.php index ac894ad..0fd6d2b 100644 --- a/src/BootstrapRenderer.php +++ b/src/BootstrapRenderer.php @@ -41,7 +41,7 @@ class BootstrapRenderer implements FormRenderer */ protected $gridBreakPoint = 'sm'; - /** @var BootstrapForm */ + /** @var BootstrapForm|Form */ protected $form; /** @var int */ @@ -64,8 +64,10 @@ public function __construct(int $mode = RenderMode::VERTICAL_MODE) /** * Sets the form for which to render. Used only if a specific function of the renderer must be executed * outside of render(), such as during assisted manual rendering. + * + * Accepts any Form instance for compatibility with {formPrint} and Blueprint::latte(). */ - public function attachForm(BootstrapForm $form): void + public function attachForm(Form $form): void { $this->form = $form; } @@ -434,10 +436,16 @@ public function renderBody(): string */ public function renderControl(BaseControl $control): string { - /** @var Html $controlHtml */ + /** @var Html|string $controlHtml */ $controlHtml = $control->getControl(); $control->setOption(RendererOptions::_RENDERED, true); - if (($this->form->showValidation || $control->hasErrors()) && $control instanceof IValidationInput) { + + // Handle string returns from getControl() for compatibility with Blueprint/formPrint + if (is_string($controlHtml)) { + return $controlHtml; + } + + if (($this->isShowValidation() || $control->hasErrors()) && $control instanceof IValidationInput) { $controlHtml = $control->showValidation($controlHtml); } @@ -498,17 +506,42 @@ public function renderEnd(): string return $this->form->getElementPrototype()->endTag() . "\n"; } + /** + * Returns whether validation styling should be shown. + * Returns false for non-BootstrapForm instances for compatibility with {formPrint}. + */ + protected function isShowValidation(): bool + { + return $this->form instanceof BootstrapForm && $this->form->isShowValidation(); + } + /** * Renders 'label' part of visual row of controls. + * + * @return Html|string Returns Html element or string for compatibility with Blueprint/formPrint */ - public function renderLabel(BaseControl $control): Html + public function renderLabel(BaseControl $control): Html|string { + // For regular BootstrapForm rendering, check caption first to maintain original behavior if ($control->caption === null) { + // Still call getLabel() to check for Blueprint/formPrint string returns + $controlLabel = $control->getLabel(); + + // Handle string returns from getLabel() for compatibility with Blueprint/formPrint + if (is_string($controlLabel)) { + return $controlLabel; + } + return Html::el(); } $controlLabel = $control->getLabel(); + // Handle string returns from getLabel() for compatibility with Blueprint/formPrint + if (is_string($controlLabel)) { + return $controlLabel; + } + if ($controlLabel instanceof Html && $controlLabel->getName() === 'label') { // the control has already provided us with the element, no need to create our own $controlLabel = $this->configElem(Cnf::LABEL, $controlLabel); @@ -676,7 +709,7 @@ protected function renderFeedback(?BaseControl $control = null): ?Html $isValid = false; $showFeedback = true; $messages = $control->getErrors(); - } elseif ($this->form->showValidation) { + } elseif ($this->isShowValidation()) { $isValid = true; // control is valid and we want to explicitly show that it's valid $message = $control->getOption(RendererOptions::FEEDBACK_VALID); diff --git a/tests/BlueprintCompatibilityTest.php b/tests/BlueprintCompatibilityTest.php new file mode 100644 index 0000000..35d1e47 --- /dev/null +++ b/tests/BlueprintCompatibilityTest.php @@ -0,0 +1,293 @@ +setAction('/submit'); + $form->addText('name', 'Name') + ->setRequired(); + $form->addEmail('email', 'Email'); + $form->addTextArea('message', 'Message'); + $form->addSubmit('submit', 'Send'); + + $blueprint = new Blueprint(); + $latte = $blueprint->generateLatte($form); + + // Verify the generated Latte contains expected elements + $this->assertStringContainsString('{input name}', $latte); + $this->assertStringContainsString('{input email}', $latte); + $this->assertStringContainsString('{input message}', $latte); + $this->assertStringContainsString('{input submit}', $latte); + $this->assertStringContainsString('{label name/}', $latte); + $this->assertStringContainsString('{label email/}', $latte); + $this->assertStringContainsString('{label message/}', $latte); + } + + /** + * Test Blueprint with BootstrapForm in vertical mode. + */ + public function testBlueprintWithVerticalMode(): void + { + $form = new BootstrapForm(); + $form->setRenderMode(RenderMode::VERTICAL_MODE); + $form->addText('username', 'Username'); + $form->addPassword('password', 'Password'); + $form->addSubmit('login', 'Login'); + + $blueprint = new Blueprint(); + $latte = $blueprint->generateLatte($form); + + $this->assertStringContainsString('{input username}', $latte); + $this->assertStringContainsString('{input password}', $latte); + $this->assertStringContainsString('{input login}', $latte); + } + + /** + * Test Blueprint with BootstrapForm in side-by-side mode. + */ + public function testBlueprintWithSideBySideMode(): void + { + $form = new BootstrapForm(); + $form->setRenderMode(RenderMode::SIDE_BY_SIDE_MODE); + $form->addText('firstName', 'First Name'); + $form->addText('lastName', 'Last Name'); + $form->addSubmit('save', 'Save'); + + $blueprint = new Blueprint(); + $latte = $blueprint->generateLatte($form); + + $this->assertStringContainsString('{input firstName}', $latte); + $this->assertStringContainsString('{input lastName}', $latte); + $this->assertStringContainsString('{input save}', $latte); + } + + /** + * Test Blueprint with BootstrapForm in inline mode. + */ + public function testBlueprintWithInlineMode(): void + { + $form = new BootstrapForm(); + $form->setRenderMode(RenderMode::INLINE); + $form->addText('search', 'Search'); + $form->addSubmit('go', 'Go'); + + $blueprint = new Blueprint(); + $latte = $blueprint->generateLatte($form); + + $this->assertStringContainsString('{input search}', $latte); + $this->assertStringContainsString('{input go}', $latte); + } + + /** + * Test Blueprint with BootstrapForm using Bootstrap 5. + */ + public function testBlueprintWithBootstrap5(): void + { + BootstrapForm::switchBootstrapVersion(BootstrapVersion::V5); + + $form = new BootstrapForm(); + $form->addText('name', 'Name'); + $form->addSelect('country', 'Country', ['us' => 'USA', 'uk' => 'UK']); + $form->addSubmit('submit', 'Submit'); + + $blueprint = new Blueprint(); + $latte = $blueprint->generateLatte($form); + + $this->assertStringContainsString('{input name}', $latte); + $this->assertStringContainsString('{input country}', $latte); + $this->assertStringContainsString('{input submit}', $latte); + } + + /** + * Test that BootstrapRenderer can be attached to a regular Form. + * This is necessary for Blueprint::generateLatte() to work. + */ + public function testRendererCanAttachToRegularForm(): void + { + $renderer = new BootstrapRenderer(); + $form = new Form(); + $form->addText('test', 'Test'); + + // Should not throw an exception + $renderer->attachForm($form); + + // Should be able to render + $html = $renderer->render($form); + $this->assertIsString($html); + $this->assertNotEmpty($html); + } + + /** + * Test that BootstrapRenderer with validation disabled works with regular Form. + */ + public function testRendererWithRegularFormNoValidation(): void + { + $renderer = new BootstrapRenderer(); + $form = new Form(); + $form->addText('field', 'Field'); + $form->addSubmit('submit', 'Submit'); + + $renderer->attachForm($form); + $html = $renderer->render($form); + + $this->assertStringContainsString('form', $html); + $this->assertStringContainsString('field', $html); + } + + /** + * Test Blueprint with complex form containing various input types. + */ + public function testBlueprintWithVariousInputTypes(): void + { + $form = new BootstrapForm(); + $form->addText('text', 'Text'); + $form->addTextArea('textarea', 'TextArea'); + $form->addEmail('email', 'Email'); + $form->addPassword('password', 'Password'); + $form->addSelect('select', 'Select', ['a' => 'A', 'b' => 'B']); + $form->addCheckbox('checkbox', 'Checkbox'); + $form->addRadioList('radio', 'Radio', ['x' => 'X', 'y' => 'Y']); + $form->addUpload('upload', 'Upload'); + $form->addHidden('hidden', 'value'); + $form->addSubmit('submit', 'Submit'); + + $blueprint = new Blueprint(); + $latte = $blueprint->generateLatte($form); + + $this->assertStringContainsString('{input text}', $latte); + $this->assertStringContainsString('{input textarea}', $latte); + $this->assertStringContainsString('{input email}', $latte); + $this->assertStringContainsString('{input password}', $latte); + $this->assertStringContainsString('{input select}', $latte); + $this->assertStringContainsString('{input checkbox}', $latte); + $this->assertStringContainsString('{input radio}', $latte); + $this->assertStringContainsString('{input upload}', $latte); + $this->assertStringContainsString('{input hidden}', $latte); + $this->assertStringContainsString('{input submit}', $latte); + } + + /** + * Test Blueprint with form groups. + */ + public function testBlueprintWithFormGroups(): void + { + $form = new BootstrapForm(); + + $form->addGroup('Personal Info'); + $form->addText('firstName', 'First Name'); + $form->addText('lastName', 'Last Name'); + + $form->addGroup('Contact'); + $form->addEmail('email', 'Email'); + $form->addText('phone', 'Phone'); + + $form->addGroup(); + $form->addSubmit('submit', 'Submit'); + + $blueprint = new Blueprint(); + $latte = $blueprint->generateLatte($form); + + $this->assertStringContainsString('{input firstName}', $latte); + $this->assertStringContainsString('{input lastName}', $latte); + $this->assertStringContainsString('{input email}', $latte); + $this->assertStringContainsString('{input phone}', $latte); + } + + /** + * Test that cloning BootstrapRenderer works for Blueprint. + */ + public function testClonedRendererWorksWithRegularForm(): void + { + $bootstrapForm = new BootstrapForm(); + $bootstrapForm->addText('test', 'Test'); + + // Clone the renderer (as Blueprint does) + $renderer = clone $bootstrapForm->getRenderer(); + + // Create a regular form + $regularForm = new Form(); + $regularForm->addText('test', 'Test'); + + // Should be able to set the cloned renderer on regular form + $regularForm->setRenderer($renderer); + + // Form::render() echoes output and returns void, so use output buffering + ob_start(); + $regularForm->render(); + $html = ob_get_clean(); + + $this->assertIsString($html); + $this->assertNotEmpty($html); + } + + /** + * Test isShowValidation returns false for non-BootstrapForm. + */ + public function testIsShowValidationReturnsFalseForRegularForm(): void + { + $renderer = new BootstrapRenderer(); + $form = new Form(); + $form->addText('test', 'Test'); + + $renderer->attachForm($form); + + // Render should succeed without errors related to showValidation + $html = $renderer->render($form); + $this->assertIsString($html); + // The validation CSS classes should not be present for valid fields + // since isShowValidation returns false for non-BootstrapForm + $this->assertStringNotContainsString('is-valid', $html); + } + + /** + * Test Blueprint::dataClass works with BootstrapForm. + */ + public function testBlueprintGenerateDataClassWithBootstrapForm(): void + { + $form = new BootstrapForm(); + $form->addText('name', 'Name') + ->setRequired(); + $form->addEmail('email', 'Email'); + $form->addInteger('age', 'Age'); + $form->addCheckbox('newsletter', 'Subscribe to newsletter'); + + $blueprint = new Blueprint(); + $dataClass = $blueprint->generateDataClass($form); + + // Verify the generated data class contains expected properties + $this->assertStringContainsString('$name', $dataClass); + $this->assertStringContainsString('$email', $dataClass); + $this->assertStringContainsString('$age', $dataClass); + $this->assertStringContainsString('$newsletter', $dataClass); + $this->assertStringContainsString('class', $dataClass); + } + +}