.. 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:

.. code-block:: 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::

    $action = $view->js()->hide();

There are other ways to generate an action, such as using :php:meth:`JsExpression`::

    $action = new JsExpression('alert([])', ['Hello world']);

Finally, actions can be used inside other actions::

    $action = new JsExpression('alert([])', [
        $view->js()->text(),
    ]);

    // will produce alert($('#button-id').text());

or::

    $action = $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
`$action`.

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:: 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:

.. code-block:: js

    $('#button-id').appendTo('#frame-id');

JavaScript Chain Building
-------------------------

.. php:class:: 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::

    $chain = new Jquery('#the-box-id');

    $chain->dropdown();

The calls to the chain are stored in the object and can be converted into JavaScript by calling :php:meth:`JsChain::jsRender()`

.. php:method:: jsRender()

    Converts actions recorded in JsChain into string of JavaScript code.

Executing::

    echo $chain->jsRender();

will output:

.. code-block:: 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:`JsExpressionable` objects and will substitute them with the result of
    :php:meth:`JsExpressionable::jsRender`. The result will not be escaped and any object implementing
    :php:interface:`JsExpressionable` interface is responsible for safe JavaScript generation.

The following code is safe::

    $b = new Button();
    $b->js(true)->text($_GET['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:class:: View
.. php:method:: 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:: 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:: 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::

    $action = new JsExpression('alert([])', [
        $view->js()->text(),
    ]);

Because :php:class:`JsChain` will typically wrap all the arguments through
:php:meth:`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!

    $expr = new JsExpression('document');
    echo (new Jquery($expr))->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::

    $h1 = $leftBox1->js()->height();
    $h2 = $leftBox2->js()->height();

    $sum = new JsExpression('[] + []', [$h1, $h2]);

    $rightBoxContainer->js(true)->height( $sum );

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::

    $sum = new JsExpression('[] + []', [$h1, $h2]);
    $sum = new JsExpression('[0] + [1]', [$h1, $h2]);
    $sum = new JsExpression('[a] + [b]', ['a' => $h1, 'b' => $h2]);

.. 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:

.. code-block:: 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`::

    $heights = [];
    foreach ($leftContainer->elements as $leftBox) {
        $heights[] = $leftBox->js()->height();
    }

    $rightContainer->js(true)->height(new JsExpression('mySum([])', [$heights]));

This will map into the following JavaScript code:

.. code-block:: js

    $('#right_container_id').height(mySum([
        $('#left_box1').height(), $('#left_box2').height(), $('#left_box3').height(), // etc
    ]));

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 `<div>` element generated by :php:class:`JsModal`
is always destroyed when the modal is closed instead of only hiding it.

Modal
-----

.. php:class:: Modal

.. php:method:: set(callback)
.. php:method:: jsShow()
.. php:method:: jsHide()
.. php:method:: addContentCss()
.. php:method:: addScrolling()
.. php:method:: setOption()

This class allows you to open modal dialogs and close them easily. It's based around Fomantic-UI
`.modal(), <https://fomantic-ui.com/modules/modal.html>`_ but integrates PHP callback for dynamically
producing content of your dialog::

    $modal = \Atk4\Ui\Modal::addTo($app, ['Modal Title']);
    $modal->set(function (View $p) use ($modal) {
        \Atk4\Ui\LoremIpsum::addTo($p);
        \Atk4\Ui\Button::addTo($p, ['Hide'])
            ->on('click', $modal->jsHide());
    });

    \Atk4\Ui\Button::addTo($app, ['Show'])
        ->on('click', $modal->jsShow());

Modal will render as a HTML `<div>` block but will be hidden. Alternatively you can use Modal without loadable content::

    $modal = \Atk4\Ui\Modal::addTo($app, ['Modal Title']);
    \Atk4\Ui\LoremIpsum::addTo($modal);
    \Atk4\Ui\Button::addTo($modal, ['Hide'])
        ->on('click', $modal->jsHide());

    \Atk4\Ui\Button::addTo($app, ['Show'])
        ->on('click', $modal->jsShow());

The second way is more convenient for creating static content, such as Terms of Service.

You can customize the CSS classes of both header and content section of the modal using the properties `headerCss`
or `contentCss` or use the method `addContentCss()`. See the Fomantic-UI modal documentation for further information.

JsModal
-------

.. php:class:: JsModal

This alternative implementation to :php:class:`Modal` is convenient for situations
when the need to open a dialog box is not known in advance. This class is not
a component, but rather an Action so you **must not** add it to the Render Tree.
To accomplish that, use a :ref:`virtualpage`::

    $vp = \Atk4\Ui\VirtualPage::addTo($app);
    \Atk4\Ui\LoremIpsum::addTo($vp, ['size' => 2]);

    \Atk4\Ui\Button::addTo($app, ['Dynamic Modal'])
        ->on('click', new \Atk4\Ui\Js\JsModal('My Popup Title', $vp->getUrl('cut')));

Note that this element is always destroyed as opposed to :php:class:`Modal`,
where it is only hidden.

.. important::

    See `Modals and reloading`_ concerning the intricacies between jsMmodals and callbacks.


Reloading
=========

.. php:class:: JsReload

JsReload is a JavaScript action that performs reload of a certain object::

    $jsReload = new JsReload($table);

This action can be used similarly to any other JsExpression. For instance submitting a form can reload some
other view::

    $bookModel = new Book($db);

    $form = \Atk4\Ui\Form::addTo($app);
    $table = \Atk4\Ui\Table::addTo($app);

    $form->setModel($bookModel);

    $form->onSubmit(function (Form $form) use ($table) {
        $form->model->save();

        return new \Atk4\Ui\Js\JsReload($table);
    });

    $t->setModel($bookModel);

In this example, filling out and submitting the form will result in table contents being refreshed using AJAX.

Modals and reloading
--------------------

Care needs to be taken when attempting to combine the above with a `JsModal`_ which requires a :ref:`virtualpage` to
store its contents. In that case, the order in which declarations are made matters because of the way the
Render Tree is processed.

For example, in order to open a modal dialog containing a form and reload a table located on the main page
with the updated data on form submission (thus without having to reload the entire page), the following elements are
needed:

* a virtual page containing a JsModal's contents (in this case a form),
* a table showing data on the main page,
* a button that opens the modal in order to add data, and
* the form's callback on submit.

The following will **not** work::

    $app = new MyApp();
    $model = new MyModel();

    // JsModal requires its contents to be put into a Virtual Page
    $vp = \Atk4\Ui\VirtualPage::addTo($app);
    $form = \Atk4\Ui\Form::addTo($vp);
    $form->setModel($model);

    $table = \Atk4\Ui\Table::addTo($app);
    $table->setModel($model));

    $button = \Atk4\Ui\Button::addTo($app, ['Add Item', 'icon' => 'plus']);
    $button->on('click', new \Atk4\Ui\Js\JsModal('JSModal Title', $vp));

    $form->onSubmit(function (Form $form) use ($table) {
        $form->model->save();

        return new \Atk4\Ui\Js\JsBlock([
            $table->jsReload(),
            $form->jsSuccess('ok'),
        ]);
    });

Table needs to be first! The following works::

    $app = new MyApp();
    $model = new MyModel();

    // This needs to be first
    $table = \Atk4\Ui\Table::addTo($app);
    $table->setModel($model));

    $vp = \Atk4\Ui\VirtualPage::addTo($app);
    $form = \Atk4\Ui\Form::addTo($vp);
    $form->setModel($model);

    $button = \Atk4\Ui\Button::addTo($app, ['Add Item', 'icon' => 'plus']);
    $button->on('click', new \Atk4\Ui\Js\JsModal('JSModal Title', $vp));

    $form->onSubmit(function (Form $form) use ($table) {
        $form->model->save();

        return new \Atk4\Ui\Js\JsBlock([
            $table->jsReload(),
            $form->jsSuccess('ok'),
        ]);
    });

