Commit fff763cd authored by Sven Franck's avatar Sven Franck

documentation: Added full documentation and writing app tutorial

parent 7ea7dd50
==============================================================================
App renderer Quick Documentation/API
==============================================================================
Documentation Version 0.1 - May 2014
This is a quick guide on how to use the app renderer on
http://git.erp5.org/gitweb/ecommerce-ui.git?js=1
<!>
NOTE:
- The tutorial is done on slapos-ui-offline-tutorial
- Latest branch is ecommerce-ui.git
- All branches have minor modifications and need to be merged into master
- This API describes the key functionalities (some elements will be missing)
<!>
------------------------------------------------------------------------------
TOC
------------------------------------------------------------------------------
0. Intro
------------------------------------------------------------------------------
1. API Portal Type Data
1. Creating a fieldlist file
2. Overrides & custom Fields
3. Field types currently supported
4. Rendering
5. Subordination
6. Formatting
7. "items" Property
8. Translation of field items
9. Validations
10. Copy&Paste Examples of all fields
------------------------------------------------------------------------------
2. API Elements
1. Why generate in memory?
2. How does the renderer work?
3. Elements
4. Element "Logic" Options
5. Form Elements
6. JQM Widgets
7. Dynos
8. Items
------------------------------------------------------------------------------
3. API Modules
1. Modules have nothing to do with require!
2. Module API
3. Existing modules
4. Plugin custom options
------------------------------------------------------------------------------
4. API Navigation
1. Basics
2. Page Levels and Views
3. API URL Query Parameter
------------------------------------------------------------------------------
5. API Translations
1. Basics
2. Special handlers (value, placeholder...)
3. Translation file structure
4. Triggering translations
------------------------------------------------------------------------------
6. API Actions
1. Basics
2. API Action Object
3. Sample Action
------------------------------------------------------------------------------
7. Tutorial Writing an App
1. Index Page
2. Defining Storage
3. Defining Global Plugins/Elements
4. Base Page
(TODOS start here)
5. Define New Page Module
6. Define New Portal Type & Sample
7. Create first gadget showing listbox with search & pagination
8. Create second gadget showing showing a form to add a new record
9. Create third gadget showing single record with custom action
------------------------------------------------------------------------------
==============================================================================
0. Intro
==============================================================================
<!>
Note upfront:
The renderer is not well coded. The target was to get something working
quickly, which has been patched, patched and patched for needs be. Time
permitting a nicer version should be made.
<!>
------------------------------------------------------------------------------
Idea
------------------------------------------------------------------------------
The idea of the renderer was to develop a system that would perform well on
mobile devices and scale to desktop (be responsive).
Major shortcomings of current frameworks are the need for heavy DOM
manipulation, so the approach taken here is to use the UI developed by
frameworks such as JQM but try to render fully asynchronously and in memory,
so that a page is only modified (= triggering repaint = slow) to inject a
fully enhanced page or large portions of it instead of inserting code and
then have JQM modify each element individually.
Still - this is only a prototype and can be much improved.
To work in memory and be able to manage the whole application through jio,
all content is generated from JSON.
------------------------------------------------------------------------------
Data (Portal Type Structure)
------------------------------------------------------------------------------
Data (items) are stored based on portal types. Every query must specifiy a
portal type at which point the renderer looks up the portal type field
definitions to know how a field should be rendered.
To have a working application, it is thus necessary to have a
(a) [portal_type]_fieldlist.json > explain field definitions
(b) [portal_type]_sample.json > to show some sample data
Also since the renderer only works with i18n, all necessary translations must
be specified in the language specific files
For details see [API Portal Type Data](www) and [API Modules > i18n](www)
------------------------------------------------------------------------------
Element Structure
------------------------------------------------------------------------------
There are five types of elements that can be generated (see [API elements](www))
(a) "elements" > plain HTML elements
(b) "widgets" > jQuery Mobile widgets (panel, list...)
(c) "items" > dynamic content (queried) mapped to HTML
(d) "modules" > plugins (currently not managed through renderer)
(e) "dynos" > dynamic containers = query and children showing dynamic data
All elements are created through a recursive asynchronous loop, which will
start from a page fragment and recursivly build the whole page including data
from all queries and necessary async requests.
Modules are currently handled on initialization only, but should eventually be
part of the renderer loop, too.
Details see [API Elements](www)
------------------------------------------------------------------------------
Storage Structure
------------------------------------------------------------------------------
Currently the app uses two storages.
(a) "items" > includes records of all portal types
(b) "settings" > includes all pages, portal_type definitions, dynos visited.
At some point, "settings" should be depreciated with all elements being
rendered as items, but no time to do this yet.
By setting it up this way, caching plugins/css via a manifest and "visting"
all necessary pages would make the application work offline.
Details see [API Modules > Storage](www)
------------------------------------------------------------------------------
Renderer
------------------------------------------------------------------------------
The renderer will run the following steps for each element to assemble a DOM
in memory:
(a) fetch content configuration to render
(b) if dyno > if sample > test for sample data, load + save if none are found
(c) if dyno > if total query > run total query
(d) if dyno > run query
(e) render element > call mappings and renderer on children of element
Details see [API Renderer](www)
------------------------------------------------------------------------------
Navigation
------------------------------------------------------------------------------
The renderer currently uses JQMs default navigation modified to allow
deeplinking and query parameters (used only for passing jio queries)
Details see [API Navigation](www)
------------------------------------------------------------------------------
Actions
------------------------------------------------------------------------------
All interactions inside an app should be handled through actions. Actions
trigger on submit, click and change of elements with a class of action.
Details see [API Actions](www)
------------------------------------------------------------------------------
Create an App
------------------------------------------------------------------------------
The basics to create an app are explained in a quick tutorial to be found:
here [API Tutorial](www)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
==============================================================================
4. Navigation
==============================================================================
This API shows how the navigation works.
------------------------------------------------------------------------------
TOC
------------------------------------------------------------------------------
1. Basics
2. Page Levels and Views
3. API URL Query Parameter
------------------------------------------------------------------------------
1. Basics
------------------------------------------------------------------------------
The navigation is based on the jQuery Mobile page navigation modell (= load
first DOM, keep it and add/remove subsequent pages) and is handled in the
parsePage method.
ParsePage will be triggered manually (on init) or on the jQueryMobile
pagebeforechange event. The plugin will catch the url of the page and
determine whether to:
a) stop JQM and self
This will block all refreshes, opening/closing popups etc from triggering
transitions or unwanted rendering (and infinite loops...).
b) stop JQM and run self
When the page to be loaded is not in the DOM, JQM will be blocked. The app
will generate missing page and inject it into the DOM and call JQM again.
Since the first call did not generate and history entry, this works without
problem.
c) stop self and run JQM
On the 2nd call, the page to be loaded is in the DOM, so the renderer does
nothing and JQM can transition to the new page.
<!>
When leaving a dynamic page, the JQM pagehide event is triggered, which removes
dynamic pages again from the DOM (clean up after the user).
<!>
There are some additional modifications made to the navigation to allow
deeplinking and query-parameters
d) deeplinking
Normally this is not possible in JQM, but the above model will work fine
on deeplinks, too (page not in DOM, generate it, inject and trigger
transition again).
This is only possible by encoding URLs, so index.html#foo/bar has to be
loaded as index.html#foo2%Fbar. No other way until JQM supports it.
The other modification necessary is renderer overwriting JQMs internal
history, setting the deeplinked page as initial page, as JQM will by default
set whatever is before the "#" as initial page (index.html#foo > index.html
overwrite to index.html#foo).
e) query-parameters
Are also not possible out of the box with JQM. To enable query parameters
we also need to encode the & when loading a page.
The API for query parameters can be found below.
------------------------------------------------------------------------------
2. Page Levels and Views
------------------------------------------------------------------------------
The page model allows for page levels and views. Normally, there should be
a page model per portal_type, so for example a link could lead to
#test_page_module
which will try to load test_page_module.json, which could look like this:
````
{
"property_dict": {},
"children": [{
"generate": "widget",
"type": "page",
"property_dict": {
"title_i18n": "portal_type_dict.test_page_dict.text_dict.title",
"theme": "slapos-white"
},
"view_dict": {
"default": [{"href": "test_page_overview"}]
}
}, {
"generate": "widget",
"type": "page",
"property_dict": {
"title_i18n": "portal_type_dict.test_page_dict.text_dict.page",
"theme": "slapos-white"
},
"view_dict": {
"default": [{"href": "test_page_view"}],
"new": [{"href": "test_page_new"}],
"some": [{
"type": "p",
"logic": {"text": "Normal elements work here, too"}
}]
}
}]
}
````
NOTES:
- Every page widget maps to a URL level, so
````
index.html#home/foo
````
will be the second level page widget while
````
index.html#home/foo/bar
````
will be 3rd page widget (not shown above)
- The page widget is the only widget which doesn't have children with
elements to render. Instead there is a view_dict. By default the app will
always load the view named "default". However, if you have
````
"view_dict": {
"default": [],
"baz": []
}
````
and the url goes to
#home/baz,
the page to load matches a view, so this one will be loaded.
- This way it is easy to add item ids to the url (no match = load default)
or show specific pages, for example:
````
#product_module/1234567/config
````
which would load the product modules, item 1234567 configuration.
- When a view is picked you can either provide content to render directly
(as the <p> tag in the example above), or provide an URL with the content
to load.
- At some point we will only have an array of elements here (no property_dict
and wrapping children array).
- There will also be an option, "show_views_as_tabs" [not implemented yet]
- Details on the page widget can be found in the [API widget](www)
------------------------------------------------------------------------------
3. API URL JIO Query Parameter
------------------------------------------------------------------------------
Currently query-parameters are only used to pass jIO querys across pages. The
following parameters can be passed (latest branches only)
> index.html#foo/bar
Start with the page and level
> &query:id=bar+foo=baz
jIO query syntax, search for 'id := "%bar%" AND foo := "%baz%"
<!>
Note: "+" will be AND, "|" will be OR (hope this works everywhere...)
<!>
> &limit:start=0+items=5
set jIO limits to 0 and 5
> &sort:id=ascending+foo=descending
sort bas "id" ascending and "foo" descending
> &select:id+foo+baz+cous
return columns "id", "foo", "baz", "cous"
> &value:foo+bar+baz
search values across all columns
URL query parameters will be merged with the object inital_query. The final
query will be generated in storage.parseQuery (console.log there) to debug.
\ No newline at end of file
==============================================================================
5. Translations
==============================================================================
This API shows how translations work
------------------------------------------------------------------------------
TOC
------------------------------------------------------------------------------
1. Basics
2. Special handlers (value, placeholder...)
3. Translation file structure
4. Triggering translations
------------------------------------------------------------------------------
1. Basics
------------------------------------------------------------------------------
Translations are done using the i18next plugin (www.i18next.com).
To translate a text, you have to add a class of "translate" and a pointer
using the "data-i18n" attribute to tell the plugin where to find the
translation.
<!>
It is recommended not to put any text into the actual application JSON. All
text will be translated into the defined language before being injected into
the DOM, so whatever is set will be overwritten (with probably the same value)
Omitting the text attribute, will thus save some ops and ensure all text
blocks are kept in the same file
<!>
Example:
````
{
"type": "a",
"direct": {"className": "translate", "href": "#some"},
"attributes": {"data-i18n": "global_dict.some"},
"logic": {"text": "THIS TEXT WILL BE OVERWRITTEN - skip it"}
}
````
As you can see you can define texts hardcoded but they will be overwritten
with the value set in the translation dictionary, so you don't need them here.
<!>
Please note all language files must be in the lang folder, which should contain
a folder for each language named with the language code (en-EN, or fr-FR).
Inside this folder, add a file called "dict.json".
To modifiy location and names, it is necessary to configure the defaults set
in the i18n initializer in the global configuration json file.
------------------------------------------------------------------------------
2. Special Handlers
------------------------------------------------------------------------------
To translate special fields, just add a [property] in front of the translation
pointer.
For example:
````
{
...
"attributes": {"data-i18n": "[placeholder]global_dict.some"}
}
This will translate the placeholder attribute (if it exists). This allows
to translate things like title [title], alt attributes [alt] or text nodes
[node].
<!>
[node] is a custom hack and may not work properly all of the time yet. It is
needed on submit, reset buttons and select elements.
<!>
------------------------------------------------------------------------------
3. Translation File Structure
------------------------------------------------------------------------------
For every language to be used, there must be a translation file with the
following structure:
````
{
"global_dict": {
[global text snippets - don't put too much here]
},
"state_dict": {
[loader messages - saved, loading, ...]
},
"validation_dict": {
[validation messages - also shown in the loader or at a form field]
},
"portal_type_dict": {
[name_of_portal_type]_dict: {
"text_dict": {
[generic text, eg for a page, options of this portal-type]
},
"field_dict": {
[field_name]: {
"title": [title to display for this field name],
"description": [descriptio to show as a title for this field]
},
[next_field_name]: {
...
},
...
}
}
}
}
````
------------------------------------------------------------------------------
4. Triggering Translations
------------------------------------------------------------------------------
To enable translations on a page, you must:
(1) add the i18n module to the global.json file
(2) add a translation button or select somewhere. The button must trigger
an action named "translate"
This will trigger manual translations and set the internal language to
whatever is selected - all new content will be provided in this language.
Example <select>:
````
{
"type":"select",
"direct": {"id": "switch_language", "className":"action responsive translate"},
"attributes": {
"data-action":"translate",
"data-icon":"flag-fr"
},
"logic": {
"wrapper_class_list":"flag",
"options": [
{"value": "en-EN", "text":"English", "text_i18n":"global_dict.english"},
{"value": "fr-FR", "text": "Chinese", "text_i18n":"global_dict.french", "selected": true}
]
}
}
````
Example <a> button:
````
{
"type":"a",
"direct": {
"href": "#"
"className":"translate action ui-btn"
},
"attributes": {
"data-i18n": "global_dict.english",
"data-action": "translate"
}
}
````
==============================================================================
6. Actions
==============================================================================
This API shows how to trigger custom actions.
------------------------------------------------------------------------------
TOC
------------------------------------------------------------------------------
1. Basics
2. API Action Object
3. Sample Action
------------------------------------------------------------------------------
1. Basics
------------------------------------------------------------------------------
At some point all interactions should be linked or form based/intiated. Until
this works, actions are used as interm layer.
An action can be triggered on "click", "change" and "submit" on link, button
and select elements. To make it work, you also need to provide the action
to run.
For example:
````
{
"type":"a",
"direct": {
"href": "#"
"className":"translate action ui-btn"
},
"attributes": {
"data-i18n": "global_dict.english",
"data-action": "translate"
}
}
````
So this will generate a link button, which - when clicked - will run the action
"translate".
Inside the main javascript file (erp5_loader.js) there is a map.actions object
which includes all default and custom actions. Custom actions should be added
for the respective application.
------------------------------------------------------------------------------
2. Action Object
------------------------------------------------------------------------------
Whenever an action is called the call is sent through the method "parseAction"
which assembles an action object. The action object includes:
> element = the element that triggered the action
> id = the id of the reference element (normally a dyno/gadget)
> gadget = the gadget itself
> state = the state of the gadget
The important parameter is the gadget state, which includes the current state
of the gadget (definition parameters, initial query, current query, etc).
To update a dyno/gadget in response to an action thus is done by modifying its
state and triggering an update by passing the state object back to the rendering
engine with the create flag set to false.
------------------------------------------------------------------------------
3. Sample Action
------------------------------------------------------------------------------
An action might look like this (Notes below):
````
"update_custom": function (obj) {
storage.write(obj)
.then(function (response) {
var i, len, dyno_list, dyno, promise_list, dump;
// clear active page, because we need to reload
dump = document.querySelector("div.ui-content");
util.deleteChildren(dump);
// refresh dynos that are left!
dyno_list = document.querySelectorAll("div.dyno");
// set first page to reload and clean it up!
delete app.deeplink_flag;
promise_list = [];
for (i = 0, len = dyno_list.length; i < len; i += 1) {
dyno = dyno_list[i];
// update gadgets
promise_list[i] = app.content.set(
{
"portal_type_source": dyno.state.type,
"portal_type_title": dyno.state.title,
"property_dict": util.mergeObject(
{"dynamic": true},
dyno.state.dyno_dict),
"scheme": dyno.state.scheme
},
{
"reference": dyno.id,
"href": dyno.state.href,
"fragment_list": dyno.state.fragment_list,
"layout_level": dyno.state.layout_level
},
false
)
.fail(app.util.error);
}
return RSVP.all(promise_list)
.then(function (response_list) {
app.util.loader("", "status_dict.saved", "check");
//app.navigate(obj, response);
})
.fail(app.util.error);
})
.then(app.setPageBindings)
.fail(function (error) {
switch (error.status) {
case 408: app.util.loader("", "status_dict.timeout", "clock-o"); break;
case 400: app.util.loader("", "validation_dict.general", "ban"); break;
default: app.util.loader("", "status_dict.error", "ban"); break;
}
});
}
````
NOTES:
> storage write (POST/PUT) and get (GET/ALLDOCS) can handle the action object as it is
so on a form submit, you can just pass the action object to the respective
storage method
> in the next chain, we need to clear a page to update all of it's gadgets.
This is done by calling util.deleteChildren on the content <div>
> we then loop all dynamic elements triggering an update in the form of a
promise.
> when all promises are resolved (all dynos updated), we just show the
loader for 1sec with a check icon.
> we then call setPageBindings, which to update all form validation setters
on the page.
Normally actions are less complex. For example the default update (PUT):
````
"update": function (obj) {
storage.write(obj)
.then(function (response) {
app.util.loader("", "status_dict.saved", "check");
app.navigate(obj, response);
})
.fail(function (error) {
switch (error.status) {
case 408: app.util.loader("", "status_dict.timeout", "clock-o"); break;
case 400: app.util.loader("", "validation_dict.general", "ban"); break;
default: app.util.loader("", "status_dict.error", "ban"); break;
}
});
}
````
NOTES:
> we only write to storage
> in the response we show the check icon and call navigate, which will go to
a page submitted as callback to a dyno.
> on fail we provide some default loader icons and messages.
This diff is collapsed.
This diff is collapsed.
==============================================================================
Draft API
==============================================================================
------------------------------------------------------------------------------
sections:
------------------------------------------------------------------------------
> Info:
> A section defines a section of content
> Syntax:
=============================================================
type Type of element (fieldlist, tabs, listbox, ...)
span "Columns" to span - 1/2/more
gadget Id of gadget to load
=============================================================
> Notes:
> Types must have a construct[Type] method to build the type of element
> The gadget configuration defines the "instance" of the type
> To add own types add construct[Type] method
> Example JSON:
[
{"type": "fieldlist", "span": 2, "gadget": "bar"},
{"type": "fieldlist", "span": 1, "gadget": "baz"},
{"type": "fieldlist", "span": 1, "gadget": "bam"},
{"type": "tabs", "span": 2, "gadget": "foo"},
{"type": "listbox": "span": 2, "gadget": "zzz"}
]
> Example HTML:
<div class="span_2">
<div class="gadget" data-gadget-type="fieldlist" data-gadget-id="bar"></div>
</div>
<div class="span_1">
<div class="gadget" data-gadget-type="fieldlist" data-gadget-id="baz"></div>
</div>
<div class="span_1">
<div class="gadget" data-gadget-type="fieldlist" data-gadget-id="bam"></div>
</div>
<div class="span_2">
<div class="gadget" data-gadget-type="tabs" data-gadget-id="foo"></div>
</div>
<div class="span_2">
<div class="gadget" data-gadget-type="listbox" data-gadget-id="zzz"></div>
</div>
------------------------------------------------------------------------------
Pages:
------------------------------------------------------------------------------
> Info:
> A module can have one ore more layouts corresponding to pages
> Every hierarchy level needs a layout (one for Persons, on for Person, etc)
> A page must have at least one "default" section
>
> Syntax:
=============================================================
[page_name] link parameter(s) to determine page (?container=a&palette=x)
title Page title to set
title_i18n Client-side translation lookup value
theme Page theme (handles all JQM CSS)
fixed Fix header/footer for this page (default to true)
sections See "sections"
=============================================================
> Example JSON (container > palette > box > items > item = 5 hierarchies)
{
"default": {
"title": "Container",
"theme": "erp5_blue",
"fixed": true,
"title_i18n": null,
"sections": [{"type": "listbox", "span": 2, "gadget": "container_a"}]
},
"palettes": {
"title": "Palette",
"title_i18n": null,
"sections": [{"type": "listbox", "span": 2, "gadget": "palette_content_x"}]
},
"boxes": {
"title": "Box",
"title_i18n": null,
"sections": [{"type": "listbox", "span": 2, "gadget": "box_content_x"}]
},
"items": {
"title": "Items",
"title_i18n": null,
"sections": [{"type": "listbox", "span": 2, "gadget": "box_items_x"}]
},
"item": {
"title": "Item",
"title_i18n": null,
"sections": [
{"type": "fieldlist", "span": 1, "gadget": "item_foo"},
{"type": "fieldlist", "span": 1, "gadget": "item_foo_seller"},
{"type": "listbox", "span": 2, "gadget": "item_ingredients"}
]
}
}
LIST:
"generate": "gadget",
"type": "listview",
"class_list": "",
"theme": "slapos-black",
"property_dict": {
"inset": true,
"filter": true,
"reveal": true,
"input": "#bar",
"divider-theme": "slapos-white",
"autodividers": true,
"count-theme": "slapos-white",
},
"id": null,
"children": [
// sample item with all options
{
"type": "item",
"external": true,
"href": "index.html",
"left": {
"icon": "foo",
"img": "http://www.xyz.com/img/foo.png"
},
"center": {
"count": 3689,
"aside": [
{"type":"p", "text":"foo", "text_i18n":null}
],
"text": [
{"type":"h1", "text":"Persons to Validate", "text_i18n":""}
]
},
"right": {
"icon": "foo/false",
"radio": true,
"checkbox": true,
"action": "foo",
"href": "http://www.foo.com",
"external": true
}
]
POPUP:
{
"generate": "widget",
"type":"Popup",
"class_list": "",
"theme": "",
"id": null,
"property_dict": {
"overlay-theme": null,
"transition": "fade",
"position-to": "window",
"tolerance": "30,30,30,30",
"shadow": true
},
"element": null,
"children": []
}
HEADER:
{
"generate": "widget",
"type": "Header",
"class_list": "",
"theme": "",
"id": null,
"property_dict": {
"title": "",
"title_i18n":"",
"fixed": true
},
"children": [
<<examples>>
{
"type":"img",
"direct": {
"src": "http://www.foo.com/bar.jpg",
"alt": "foo",
"alt_i18n": "bar"
}
}, {
"generate": "widget",
"type": "controlgroup",
"class_list": null,
"id": null,
"form": null,
"theme": null,
"children": [
{"type": "a", "direct": {}, "attributes": {}, "logic": {}}
]
}
]
}
CONTROLGROUP:
{
"generate": "controlgroup",
"id": null,
"class_list": null,
"theme": null,
"property_dict": {
"direction": "horizontal"
},
"children": [
{"type":a, "direct": {}, "attributes": {}, "logic": {}}
]
}
FOOTER:
{
"generate":"widget",
"type": "footer",
"class_list": null,
"id": "null",
"theme": "slapos-white",
"property_dict": {
"fixed": true
},
"children": []
}
NAVBAR:
{
"generate": "widget",
"type": "navbar",
"class_list": null,
"id": null,
"theme": "slapos-white",
"property_dict": {},
"children":[]
}
PANEL:
{
"generate": "widget",
"type": "panel",
"class_list": "",
"id": "global_panel",
"theme": "slapos-black",
"property_dict": {
"close": true
},
"children": []
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment