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:

$('#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.

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 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 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 App:requireJs() and App::requireCss() for details.

Building actions with JsExpressionable

interface JsExpressionable

Allow objects of the class implementing this interface to participate in building JavaScript expressions.

JsExpressionable::jsRender()

Express object as a string containing valid JavaScript statement or expression.

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:

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

JavaScript Chain Building

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 JsChain::jsRender()

JsChain::jsRender()

Converts actions recorded in JsChain into string of JavaScript code.

Executing:

echo $chain->jsRender();

will output:

$('#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.

JsChain::_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 JsExpressionable objects and will substitute them with the result of JsExpressionable::jsRender. The result will not be escaped and any object implementing 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: View::js() and View::on().

class View
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());
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();
// });

View::on() is handy when multiple elements exist inside a view which you want to trigger individually. The best example would be a 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

class JsExpression
JsExpression::__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 JsChain will typically wrap all the arguments through 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:

function mySum(arr) {
    return arr.reduce(function (a, b) {
        return a + b;
    }, 0);
}

Then load this JavaScript dependency on your page (see App::includeJS() and 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:

$('#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 Modal, the HTML <div> element generated by JsModal is always destroyed when the modal is closed instead of only hiding it.

JsModal

class JsModal

This alternative implementation to 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 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 Modal, where it is only hidden.

Important

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

Reloading

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

Server Sent Event (JsSse)

class JsSse
JsSse::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 Console and ProgressBar.