Scene Components

README
Login

File README from the latest check-in


The sc module/behaviour implements workflow patterns to program
complex web UIs with the Nitrogen framework. These abstractions sit
on top of the base Nitrogen's primitives to simplify the development
and to avoid UI related bugs in production.

From an aesthetical point of view, a progressive web application's
structure is based on some common ui pattern, like master/detail,
content/offcanvas, tabbed contents, simply flat or hybrid, where
there will always be some areas for certain UI components. There will
probably be a nav on the top, maybe a footer panel in the bottom. It
all depends on the application and the user needs...  This library's
objective is then to consistently patch the different parts of the
UI that must be replaced whenever the UI context changes.

For example, lets assume our app's job is to display a CRUD for a
Computer Repair Store. In the main screen there will be the list of
computers the store is currently repairing. In the nav there will
always be a link to display the history of all computers that have
ever entered to the store... Anyways, after clicking any computer icon
the interface gets nicely updated with the details of that computer:
the customer/owner, reported problem, model, brand, etcetera.
When you click the customer name the application will display the
data about that customer: phone, email, a picture maybe, customer
history, etcetera.

The general structure will look like this:
____________
|____________|  <- nav
|            |
|            |  <- content
|            |
|____________|

The scenes should look like this:

COMPUTERS TO BE REPAIRED
____________
|____show all|   <-- Click to view all the computers that have
|            |             ever entered to the store
| computer 1 |
| computer 2 |   <-- Click any computer to view its details
| computer 3 |
| ...        |
|____________|

COMPUTER DETAILS
____________
|_____go back|  <-- This link gets us back to the list of computers
|            |
| computer 1 |
| ---------- |
|            |
| owner:     |
|   bob      |  <- owners' name is clickable
| problem:   |
|   wont boot|
| ...        |
|____________|

CUSTOMER DETAILS
____________
|_____go back|  <-- go back to bob's computer details
|            |
| bob        |
| ---        |
|            |
| phone:911  |
| email:a@b.c|
| ...        |
|____________|


From now on we will refer to "scenes" to the encapsulation the
different components that are to be manipulated together in the course
of the user interacting with the app. We will refer to this components
as "snippets" (they be can any renderizable term).

| Scenes are defined as modules implementing the sc behaviour or with
| maps whose keys resemble the sc behaviour callback functions. We
| call this callbacks, "properties", and they determine how each scene
| should behave in the grand.  Read the callback's comments to get
| a grasp of how scenes can be tuned to your needs.  Properties not
| specified as behaviour callbacks are intended to be the snippets,
| where the actual contents are defined.  Properties in map() scenes
| scenes can be optionally *thunked* as a function and get evaluated
| the moment the scene kicks in.
|
| My personal preference is to implement the first scene as a module
| (acting like the application controller) and subsequent scenes with
| maps(). It is up to you to decide which way suits you best...

Our first scene, named 'computers', is composed of two snippets,
'nav' and 'content':

 Computers = #{
    name => computers,
    nav => #link{text="show all" ...},
    content => [LinkComputer1, LinkComputer2, LinkComputer3, ...]
 }

To use the first scene, 'computers', we apply it to sc:create/2. We
must indicate the sum of all snippets our application comprehends,
in this case [nav, content]. We do so because this library must keep
track of every node the application is supposed to manipulate within
the DOM as the UI progresses.

 [Nav, Content] = sc:create(Computers, [nav, content])

What's happening under the hood, aside of getting the renderized
snippets, is that sc:create(...) initializes internal machinery by
injecting some extra data and code for the server and the viewport.

We are going to emit the application's visual structure with the
snippets embedded where due. For this example we assume that a couple
of panels are enough for our user interface; in a real application
this will be more complex, right?

 main() ->
   [
      #panel{style="position:fixed; top:0; background:blue", body=Nav},
      #panel{body=Content}
   ].

That's it! The user is now staring at the screen in awe! The current
scene is 'computers':
  ____________
 |____show all|
 |            |
 | computer 1 |
 | computer 2 |   <-- The user clicks any computer
 | computer 3 |
 | ...        |
 |____________|

When the user clicks any computer we must update the screen to display
that computer details. To do so we handle the postback and perform a
sc:replace(...).  This function will take care of removing/detaching
the current scene's snippets and patch those areas with the new scene's.

 event({click_computer, ComputerName}) ->
   LinkOwner = #link{text="Bob", postback={click_customer, "Bob"}},
   ComputerDetails = #{
     name => computer_details,
     nav => #link{text="go back" ...},
     content => ["Computer ", ComputerName, LinkOwner, Problem]
   },
   sc:replace(ComputerDetails)
  ____________
 |_____go back|
 |            |
 | computer 1 |
 | ---------- |
 |            |
 | owner:     |
 |   bob      |  <- The user clicks owners' name
 | problem:   |
 |   wont boot|
 | ...        |
 |____________|

 event({click_customer, Customer}) ->
   sc:replace(#{
     name => customer_details,
     content => [Customer, Phone, Email]
   })
  ____________
 |____________|
 |            |
 | bob        |
 | ---        |
 |            |
 | phone:911  |
 | email:a@b.c|
 | ...        |
 |____________|