The first will not work because of how the render tree is called and because VirtualPage is special.
While rendering, if a reload is caught, the rendering process stops and only renders what was asked to be reloaded.
Since VirtualPage is special, when asked to be rendered and it gets triggered, rendering stops and only the
VirtualPage content is rendered. To force yourself to put things in order you can write the above like this::

    $table = \Atk4\Ui\Table::addTo($app);
    $table->setModel($model);

    $vp = \Atk4\Ui\VirtualPage::addTo($app);
    $vp->set(function (\Atk4\Ui\VirtualPage $p) use ($table, $model) {
        $form = \Atk4\Ui\Form::addTo($p);
        $form->setModel($model);
        $form->onSubmit(function (Form $form) use ($table) {
            $form->model->save();

            return new \Atk4\Ui\Js\JsBlock([
                $table->jsReload(),
                $form->jsSuccess('ok'),
            ]);
        });
    });

    $button = \Atk4\Ui\Button::addTo($app, ['Add Item', 'icon' => 'plus']);
    $button->on('click', new \Atk4\Ui\Js\JsModal('JSModal Title', $vp));

Note that in no case you will be able to render the button *above* the table (because the button needs a
reference to `$vp` which references `$table` for reload), so `$button` must be last.


Background Tasks
================

Agile UI has addressed one of the big shortcomings of the PHP language: the ability to execute running / background
processes. It is best illustrated with an example:

Processing a large image, resize, find face, watermark, create thumbnails and store externally can take an
average of 5-10 seconds, so you'd like to user updated about the process. There are various ways to do so.

The most basic approach is::

    $button = \Atk4\Ui\Button::addTo($app, ['Process Image']);
    $button->on('click', function () use ($button, $image) {
        sleep(1); // $image->resize();
        sleep(1); // $image->findFace();
        sleep(1); // $image->watermark();
        sleep(1); // $image->createThumbnails();

        return $button->js()->text('Success')->addClass('disabled');
    });

However, it would be nice if the user was aware of the progress of your process, which is when `Server Sent Event (JsSse)`_
comes into play.

.. _sse:

Server Sent Event (JsSse)
-------------------------

.. php:class:: JsSse

.. php:method:: send(action)

This class implements ability for your PHP code to send messages to the browser during process execution::

    $button = \Atk4\Ui\Button::addTo($app, ['Process Image']);

    $sse = \Atk4\Ui\JsSse::addTo($button);

    $button->on('click', $sse->set(function () use ($sse, $button, $image) {
        $sse->send($button->js()->text('Processing'));
        sleep(1); // $image->resize();

        $sse->send($button->js()->text('Looking for face'));
        sleep(1); // $image->findFace();

        $sse->send($button->js()->text('Adding watermark'));
        sleep(1); // $image->watermark();

        $sse->send($button->js()->text('Creating thumbnail'));
        sleep(1); // $image->createThumbnails();

        return $button->js()->text('Success')->addClass('disabled');
    }));

The JsSse component plays a crucial role in some high-level components such as :php:class:`Console` and :php:class:`ProgressBar`.
