diff --git a/demos/_unit-test/crud-nested.php b/demos/_unit-test/crud-nested.php index de82b79ed3..7cea7c5515 100644 --- a/demos/_unit-test/crud-nested.php +++ b/demos/_unit-test/crud-nested.php @@ -20,5 +20,5 @@ $entity = $model->load($id); $innerCrud = Crud::addTo($v); - $innerCrud->setModel($entity->Products); + $innerCrud->setModel($entity->products); }); diff --git a/demos/basic/view.php b/demos/basic/view.php index 9cf5c75e51..2854d66a87 100644 --- a/demos/basic/view.php +++ b/demos/basic/view.php @@ -59,7 +59,7 @@ $plane = View::addTo($app, ['template' => $planeTemplate]); Header::addTo($app, ['Can be rendered into HTML']); -View::addTo($app, ['ui' => 'segment', 'class.raised' => true, 'element' => 'pre'])->set($plane->render()); +View::addTo($app, ['ui' => 'segment', 'class.raised' => true, 'element' => 'pre'])->set($plane->renderToHtml()); Header::addTo($app, ['Has a unique global identifier']); Label::addTo($app, ['Plane ID:', 'detail' => $plane->name]); diff --git a/demos/collection/crud.php b/demos/collection/crud.php index 7f8d50f705..0ac096634c 100644 --- a/demos/collection/crud.php +++ b/demos/collection/crud.php @@ -86,7 +86,7 @@ public function addFormTo(View $view): Form if (File::assertInstanceOf($this->action->getEntity())->is_folder) { Grid::addTo($right, ['menu' => false, 'ipp' => 5]) - ->setModel(File::assertInstanceOf($this->getAction()->getModel())->SubFolder); + ->setModel(File::assertInstanceOf($this->getAction()->getModel())->subFolder); } else { Message::addTo($right, ['Not a folder', 'type' => 'warning']); } diff --git a/demos/collection/multitable.php b/demos/collection/multitable.php index c86f48152b..68a9de4ebd 100644 --- a/demos/collection/multitable.php +++ b/demos/collection/multitable.php @@ -100,4 +100,4 @@ public function setModel(Model $model, array $route = []): void ->on('click', new JsModal('Now importing ... ', $vp)); $finderClass::addTo($app, ['bottom attached segment']) - ->setModel($model->setLimit(10), [$model->fieldName()->SubFolder]); + ->setModel($model->setLimit(10), [$model->fieldName()->subFolder]); diff --git a/demos/form-control/dropdown-plus.php b/demos/form-control/dropdown-plus.php index 3707ea0473..bd3d0e68da 100644 --- a/demos/form-control/dropdown-plus.php +++ b/demos/form-control/dropdown-plus.php @@ -23,8 +23,8 @@ $form = Form::addTo($demo->right); $form->addControl('category_id', [Form\Control\Dropdown::class, 'model' => new Category($app->db)]); -$form->addControl('sub_category_id', [Form\Control\DropdownCascade::class, 'cascadeFrom' => 'category_id', 'reference' => Category::hinting()->fieldName()->SubCategories]); -$form->addControl('product_id', [Form\Control\DropdownCascade::class, 'cascadeFrom' => 'sub_category_id', 'reference' => SubCategory::hinting()->fieldName()->Products]); +$form->addControl('sub_category_id', [Form\Control\DropdownCascade::class, 'cascadeFrom' => 'category_id', 'reference' => Category::hinting()->fieldName()->subCategories]); +$form->addControl('product_id', [Form\Control\DropdownCascade::class, 'cascadeFrom' => 'sub_category_id', 'reference' => SubCategory::hinting()->fieldName()->products]); $form->onSubmit(static function (Form $form) use ($app) { $message = $app->encodeJson($app->uiPersistence->typecastSaveRow($form->entity, $form->entity->get())); diff --git a/demos/init-db.php b/demos/init-db.php index af9f400d0b..a7b7c44b16 100644 --- a/demos/init-db.php +++ b/demos/init-db.php @@ -471,7 +471,7 @@ class Percent extends Field * @property string $name @Atk4\Field() * @property string $type @Atk4\Field() * @property bool $is_folder @Atk4\Field() - * @property File $SubFolder @Atk4\RefMany() + * @property File $subFolder @Atk4\RefMany() * @property int $count @Atk4\Field() * @property Folder $parent_folder_id @Atk4\RefOne() */ @@ -490,7 +490,7 @@ protected function init(): void $this->addField($this->fieldName()->type, ['caption' => 'MIME Type']); $this->addField($this->fieldName()->is_folder, ['type' => 'boolean']); - $this->hasMany($this->fieldName()->SubFolder, [ + $this->hasMany($this->fieldName()->subFolder, [ 'model' => [self::class], 'theirField' => self::hinting()->fieldName()->parent_folder_id, ]) @@ -538,7 +538,7 @@ public function importFromFilesystem(string $path, ?bool $isSub = null): void ]); if ($fileinfo->isDir()) { - $entity->SubFolder->importFromFilesystem($fileinfo->getPath() . '/' . $fileinfo->getFilename(), true); + $entity->subFolder->importFromFilesystem($fileinfo->getPath() . '/' . $fileinfo->getFilename(), true); } // skip full/slow import for Behat CI testing @@ -562,8 +562,8 @@ protected function init(): void /** * @property string $name @Atk4\Field() - * @property SubCategory $SubCategories @Atk4\RefMany() - * @property Product $Products @Atk4\RefMany() + * @property SubCategory $subCategories @Atk4\RefMany() + * @property Product $products @Atk4\RefMany() */ class Category extends ModelWithPrefixedFields { @@ -576,11 +576,11 @@ protected function init(): void $this->addField($this->fieldName()->name); - $this->hasMany($this->fieldName()->SubCategories, [ + $this->hasMany($this->fieldName()->subCategories, [ 'model' => [SubCategory::class], 'theirField' => SubCategory::hinting()->fieldName()->product_category_id, ]); - $this->hasMany($this->fieldName()->Products, [ + $this->hasMany($this->fieldName()->products, [ 'model' => [Product::class], 'theirField' => Product::hinting()->fieldName()->product_category_id, ]); @@ -590,7 +590,7 @@ protected function init(): void /** * @property string $name @Atk4\Field() * @property Category $product_category_id @Atk4\RefOne() - * @property Product $Products @Atk4\RefMany() + * @property Product $products @Atk4\RefMany() */ class SubCategory extends ModelWithPrefixedFields { @@ -606,7 +606,7 @@ protected function init(): void $this->hasOne($this->fieldName()->product_category_id, [ 'model' => [Category::class], ]); - $this->hasMany($this->fieldName()->Products, [ + $this->hasMany($this->fieldName()->products, [ 'model' => [Product::class], 'theirField' => Product::hinting()->fieldName()->product_sub_category_id, ]); diff --git a/demos/layout/layouts_manual.php b/demos/layout/layouts_manual.php index c1366bd5c4..2a6ef19c66 100644 --- a/demos/layout/layouts_manual.php +++ b/demos/layout/layouts_manual.php @@ -21,4 +21,4 @@ $app->initLayout([Layout::class]); $layout->setApp($app); -Text::addTo($app->layout)->addHtml($layout->render()); +Text::addTo($app->layout)->addHtml($layout->renderToHtml()); diff --git a/docs/app.md b/docs/app.md index 94635a8cfa..afad184be8 100644 --- a/docs/app.md +++ b/docs/app.md @@ -378,7 +378,7 @@ $grid->setModel($user); $grid->addPaginator(); // initialize and populate paginator $grid->addButton('Test'); // initialize and populate toolbar -echo $grid->render(); +echo $grid->renderToHtml(); ``` All of the objects created above - button, grid, toolbar and paginator will share the same @@ -393,7 +393,7 @@ You can create App object on your own then add elements into it: $app = new App('My App'); $app->add($grid); -echo $grid->render(); +echo $grid->renderToHtml(); ``` This does not change the output, but you can use the 'App' class to your advantage as a diff --git a/docs/form-control.md b/docs/form-control.md index 647e56069d..b5e53f5312 100644 --- a/docs/form-control.md +++ b/docs/form-control.md @@ -414,7 +414,7 @@ class MyDropdown extends \Atk4\Ui\Dropdown $this->_tItem->set('someOtherField', $res['someOtherField]); $this->_tItem->set('someOtherField2', $res['someOtherField2]); // add item to template - $this->template->dangerouslyAppendHtml('Item', $this->_tItem->render()); + $this->template->dangerouslyAppendHtml('Item', $this->_tItem->renderToHtml()); } } ``` @@ -544,8 +544,8 @@ Assume that each data model are defined and model Category has many Sub-Category ``` $form = \Atk4\Ui\Form::addTo($app); $form->addControl('category_id', [Dropdown::class, 'model' => new Category($db)]); -$form->addControl('sub_category_id', [DropdownCascade::class, 'cascadeFrom' => 'category_id', 'reference' => Category::hinting()->fieldName()->SubCategories]); -$form->addControl('product_id', [DropdownCascade::class, 'cascadeFrom' => 'sub_category_id', 'reference' => SubCategory::hinting()->fieldName()->Products]); +$form->addControl('sub_category_id', [DropdownCascade::class, 'cascadeFrom' => 'category_id', 'reference' => Category::hinting()->fieldName()->subCategories]); +$form->addControl('product_id', [DropdownCascade::class, 'cascadeFrom' => 'sub_category_id', 'reference' => SubCategory::hinting()->fieldName()->products]); ``` ## Lookup diff --git a/docs/overview.md b/docs/overview.md index 6caec45ca8..c8be8fb65a 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -222,7 +222,7 @@ $form->setApp($app); $form->invokeInit(); $form->setModel(new User($db)); -$html = $form->render(); +$html = $form->renderToHtml(); ``` This would render an individual component and will return HTML: @@ -236,7 +236,7 @@ This would render an individual component and will return HTML: ``` -For other use-cases please look into {php:meth}`View::render()` +For other use-cases please look into {php:meth}`View::renderToHtml()` ### Factory diff --git a/docs/render.md b/docs/render.md index 7f4d0e92a8..b02778a068 100644 --- a/docs/render.md +++ b/docs/render.md @@ -11,7 +11,7 @@ $view = new \Atk4\Ui\View(); \Atk4\Ui\Button::addTo($view, ['test']); -echo $view->render(); +echo $view->renderToHtml(); ``` When render on the $view is executed, it will render button first then incorporate HTML into it's own template before rendering. @@ -25,9 +25,9 @@ Here is a breakdown of how the above code works: At this point Button is NOT element of a view just yet. This is because we can't be sure if $view will be rendered individually or will become child of another view. Method init() is not executed on either objects. -4. render() method will call renderAll() -5. renderAll will find out that the $app property of a view is not set and will initialize it with default App. -6. renderAll will also find out that the init() has not been called for the $view and will call it. +4. renderToHtml() method will call renderAll() +5. renderAll() will find out that the $app property of a view is not set and will initialize it with default App. +6. renderAll() will also find out that the init() has not been called for the $view and will call it. 7. init() will identify that there are some "pending children" and will add them in properly. Most of the UI classes will allow you to operate even if they are not initialized. For instance calling 'setModel()' will @@ -77,7 +77,7 @@ $b2 = \Atk4\Ui\Button::addTo($v, ['Test2']); echo $b2->name; // not set!! Not part of render tree ``` -At this point, if you execute $v->render() it will create it's own App and will create its own render tree. On the other +At this point, if you execute $v->renderToHtml() it will create it's own App and will create its own render tree. On the other hand, if you add $v inside layout, trees will merge and the same $app will be used: ``` @@ -91,7 +91,7 @@ should there be any problems with the initialization. # Rendering outside -It's possible for some views to be rendered outside of the app. In the previous section I speculated that calling $v->render() +It's possible for some views to be rendered outside of the app. In the previous section I speculated that calling $v->renderToHtml() will create its own tree independent from the main one. Agile UI sometimes uses the following approach to render element on the outside: diff --git a/docs/view.md b/docs/view.md index a642e394c7..6e964904b2 100644 --- a/docs/view.md +++ b/docs/view.md @@ -29,11 +29,11 @@ The above code will produce the following HTML block: ``` All of the views combined form a "render tree". In order to get the HTML output -from all the render tree `View`s you need to execute `render()` for the top-most +from all the render tree `View`s you need to execute `renderToHtml()` for the top-most leaf: ``` -echo $v->render(); +echo $v->renderToHtml(); ``` Each of the views will automatically render all of the child views. @@ -307,7 +307,7 @@ $button = Button::addTo($app, ['icon' => new MyAwesomeIcon('book')]); ## Rendering of a Tree -:::{php:method} render() +:::{php:method} renderToHtml() Perform render of this View and all the child Views recursively returning a valid HTML string. ::: @@ -317,8 +317,8 @@ Any view has the ability to render itself. Once executed, render will perform th - call recursiveRender() to recursively render sub-elements. - return JS code with on-dom-ready instructions along with HTML code of a current view. -You must not override render() in your objects. If you are integrating Agile UI into your -framework you shouldn't even use `render()`, but instead use `getHtml` and `getJs`. +You should not override `renderToHtml()` in your objects. If you are integrating Agile UI into your +framework you shouldn't even use `renderToHtml()`, but instead use `getHtml` and `getJs`. :::{php:method} getHtml() Returns HTML for this View as well as all the child views. @@ -451,7 +451,7 @@ Agile UI will maintain unique ID for all the elements. The tag is set through 'n ``` $b = new \Atk4\Ui\Button(['name' => 'my-button3']); -echo $b->render(); +echo $b->renderToHtml(); ``` Outputs: diff --git a/src/App.php b/src/App.php index 17ca38a28c..c55ba87878 100644 --- a/src/App.php +++ b/src/App.php @@ -546,7 +546,7 @@ public function terminate($output = ''): void public function terminateHtml($output): void { if ($output instanceof View) { - $output = $output->render(); + $output = $output->renderToHtml(); } elseif ($output instanceof HtmlTemplate) { $output = $output->renderToHtml(); } diff --git a/src/JsSse.php b/src/JsSse.php index 66db2b8de8..637f2dc65c 100644 --- a/src/JsSse.php +++ b/src/JsSse.php @@ -124,17 +124,17 @@ protected function flush(): void private function outputEventResponse(string $content): void { + // workaround flush() ignored by Apache mod_proxy_fcgi + // https://stackoverflow.com/questions/30707792/how-to-disable-buffering-with-apache2-and-mod-proxy-fcgi#36298336 + // https://bz.apache.org/bugzilla/show_bug.cgi?id=68827 + $content .= ': ' . str_repeat('x', 4_096) . "\n\n"; + if ($this->echoFunction) { ($this->echoFunction)($content); return; } - // workaround flush() ignored by Apache mod_proxy_fcgi - // https://stackoverflow.com/questions/30707792/how-to-disable-buffering-with-apache2-and-mod-proxy-fcgi#36298336 - // https://bz.apache.org/bugzilla/show_bug.cgi?id=68827 - $content .= ': ' . str_repeat('x', 4_096) . "\n\n"; - $app = $this->getApp(); \Closure::bind(static function () use ($app, $content): void { $app->outputResponse($content); diff --git a/src/Text.php b/src/Text.php index 1c37ab4811..1ceb26c63d 100644 --- a/src/Text.php +++ b/src/Text.php @@ -14,7 +14,7 @@ class Text extends View public $content = ''; #[\Override] - public function render(): string + public function renderToHtml(): string { return $this->content; } diff --git a/src/View.php b/src/View.php index b9d657640b..33c884755b 100644 --- a/src/View.php +++ b/src/View.php @@ -24,7 +24,7 @@ class View extends AbstractView { /** - * When you call render() this will be populated with JavaScript chains. + * When you call renderAll() this will be populated with JavaScript chains. * * @var array<1|string, array> * @@ -668,7 +668,7 @@ protected function renderTemplateToHtml(): string * This method is for those cases when developer want to simply render his * view and grab HTML himself. */ - public function render(): string + public function renderToHtml(): string { $this->renderAll(); diff --git a/tests/ButtonTest.php b/tests/ButtonTest.php index 0bf5cca488..a102945ade 100644 --- a/tests/ButtonTest.php +++ b/tests/ButtonTest.php @@ -20,6 +20,6 @@ public function testButtonIcon(): void { $button = new Button(['Load', 'icon' => 'pause']); $button->setApp($this->createApp()); - $button->render(); + $button->renderToHtml(); } } diff --git a/tests/DemosTest.php b/tests/DemosTest.php index afdb282cbf..63a699753c 100644 --- a/tests/DemosTest.php +++ b/tests/DemosTest.php @@ -43,7 +43,7 @@ class DemosTest extends TestCase |(?\[(?:(?&json)(?:,(?&json))*|\s*)\]) |(?\{(?:(?\s*(?&string)\s*:(?&json))(?:,(?&pair))*|\s*)\}) )\s*)$~sx'; - protected static string $regexSseLine = '~^(event|data|(?=: x{4096}$)): .*$~'; + protected static string $regexSse = '~^(event: [^\n]+\n(data: [^\n]*\n)+\n: x{4096}\n\n)+$~'; #[\Override] public static function setUpBeforeClass(): void @@ -452,22 +452,7 @@ public function testDemoSseResponse(string $path): void $response = $this->getResponseFromRequest($path); self::assertSame(200, $response->getStatusCode()); - - $outputLines = array_filter( - explode("\n", $response->getBody()->getContents()), - static fn ($v) => $v !== '' - ); - - // check SSE Syntax - self::assertGreaterThan(0, count($outputLines)); - foreach ($outputLines as $index => $line) { - preg_match_all(self::$regexSseLine, $line, $matchesAll); - self::assertSame( - $line, - implode('', $matchesAll[0] ?? ['error']), - 'Testing SSE response line ' . $index . ' with content ' . $line - ); - } + self::assertMatchesRegularExpression(self::$regexSse, $response->getBody()->getContents()); } public static function provideDemoJsonResponsePostCases(): iterable diff --git a/tests/FormTest.php b/tests/FormTest.php index 6259d845c5..f00f3617c4 100644 --- a/tests/FormTest.php +++ b/tests/FormTest.php @@ -86,7 +86,7 @@ protected function assertFormSubmit(\Closure $createFormFx, array $postData, ?\C } }); - $form->render(); + $form->renderToHtml(); $res = AppFormTestMock::assertInstanceOf($form->getApp())->output; if ($checkExpectedErrorsFx !== null) { @@ -344,24 +344,24 @@ public function testNoDisabledAttrWithHiddenType(): void $input->readOnly = true; $input->setApp($this->createApp()); $input->shortName = 'i'; - self::assertStringContainsString(' readonly="readonly"', $input->render()); - self::assertStringNotContainsString('disabled', $input->render()); + self::assertStringContainsString(' readonly="readonly"', $input->renderToHtml()); + self::assertStringNotContainsString('disabled', $input->renderToHtml()); $input = new Form\Control\Line(); $input->disabled = true; $input->readOnly = true; $input->setApp($this->createApp()); $input->shortName = 'i'; - self::assertStringContainsString(' disabled="disabled"', $input->render()); - self::assertStringNotContainsString('readonly', $input->render()); + self::assertStringContainsString(' disabled="disabled"', $input->renderToHtml()); + self::assertStringNotContainsString('readonly', $input->renderToHtml()); $input = new Form\Control\Hidden(); $input->disabled = true; $input->readOnly = true; $input->setApp($this->createApp()); $input->shortName = 'i'; - self::assertStringNotContainsString('disabled', $input->render()); - self::assertStringNotContainsString('readonly', $input->render()); + self::assertStringNotContainsString('disabled', $input->renderToHtml()); + self::assertStringNotContainsString('readonly', $input->renderToHtml()); } public function testCheckboxWithNonBooleanException(): void @@ -377,7 +377,7 @@ public function testCheckboxWithNonBooleanException(): void $this->expectException(Exception::class); $this->expectExceptionMessage('Checkbox form control requires field with boolean type'); - $input->render(); + $input->renderToHtml(); } public function testUploadNoUploadCallbackException(): void @@ -396,7 +396,7 @@ public function testUploadNoUploadCallbackException(): void $this->expectException(Exception::class); $this->expectExceptionMessage('Missing onUpload callback'); - $input->render(); + $input->renderToHtml(); } public function testUploadNoDeleteCallbackException(): void @@ -415,7 +415,7 @@ public function testUploadNoDeleteCallbackException(): void $this->expectException(Exception::class); $this->expectExceptionMessage('Missing onDelete callback'); - $input->render(); + $input->renderToHtml(); } } diff --git a/tests/JsIntegrationTest.php b/tests/JsIntegrationTest.php index 5e05b04d04..273492c637 100644 --- a/tests/JsIntegrationTest.php +++ b/tests/JsIntegrationTest.php @@ -19,7 +19,7 @@ public function testUniqueId1(): void { $v = new Button(['icon' => 'pencil']); $v->setApp($this->createApp()); - $v->render(); + $v->renderToHtml(); self::assertNotEmpty($v->icon); self::assertNotEmpty($v->icon->name); // @phpstan-ignore-line @@ -32,7 +32,7 @@ public function testUniqueId2(): void $b1 = Button::addTo($v); $b2 = Button::addTo($v); $v->setApp($this->createApp()); - $v->render(); + $v->renderToHtml(); self::assertNotSame($b1->name, $b2->name); } @@ -42,7 +42,7 @@ public function testChainFalse(): void $v = new Button(['name' => 'b']); $j = $v->js()->hide(); $v->setApp($this->createApp()); - $v->render(); + $v->renderToHtml(); self::assertSame('$(\'#b\').hide()', $j->jsRender()); } diff --git a/tests/ListerTest.php b/tests/ListerTest.php index 6d91156e9f..7d189050d1 100644 --- a/tests/ListerTest.php +++ b/tests/ListerTest.php @@ -38,7 +38,7 @@ public function testListerRender2(): void $view->invokeInit(); $lister = Lister::addTo($view, [], ['list']); $lister->setSource(['foo', 'bar']); - self::assertSame('hello, world, world', $view->render()); + self::assertSame('hello, world, world', $view->renderToHtml()); } public function testAddAfterRender(): void diff --git a/tests/RenderTreeTest.php b/tests/RenderTreeTest.php index a50c188161..07e66f9a5e 100644 --- a/tests/RenderTreeTest.php +++ b/tests/RenderTreeTest.php @@ -19,7 +19,7 @@ public function testBasic(): void { $view = new View(); $view->setApp($this->createApp()); - $view->render(); + $view->renderToHtml(); $view->getApp(); self::assertNotNull($view->template); @@ -30,7 +30,7 @@ public function testBasicNested(): void $view = new View(); $view2 = View::addTo($view); $view->setApp($this->createApp()); - $view->render(); + $view->renderToHtml(); $view2->getApp(); self::assertNotNull($view2->template); diff --git a/tests/TableTest.php b/tests/TableTest.php index c999061fce..3ef7311cdd 100644 --- a/tests/TableTest.php +++ b/tests/TableTest.php @@ -36,7 +36,7 @@ public function testAddColumnWithoutModel(): void $table->addColumn('eight', [Table\Column\Link::class, ['id' => 3]]); $table->addColumn('nine'); - $table->render(); + $table->renderToHtml(); } public function testAddColumnAlreadyExistsException(): void diff --git a/tests/TableTestTrait.php b/tests/TableTestTrait.php index 4a2399a254..86d8a24a67 100644 --- a/tests/TableTestTrait.php +++ b/tests/TableTestTrait.php @@ -13,7 +13,7 @@ trait TableTestTrait */ protected function extractTableRow(Table $table, string $rowDataId = '1'): string { - preg_match('~<.*data-id="' . $rowDataId . '".*>~m', $table->render(), $matches); + preg_match('~<.*data-id="' . $rowDataId . '".*>~m', $table->renderToHtml(), $matches); return preg_replace('~\r?\n|\r~', '', $matches[0]); } diff --git a/tests/ViewTest.php b/tests/ViewTest.php index 5fc62b7476..5a580e6ff0 100644 --- a/tests/ViewTest.php +++ b/tests/ViewTest.php @@ -30,8 +30,8 @@ public function testMultipleTimesRender(): void $v->set('foo'); $v->setApp($this->createApp()); - $a = $v->render(); - $b = $v->render(); + $a = $v->renderToHtml(); + $b = $v->renderToHtml(); self::assertSame($a, $b); } @@ -41,7 +41,7 @@ public function testAddAfterRenderException(): void $v->set('foo'); $v->setApp($this->createApp()); - $v->render(); + $v->renderToHtml(); $this->expectException(Exception::class); View::addTo($v); @@ -51,12 +51,12 @@ public function testVoidTagRender(): void { $v = new View(); $v->setApp($this->createApp()); - self::assertSame('
', $v->render()); + self::assertSame('
', $v->renderToHtml()); $v = new View(); $v->element = 'img'; $v->setApp($this->createApp()); - self::assertSame('', $v->render()); + self::assertSame('', $v->renderToHtml()); } public function testAddDelayedInit(): void