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 Action sand Events work.

Actions

Action is represented through 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();

We used this hide method to previously hide the button. There are other ways to generate 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 mentioned 4 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 your 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 js():

$view->js(
    true,
    new jsExpression('alert([])', ['Hello world']
);

You can also combine both forms together:

$view->js(true)->hide();

Finally, you can specify name of JavaScript event:

$view->js('click')->hide();

Agile UI also provides support for an on event binding. This allows you to apply events on multiple elements:

$buttons = new Buttons();

$buttons->add(new Button('One'));
$buttons->add(new Button('Two'));
$buttons->add(new Button('Three'));

$buttons->on('click', '.button')->hide();

All the above examples will map themselves into a simple and readable JavaScript code. If you wish to see what JavaScript code is produced by certain view or it’s sub-elements, see debugging

Extending

Agile UI builds upon the concepts of actions and events in the following ways:

  • Action can be any arbitraty JavaScript with parameters: - parameters are always escaped with json_encode, - action can have another nested actions, - you can build your own integration patterns.
  • jsChain provide 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::includeJS() and App::includeCSS() 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 implements jsExpressionable and will present itself as a valid selector. Example:

$frame = new View();

$button->js(true)->appendTo($frame);

// Resulting code:
// $('#button-id').appendTo('#frame-id');
// which will be executed on page load

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 my examples I’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 a vary bad practice if you perform jsRender and output the JavaScript code manually. Agile UI takes care of JavaScript binding and also decides which actions should be appearing for you as long as you create actions for your chain.

jsChain::_json_encode()

jsChain will map all the other methods into JS counterparts while encoding all the arguments through _json_encode(). Although similar to a standard json_encode function, this method 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 wrapped through json_encode before being included as an argument to text().

View to JS integration

We are not building JavaScript code just for the excercise. 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[, $other_action]])

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 $other_chain is specified together with event, it will also be bound to said event. $other_chain 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 3 buttons and clicking any button will hide itself. Only a single action is created:

$buttons = Buttons();

$buttons->add(new Button('One'));
$buttons->add(new Button('Two'));
$buttons->add(new Button('Three'));

$buttons->on('click', '.button')->hide();


// Generates:
// $('#top-element-id').on('click', '.button', function($event){
//   event.stopPropagation();
//   event.preventDefault();
//   $(this).hide();
// });

Method on() is handy when you have multiple elements inside your view that you want to trigger action individually. The best example would be a Lister with interractive elements:

$buttons = Buttons();

$b1 = $buttons->add(new Button('One'));
$b2 = $buttons->add(new Button('Two'));
$b3 = $buttons->add(new Button('Three'));

$buttons->on('click', '.button', $b3->js()->hide());

// Generates:
// $('#top-element-id').on('click', '.button', function($event){
//   event.stopPropagation();
//   event.preventDefault();
//   $('#b3-element-id').hide();
// });

You can use both actions together. The next example will allow only one button to be active:

$buttons = Buttons();

$b1 = $buttons->add(new Button('One'));
$b2 = $buttons->add(new Button('Two'));
$b3 = $buttons->add(new Button('Three'));

$buttons->on('click', '.button', $b3->js()->hide());

// Generates:
// $('#top-element-id').on('click', '.button', function($event){
//   event.stopPropagation();
//   event.preventDefault();
//   $('#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::_json_encode(), it prevents you from accidentally injecting a 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 a plain JS 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 = $left_box1->js()->height();
$h2 = $left_box2->js()->height();

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

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

It is important that you remember that 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 three lines are identical:

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

Important

We have specifically selected a very simple tag format as a reminder to you 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.

Open a new file test.js and type:

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

Then load this JavaScript dependency on your page. Refer to App::includeJS() and App::includeCSS(). Finally use UI code as a “glue” between your routine and the actual View objects. In my example, I’ll be trying to match the size of $right_container with the size of $left_container:

$heights = [];

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

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

Reloading

class jsReload

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

$js_reload_table = new jsReload($table);

This action can be used similar to any other jsExpression. For intance completing the form can reload some other view:

$m_book = new Book($db);

$f = $app->layout->add('Form');
$t = $app->layout->add('Table');

$f->setModel($m_book);

$f->onSubmit(function($f) use($t) {
    $f->model->save();
    return new \atk4\ui\jsReload($t);
});

$t->setModel($m_book);

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