fast framework Help

Reactive Websites (Callers and Listeners)

Introduction

By default, a fast framework based website renders the complete html-output of a web-page server side and sends the full page back to the browser.

Often we want to control user interaction more granular, e.g. we want UI elements to trigger some functionality other than navigating the website, and we just want to update fragments of the page.

fast framework provides tools to set up such "internal APIs". They are flexible enough to cover common use cases with very little code, but is extendable to handle more complex cases.

Its use is best explained by an example:

Example: counter

To demonstrate the concept we will implement a simple counter and its API in 3 different ways. Based on the specific needs of your project you can define your API you can choose which way is the appropriate for the specific implementation.

We will cover 3 approaches for our counter API:

  • front-end only API: html <-> js <-> html

  • back-end only API: html <-> php <-> html

  • full stack API: html <-> js <-> php <-> js <-> html

It is good practice to create separated APIs per project, based on their specific use case and name them according to that scope.
In our "counter" example the components that define the api will be called:
html: data-counter-api
js: counterCaller.js
php: counterListener.php\

Option 1. front-end only API

In a front-end only API only template and front-end interact. No need to define a backend.

To set up the front-end only API we need two files:

File presenters/counter.html

<section> <div class="count">0</div> <button data-counter-api="remove">-1</button> <button data-counter-api="add">+1</button> <button data-counter-api="reset">reset</button> </section>

File js/counterCaller.js

ffCaller.add({ hook:'data-counter-api', actions: { remove: function(){}, add: function(){}, reset: function(){}, } });

The ffCaller.add() function instantiates the caller API and binds clicks on the element with data-counter-api attribute to its actions-functions of the same name.

The logic could then be implemented like this:

File js/counterCaller.js

ffCaller.add({ hook:'data-counter-api', actions: { remove: function(){ const count = document.querySelector('.frontend-count'); count.innerText = Math.max(parseInt(count.innerText) -1, 0); }, add: function(){ const count = document.querySelector('.frontend-count'); count.innerText = parseInt(count.innerText) + 1; }, reset: function(){ document.querySelector('.frontend-count').innerText = 0; } } });

Since ff does not provide front-end state management out-of-the-box, the counter will not persist a page reload.

Option 2. back-end only API

Connecting front-end directly with back-end allows us to keep the application logic and state management in the back-end.

To set up a back-end only API, we remove the logic from our front-end caller and add it to the listener in the back-end:

First, we add a method hook to the template presenter/counter.html.

<section> <div class="count" data-ffMethod="showCount">0</div> <button data-counter-api="remove">-1</button> <button data-counter-api="add">+1</button> <button data-counter-api="reset">reset</button> </section>

We then create the presenter class file presenter/counter.php to access the count state and inject it in the template.
For this example we're using a very simple session-based storage.

class counter extends ffPresenter { public function showCount(){ return (string) ($_SESSION['counter'] ?? 0); } }

Next, we remove all the logic from the caller js/counterCaller.js.
We keep only a skeleton, that serves to instantiate the API.

ffCaller.add({ hook:'data-counter-api' });

The counter-logic and state management happens in the file classes/counterListener.php.
A Listener needs to extend the ffListener class.

As the ffListener class opens the door to the back-end, it brings some default security measures.
For our counter example we deactivate those by changing some default properties. This will make the listener respond to any request.

class counterListener extends ffListener { // deactivate security measures protected $ticketRequired = false; protected $actionValidationRequired = false; protected $filterRequests = false; // logic public function add(){} public function remove(){} public function reset(){} }

The logic could then be implemented like this:

File classes/counterListener.php

class counterListener extends ffListener { // deactivate security measures protected $ticketRequired = false; protected $actionValidationRequired = false; protected $filterRequests = false; // logic public function add(){ $_SESSION['counter'] = ($_SESSION['counter'] ?? 0) + 1; } public function remove(){ $_SESSION['counter'] = max((($_SESSION['counter'] ?? 0) - 1), 0); } public function reset(){ $_SESSION['counter'] = 0; } }

In a back-end only API, each API call triggers a reload of the re-rendered presenter.

Option 3. full stack API

If you need more control (i.e. combine front-end and back-end logic) you can extend the API's functionality.

Starting from Option 2 we can intercept the back-end-call and work with the data by adding the action property:

File js/counterCaller.js

ffCaller.add({ hook:'data-counter-api', actions: { reset: function (options, el) { el.style.backgroundColor = "blue"; this.send(options).then((xhr) => { window.setTimeout(() => { el.style.backgroundColor = "unset"; document.querySelector('.count').innerText = xhr.responseText; }, 500); }); } } });

built-in functions:

this.send(options) // sends an API request and returns a promise that contains an xhr object // options is a simple key-value object, that contains the data this.sendWithReload(options, el) // sends an API request and reloads the presenter, that is closest to the el DOMNode

Form submission

api attributes can also be attached to form elements. In this case the api will use formData to assemble the api's option property

11 März 2024