Easy, uh?

Notice how the 'customer_details' scene does not define the 'nav' snippet
even though in our requirement that scene specified a "go back" link,
just like the 'computer_details' scene.

| Scenes dont need to define all snippets, as different UI contexts
| dont necessarily require them all. Undefined snippets will be displayed
| with no children elements, empty.

To fix the missing "go back" link we should declare the 'nav' snippet...

 nav => #link{text="go back" ...},

...or we could get more sophisticated...

| Master mode:
|
| Any rendered scene can be set to be *master*. This is declared in
| the options parameter of the sc:create/3 and sc:replace/2 functions.
| We can set and/or override the master whenever a replace takes place.
|
| This mode implies that any non-master scene rendered after the
| master scene will inherit the master's snippets it does not override.

To fix the missing 'nav' we will make the 'computer_details' scene the
master. This way, when 'customer_details' kicks in, it will inherit the
'nav' snippet.  We also need to enhance the former link to dynamically
determine which is the previous scene whenever we click it:

 event({click_computer, ComputerName}) ->
   GoBackHandler = fun
     ("computer_details") -> sc:replace(computers);
     ("customer_details") -> sc:replace(computer_details)
   end,
   GoBack = #link{
     text="go back",
     click=sc_utils:wire_request_call(GoBackHandler, "sc.escena")
   },
   LinkOwner = #link{text="Bob", postback={click_customer, "Bob"}},
   ComputerDetails = #{
     name => computer_details,
     nav => GoBack,
     content => ["Computer ", ComputerName, LinkOwner, Problem]
   },
   sc:replace(ComputerDetails, [master])

When the "go back" link is clicked, the postback will be submitted
with the name of the current scene in the viewport, this way we can
decide which scene we must get back to (sc_utils:wire_request_call
abstracts an #api call and "sc.escena" is the js variable where
the current scene's name is stored).

TO SUM UP. WE INITIALIZE THE SCENE MACHINERY PROVIDING THE FIRST SCENE
AND WE EMIT THE BASE UI STRUCTURE WITH THE RESULTING SNIPPETS EMBEDDED.
AFTER THAT, THE GAME IS MAINLY ABOUT PERFORMING SC:REPLACE(...) TO
CHANGE SCENES ACCORDING TO THE USERS' INPUT.

Other topics
============

- spa controller behaviour
- the route handler
- Permanent scenes
- Scene interrupts
- Transition effects
- Css styles
- Access routines and sc aware events
- Prefetching scenes
- Debug mode
- onsync reloads
- Getting used to query the viewport and making requests (wire_request)
 and sc-related js variables and functions we could need in more
- with_location
- with_title
- dynamic routes
- evaluate api
- sc_info dashboard.
- custom_styles
- data storage
- topics
- flow
- form input validation

TODO
====

- FIXME: is action_comet:get_accumulator_pid(SeriesID) a precise indicator to
 determine a page is alive? When the connection is lost and the comets
 get killed there will be a new accumulator spawned which provoke losing
 track of the scene state (sc_utils:state) and fail because the
 tables are not initialized for the new accumulator pid.

- Use queueMicrotask API to produce more atomic and serialized code
 execution in the client

- audit and show comet metrics per scene

- take a screenshot of scenes on annotate_sent_scene

- metricas de generacion por snippet y general. Menor, mayor, promedio
   y media de los tiempos de generacion.

- sc_info: Escenas que renderizan en tiempos muy variables presentan
  una advertencia por ser esto una experiencia de usuario poco confiable
  (not reliable)

- add a link to remove routes generated when in force_routes mode

- las funciones que usan el modulo sbw (simple_bridge) no
  estan disponibles para los comets. Y actualmente hay
  codigo que puede llagar a generar errores: por ejemplo,
  server_warn usa wf:uri() y eso puede suceder dentro
  de un comet al hacer un replace.

  Ideas para corregir esto de una vez por todas:
   a) copiar esos datos al generar la pagina y almacenarlos
     con sc:state.
   b) encontrar algun hack en la implementacion de simple_bridge
   c) a + b

- route code prone to race conditions related to ets access

- history back/forward broken (related to the empty_scene)