:::{php:namespace} Atk4\Ui ::: (js)= # JavaScript Mapping A modern user interface cannot exist without JavaScript. Agile UI provides you assistance with generating and executing events directly from PHP and the context of your Views. The most basic example of such integration would be a button, that hides itself when clicked: ``` $b = new Button(); $b->js('click')->hide(); ``` ## Introduction Agile UI does not replace JavaScript. It encourages you to keep JavaScript routines as generic as possible, then associate them with your UI through actions and events. A great example would be `jQuery` library. It is designed to be usable with any HTML mark-up and by specifying selector, you can perform certain actions: ```js $('#my-long-id').hide(); ``` Agile UI provides a built-in integration for jQuery. To use jQuery and any other JavaScript library in Agile UI you need to understand how Actions and Events work. (js_action)= ### Actions An action is represented by a PHP object that can map itself into a JavaScript code. For instance the code for hiding a view can be generated by calling: ``` $jsHide = $view->js()->hide(); ``` There are other ways to generate an action, such as using {php:class}`Js\JsExpression`: ``` $jsAlert = new JsExpression('alert([])', ['Hello world']); ``` Finally, actions can be used inside other actions: ``` $jsAlert = new JsExpression('alert([])', [ $view->js()->text(), ]); // will produce alert($('#button-id').text()); ``` or: ``` $jsAction = $view->js()->text(new JsExpression('[] + []', [ 5, 10, ])); ``` All of the above examples will produce a valid "Action" object that can be used further. :::{important} We never encourage writing JavaScript logic in PHP. The purpose of JS layer is for binding events and actions with your generic JavaScript routines. ::: ### Events Agile UI also offers a great way to associate actions with certain client-side events. Those events can be triggered by the user or by other JavaScript code. There are several ways to bind `$jsXxx`. To execute actions instantly on page load, use `true` as first argument to {php:meth}`View::js()`: ``` $view->js(true, new JsExpression('alert([])', ['Hello world'])); ``` You can also combine both forms: ``` $view->js(true)->hide(); ``` Finally, you can specify the name of the JavaScript event: ``` $view->js('click')->hide(); ``` Agile UI also provides support for an `on` event binding. This allows to apply events on multiple elements: ``` $buttons = View::addTo($app, ['ui' => 'basic buttons']); \Atk4\Ui\Button::addTo($buttons, ['One']); \Atk4\Ui\Button::addTo($buttons, ['Two']); \Atk4\Ui\Button::addTo($buttons, ['Three']); $buttons->on('click', '.button')->hide(); ``` All the above examples will map themselves into a simple and readable JavaScript code. ### Extending Agile UI builds upon the concepts of actions and events in the following ways: - Action can be any arbitrary JavaScript with parameters: - parameters are always encoded/escaped, - action can contain nested actions, - you can build your own integration patterns. - JsChain provides Action extension for JavaScript frameworks: - Jquery is implementation of jQuery binding through JsChain, - various 3rd party extensions can integrate other frameworks, - any jQuery plugin will work out-of-the-box. - PHP closure can be used to wrap action-generation code: - Agile UI event will map AJAX call to the event, - closure can respond with additional actions, - various UI elements (such as Form) extend this concept further. ### Including JS/CSS Sometimes you need to include an additional .js or .css file for your code to work. See {php:meth}`App:requireJs()` and {php:meth}`App::requireCss()` for details. ## Building actions with JsExpressionable :::{php:interface} Js\JsExpressionable Allow objects of the class implementing this interface to participate in building JavaScript expressions. ::: :::{php:method} jsRender Express object as a string containing valid JavaScript statement or expression. ::: {php:class}`View` class is supported as JsExpression argument natively and will present itself as a valid selector. Example: ``` $frame = new View(); $button->js(true)->appendTo($frame); ``` The resulting Javascript will be: ```js $('#button-id').appendTo('#frame-id'); ``` ### JavaScript Chain Building :::{php:class} Js\JsChain Base class JsChain can be extended by other classes such as Jquery to provide transparent mappers for any JavaScript framework. ::: Chain is a PHP object that represents one or several actions that are to be executed on the client side. The JsChain objects themselves are generic, so in these examples we'll be using Jquery which is a descendant of JsChain: ``` $jsChain = new Jquery('#the-box-id'); $jsChain->dropdown(); ``` The calls to the chain are stored in the object and can be converted into JavaScript by calling {php:meth}`Js\JsChain::jsRender()` :::{php:method} jsRender() Converts actions recorded in JsChain into string of JavaScript code. ::: Executing: ``` echo $jsChain->jsRender(); ``` will output: ```js $('#the-box-id').dropdown(); ``` :::{important} It's considered very bad practice to use jsRender to output JavaScript manually. Agile UI takes care of JavaScript binding and also decides which actions should be available while creating actions for your chain. ::: :::{php:method} _jsEncode JsChain will map all the other methods into JS counterparts while encoding all the arguments using `_jsEncode()`. Although similar to the standard JSON encode function, this method quotes strings using single quotes and recognizes {php:interface}`Js\JsExpressionable` objects and will substitute them with the result of {php:meth}`Js\JsExpressionable::jsRender`. The result will not be escaped and any object implementing {php:interface}`Js\JsExpressionable` interface is responsible for safe JavaScript generation. ::: The following code is safe: ``` $b = new Button(); $b->js(true)->text($app->getRequestQueryParam('button_text')); ``` Any malicious input through the GET arguments will be encoded as JS string before being included as an argument to `text()`. ### View to JS integration We are not building JavaScript code just for the exercise. Our whole point is ability to link that code between actual views. All views support JavaScript binding through two methods: {php:meth}`View::js()` and {php:meth}`View::on()`. :::{php:method} View::js([$event, [$otherChain]]) Return action chain that targets this view. As event you can specify `true` which will make chain automatically execute on document ready event. You can specify a specific JavaScript event such as `click` or `mousein`. You can also use your custom event that you would trigger manually. If `$event` is false or null, no event binding will be performed. If `$otherChain` is specified together with event, it will also be bound to said event. `$otherChain` can also be a PHP closure. ::: Several usage cases for plain `js()` method. The most basic scenario is to perform action on the view when event happens: ``` $b1 = new Button('One'); $b1->js('click')->hide(); $b2 = new Button('Two'); $b2->js('click', $b1->js()->hide()); ``` :::{php:method} View::on(String $event, [String selector], $callback = null) Returns chain that will be automatically executed if $event occurs. If $callback is specified, it will also be executed on event. ::: The following code will show three buttons and clicking any one will hide it. Only a single action is created: ``` $buttons = View::addTo($app, ['ui' => 'basic buttons']); \Atk4\Ui\Button::addTo($buttons, ['One']); \Atk4\Ui\Button::addTo($buttons, ['Two']); \Atk4\Ui\Button::addTo($buttons, ['Three']); $buttons->on('click', '.button')->hide(); // generates: // $('#top-element-id').on('click', '.button', function (event) { // event.preventDefault(); // event.stopPropagation(); // $(this).hide(); // }); ``` {php:meth}`View::on()` is handy when multiple elements exist inside a view which you want to trigger individually. The best example would be a {php:class}`Lister` with interactive elements. You can use both actions together. The next example will allow only one button to be active: ``` $buttons = View::addTo($app, ['ui' => 'basic buttons']); \Atk4\Ui\Button::addTo($buttons, ['One']); \Atk4\Ui\Button::addTo($buttons, ['Two']); \Atk4\Ui\Button::addTo($buttons, ['Three']); $buttons->on('click', '.button', $b3->js()->hide()); // generates: // $('#top-element-id').on('click', '.button', function (event) { // event.preventDefault(); // event.stopPropagation(); // $('#b3-element-id').hide(); // }); ``` ## JsExpression :::{php:class} Js\JsExpression ::: :::{php:method} __construct(template, args) Returns object that renders into template by substituting args into it. ::: Sometimes you want to execute action by calling a global JavaScript method. For this and other cases you can use JsExpression: ``` $jsAlert = new JsExpression('alert([])', [ $view->js()->text(), ]); ``` Because {php:class}`Js\JsChain` will typically wrap all the arguments through {php:meth}`Js\JsChain::_jsonEncode()`, it prevents you from accidentally injecting JavaScript code: ``` $b = new Button(); $b->js(true)->text('2 + 2'); ``` This will result in a button having a label `2 + 2` instead of having a label `4`. To get around this, you can use JsExpression: ``` $b = new Button(); $b->js(true)->text(new JsExpression('2 + 2')); ``` This time `2 + 2` is no longer escaped and will be used as plain JavaScript code. Another example shows how you can use global variables: ``` echo (new Jquery('document'))->find('h1')->hide()->jsRender(); // produces $('document').find('h1').hide(); // does not hide anything because document is treated as string selector! $js = new JsExpression('document'); echo (new Jquery($js))->find('h1')->hide()->jsRender(); // produces $(document).find('h1').hide(); // works correctly!! ``` ### Template of JsExpression The JsExpression class provides the most simple implementation that can be useful for providing any JavaScript expressions. My next example will set height of right container to the sum of 2 boxes on the left: ``` $jsH1 = $leftBox1->js()->height(); $jsH2 = $leftBox2->js()->height(); $jsSum = new JsExpression('[] + []', [$jsH1, $jsH2]); $rightBoxContainer->js(true)->height($jsSum); ``` It is important to remember that the height of an element is a browser-side property and you must operate with it in your browser by passing expressions into chain. The template language for JsExpression is super-simple: - `[]` will be mapped to next argument in the argument array - `[foo]` will be mapped to named argument in argument array So the following lines are identical: ``` $jsSum = new JsExpression('[] + []', [$jsH1, $jsH2]); $jsSum = new JsExpression('[0] + [1]', [$jsH1, $jsH2]); $jsSum = new JsExpression('[a] + [b]', ['a' => $jsH1, 'b' => $jsH2]); ``` :::{important} We have specifically selected a very simple tag format as a reminder not to write any code as part of JsExpression. You must not use JsExpression() for anything complex. ::: ### Writing JavaScript code If you know JavaScript you are likely to write more extensive methods to provide extended functionality for your user browsers. Agile UI does not attempt to stop you from doing that, but you should follow a proper pattern. Create a file `test.js` containing: ```js function mySum(arr) { return arr.reduce(function (a, b) { return a + b; }, 0); } ``` Then load this JavaScript dependency on your page (see {php:meth}`App::includeJS()` and {php:meth}`App::includeCSS()`). Finally use UI code as a "glue" between your routine and the actual View objects. For example, to match the size of `$rightContainer` with the size of `$leftContainer`: ``` $jsHeights = []; foreach ($leftContainer->elements as $leftBox) { $jsHeights[] = $leftBox->js()->height(); } $rightContainer->js(true)->height(new JsExpression('mySum([])', [$jsHeights])); ``` This will map into the following JavaScript code: ```js $('#right_container_id').height( mySum([ $('#left_box1').height(), $('#left_box2').height(), $('#left_box3').height(), // ... ]) ); ``` You can further simplify JavaScript code yourself, but keep the JavaScript logic inside the `.js` files and leave PHP only for binding. ## Modals There are two modal implementations in ATK: - View - Modal: This works with a pre-existing Div, shows it and can be populated with contents; - JsModal: This creates an entirely new modal Div and then populates it. In contrast to {php:class}`Modal`, the HTML `