From 4addf6a70bd1f9f202a1d772ecf5ef1945ba8896 Mon Sep 17 00:00:00 2001 From: Belisoful Date: Wed, 26 Aug 2026 06:04:43 +0000 Subject: [PATCH 1/4] TButton unit test for Postback Event --- .../WebControls/TButtonPostBackEventTest.php | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 tests/unit/Web/UI/WebControls/TButtonPostBackEventTest.php diff --git a/tests/unit/Web/UI/WebControls/TButtonPostBackEventTest.php b/tests/unit/Web/UI/WebControls/TButtonPostBackEventTest.php new file mode 100644 index 000000000..e4bfa0cea --- /dev/null +++ b/tests/unit/Web/UI/WebControls/TButtonPostBackEventTest.php @@ -0,0 +1,109 @@ +attachEventHandler('OnClick', function ($sender, $param) use (&$raised) { + $raised = $param; + }); + } + + public static function buttonProvider(): array + { + return [ + 'TButton' => [TButton::class], + 'TLinkButton' => [TLinkButton::class], + 'TActiveButton' => [TActiveButton::class], + 'TActiveLinkButton' => [TActiveLinkButton::class], + ]; + } + + /** + * The callback parameter is not click data, so it stops at raisePostBackEvent(). + * Forwarding it would make the OnClick parameter depend on the request type and + * would hand OnClick the response channel of the callback. + * @dataProvider buttonProvider + */ + public function testCallbackEventParameterDoesNotReachOnClick(string $class) + { + $button = new $class(); + $button->setCausesValidation(false); + $this->captureClick($button, $raised); + + $button->raisePostBackEvent($this->createMock(TCallbackEventParameter::class)); + + $this->assertNull($raised); + } + + /** + * @dataProvider buttonProvider + */ + public function testPostBackParameterStringDoesNotReachOnClick(string $class) + { + $button = new $class(); + $button->setCausesValidation(false); + $this->captureClick($button, $raised); + + $button->raisePostBackEvent('raw postback parameter'); + + $this->assertNull($raised); + } + + /** + * @dataProvider buttonProvider + */ + public function testNullParameterReachesOnClickAsNull(string $class) + { + $button = new $class(); + $button->setCausesValidation(false); + $this->captureClick($button, $raised); + + $button->raisePostBackEvent(null); + + $this->assertNull($raised); + } + + /** + * @dataProvider buttonProvider + */ + public function testOnCommandStillReceivesACommandParameter(string $class) + { + $button = new $class(); + $button->setCausesValidation(false); + $button->setCommandName('doThing'); + $button->setCommandParameter('42'); + + $raised = null; + $button->attachEventHandler('OnCommand', function ($sender, $param) use (&$raised) { + $raised = $param; + }); + $button->raisePostBackEvent($this->createMock(TCallbackEventParameter::class)); + + $this->assertInstanceOf(\Prado\Web\UI\TCommandEventParameter::class, $raised); + $this->assertSame('doThing', $raised->getCommandName()); + $this->assertSame('42', $raised->getCommandParameter()); + } +} From 6a229b5861eec106033df045fc1511a06c0e0198 Mon Sep 17 00:00:00 2001 From: Belisoful Date: Wed, 26 Aug 2026 06:05:31 +0000 Subject: [PATCH 2/4] TActiveDetails and TActiveDialog js active controls and adapters tests --- .../js/activecontrols/activecontrols.test.js | 322 ++++++++++++++++++ tests/js/adapters/activecontrols.js | 4 + 2 files changed, 326 insertions(+) diff --git a/tests/js/activecontrols/activecontrols.test.js b/tests/js/activecontrols/activecontrols.test.js index 1ae5aa8b1..0a183b868 100644 --- a/tests/js/activecontrols/activecontrols.test.js +++ b/tests/js/activecontrols/activecontrols.test.js @@ -39,6 +39,8 @@ import { TValueTriggeredCallback, TActiveTableCell, TActiveTableRow, + TActiveDetails, + TActiveDialog, } from '../adapters/activecontrols.js'; // ─── Helpers ───────────────────────────────────────────────────────────────── @@ -129,6 +131,8 @@ describe('Class definitions exist', () => { ['TValueTriggeredCallback', TValueTriggeredCallback], ['TActiveTableCell', TActiveTableCell], ['TActiveTableRow', TActiveTableRow], + ['TActiveDetails', TActiveDetails], + ['TActiveDialog', TActiveDialog], ])('%s is a function (constructor)', (_name, klass) => { expect(typeof klass).toBe('function'); }); @@ -1030,3 +1034,321 @@ describe('TActiveTableRow', () => { expect(evt.preventDefault).toHaveBeenCalled(); }); }); + +// ─── TActiveDetails ────────────────────────────────────────────────────────── + +/** + * jsdom reflects the `open` attribute of
but never fires the native + * `toggle` event of its own accord, so a toggle is dispatched by hand where a + * browser would raise one. The end-to-end behavior is covered by + * tests/playwright/active-controls/ActiveDetailsTestCase.spec.js. + */ +describe('TActiveDetails', () => { + let details; + + /** Raise the `toggle` event the browser fires after `open` changes. */ + function toggle(open) { + details.open = open; + details.dispatchEvent(new Event('toggle')); + } + + beforeEach(() => { + clearRegistry(); + details = document.createElement('details'); + details.id = 'details1'; + details.appendChild(document.createElement('summary')); + document.body.appendChild(details); + }); + + afterEach(() => { + restoreMocks(); + details.remove(); + }); + + it('registers itself in Prado.Registry on construction', () => { + new TActiveDetails({ ID: 'details1', EventTarget: 'details1' }); + expect(Registry['details1']).toBeDefined(); + }); + + it('dispatches a callback with CallbackParameter "open" when the user opens it', () => { + const { MockCtor, dispatchMock } = mockCallbackRequest(); + new TActiveDetails({ ID: 'details1', EventTarget: 'details1' }); + + toggle(true); + + expect(dispatchMock).toHaveBeenCalledTimes(1); + expect(MockCtor).toHaveBeenCalledWith( + 'details1', + expect.objectContaining({ CallbackParameter: 'open' }), + ); + }); + + it('dispatches a callback with CallbackParameter "close" when the user closes it', () => { + const { MockCtor, dispatchMock } = mockCallbackRequest(); + new TActiveDetails({ ID: 'details1', EventTarget: 'details1' }); + details.open = true; + + toggle(false); + + expect(dispatchMock).toHaveBeenCalledTimes(1); + expect(MockCtor).toHaveBeenCalledWith( + 'details1', + expect.objectContaining({ CallbackParameter: 'close' }), + ); + }); + + it('does not mutate the options object it was constructed with', () => { + const { MockCtor } = mockCallbackRequest(); + const options = { ID: 'details1', EventTarget: 'details1' }; + new TActiveDetails(options); + + toggle(true); + + expect(options.CallbackParameter).toBeUndefined(); + expect(MockCtor.mock.calls[0][1]).not.toBe(options); + }); + + it('setOpen() opens the widget without echoing a callback to the server', () => { + const { dispatchMock } = mockCallbackRequest(); + const ctrl = new TActiveDetails({ ID: 'details1', EventTarget: 'details1' }); + + ctrl.setOpen(true); + expect(details.open).toBe(true); + // the browser raises toggle for the change the server just made + details.dispatchEvent(new Event('toggle')); + + expect(dispatchMock).not.toHaveBeenCalled(); + }); + + it('setOpen() closes the widget without echoing a callback to the server', () => { + const { dispatchMock } = mockCallbackRequest(); + const ctrl = new TActiveDetails({ ID: 'details1', EventTarget: 'details1' }); + details.open = true; + + ctrl.setOpen(false); + expect(details.open).toBe(false); + details.dispatchEvent(new Event('toggle')); + + expect(dispatchMock).not.toHaveBeenCalled(); + }); + + it('suppresses only the toggle raised by setOpen(), not the next user toggle', () => { + const { dispatchMock } = mockCallbackRequest(); + const ctrl = new TActiveDetails({ ID: 'details1', EventTarget: 'details1' }); + + ctrl.setOpen(true); + details.dispatchEvent(new Event('toggle')); + expect(dispatchMock).not.toHaveBeenCalled(); + + toggle(false); + expect(dispatchMock).toHaveBeenCalledTimes(1); + }); + + it('setOpen() to the value already held arms no suppression', () => { + const { dispatchMock } = mockCallbackRequest(); + const ctrl = new TActiveDetails({ ID: 'details1', EventTarget: 'details1' }); + + ctrl.setOpen(false); // already closed, nothing to do + toggle(true); // a genuine user toggle must still reach the server + + expect(dispatchMock).toHaveBeenCalledTimes(1); + }); + + it('the static setOpen() forwards to the instance found in the Registry', () => { + mockCallbackRequest(); + const ctrl = new TActiveDetails({ ID: 'details1', EventTarget: 'details1' }); + const spy = vi.spyOn(ctrl, 'setOpen'); + + TActiveDetails.setOpen('details1', true); + + expect(spy).toHaveBeenCalledWith(true); + expect(details.open).toBe(true); + }); + + it('the static setOpen() ignores an unknown id', () => { + expect(() => TActiveDetails.setOpen('no-such-control', true)).not.toThrow(); + }); +}); + +// ─── TActiveDialog ─────────────────────────────────────────────────────────── + +/** + * jsdom reflects the `open` attribute of but implements neither + * show() nor close(), so both are stubbed with the semantics a browser gives + * them: show() sets the attribute, close() clears it and fires `close`. The + * MutationObserver that detects a programmatic open delivers asynchronously, + * hence the awaited ticks. The end-to-end behavior is covered by + * tests/playwright/active-controls/ActiveDialogTestCase.spec.js. + */ +describe('TActiveDialog', () => { + let dialog; + let controls; + + /** Let the MutationObserver deliver its queued records. */ + const tick = () => new Promise((resolve) => setTimeout(resolve, 0)); + + /** + * Build a control and remember it, so afterEach can disconnect its + * MutationObserver. An observer left connected delivers its queued records + * into a later test and dispatches through that test's mock. + */ + function makeDialog(options) { + const ctrl = new TActiveDialog(options ?? { ID: 'dialog1', EventTarget: 'dialog1' }); + controls.push(ctrl); + return ctrl; + } + + beforeEach(async () => { + clearRegistry(); + controls = []; + dialog = document.createElement('dialog'); + dialog.id = 'dialog1'; + dialog.show = function () { + this.open = true; + }; + dialog.close = function () { + this.open = false; + this.dispatchEvent(new Event('close')); + }; + document.body.appendChild(dialog); + + // The synchronous tests above never yield to the macrotask queue, so the + // timers their trigger controls left behind fire on the first awaited + // tick in this file. Drain them here, against a throwaway mock so no + // real request is attempted, and every test below counts only its own + // dispatches. + mockCallbackRequest(); + await tick(); + restoreMocks(); + }); + + afterEach(() => { + controls.forEach((ctrl) => ctrl.onDone()); + restoreMocks(); + dialog.remove(); + }); + + it('registers itself in Prado.Registry on construction', () => { + makeDialog(); + expect(Registry['dialog1']).toBeDefined(); + }); + + it('dispatches a callback with CallbackParameter "close" when the dialog is dismissed', () => { + const { MockCtor, dispatchMock } = mockCallbackRequest(); + makeDialog(); + dialog.open = true; + + dialog.close(); + + expect(dispatchMock).toHaveBeenCalledTimes(1); + expect(MockCtor).toHaveBeenCalledWith( + 'dialog1', + expect.objectContaining({ CallbackParameter: 'close' }), + ); + }); + + it('dispatches a callback with CallbackParameter "open" when opened programmatically', async () => { + const { MockCtor, dispatchMock } = mockCallbackRequest(); + makeDialog(); + + dialog.show(); // showModal()/show() called by page script, not by the server + await tick(); + + expect(dispatchMock).toHaveBeenCalledTimes(1); + expect(MockCtor).toHaveBeenCalledWith( + 'dialog1', + expect.objectContaining({ CallbackParameter: 'open' }), + ); + }); + + it('reports an open only once for a single attribute change', async () => { + const { dispatchMock } = mockCallbackRequest(); + makeDialog(); + + dialog.show(); + dialog.setAttribute('open', ''); // redundant write, state unchanged + await tick(); + + expect(dispatchMock).toHaveBeenCalledTimes(1); + }); + + it('setOpen(true) opens the dialog without echoing a callback to the server', async () => { + const { dispatchMock } = mockCallbackRequest(); + const ctrl = makeDialog(); + + ctrl.setOpen(true); + await tick(); + + expect(dialog.open).toBe(true); + expect(dispatchMock).not.toHaveBeenCalled(); + }); + + it('setOpen(false) closes the dialog without echoing a callback to the server', async () => { + const { dispatchMock } = mockCallbackRequest(); + const ctrl = makeDialog(); + ctrl.setOpen(true); + await tick(); + + ctrl.setOpen(false); + await tick(); + + expect(dialog.open).toBe(false); + expect(dispatchMock).not.toHaveBeenCalled(); + }); + + it('suppresses only the close raised by setOpen(), not the next user dismissal', async () => { + const { dispatchMock } = mockCallbackRequest(); + const ctrl = makeDialog(); + + ctrl.setOpen(true); + await tick(); + ctrl.setOpen(false); + await tick(); + expect(dispatchMock).not.toHaveBeenCalled(); + + dialog.show(); + await tick(); + dispatchMock.mockClear(); + dialog.close(); // the user dismisses it + expect(dispatchMock).toHaveBeenCalledTimes(1); + }); + + it('does not mutate the options object it was constructed with', () => { + const { MockCtor } = mockCallbackRequest(); + const options = { ID: 'dialog1', EventTarget: 'dialog1' }; + makeDialog(options); + dialog.open = true; + + dialog.close(); + + expect(options.CallbackParameter).toBeUndefined(); + expect(MockCtor.mock.calls[0][1]).not.toBe(options); + }); + + it('onDone() disconnects the observer so a later open reports nothing', async () => { + const { dispatchMock } = mockCallbackRequest(); + const ctrl = makeDialog(); + + ctrl.onDone(); + dialog.show(); + await tick(); + + expect(dispatchMock).not.toHaveBeenCalled(); + }); + + it('the static setOpen() forwards to the instance found in the Registry', async () => { + mockCallbackRequest(); + const ctrl = makeDialog(); + const spy = vi.spyOn(ctrl, 'setOpen'); + + TActiveDialog.setOpen('dialog1', true); + await tick(); + + expect(spy).toHaveBeenCalledWith(true); + expect(dialog.open).toBe(true); + }); + + it('the static setOpen() ignores an unknown id', () => { + expect(() => TActiveDialog.setOpen('no-such-control', true)).not.toThrow(); + }); +}); diff --git a/tests/js/adapters/activecontrols.js b/tests/js/adapters/activecontrols.js index d8a8c3560..8115bb8a3 100644 --- a/tests/js/adapters/activecontrols.js +++ b/tests/js/adapters/activecontrols.js @@ -55,3 +55,7 @@ export const TValueTriggeredCallback = global.Prado.WebUI.TValueTriggeredCallba // Table controls export const TActiveTableCell = global.Prado.WebUI.TActiveTableCell; export const TActiveTableRow = global.Prado.WebUI.TActiveTableRow; + +// Disclosure / dialog controls +export const TActiveDetails = global.Prado.WebUI.TActiveDetails; +export const TActiveDialog = global.Prado.WebUI.TActiveDialog; From 85a3244c1b2bfa5a9e48f3406fb449385cafc8bc Mon Sep 17 00:00:00 2001 From: Belisoful Date: Wed, 26 Aug 2026 06:06:12 +0000 Subject: [PATCH 3/4] WebControl Doc Block additions and corrections --- .../Web/UI/ActiveControls/TActiveButton.php | 8 +- .../UI/ActiveControls/TActiveImageButton.php | 10 ++- .../UI/ActiveControls/TActiveLinkButton.php | 13 +-- framework/Web/UI/IPostBackEventHandler.php | 6 +- framework/Web/UI/TPage.php | 86 +++++++++++++++---- .../Web/UI/WebControls/TBulletedList.php | 2 +- framework/Web/UI/WebControls/TButton.php | 34 +++++--- framework/Web/UI/WebControls/TImageButton.php | 39 ++++++--- framework/Web/UI/WebControls/TImageMap.php | 5 +- framework/Web/UI/WebControls/TLinkButton.php | 26 ++++-- framework/Web/UI/WebControls/TPanel.php | 51 +++++++---- 11 files changed, 206 insertions(+), 74 deletions(-) diff --git a/framework/Web/UI/ActiveControls/TActiveButton.php b/framework/Web/UI/ActiveControls/TActiveButton.php index 6dde0cda3..c125c0ecf 100644 --- a/framework/Web/UI/ActiveControls/TActiveButton.php +++ b/framework/Web/UI/ActiveControls/TActiveButton.php @@ -20,7 +20,7 @@ * callback request is initiated. * * The {@see onCallback OnCallback} event is raised during a callback request - * and it is raise after the {@see onClick OnClick} event. + * and it is raised after the {@see onClick OnClick} event. * * When the {@see \Prado\Web\UI\ActiveControls\TBaseActiveCallbackControl::setEnableUpdate ActiveControl.EnableUpdate} * property is true, changing the {@see setText Text} property during callback request @@ -66,6 +66,12 @@ public function getClientSide() * method first. It will raise {@see onClick OnClick} event first * and then the {@see onCallback OnCallback} event. * This method is mainly used by framework and control developers. + * + * {@see \Prado\Web\UI\WebControls\TButton::raisePostBackEvent() raisePostBackEvent()} + * raises {@see onClick OnClick} with null and {@see onCommand OnCommand} with the + * command properties of the button. `$param` reaches {@see onCallback OnCallback} + * alone, so a handler reads the client-supplied value from + * {@see TCallbackEventParameter::getCallbackParameter CallbackParameter} there. * @param TCallbackEventParameter $param the event parameter */ public function raiseCallbackEvent($param) diff --git a/framework/Web/UI/ActiveControls/TActiveImageButton.php b/framework/Web/UI/ActiveControls/TActiveImageButton.php index ed23624f6..675c27b88 100644 --- a/framework/Web/UI/ActiveControls/TActiveImageButton.php +++ b/framework/Web/UI/ActiveControls/TActiveImageButton.php @@ -20,7 +20,7 @@ * callback request is initiated. * * The {@see onCallback OnCallback} event is raised during a callback request - * and it is raise after the {@see onClick OnClick} event. + * and it is raised after the {@see onClick OnClick} event. * * @author Wei Zhuo * @since 3.1 @@ -129,6 +129,14 @@ public function setDescriptionUrl($value) * {@see \Prado\Web\UI\TPage::validate} method first. It will raise * {@see onClick OnClick} event first and then the {@see onCallback OnCallback} event. * This method is mainly used by framework and control developers. + * + * {@see \Prado\Web\UI\WebControls\TImageButton::raisePostBackEvent() raisePostBackEvent()} + * raises {@see onClick OnClick} with a + * {@see \Prado\Web\UI\WebControls\TImageClickEventParameter} holding the click + * coordinates, and {@see onCommand OnCommand} with the command properties of the + * button. `$param` reaches {@see onCallback OnCallback} alone, so a handler reads the + * client-supplied value from + * {@see TCallbackEventParameter::getCallbackParameter CallbackParameter} there. * @param TCallbackEventParameter $param the event parameter */ public function raiseCallbackEvent($param) diff --git a/framework/Web/UI/ActiveControls/TActiveLinkButton.php b/framework/Web/UI/ActiveControls/TActiveLinkButton.php index 537118166..6f038845e 100644 --- a/framework/Web/UI/ActiveControls/TActiveLinkButton.php +++ b/framework/Web/UI/ActiveControls/TActiveLinkButton.php @@ -10,9 +10,6 @@ namespace Prado\Web\UI\ActiveControls; -/** - * Load active control adapter. - */ use Prado\Prado; use Prado\Web\UI\WebControls\TLinkButton; @@ -23,7 +20,7 @@ * callback request is initiated. * * The {@see onCallback OnCallback} event is raised during a callback request - * and it is raise after the {@see onClick OnClick} event. + * and it is raised after the {@see onClick OnClick} event. * * When the {@see \Prado\Web\UI\ActiveControls\TBaseActiveCallbackControl::setEnableUpdate ActiveControl.EnableUpdate} * property is true, changing the {@see setText Text} property during callback request @@ -64,12 +61,18 @@ public function getClientSide() /** * Raises the callback event. This method is required by - * {@see ICallbackEventHandlerICallbackEventHandler} interface. If + * {@see ICallbackEventHandler} interface. If * {@see getCausesValidation CausesValidation} is true, it will * invoke the page's {@see \Prado\Web\UI\TPage::validate validate} method first. It will raise * {@see onClick OnClick} event first and then the {@see onCallback OnCallback} * event. * This method is mainly used by framework and control developers. + * + * {@see \Prado\Web\UI\WebControls\TLinkButton::raisePostBackEvent() raisePostBackEvent()} + * raises {@see onClick OnClick} with null and {@see onCommand OnCommand} with the + * command properties of the button. `$param` reaches {@see onCallback OnCallback} + * alone, so a handler reads the client-supplied value from + * {@see TCallbackEventParameter::getCallbackParameter CallbackParameter} there. * @param TCallbackEventParameter $param the event parameter */ public function raiseCallbackEvent($param) diff --git a/framework/Web/UI/IPostBackEventHandler.php b/framework/Web/UI/IPostBackEventHandler.php index 21b8789f4..7bc459357 100644 --- a/framework/Web/UI/IPostBackEventHandler.php +++ b/framework/Web/UI/IPostBackEventHandler.php @@ -24,7 +24,11 @@ interface IPostBackEventHandler * Raises postback event. * The implementation of this function should raise appropriate event(s) (e.g. OnClick, OnCommand) * indicating the component is responsible for the postback event. - * @param string $param the parameter associated with the postback event + * {@see \Prado\Web\UI\TPage} supplies the postback parameter of the request, a string. + * A control that also serves callbacks reuses this method for its callback event and + * supplies its {@see \Prado\Web\UI\ActiveControls\TCallbackEventParameter} instead. + * An implementation that never serves a callback receives only the string. + * @param \Prado\TEventParameter|string $param the parameter associated with the postback event */ public function raisePostBackEvent($param); } diff --git a/framework/Web/UI/TPage.php b/framework/Web/UI/TPage.php index 33a31d187..253f1c248 100644 --- a/framework/Web/UI/TPage.php +++ b/framework/Web/UI/TPage.php @@ -28,6 +28,39 @@ /** * TPage class * + * TPage is the root of the control tree serving a page request. It is created + * by {@see \Prado\Web\Services\TPageService}, which calls {@see run()} to + * execute the page life cycles. The life cycle taken depends on the request + * type: a normal request, a postback ({@see getIsPostBack IsPostBack}) or a + * callback ({@see getIsCallback IsCallback}). A callback delegates its event + * processing and its response rendering to {@see TActivePageAdapter}. + * + * A page contains at most one {@see TForm} and at most one + * {@see \Prado\Web\UI\WebControls\THead}. Controls that post back call + * {@see ensureRenderInForm()} while rendering to verify they are within the form. + * + * Post data is dispatched to the controls implementing + * {@see \Prado\Web\UI\IPostBackDataHandler} by {@see processPostData()}. + * The control named by the post data as the event target receives + * {@see \Prado\Web\UI\IPostBackEventHandler::raisePostBackEvent()}. + * + * The state of the page and its controls is written to the client through + * {@see getStatePersister StatePersister}. The state can be HMAC validated, + * encrypted, compressed and serialized with igbinary, controlled by + * {@see setEnableStateValidation EnableStateValidation}, + * {@see setEnableStateEncryption EnableStateEncryption}, + * {@see setEnableStateCompression EnableStateCompression} and + * {@see setEnableStateIGBinary EnableStateIGBinary}. + * + * Validators add themselves to {@see getValidators Validators}. {@see validate()} + * runs them, either all of them or those of a single validation group, and + * {@see getIsValid IsValid} reports the outcome. + * + * {@see setTheme Theme} and {@see setStyleSheetTheme StyleSheetTheme} apply skins + * to the controls of the page and contribute their stylesheet and javascript + * files. {@see getClientScript ClientScript} collects the client-side scripts, + * stylesheets and hidden fields to be rendered. + * * @author Qiang Xue * @since 3.0 * @method TActivePageAdapter getAdapter() @@ -89,7 +122,7 @@ class TPage extends TTemplateControl */ private $_clientScript; /** - * @var TMap data post back by user + * @var ?\Prado\Web\THttpRequest data post back by user, null if not a postback */ protected $_postData; /** @@ -203,6 +236,7 @@ public function getAutoGlobalListen() /** * Runs through the page lifecycles. + * The life cycle run depends on the request type: normal, postback or callback. * @param \Prado\Web\UI\THtmlWriter $writer the HTML writer */ public function run($writer) @@ -319,9 +353,11 @@ protected static function decodeUTF8($data, $enc) } /** - * Sets Adapter to TActivePageAdapter and calls apter to process the - * callback request. - * @param mixed $writer + * Sets the Adapter to a TActivePageAdapter and runs the callback life cycle. + * The callback parameter is JSON decoded and the post data is decoded from + * UTF-8 to the application charset. The adapter raises the callback event + * and renders the callback response. + * @param \Prado\Web\UI\THtmlWriter $writer the HTML writer */ protected function processCallbackRequest($writer) { @@ -429,7 +465,7 @@ public function setCallbackEventTarget(TControl $control) /** * Callback parameter is decoded assuming JSON encoding. - * @return string callback event parameter + * @return mixed callback event parameter */ public function getCallbackEventParameter() { @@ -445,7 +481,7 @@ public function setCallbackEventParameter($value) } /** - * @return TForm the form on the page + * @return ?TForm the form on the page, null if no form is registered yet */ public function getForm() { @@ -495,7 +531,7 @@ public function getValidators($validationGroup = null) * Performs input validation. * This method will invoke the registered validators to perform the actual validation. * If validation group is specified, only the validators in that group will be invoked. - * @param string $validationGroup validation group. If null, all validators will perform validation. + * @param ?string $validationGroup validation group. If null, all validators will perform validation. */ public function validate($validationGroup = null) { @@ -539,7 +575,7 @@ public function getIsValid() } /** - * @return TTheme the theme used for the page. Defaults to null. + * @return ?TTheme the theme used for the page. Defaults to null. */ public function getTheme() { @@ -560,7 +596,7 @@ public function setTheme($value) /** - * @return TTheme the stylesheet theme used for the page. Defaults to null. + * @return ?TTheme the stylesheet theme used for the page. Defaults to null. */ public function getStyleSheetTheme() { @@ -678,8 +714,12 @@ public function onLoadComplete($param) * This method is invoked right after {@see onPreRender OnPreRender} stage. * You may override this method to provide additional preparation for page rendering * that should be done after {@see onPreRender OnPreRender}. + * The parent implementation registers the stylesheet and javascript files of + * the {@see setTheme Theme} and the {@see setStyleSheetTheme StyleSheetTheme} + * with the client script manager. * Remember to call the parent implementation to ensure OnPreRenderComplete event is raised. * @param mixed $param event parameter + * @throws TConfigurationException if a THead is required by the registered client scripts and the page has none. */ public function onPreRenderComplete($param) { @@ -714,6 +754,7 @@ public function onPreRenderComplete($param) * The media type is determined according to the following file name pattern: * xxx.media-type.extension * For example, 'mystyle.print.css' means its media type is 'print'. + * A theme overrides the outcome by handling the `dyCssMediaType` event. * @param string $url CSS URL * @param object $theme the theme being applied * @return string media type of the CSS file @@ -804,7 +845,7 @@ protected function loadPageState() } /** - * Saves page state from persistent storage. + * Saves page state to persistent storage. */ protected function savePageState() { @@ -827,7 +868,7 @@ protected function isSystemPostField($field) * This method needs to be invoked if the control to load post data * may not have a post variable in some cases. For example, a checkbox, * if not checked, will not have a post value. - * @param \Prado\Web\UI\TControl $control control registered for loading post data + * @param \Prado\Web\UI\TControl|string $control control, or the unique ID of the control, registered for loading post data */ public function registerRequiresPostData($control) { @@ -885,7 +926,10 @@ public function setPostBackEventParameter($value) /** * Processes post data. - * @param TMap $postData post data to be processed + * Controls implementing {@see \Prado\Web\UI\IPostBackDataHandler} are given + * their post data and are collected for {@see raiseChangedEvents()}. Data of + * unknown controls is kept for the second invocation, after {@see onLoad OnLoad}. + * @param \Prado\Web\THttpRequest|TMap $postData post data to be processed * @param bool $beforeLoad whether this method is invoked before {@see onLoad OnLoad}. */ protected function processPostData($postData, $beforeLoad) @@ -947,7 +991,8 @@ protected function raiseChangedEvents() } /** - * Raises PostBack event. + * Raises PostBack event on the control registered as the postback event target. + * When no control is registered, the page performs validation instead. */ protected function raisePostBackEvent() { @@ -968,6 +1013,7 @@ public function getInFormRender() /** * Ensures the control is rendered within a form. + * The check is skipped during a callback request, where the form is not rendered. * @param \Prado\Web\UI\TControl $control the control to be rendered * @throws TConfigurationException if the control is outside of the form */ @@ -980,7 +1026,8 @@ public function ensureRenderInForm($control) /** * @internal This method is invoked by TForm at the beginning of its rendering - * @param mixed $writer + * @param \Prado\Web\UI\THtmlWriter $writer the HTML writer + * @throws TConfigurationException if more than one form is rendered on the page. */ public function beginFormRender($writer) { @@ -993,8 +1040,8 @@ public function beginFormRender($writer) } /** - * @internal This method is invoked by TForm at the end of its rendering - * @param mixed $writer + * @internal This method is invoked by TForm at the end of its rendering + * @param \Prado\Web\UI\THtmlWriter $writer the HTML writer */ public function endFormRender($writer) { @@ -1037,7 +1084,7 @@ public function setClientSupportsJavaScript($value) } /** - * @return THead page head, null if not available + * @return ?THead page head, null if not available */ public function getHead() { @@ -1108,7 +1155,7 @@ public function setClientState($state) } /** - * @return string the state postback from client side + * @return ?string the state postback from client side, null if not present */ public function getRequestClientState() { @@ -1132,6 +1179,7 @@ public function setStatePersisterClass($value) } /** + * @throws TInvalidDataTypeException if the persister class does not implement IPageStatePersister. * @return IPageStatePersister page state persister */ public function getStatePersister() @@ -1260,7 +1308,7 @@ public function getCachingStack() } /** - * Flushes output + * Flushes the content rendered so far to the response. */ public function flushWriter() { diff --git a/framework/Web/UI/WebControls/TBulletedList.php b/framework/Web/UI/WebControls/TBulletedList.php index e959ac494..d03c9a7f4 100644 --- a/framework/Web/UI/WebControls/TBulletedList.php +++ b/framework/Web/UI/WebControls/TBulletedList.php @@ -54,7 +54,7 @@ class TBulletedList extends TListControl implements \Prado\Web\UI\IPostBackEvent * invoke the page's {@see \Prado\Web\UI\TPage::validate validate} method first. * It will raise {@see onClick OnClick} events. * This method is mainly used by framework and control developers. - * @param mixed $param the event parameter + * @param string $param the index of the clicked item */ public function raisePostBackEvent($param) { diff --git a/framework/Web/UI/WebControls/TButton.php b/framework/Web/UI/WebControls/TButton.php index 3166e24ed..132457991 100644 --- a/framework/Web/UI/WebControls/TButton.php +++ b/framework/Web/UI/WebControls/TButton.php @@ -36,15 +36,21 @@ * TButton displays the {@see setText Text} property as the button caption. * * TButton by default renders an input tag; the {@see setButtonTag ButtonTag} - * property can be used to render a button tag (introduced in html5). + * property can be used to render a button tag (introduced in html5). An input + * tag carries the caption in its value attribute; a button tag carries it as + * the body content of the tag. * * TButton can be one of three {@see setButtonType ButtonType}: Submit, Button and Reset. * By default, it is a Submit button and the form submission uses the browser's - * default submission capability. If it is Button or Reset, postback may occur - * if one of the following conditions is met: - * - an event handler is attached to {@see onClick OnClick} event; - * - an event handler is attached to {@see onCommand OnCommand} event; - * - the button is in a non-empty validation group. + * default submission capability. + * + * When {@see setEnableClientScript EnableClientScript} is true, javascript + * handling the postback is rendered if one of the following conditions is met: + * - {@see setCausesValidation CausesValidation} is true and the button's + * validation group contains at least one validator; + * - the button is the default button of a {@see \Prado\Web\UI\WebControls\TPanel}; + * - {@see setButtonType ButtonType} is Button or Reset and an event handler is + * attached to the {@see onClick OnClick} or {@see onCommand OnCommand} event. * In addition, clicking on a Reset button will clear up all input fields * if the button does not cause a postback. * @@ -62,7 +68,7 @@ protected function getTagName() } /** - * @return TButtonTag the tag name of the button. Defaults to TButtonType::Input. + * @return TButtonTag the tag name of the button. Defaults to TButtonTag::Input. */ public function getButtonTag() { @@ -124,7 +130,9 @@ protected function addAttributesToRender($writer) /** * Renders the client-script code. - * @param mixed $writer + * The client ID is rendered and the button is registered with the client + * script manager so that its postback javascript is generated. + * @param \Prado\Web\UI\THtmlWriter $writer the writer used for the rendering purpose */ protected function renderClientControlScript($writer) { @@ -214,7 +222,9 @@ public function renderContents($writer) * The method raises 'OnClick' event to fire up the event handlers. * If you override this method, be sure to call the parent implementation * so that the event handler can be invoked. - * @param \Prado\TEventParameter $param event parameter to be passed to the event handlers + * {@see raisePostBackEvent()} raises this event with null, in a postback and in + * a callback alike, because a button click posts no payload of its own. + * @param ?\Prado\TEventParameter $param event parameter to be passed to the event handlers */ public function onClick($param) { @@ -241,7 +251,11 @@ public function onCommand($param) * invoke the page's {@see \Prado\Web\UI\TPage::validate validate} method first. * It will raise {@see onClick OnClick} and {@see onCommand OnCommand} events. * This method is mainly used by framework and control developers. - * @param \Prado\TEventParameter $param the event parameter + * + * `$param` is not used. A button click carries no payload, so + * {@see onClick OnClick} is raised with null, while {@see onCommand OnCommand} + * carries the command properties of the button. + * @param \Prado\TEventParameter|string $param the event parameter */ public function raisePostBackEvent($param) { diff --git a/framework/Web/UI/WebControls/TImageButton.php b/framework/Web/UI/WebControls/TImageButton.php index 2bc8b827d..bc94d4d7d 100644 --- a/framework/Web/UI/WebControls/TImageButton.php +++ b/framework/Web/UI/WebControls/TImageButton.php @@ -20,18 +20,19 @@ * You can create either a submit button or a command button. * * A command button has a command name (specified by - * the {@see setCommandName CommandName} property) and and a command parameter + * the {@see setCommandName CommandName} property) and a command parameter * (specified by {@see setCommandParameter CommandParameter} property) - * associated with the button. This allows you to create multiple TLinkButton + * associated with the button. This allows you to create multiple TImageButton * components on a Web page and programmatically determine which one is clicked * with what parameter. You can provide an event handler for * {@see onCommand OnCommand} event to programmatically control the actions performed * when the command button is clicked. In the event handler, you can determine * the {@see setCommandName CommandName} property value and * the {@see setCommandParameter CommandParameter} property value - * through the {@see TCommandParameter::getName Name} and - * {@see TCommandParameter::getParameter Parameter} properties of the event - * parameter which is of type {@see \Prado\Web\UI\TCommandEventParameter}. + * through the {@see \Prado\Web\UI\TCommandEventParameter::getCommandName CommandName} + * and {@see \Prado\Web\UI\TCommandEventParameter::getCommandParameter CommandParameter} + * properties of the event parameter which is of type + * {@see \Prado\Web\UI\TCommandEventParameter}. * * A submit button does not have a command name associated with the button * and clicking on it simply posts the Web page back to the server. @@ -49,6 +50,11 @@ * * TImageButton displays the {@see setText Text} property as the hint text to the displayed image. * + * TImageButton extends {@see \Prado\Web\UI\WebControls\TImage} and renders an + * input tag of type image. The browser posts the click coordinates in two fields + * named after the control, which {@see loadPostData()} reads. The control therefore + * registers itself with the page through {@see \Prado\Web\UI\TPage::registerRequiresPostData()}. + * * @author Qiang Xue * @since 3.0 */ @@ -62,6 +68,9 @@ class TImageButton extends TImage implements \Prado\Web\UI\IPostBackDataHandler, * @var int y coordinate that the image is being clicked at */ private $_y = 0; + /** + * @var bool whether the image button has been clicked in this postback + */ private $_dataChanged = false; /** @@ -113,7 +122,9 @@ protected function addAttributesToRender($writer) /** * Renders the client-script code. - * @param mixed $writer + * The client ID is rendered and the button is registered with the client + * script manager so that its postback javascript is generated. + * @param \Prado\Web\UI\THtmlWriter $writer the writer used for the rendering purpose */ protected function renderClientControlScript($writer) { @@ -186,10 +197,14 @@ protected function getPostBackOptions() /** * This method checks if the TImageButton is clicked and loads the coordinates of the clicking position. - * This method is primarly used by framework developers. + * A clicked button becomes the postback event target of the page when no other + * target is registered. The button reports no data change, so + * {@see raisePostDataChangedEvent()} is never invoked for it, while + * {@see getDataChanged DataChanged} reports the click. + * This method is primarily used by framework developers. * @param string $key the key that can be used to retrieve data from the input data collection - * @param array $values the input data collection - * @return bool whether the data of the component has been changed + * @param array|\ArrayAccess $values the input data collection + * @return bool false, the image button raises no post data changed event */ public function loadPostData($key, $values) { @@ -245,7 +260,11 @@ public function onCommand($param) * invoke the page's {@see \Prado\Web\UI\TPage::validate validate} method first. * It will raise {@see onClick OnClick} and {@see onCommand OnCommand} events. * This method is mainly used by framework and control developers. - * @param \Prado\TEventParameter $param the event parameter + * + * `$param` is not used. The coordinates carried by the + * {@see \Prado\Web\UI\WebControls\TImageClickEventParameter} of + * {@see onClick OnClick} come from the post data read by {@see loadPostData()}. + * @param \Prado\TEventParameter|string $param the event parameter */ public function raisePostBackEvent($param) { diff --git a/framework/Web/UI/WebControls/TImageMap.php b/framework/Web/UI/WebControls/TImageMap.php index f6ffe27fc..53e3aff09 100644 --- a/framework/Web/UI/WebControls/TImageMap.php +++ b/framework/Web/UI/WebControls/TImageMap.php @@ -129,8 +129,11 @@ protected function getClientClassName() /** * Raises the postback event. * This method is required by {@see \Prado\Web\UI\IPostBackEventHandler} interface. + * {@see onClick OnClick} is raised for a hot spot whose mode is PostBack, after + * the validators of the hot spot are invoked. A hot spot of any other mode + * raises no event. * This method is mainly used by framework and control developers. - * @param \Prado\TEventParameter $param the event parameter + * @param string $param the index of the clicked hot spot */ public function raisePostBackEvent($param) { diff --git a/framework/Web/UI/WebControls/TLinkButton.php b/framework/Web/UI/WebControls/TLinkButton.php index 13f9e1367..2ea1ec1fd 100644 --- a/framework/Web/UI/WebControls/TLinkButton.php +++ b/framework/Web/UI/WebControls/TLinkButton.php @@ -21,7 +21,7 @@ * a submit button or a command button. * * A command button has a command name (specified by - * the {@see setCommandName CommandName} property) and and a command parameter + * the {@see setCommandName CommandName} property) and a command parameter * (specified by {@see setCommandParameter CommandParameter} property) * associated with the button. This allows you to create multiple TLinkButton * components on a Web page and programmatically determine which one is clicked @@ -30,9 +30,10 @@ * when the command button is clicked. In the event handler, you can determine * the {@see setCommandName CommandName} property value and * the {@see setCommandParameter CommandParameter} property value - * through the {@see TCommandParameter::getName Name} and - * {@see TCommandParameter::getParameter Parameter} properties of the event - * parameter which is of type {@see \Prado\Web\UI\TCommandEventParameter}. + * through the {@see \Prado\Web\UI\TCommandEventParameter::getCommandName CommandName} + * and {@see \Prado\Web\UI\TCommandEventParameter::getCommandParameter CommandParameter} + * properties of the event parameter which is of type + * {@see \Prado\Web\UI\TCommandEventParameter}. * * A submit button does not have a command name associated with the button * and clicking on it simply posts the Web page back to the server. @@ -51,6 +52,11 @@ * of TLinkButton will be displayed. Therefore, you can use TLinkButton * as an image button by enclosing an <img> tag as the body of TLinkButton. * + * TLinkButton posts back through javascript. The anchor renders a no-op href + * and the postback script only when {@see setEnableClientScript EnableClientScript} + * is true, which is the default. Setting it to false renders a plain anchor + * that performs no postback. + * * @author Qiang Xue * @since 3.0 */ @@ -257,7 +263,7 @@ public function setCommandParameter($value) } /** - * @return bool whether postback event trigger by this button will cause input validation + * @return bool whether postback event trigger by this button will cause input validation, default is true */ public function getCausesValidation() { @@ -296,7 +302,11 @@ public function setValidationGroup($value) * invoke the page's {@see \Prado\Web\UI\TPage::validate validate} method first. * It will raise {@see onClick OnClick} and {@see onCommand OnCommand} events. * This method is mainly used by framework and control developers. - * @param \Prado\TEventParameter $param the event parameter + * + * `$param` is not used. A button click carries no payload, so + * {@see onClick OnClick} is raised with null, while {@see onCommand OnCommand} + * carries the command properties of the button. + * @param \Prado\TEventParameter|string $param the event parameter */ public function raisePostBackEvent($param) { @@ -312,7 +322,9 @@ public function raisePostBackEvent($param) * The method raises 'OnClick' event to fire up the event handlers. * If you override this method, be sure to call the parent implementation * so that the event handler can be invoked. - * @param \Prado\TEventParameter $param event parameter to be passed to the event handlers + * {@see raisePostBackEvent()} raises this event with null, in a postback and in + * a callback alike, because a button click posts no payload of its own. + * @param ?\Prado\TEventParameter $param event parameter to be passed to the event handlers */ public function onClick($param) { diff --git a/framework/Web/UI/WebControls/TPanel.php b/framework/Web/UI/WebControls/TPanel.php index 6d07346f7..3d3cf5e25 100644 --- a/framework/Web/UI/WebControls/TPanel.php +++ b/framework/Web/UI/WebControls/TPanel.php @@ -16,7 +16,7 @@ /** * TPanel class * - * TPanel represents a component that acts as a container for other component. + * TPanel represents a component that acts as a container for other components. * It is especially useful when you want to generate components programmatically * or hide/show a group of components. * @@ -24,11 +24,13 @@ * Children of TPanel are displayed as the body content of the element. * The property {@see setWrap Wrap} can be used to set whether the body content * should wrap or not. {@see setHorizontalAlign HorizontalAlign} governs how - * the content is aligned horizontally, and {@see getDirection Direction} indicates - * the content direction (left to right or right to left). You can set + * the content is aligned horizontally, and {@see setDirection Direction} indicates + * the content direction (left to right or right to left). + * {@see setScrollBars ScrollBars} sets the visibility and position of the scroll + * bars of the panel. You can set * {@see setBackImageUrl BackImageUrl} to give a background image to the panel, - * and you can ste {@see setGroupingText GroupingText} so that the panel is - * displayed as a field set with a legend text. Finally, you can specify + * and you can set {@see setGroupingText GroupingText} so that the body content is + * enclosed in a field set with a legend text. Finally, you can specify * a default button to be fired when users press 'return' key within the panel * by setting the {@see setDefaultButton DefaultButton} property. * @@ -53,7 +55,7 @@ protected function getTagName() /** * Creates a style object to be used by the control. - * This method overrides the parent impementation by creating a TPanelStyle object. + * This method overrides the parent implementation by creating a TPanelStyle object. * @return TPanelStyle the style used by TPanel. */ protected function createStyle() @@ -63,8 +65,9 @@ protected function createStyle() /** * Adds attributes to renderer. + * The client ID is rendered when a {@see setDefaultButton DefaultButton} is set, + * because the default button script addresses the panel by that ID. * @param \Prado\Web\UI\THtmlWriter $writer the renderer - * @throws TInvalidDataValueException if default button is not right. */ protected function addAttributesToRender($writer) { @@ -92,7 +95,7 @@ public function setWrap($value) } /** - * @return string the horizontal alignment of the contents within the panel, defaults to 'NotSet'. + * @return THorizontalAlign the horizontal alignment of the contents within the panel, defaults to THorizontalAlign::NotSet. */ public function getHorizontalAlign() { @@ -102,7 +105,7 @@ public function getHorizontalAlign() /** * Sets the horizontal alignment of the contents within the panel. * Valid values include 'NotSet', 'Justify', 'Left', 'Right', 'Center' - * @param string $value the horizontal alignment + * @param THorizontalAlign $value the horizontal alignment */ public function setHorizontalAlign($value) { @@ -127,7 +130,7 @@ public function setBackImageUrl($value) } /** - * @return string alignment of the content in the panel. Defaults to 'NotSet'. + * @return TContentDirection direction of the content in the panel. Defaults to TContentDirection::NotSet. */ public function getDirection() { @@ -135,7 +138,7 @@ public function getDirection() } /** - * @param string $value alignment of the content in the panel. + * @param TContentDirection $value direction of the content in the panel. * Valid values include 'NotSet', 'LeftToRight', 'RightToLeft'. */ public function setDirection($value) @@ -155,7 +158,9 @@ public function getDefaultButton() * Specifies the default button for the panel. * The default button will be fired (clicked) whenever a user enters 'return' * key within the panel. - * The button must be locatable via the function call {@see \Prado\Web\UI\TControl::findControl findControl}. + * The button must be locatable via the function call {@see \Prado\Web\UI\TControl::findControl findControl} + * and must implement {@see \Prado\Web\UI\IButtonControl}. A control that is + * found but is not a button registers no default button behavior. * @param string $value the ID path to the default button. */ public function setDefaultButton($value) @@ -164,7 +169,7 @@ public function setDefaultButton($value) } /** - * @return string the legend text when the panel is used as a fieldset. Defaults to empty. + * @return string the legend text of the fieldset enclosing the body content. Defaults to empty. */ public function getGroupingText() { @@ -172,7 +177,7 @@ public function getGroupingText() } /** - * @param string $value the legend text. If this value is not empty, the panel will be rendered as a fieldset. + * @param string $value the legend text. If this value is not empty, the body content is enclosed in a fieldset with this legend. */ public function setGroupingText($value) { @@ -180,7 +185,7 @@ public function setGroupingText($value) } /** - * @return string the visibility and position of scroll bars in a panel control, defaults to None. + * @return TScrollBars the visibility and position of scroll bars in a panel control, defaults to TScrollBars::None. */ public function getScrollBars() { @@ -188,7 +193,7 @@ public function getScrollBars() } /** - * @param string $value the visibility and position of scroll bars in a panel control. + * @param TScrollBars $value the visibility and position of scroll bars in a panel control. * Valid values include None, Auto, Both, Horizontal and Vertical. */ public function setScrollBars($value) @@ -197,7 +202,9 @@ public function setScrollBars($value) } /** - * Renders the openning tag for the control (including attributes) + * Renders the opening tag for the control (including attributes). + * A fieldset and its legend are opened within the tag when + * {@see setGroupingText GroupingText} is not empty. * @param \Prado\Web\UI\THtmlWriter $writer the writer used for the rendering purpose */ public function renderBeginTag($writer) @@ -212,7 +219,8 @@ public function renderBeginTag($writer) } /** - * Renders the closing tag for the control + * Renders the closing tag for the control. + * The fieldset opened by {@see renderBeginTag()} is closed first. * @param \Prado\Web\UI\THtmlWriter $writer the writer used for the rendering purpose */ public function renderEndTag($writer) @@ -223,6 +231,13 @@ public function renderEndTag($writer) parent::renderEndTag($writer); } + /** + * Renders the panel and registers its default button. + * The {@see setDefaultButton DefaultButton} is resolved after the children are + * rendered, so that a button declared within the panel is found. + * @param \Prado\Web\UI\THtmlWriter $writer the writer used for the rendering purpose + * @throws TInvalidDataValueException if the default button cannot be found. + */ public function render($writer) { parent::render($writer); From 6c67fdc1c48582deeb47f251fe167bc7e674603d Mon Sep 17 00:00:00 2001 From: Belisoful Date: Wed, 26 Aug 2026 06:06:45 +0000 Subject: [PATCH 4/4] agents/framework/Web documentation corrections --- agents/framework/Web/UI/IAdapterControl.md | 39 ++--- agents/framework/Web/UI/IFilterRenderable.md | 47 +++--- agents/framework/Web/UI/TPage.md | 157 ++++++++---------- .../Web/UI/TRenderFilterParameter.md | 136 ++++++++------- .../framework/Web/UI/WebControls/TButton.md | 49 +++--- .../Web/UI/WebControls/TImageButton.md | 11 +- .../Web/UI/WebControls/TLinkButton.md | 16 +- agents/framework/Web/UI/WebControls/TPanel.md | 5 +- 8 files changed, 230 insertions(+), 230 deletions(-) diff --git a/agents/framework/Web/UI/IAdapterControl.md b/agents/framework/Web/UI/IAdapterControl.md index 36ab8fae0..cc042c349 100644 --- a/agents/framework/Web/UI/IAdapterControl.md +++ b/agents/framework/Web/UI/IAdapterControl.md @@ -3,42 +3,39 @@ ### Directories [framework](../../INDEX.md) / [Web](../INDEX.md) / [UI](./INDEX.md) / **`IAdapterControl`** -## Class Info +## Interface Info **Location:** `framework/Web/UI/IAdapterControl.php` **Namespace:** `Prado\Web\UI` +**Since:** 4.3.3 ## Overview -Interface defining the contract for objects returned by `TControl::getAdapterControl()`. That method returns either the control itself or its attached `TControlAdapter`. Both `TControl` and `TControlAdapter` implement this interface so the framework can dispatch lifecycle calls through a single, uniform pointer without knowing whether an adapter is present. -All lifecycle methods, the render entry-point, and state hooks are called through this interface during the page request cycle. +Common contract for the object returned by `TControl::getAdapterControl()`. That method returns either the control itself or its `TControlAdapter` when one is set, so both classes implement this interface. Lifecycle methods, the render entry-point, and state hooks are all called through it, letting `TControl` dispatch to either the adapter or itself with a single typed call. -## Interface Methods +## Methods | Method | Description | |---|---| | `createChildControls()` | Creates child controls | -| `onInit($param)` | Called at the `OnInit` lifecycle stage | -| `onLoad($param)` | Called at the `OnLoad` lifecycle stage | -| `onPreRender($param)` | Called at the `OnPreRender` lifecycle stage | -| `onUnload($param)` | Called at the `OnUnload` lifecycle stage | -| `render(THtmlWriter $writer)` | Renders the control | +| `onInit($param)` | Invoked at the `OnInit` lifecycle stage | +| `onLoad($param)` | Invoked at the `OnLoad` lifecycle stage | +| `onPreRender($param)` | Invoked at the `OnPreRender` lifecycle stage | +| `onUnload($param)` | Invoked at the `OnUnload` lifecycle stage | +| `render($writer)` | Renders the control to `$writer` | | `loadState()` | Loads additional persistent control state | | `saveState()` | Saves additional persistent control state | ## Implementors -- **`TControl`** — implements it directly; `getAdapterControl()` returns `$this` when no adapter is set. -- **`TControlAdapter`** — base adapter class; each method delegates to the attached control by default. Subclasses override only what they need. +- **[`TControl`](TControl.md)** — satisfies all methods natively; `getAdapterControl()` returns `$this` when no adapter is set. +- **[`TControlAdapter`](TControlAdapter.md)** — provides pass-through implementations that delegate to the attached control; subclasses override only what they customise. -## Patterns & Gotchas +## Usage -- **Purpose of `getAdapterControl()`** — `TControl::initRecursive`, `loadRecursive`, `preRenderRecursive`, `unloadRecursive`, `renderControl`, and the state hooks all route through `getAdapterControl()` so that an adapter transparently intercepts any or all of these stages. -- **Attach via `TControl::setAdapter()`** — once an adapter is attached, `getAdapterControl()` returns it instead of the control itself. -- **Adding adapters post-init** — setting an adapter after the control has been initialized can result in lifecycle methods being called on the adapter for stages the control has already passed. +```php +// Inside TControl — getAdapterControl() is protected; returns IAdapterControl +$this->getAdapterControl()->onPreRender($param); +$this->getAdapterControl()->render($writer); +``` -## See Also - -- [TControlAdapter](./TControlAdapter.md) -- [TControl](./TControl.md) - -**@since 4.3.3** +Application code never calls `getAdapterControl()` directly; it is used exclusively inside `TControl`'s own lifecycle and rendering methods. diff --git a/agents/framework/Web/UI/IFilterRenderable.md b/agents/framework/Web/UI/IFilterRenderable.md index 31a073daa..0ab10e446 100644 --- a/agents/framework/Web/UI/IFilterRenderable.md +++ b/agents/framework/Web/UI/IFilterRenderable.md @@ -3,49 +3,46 @@ ### Directories [framework](../../INDEX.md) / [Web](../INDEX.md) / [UI](./INDEX.md) / **`IFilterRenderable`** -## Class Info +## Interface Info **Location:** `framework/Web/UI/IFilterRenderable.php` **Namespace:** `Prado\Web\UI` +**Extends:** `IRenderable` +**Since:** 4.3.3 ## Overview -Interface that marks a control as supporting render-output filtering via the `onRenderFilter` event. Extends `IRenderable`. -`TControl::renderControl` and `TControl::renderChildren` detect this interface and automatically handle the capture-and-restore lifecycle: when at least one `onRenderFilter` handler is registered, the writer's inner `ITextWriter` is swapped for a fresh buffer, `render()` runs into that buffer, and the captured HTML is then passed through `onRenderFilter` handlers before being written to the real output. +Marks a control as supporting render-output filtering via the `onRenderFilter` event. `TControl::renderControl` and `TControl::renderChildren` detect this interface and automatically wrap the render call in a capture-and-restore filter lifecycle (`preRenderFilter` / `processRenderFilter`). Implement using [`TFilterRenderableTrait`](Traits/TFilterRenderableTrait.md). -Implement using [TFilterRenderableTrait](./Traits/TFilterRenderableTrait.md). - -## Interface Methods +## Methods | Method | Description | |---|---| -| `hasEventHandler(string $name): bool` | Returns whether at least one handler is registered for the named event. Required so `TControl::preRenderFilter` can test `onRenderFilter` without assuming the object is a `TComponent`. | -| `onRenderFilter(string $renderedText): string` | Raises the `onRenderFilter` event via a [TRenderFilterParameter](./TRenderFilterParameter.md), passing captured HTML through all registered handlers, and returns the (possibly modified) string. | +| `hasEventHandler($name)` | Returns whether at least one handler is registered for the named event. Required so `TControl::preRenderFilter` can test the event without assuming a `TComponent` base. | +| `onRenderFilter($output)` | Raises the `onRenderFilter` event, passes HTML through handlers via a [`TRenderFilterParameter`](TRenderFilterParameter.md), returns the (possibly modified) HTML string. | -Inherits `render(THtmlWriter $writer)` from `IRenderable`. +## Implementation -## How to Implement +Implement the interface by using [`TFilterRenderableTrait`](Traits/TFilterRenderableTrait.md), which provides `onRenderFilter`. `hasEventHandler` is satisfied by `TComponent`, which all practical implementors extend: ```php -use Prado\Web\UI\IFilterRenderable; -use Prado\Web\UI\Traits\TFilterRenderableTrait; - -class MyControl extends TCompositeControl implements IFilterRenderable +class MyControl extends TComponent implements IFilterRenderable { use TFilterRenderableTrait; - // onRenderFilter() and hasEventHandler() are provided by the trait + TComponent + + public function render($writer): void + { + $writer->write('

content

'); + } } ``` -## Patterns & Gotchas - -- **No-op when no handlers** — `preRenderFilter` calls `hasEventHandler('onRenderFilter')` and returns `null` (no buffer swap) when the result is false. Zero overhead for controls without handlers. -- **`TControl` itself implements this** — `TControl` implements `IFilterRenderable` via `TFilterRenderableTrait`, so all controls can receive `onRenderFilter` handlers out of the box. -- **Non-`TControl` implementors** — `renderChildren` also checks `IFilterRenderable` on non-`TControl` children (plain `IRenderable` objects), applying the same lifecycle when they implement this interface. +`TControl` already implements `IFilterRenderable` — no extra work is needed for controls that extend it. -## See Also +## Filter lifecycle -- [TFilterRenderableTrait](./Traits/TFilterRenderableTrait.md) -- [TRenderFilterParameter](./TRenderFilterParameter.md) -- [TControl](./TControl.md) +When `TControl::renderChildren` encounters a non-`TControl` child that implements `IFilterRenderable`: +1. `preRenderFilter($writer, $child)` — checks `$child->hasEventHandler('onRenderFilter')`. If true, swaps the writer's inner buffer and saves the original. +2. `$child->render($writer)` — renders into the capture buffer. +3. `processRenderFilter($writer, $oldWriter, $child)` — flushes the buffer, calls `$child->onRenderFilter($output)`, writes the result to the original writer, and restores it. -**@since 4.3.3** +If no handler is registered, all three steps are no-ops and output goes directly to the writer. diff --git a/agents/framework/Web/UI/TPage.md b/agents/framework/Web/UI/TPage.md index c6c63f723..5f5e31177 100644 --- a/agents/framework/Web/UI/TPage.md +++ b/agents/framework/Web/UI/TPage.md @@ -24,86 +24,72 @@ TPage is the base class for all web pages in PRADO framework. It extends [TTempl - `Head` ([THead](./WebControls/THead.md)): Page header element - `Validators` ([TList](../../Collections/TList.md)): Collection of registered validators - `Theme` ([TTheme](./TTheme.md)): Page theme for styling -- `StyleSheet` ([TTheme](./TTheme.md)): Page stylesheet theme +- `StyleSheetTheme` ([TTheme](./TTheme.md)): Page stylesheet theme - `ClientScript` ([TClientScriptManager](./TClientScriptManager.md)): Manages client-side scripts +- `Title` (string): Page title, held until a THead is set on the page - `PagePath` (string): Path to the current page +- `IsPostBack` (bool): Whether the request is a postback (read-only) +- `IsCallback` (bool): Whether the request is a callback (read-only) +- `StatePersisterClass` / `StatePersister` ([IPageStatePersister](./IPageStatePersister.md)): Where page state is stored - `EnableStateValidation` (bool): Whether page state should be HMAC validated -- `EnableStateEncryption` (bool): Whether page state should be encrypted +- `EnableStateEncryption` (bool): Whether page state should be encrypted - `EnableStateCompression` (bool): Whether page state should be compressed -- `EnableJavaScript` (bool): Whether client supports JavaScript -- `Focus` (string|[TControl](./TControl.md)): Control or element to be focused on page load +- `EnableStateIGBinary` (bool): Whether page state uses the igbinary serializer when available +- `ClientSupportsJavaScript` (bool): Whether client supports JavaScript +- `Focus` (string|[TControl](./TControl.md)): Control or element to be focused on page load (write-only) +- `CallbackClient` ([TCallbackClientScript](./ActiveControls/TCallbackClientScript.md)): Client-side commands for a callback response ## Core Methods ### Page Lifecycle -- `initRecursive()`: Initializes page and child controls -- `loadRecursive()`: Loads page and child controls -- `preRenderRecursive()`: Pre-renders page and child controls -- `unloadRecursive()`: Unloads page and child controls -- `saveState()`: Saves page state to persister +- `run($writer)`: Entry point called by [TPageService](../Services/TPageService.md) +- `processNormalRequest($writer)`, `processPostBackRequest($writer)`, `processCallbackRequest($writer)`: The three life cycles +- `onPreInit()`, `onInitComplete()`, `onPreLoad()`, `onLoadComplete()`, `onPreRenderComplete()`, `onSaveStateComplete()`: Page-only life cycle events +- `flushWriter()`: Flushes the content rendered so far to the response ### Form Management -- `getForm()`: Gets form instance -- `setForm()`: Sets form instance -- `setFocus($control)`: Sets focus to a control -- `getFocus()`: Gets focus control or element ID -- `renderForm()`: Renders HTML form +- `getForm()` / `setForm($form)`: The single [TForm](./TForm.md) of the page +- `getHead()` / `setHead($head)`: The single [THead](./WebControls/THead.md) of the page +- `setFocus($control)`: Sets focus to a control or element ID +- `ensureRenderInForm($control)`: Throws when a control renders outside the form +- `getInFormRender()`, `beginFormRender($writer)`, `endFormRender($writer)`: Form render state, invoked by TForm ### Validation -- `getValidators()`: Gets list of registered validators -- `registerValidator($validator)`: Registers a validator -- `unregisterValidator($validator)`: Unregisters a validator -- `validate()`: Performs page validation -- `getIsValid()`: Checks if page is valid +- `getValidators($validationGroup = null)`: Gets the [TList](../../Collections/TList.md) of registered validators; validators add and remove themselves +- `validate($validationGroup = null)`: Performs page validation +- `getIsValid()`: Whether the input is valid; throws when `validate()` has not run ### State Management -- `getPageStatePersister()`: Gets page state persister instance -- `loadPageState()`: Loads page state from request -- `savePageState()`: Saves page state to response -- `getPageState()`: Gets page state data -- `getControlState()`: Gets control state data for page -- `setStateValidation()`: Sets page state validation -- `getStateValidation()`: Gets page state validation -- `setViewState()`: Sets viewstate data for page -- `getViewState()`: Gets viewstate data for page - -### Event Handling -- `raisePostBackEvent($sender, $param)`: Raises postback event -- `onLoadPostData()`: Handles loading postback data -- `onLoad()`: Raises OnLoad event -- `onPreRender()`: Raises OnPreRender event -- `onUnload()`: Raises OnUnload event +- `getStatePersister()`: Gets page state persister instance +- `loadPageState()` / `savePageState()`: Reads and writes the page state through the persister +- `saveState()` / `loadState()`: Adds the controls requiring post data to the page's own state +- `getClientState()` / `setClientState($state)`: State to be written to the client +- `getRequestClientState()`: State posted back from the client + +### Postback & Data Handling +- `getIsPostBack()`, `getIsCallback()`: Request type +- `processPostData($postData, $beforeLoad)`: Dispatches post data to the controls +- `registerRequiresPostData($control)`: Registers a control to load post data on the next postback +- `getIsLoadingPostData()`: Whether post data is being loaded +- `getPostBackEventTarget()` / `setPostBackEventTarget($control)`: Control raising the postback event +- `getPostBackEventParameter()` / `setPostBackEventParameter($value)`: Postback event parameter +- `raiseChangedEvents()`: Raises OnPostDataChanged for the controls whose data changed +- `isSystemPostField($field)`: Whether a post field is one of the framework `FIELD_*` fields + +### Callbacks +- `getCallbackClient()`: Client-side script handler for the callback response +- `getCallbackEventTarget()` / `setCallbackEventTarget($control)`: Control raising the callback event +- `getCallbackEventParameter()` / `setCallbackEventParameter($value)`: JSON decoded callback parameter ### Theme Management -- `getTheme()`: Gets page theme -- `setTheme($value)`: Sets page theme -- `getStyleSheet()`: Gets page stylesheet -- `setStyleSheet($value)`: Sets page stylesheet -- `applyControlSkin()`: Applies skin to controls - -### Client Script Management -- `getClientScript()`: Gets client script manager -- `registerClientScript()`: Registers client script -- `registerStyleSheet()`: Registers CSS stylesheet -- `addStyleSheet()`: Adds CSS stylesheet -- `getJavaScript()`: Gets JavaScript object +- `getTheme()` / `setTheme($value)`: Gets and sets the page theme +- `getStyleSheetTheme()` / `setStyleSheetTheme($value)`: Gets and sets the page stylesheet theme +- `applyControlSkin($control)` / `applyControlStyleSheet($control)`: Applies a skin to a control -### Postback & Data Handling -- `getPostBackEventTarget()`: Gets control that raised postback event -- `setPostBackEventTarget()`: Sets control that raised postback event -- `getPostBackEventParameter()`: Gets postback event parameter -- `setPostBackEventParameter()`: Sets postback event parameter -- `getPostBackData()`: Gets postback data -- `setPostBackData()`: Sets postback data -- `getHasChanged()`: Checks if page data has changed - -### Rendering -- `render()`: Renders page to HTML output -- `renderChildren()`: Renders page child controls -- `renderFormContent()`: Renders form content -- `renderHead()`: Renders page head element -- `renderClientScript()`: Renders client-side scripts -- `renderPostData()`: Renders postback data fields +### Client Script and Caching +- `getClientScript()`: Gets the client script manager, which registers scripts, stylesheets and hidden fields +- `getCachingStack()`: Stack of the active [TOutputCache](./WebControls/TOutputCache.md) controls +- `registerCachingAction($context, $funcName, $funcParams)`: Records an action to replay when cached content is served ## Constants - `FIELD_POSTBACK_TARGET`: System postback target field name @@ -114,35 +100,32 @@ TPage is the base class for all web pages in PRADO framework. It extends [TTempl - `FIELD_CALLBACK_PARAMETER`: System callback parameter field name ## Page Lifecycle Stages -1. **Init**: Page and control initialization -2. **Load**: Loading of page state and postback data -3. **PreRender**: Page preparation for rendering -4. **Render**: Actual HTML output generation -5. **Unload**: Page cleanup +`onPreInit` -> `initRecursive` -> `onInitComplete` -> `loadPageState` *(POST/Callback)* -> `processPostData` *(POST/Callback)* -> `onPreLoad` -> `loadRecursive` -> `processPostData` *(POST/Callback)* -> `raiseChangedEvents` *(POST/Callback)* -> `raisePostBackEvent` *(POST-only)* -> `processCallbackEvent` *(Callback-only)* -> `onLoadComplete` -> `preRenderRecursive` -> `onPreRenderComplete` -> `savePageState` -> `onSaveStateComplete` -> `renderControl` *(GET/POST)* / `renderCallbackResponse` *(Callback-only)* -> `unloadRecursive` ## Validation Flow -1. Validators are registered during Init lifecycle -2. Validation is triggered in PreRender lifecycle -3. Page is validated in Validate method -4. Validation results are stored in IsValid property +1. A validator adds itself to `Validators` when it is initialized, and removes itself when it is unloaded +2. A button with `CausesValidation` calls `validate($validationGroup)` from its `raisePostBackEvent()` +3. `raisePostBackEvent()` on the page validates when no control is registered as the event target +4. `getIsValid()` reports the outcome and throws when `validate()` has not run ## Usage Example ```php -// Create page with form -$page = new TPage(); -$form = new TForm(); -$page->setForm($form); - -// Register validator -$validator = new TRequiredFieldValidator(); -$page->registerValidator($validator); - -// Process page -$page->initRecursive(); -$page->loadRecursive(); -if ($page->validate()) { - $page->preRenderRecursive(); - $page->render($writer); +class HomePage extends TPage +{ + public function onLoad($param) + { + parent::onLoad($param); + if (!$this->getIsPostBack()) { + $this->setTitle('Home'); + } + } + + public function buttonClicked($sender, $param) + { + if ($this->getIsValid()) { + // handle the click + } + } } ``` diff --git a/agents/framework/Web/UI/TRenderFilterParameter.md b/agents/framework/Web/UI/TRenderFilterParameter.md index 23a7c7df5..f9f7c2014 100644 --- a/agents/framework/Web/UI/TRenderFilterParameter.md +++ b/agents/framework/Web/UI/TRenderFilterParameter.md @@ -8,91 +8,105 @@ **Namespace:** `Prado\Web\UI` **Extends:** `TEventParameter` **Implements:** `IEventCycleParameter` +**Since:** 4.3.3 ## Overview -Event parameter for the `onRenderFilter` event raised by `TControl::renderControl`. It carries the captured rendered HTML and exposes two representations that can be switched between transparently: -- **HTML string** — raw rendered markup, via `getFilterText()` / `setFilterText()` or array-access key `'html'` (`RENDER_FILTER_TEXT`). -- **DOMDocument** — a parsed DOM tree, via `getFilterDOM()` / `setFilterDOM()` or array-access key `'dom'` (`RENDER_FILTER_DOM`). - -The parameter tracks which representation is *current* (authoritative) and lazily syncs between them. +Event parameter for the `onRenderFilter` event raised by `TControl::renderControl`. Carries the rendered HTML and exposes two transparently-switchable representations — an HTML string and a `DOMDocument` — plus the libxml parse error list. All three are stored in the parent `TEventParameter` array under reserved keys. ## Constants | Constant | Value | Description | |---|---|---| -| `RENDER_FILTER_TEXT` | `'html'` | Array-access key for the HTML string | -| `RENDER_FILTER_DOM` | `'dom'` | Array-access key for the DOMDocument | -| `RENDER_FILTER_ERRORS` | `'errors'` | Array-access key for libxml parse errors (`LibXMLError[]` or `null`) | +| `RENDER_FILTER_TEXT` | `'html'` | Array key for the HTML string | +| `RENDER_FILTER_DOM` | `'dom'` | Array key for the DOMDocument | +| `RENDER_FILTER_ERRORS` | `'errors'` | Array key for the libxml error list | -## Resource Switching +## Resource switching -| Action | Effect | -|---|---| -| `getFilterDOM()` | Parses HTML → DOM (if needed); makes DOM the current resource | -| `getFilterText()` | Serialises DOM → HTML (if DOM is current); makes string the current resource | -| `setFilterText($html)` | Sets string; discards DOM cache and parse errors; string becomes current | -| `setFilterDOM($dom)` | Sets DOM; DOM becomes current | +The parameter tracks which representation is *current*: +- `getFilterDOM()` (or `$param['dom']`) → parses HTML into DOM, makes DOM current. +- `getFilterText()` (or `$param['html']`) while DOM is current → serialises DOM back to HTML, makes string current. +- `setFilterText()` / `setFilterDOM()` makes the set representation current and discards the other. +- `postRaiseEvent` automatically serialises DOM → HTML after all handlers run, so `processRenderFilter` always receives a valid string. ## Key Methods -- `getFilterText(): string` — Current HTML string (syncs from DOM first if needed). -- `setFilterText(string $html): void` — Replaces the HTML string; discards any cached DOM. -- `getFilterDOM(): DOMDocument|false` — Lazily parsed DOM; `false` on fatal libxml parse failure. Makes DOM current. -- `setFilterDOM(DOMDocument $dom): void` — Replaces the DOM; makes DOM current. -- `getFilterErrors(): ?array` — `LibXMLError[]` from the last parse, or `null` when no errors occurred. -- `getHasFilterError(): bool` — `true` when the last parse captured at least one libxml error. -- `walkElements(callable $callback, ?DOMNode $node = null, bool $recursive = true): void` — Depth-first traversal of every `DOMElement` in the document (or a subtree). Callback signature: `(DOMElement $el, TRenderFilterParameter $p, int $depth): void`. The visit list is snapshotted before the first callback fires, so DOM mutations during the walk do not affect which elements are visited. -- `postRaiseEvent(...)` — `IEventCycleParameter` hook. Serialises DOM → HTML after all handlers run so `TControl::processRenderFilter` always receives a valid string. +### HTML accessor + +| Method | Description | +|---|---| +| `getFilterText(): string` | Current HTML (serialises from DOM first if DOM is current) | +| `setFilterText(string $html): void` | Replace HTML; discard DOM and errors | + +### DOM accessor + +| Method | Description | +|---|---| +| `getFilterDOM(): DOMDocument\|false` | Parsed DOM (lazy parse on first call); `false` on fatal libxml failure | +| `setFilterDOM(DOMDocument $dom): void` | Replace DOM; clear errors | + +### Error accessors + +| Method | Description | +|---|---| +| `getFilterErrors(): ?array` | `LibXMLError[]` from the most recent parse, or `null` when no errors | +| `getHasFilterError(): bool` | `true` when at least one libxml error was captured | + +### DOM walker + +```php +$param->walkElements(function (\DOMElement $el, $param, int $depth) { + if ($el->tagName === 'img' && !$el->hasAttribute('alt')) { + $el->setAttribute('alt', ''); + } +}); +``` + +`walkElements(callable, ?DOMNode $node = null, bool $recursive = true)` — depth-first traversal of every `DOMElement`. The visit list is snapshotted before the first callback, so DOM mutations during the walk do not affect which elements are visited. + +## Array-access -## Array-Access Behaviour +All three reserved keys are proxied through the getters/setters: ```php -$param[TRenderFilterParameter::RENDER_FILTER_TEXT] // proxies getFilterText() -$param[TRenderFilterParameter::RENDER_FILTER_DOM] // proxies getFilterDOM() -$param[TRenderFilterParameter::RENDER_FILTER_ERRORS] // proxies getFilterErrors() - -$param['html'] = '

new

'; // setFilterText() -$param['dom'] = $domDocument; // setFilterDOM() -unset($param['html']); // clears string to '' -unset($param['dom']); // commits DOM→HTML then discards DOM -unset($param['errors']); // clears stored parse errors +$param[TRenderFilterParameter::RENDER_FILTER_TEXT] // → getFilterText() +$param[TRenderFilterParameter::RENDER_FILTER_TEXT] = $html; // → setFilterText() +$param[TRenderFilterParameter::RENDER_FILTER_DOM] // → getFilterDOM() +$param[TRenderFilterParameter::RENDER_FILTER_DOM] = $dom; // → setFilterDOM() (must be DOMDocument) +$param[TRenderFilterParameter::RENDER_FILTER_ERRORS] // → getFilterErrors() (null or array) +$param[TRenderFilterParameter::RENDER_FILTER_ERRORS] = $v; // no-op — errors are read-only +unset($param[TRenderFilterParameter::RENDER_FILTER_ERRORS]); // clears errors → null ``` -## Usage Examples +Extra keys (not one of the three reserved ones) pass through to the parent `TEventParameter` array and can be used for handler-to-handler state passing. + +## Error semantics + +`RENDER_FILTER_ERRORS` stores `null` when no errors have been captured (fresh instance, clean parse, or after `setFilterText`/`setFilterDOM`). A non-null value means the last parse produced at least one libxml error. Use `getHasFilterError()` as the canonical check. + +## Subclassing + +Override `htmlToDom(string $html): DOMDocument|false` to substitute a custom parser. Call `$this->storeErrors($errors)` inside the override to keep the errors slot consistent (it bypasses the public no-op on `offsetSet('errors', ...)`). + +## Typical handler ```php -// String-based handler $control->onRenderFilter[] = function ($sender, TRenderFilterParameter $param) { + // String API $param->setFilterText(strtoupper($param->getFilterText())); -}; -// DOM-based handler — add missing alt attributes to all elements -$control->onRenderFilter[] = function ($sender, TRenderFilterParameter $param) { + // DOM API $dom = $param->getFilterDOM(); // DOMDocument|false - if ($dom === false) { - return; // libxml could not parse the fragment + if ($dom !== false) { + $param->walkElements(function (\DOMElement $el, $p) { + if ($el->tagName === 'img' && !$el->hasAttribute('alt')) { + $el->setAttribute('alt', ''); + } + }); } - $param->walkElements(function (\DOMElement $el, TRenderFilterParameter $p) { - if ($el->tagName === 'img' && !$el->hasAttribute('alt')) { - $el->setAttribute('alt', ''); - } - }); - // DOM → HTML serialisation is automatic via postRaiseEvent + + // Extra state for a downstream handler + $param['processed-by'] = 'my-filter'; }; ``` - -## Patterns & Gotchas - -- **DOM parsed with `LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD`** — no `//` wrappers are added. The processing instruction `` is injected for correct encoding and then removed after parsing. -- **`false` return from `getFilterDOM()`** — means libxml reported a fatal parse failure. The HTML string remains current and unmodified. Check `getFilterErrors()` for details. -- **Parse errors are always retained** — even when parsing succeeded, libxml warnings/notices are captured. Only `null` means "no errors at all". -- **`postRaiseEvent` serialises automatically** — handlers that work exclusively through the DOM API do not need to call `getFilterText()` themselves. - -## See Also - -- [IFilterRenderable](./IFilterRenderable.md) -- [TFilterRenderableTrait](./Traits/TFilterRenderableTrait.md) -- [TControl](./TControl.md) - -**@since 4.3.3** diff --git a/agents/framework/Web/UI/WebControls/TButton.md b/agents/framework/Web/UI/WebControls/TButton.md index d478576c3..4a0e2a9c0 100644 --- a/agents/framework/Web/UI/WebControls/TButton.md +++ b/agents/framework/Web/UI/WebControls/TButton.md @@ -19,7 +19,7 @@ TButton creates a clickable button control on the page. It is primarily used for - **Command Parameters**: Supports command name and parameter for distinguishing multiple buttons ## Core Properties -- `ButtonTag` (TButtonTag): Tag name of button (Input, Button, or Reset) +- `ButtonTag` (TButtonTag): Tag name of button (Input or Button) - `ButtonType` (TButtonType): Type of button (Submit, Button, or Reset) - `CommandName` (string): Command name for command events - `CommandParameter` (string): Command parameter for command events @@ -27,7 +27,7 @@ TButton creates a clickable button control on the page. It is primarily used for - `ValidationGroup` (string): Validation group for restricting validation - `Text` (string): Button caption text - `EnableClientScript` (bool): Whether JavaScript is rendered for button -- `UseSubmitBehavior` (bool): Whether to use submit behavior for postback +- `IsDefaultButton` (bool): Set by a [TPanel](./TPanel.md) to make this the panel's default button ## Core Events - `OnClick`: Raised when button is clicked @@ -39,28 +39,27 @@ TButton creates a clickable button control on the page. It is primarily used for - **Reset**: Resets form fields when clicked (clears input values) ## Button Tags -- **Input**: Renders as `` (default) -- **Button**: Renders as `