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)
==============================================================================
1. API Portal Type Data
==============================================================================
This API shows how to "create" and set data for a renderer application.
<!>
Please note:
This API still uses too much duplicate code and needs something like "BaseField"
for every field type to inherit from. You will see...
<!>
------------------------------------------------------------------------------
TOC
------------------------------------------------------------------------------
1. Creating a fieldlist file
2. Overrides & custom Fields
3. Field types currently supported
4. Rendering
5. Subordination
6. Formatting
7. "items" Property
8. Translations
9. Validations
10. Copy&Paste Examples of all fields
------------------------------------------------------------------------------
Creating a fieldlist file
------------------------------------------------------------------------------
To show data in the app, the renderer needs to know how data should look.
This is specified in a [portal_type]_fieldlist.json file, which contains an
object with all the fields that should exist for a respective portal type.
When the renderer encounters a portal_type for the first time, it loads a
the field_list from file and stores it in JIO under
settings/portal_types/[portal_type_name]
From there it will be fetched every time it is needed.
<!>
The fieldlist definition is also used to validate forms, so fields that
are not on the fieldlist will cause a form validation to fail and not be
submitted.
<!>
A field definition consists of the following:
````
[field_title]: {
"type": [type of field, eg. StringField],
"widget": {
[field widget properties]
},
"properties": {
[field properties]
},
"message": {
[field validation rules and message]
}
}
````
------------------------------------------------------------------------------
Overrides & custom Fields
------------------------------------------------------------------------------
To use a field for example in a form, you just have to include the name of
the field like this:
````
{"field": [field_title]}
````
Depending on the type of widget being rendered, there are numerous extra
properties described in the respective widget.
All properties of a field can be overriden by using the following syntax:
````
{"field": [field_title], "overrides": {"widget": {[property]: [new_value]}...}
````
By default overrides... override the default values declared in the field_list.
It is also possible to declare custom fields both in the scheme AND and as child
of the actual wrapping element (form, listview). To do this, just use the normal
element structure described in [API Elements](www). This way it is possible to
generate custom field or custom forms as needed.
Example of a custom field in a form definition:
````
"scheme": [{
"position": "left",
"field_list": [{
"field": "title"
}, {
"field": "short_title"
"overrides": {"widget": {"required": true}}}
}]
}, {
"position": "right",
"field_list": [{
"custom": true,
"type": "input",
"direct": {"id": "foo_field", "name":"foo_field", "className": "ui-btn-text"},
"attributes": {"data-i18n":"[value]global_dict.some"}
}
]
}]
````
NOTES:
> The form scheme includes two regular fields. The 2nd field has an override
for the required property making this field a required field.
> There is a custom field, named "foo_field", which will be generated as
a plain text-input.
Example of a custom field set as child of the containing element:
````
{
"generate": "widget",
"type": "form",
"property_dict": {
"class_list": "responsive custom_form_display",
"dynamic": true,
"map_children": "formItem",
"editable": true,
"secure": "default",
"secret_hash": "foo",
"public_key": "6Ldpb-oSAAAAAGwriKpk4ol1n4yjN_as6M4xv0zA"
},
"children": [{
"generate": "widget",
"type": "listview",
"property_dict": {
"inset": "true",
"class_list": "subscription_selector force_corners",
"map_children": "listItem",
"wrap": 2
},
"children": [{
"type": "divider",
"center": [{"type": "h3", "text_i18n":"portal_type_dict.sales_order_dict.text_dict.choose_offer"}]
}, {
"type": "item",
"class_list": "violet",
"left": [{"type": "image", "src": "img/bg-offre01.png", "alt":""}],
"center": [{"type": "h3", "text_i18n":"portal_type_dict.sales_order_dict.text_dict.fixed_price"}, {"type": "p", "text_i18n":"portal_type_dict.sales_order_dict.text_dict.fixed_details"}],
"right": [{"type": "radio", "id": "subscription_plan-0", "name": "subscription_plan"}]
}, {
"type": "item", "class_list": "rose",
"left": [{"type": "image", "src": "img/bg-offre02.png", "alt":""}],
"center": [{"type": "h3", "text_i18n":"portal_type_dict.sales_order_dict.text_dict.carte_fixed"}, {"type": "p", "text_i18n":"portal_type_dict.sales_order_dict.text_dict.carte_details"}],
"right": [{"type": "radio", "id": "subscription_plan-1", "name": "subscription_plan"}]
}]
}]
}
````
NOTES:
> The form will received the dynamic data mapped using the "formItem" mapping
> Form items will be appended to the children array, which already includes
a listview
> The listview contains a divider and 2 list items, both of which have a
checkbox. The name of the checkbox will match to a field of this portal
type, so it will get evaluated and included in the form submission
------------------------------------------------------------------------------
Field types currently supported
------------------------------------------------------------------------------
The following field types are currently possible:
- StringField > rendered as text input
- IntegerField > rendered as text input
- RelationStringField > rendered as text input (subordination see below)
- IntegerField > rendered as text input
- PasswordField > rendered as password input
- CheckboxField > rendered as checkbox (multiple options see items)
- RadioField > rendered as radio button (multiple options see items)
- ListField > rendered as select (options, see below)
- MultiListField > rendered as normal(!) select
- ParallelListField > rendered as normal(!) select
- DateTimeField > rendered as date input
- TextareaField > rendered as textarea
- EmailField > rendered as email input
- ImageField > rendered as image
While I tried to include most of the properties specified in ERP5, they are
included, but most of them are not working yet. Needs time to mature...
------------------------------------------------------------------------------
Rendering
------------------------------------------------------------------------------
Field are rendered through mapping. Currently there three mappers set up.
(a) listItem > only generates text, no form inputs yet
(b) tableItem > only generates table rows with text, no form inputs yet
(c) formItem > generates form items.
You can specify the mapper to use in the respective element. Be sure to include
a mapper and a scheme, because they ensure that the response of JIO is
converted into something that can be rendered!
In the latest branch, there is another mapper, which must be set on the dyno
itself (see [API Elements](www)), called "direct_map". This will allow to
just insert record values in normal HTML syntax using the setParam method.
More on this in the element API.
When an form field is generated, the main renderer is called, but before
creating the form field, it is run through the mapper (creating something we
can use) and afterward through an optional generator, which will generate
the necessary HTML.
This is the flow of the data from JIO to the final HTML.
(1) The record returned from JIO will include this:
````
{"doc": {"id":"123", "title": "Some", "price": "35", "currency": "Euro}}
````
(2) The listview scheme is this:
````
{
"position": "left",
"field_list": [
{"type": "icon", "icon": "price-tag"}
]
}, {
"position": "center",
"field_list": [
{"field": "title", "type": "h3"},
{"field": "price", "type": "p", "aside": true, "mergeWith": "currency"},
{"field": "currency"}
]
}
````
(3) Which resulsts in a mapping to the following:
````
{
"type": "item",
"left": [{"icon": "price_tag"}],
"center": [{"type": "h3", "text": "Some"}, {"type": "p", "text": "35 Euro"}]
}
````
(4) And which is then run through the list item generator to add this to
the respective fragment in memory:
````
<li class="ui-li-static ui-li-has-icon ui-icon-price-tag">
<h3>Some</h3><p class="ui-li-aside">35 Euro</p>
</li>
````
------------------------------------------------------------------------------
Subordination
------------------------------------------------------------------------------
Subordination is possible for single items in the latest branches. To use a
field name must have "subordinate_" in its name, so a field with a name:
````
some_form_subordinate_description
````
And a returned value of
````
"some_form_subordinate_description": "id_12345"
````
will trigger an jIO query to fetch the field called "description" from record
with id of "id_12345". Once fetched, the record is kept in memory until it is
overwritten by the next record, so multiple subordinate fields to the same
record can be handled with one request.
Time permitting I will try to extend subordination into requesting a full
query which could return multiple results in different types of display.
------------------------------------------------------------------------------
Formatting
------------------------------------------------------------------------------
There are some field formatting methods available. Currently the following are
implemented:
(1) Crop > crop the specified string from the field. Use like this in
the scheme:
````
{"field": "some_thing", "crop": "the_string_to_crop"}
````
(2) Format > currently only integers. Use like this:
````
{"field": "some_integer", "format": {"type": "integer", "digits": 2}}
````
(3) MergeText > Merge text with the text from another column
````
{"field": "some_text", "mergeText": "[field_to_merge_with]"}}
````
Some of these formatters will probably be set through the field definition,
however at this point this is not implemented.
------------------------------------------------------------------------------
Items
------------------------------------------------------------------------------
The items property is used to load options for a select or a radio/checkbox
group. For example, if this specified in the field definition:
````
"items": "getCountries"
````
The mapper will trigger a request to "getCountries.json" when rendering the
form field. The file should include something like this:
````
[
{"text_i18n": null, "value": "", "selected"_ true},
{"text_i18n":"portal_type_dict.sales_order_dict.text_dict.be", "class":"translate", "value":"be"},
{"text_i18n":"portal_type_dict.sales_order_dict.text_dict.fr", "class":"translate", "value":"f"},
{"text_i18n":"portal_type_dict.sales_order_dict.text_dict.de", "class":"translate", "value":"de"},
{"text_i18n":"portal_type_dict.sales_order_dict.text_dict.lu", "class":"translate", "value":"lux"},
{"text_i18n":"portal_type_dict.sales_order_dict.text_dict.nl", "class":"translate", "value":"nl"},
{"text_i18n":"portal_type_dict.sales_order_dict.text_dict.ch", "class":"translate", "value":"ch"}
]
````
Which will generate the respective select options. You can also include
empty or selected options if you want.
When rendering a RadioField or CheckBoxField, specifying the "items" property
will also request the respective JSON file. Instead of a single radio button
the renderer will generate a list of radio buttons/checkboxes.
Of course it is also possible to skip loading hardcoded options. The alternative
of implementing "portal_categories" is used in the e5g-ecommerce branch,
where all categories, brands, sizes are queriable items in the storage.
Quite the slowdown...
------------------------------------------------------------------------------
Translations
------------------------------------------------------------------------------
Translations should be set inside the respective language files. The translation
file must include the following structure:
````
{
"portal_type_dict": {
[name_of_portal_type]_dict: {
"text_dict": {
[generic texts, 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]
},
[...
}
}
}
}
````
This is the only place you should keep all translations. In theory they
could be included everywhere (see older branches...) but forcing the translation
file ensures all texts are in the same place.
In addition all fragments will be translated in memory before appending them
to the DOM, so if you set a hardcoded text, it will be translated before
appending anyway - save some processing.
------------------------------------------------------------------------------
Validation
------------------------------------------------------------------------------
[Based on form field definitions is currently not implemented!]
------------------------------------------------------------------------------
Examples
------------------------------------------------------------------------------
Copy-Paste galore. To use, the only thing you have to change is 5x the
field name (title, id, alternate name and two translation pointers) and the
translation pointer portal type.
------------------------- COPY STRINGFIELD -----------------------------------
````
"[name_of_field]": {
"type":"StringField",
"widget": {
"id": "[name_of_field]",
"title_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].title",
"description_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].description",
"alternate_name": "[name_of_field]",
"default_value": null,
"css_class": null,
"hidden": null,
"display_width": null,
"maximum_input": null,
"extra": null
},
"properties": {
"enabled": true,
"editable": true,
"external_validator": null,
"required": true,
"preserve_whitespace": null,
"unicode": null,
"maximum_length": null,
"truncate": null
},
"message": {
"external_validator_failed": {
"message": "Password and confirm don't match.",
"i18n": "validation_dict.external"
},
"required_not_found": {
"message": "Input required but not found.",
"i18n": "validation_dict.required"
},
"too_long": {
"message": "Too much input give.",
"i18n": "validation_dict.too_much_input"
}
}
}
````
------------------------- COPY INTEGERFIELD ----------------------------------
````
"[name_of_field]": {
"type":"IntegerField",
"widget": {
"id": "[name_of_field]",
"title_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].title",
"description_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].description",
"alternate_name": "[name_of_field]",
"default_value": null,
"css_class": null,
"hidden": null,
"display_width": null,
"maximum_input": null,
"extra": null
},
"properties": {
"enabled": true,
"editable": true,
"external_validator": null,
"required": true,
"preserve_whitespace": null,
"start": null,
"end": null
},
"message": {
"external_validator_failed": {
"message": "The input failed the external validator.",
"i18n": "validation_dict.external"
},
"required_not_found": {
"message": "Input required but not found.",
"i18n": "validation_dict.required"
},
"not_integer": {
"message": "You did not enter an integer",
"i18n": "validation_dict.no_integer"
},
"integer_out_of_range": {
"message": "The integer you entered is out of range.",
"i18n": "validation_dict.out_of_range"
}
}
}
````
---------------------- COPY RELATIONSTRINGFIELD ------------------------------
````
"[name_of_field]": {
"type":"RelationStringField",
"widget": {
"id": "[name_of_field]",
"title": "Company",
"title_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].title",
"description": "The company posting this job offer.",
"description_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].description",
"alternate_name": "[name_of_field]",
"default_value": null,
"css_class": null,
"hidden": null,
"display_width": null,
"display_maxwidth": null,
"maximum_input": null,
"extra": null
},
"properties": {
"enabled": true,
"editable": true,
"maximum_length": null,
"first_item": null,
"extra_item": null,
"external_validator": null,
"items": null,
"list_method": null,
"jump_method": null,
"max_length": null,
"max_linelength": null,
"max_lines": null,
"parameter_list": null,
"portal_type": null,
"catalog_index": null,
"base_category": null,
"allow_jump": null,
"allow_creation": null,
"columns": null,
"container_getter_id": null,
"relation_setter_id": null,
"required": true,
"size": 1,
"sort": null,
"truncate": null,
"unicode": null,
"preserve_whitespace": 0,
"update_method": null
},
"message": {
"external_validator_failed": {
"message": "The input failed the external validator.",
"i18n": "validation_dict.external_validator_failed"
},
"line_too_long": {
"message": "A line was too long.",
"i18n": "validation_dict.line_too_long"
},
"relation_result_ambiguous": {
"message": "Relation_result_ambiguous.",
"i18n": "validation_dict.relation_result_ambiguous"
},
"relation_result_ambiguous": {
"message": "Select appropriate document in the list.",
"i18n": "validation_dict.relation_result_ambiguous"
},
"relation_result_empty": {
"message": "No such document was found.",
"i18n": "validation_dict.relation_result_empty"
},
"relation_result_too_long": {
"message": "Too many documents were found.",
"i18n": "validation_dict.relation_result_too_long"
},
"required_not_found": {
"message": "Input is required but no input given.",
"i18n": "validation_dict.required_not_found"
},
"too_long": {
"message": "You entered too many characters.",
"i18n": "validation_dict.too_long"
},
"too_many_lines": {
"message": "You entered too many lines.",
"i18n": "validation_dict.too_many_lines"
}
}
}
````
------------------------ COPY PASSWORDFIELD ----------------------------------
````
"[name_of_field]": {
"type":"PasswordField",
"widget": {
"id": "[name_of_field]",
"title_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].title",
"description_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].description",
"alternate_name": "[name_of_field]",
"default_value": null,
"css_class": null,
"hidden": null,
"display_width": null,
"maximum_input": null,
"extra": null
},
"properties": {
"enabled": true,
"editable": true,
"external_validator": null,
"required": true,
"preserve_whitespace": null,
"unicode": null,
"maximum_length": null,
"truncate": null
},
"message": {
"external_validator_failed": {
"message": "Password and confirm don't match.",
"i18n": "validation_dict.external"
},
"required_not_found": {
"message": "Input required but not found.",
"i18n": "validation_dict.required"
},
"too_long": {
"message": "Too much input give.",
"i18n": "validation_dict.too_much_input"
}
}
}
````
------------------------ COPY CHECKBOXFIELD ----------------------------------
````
"[name_of_field]": {
"type":"CheckboxField",
"widget": {
"id": "[name_of_field]",
"title_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].title",
"description_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].description",
"alternate_name": "[name_of_field]",
"css_class": null,
"hidden": null,
"items": null,
"select_first_item": true,
"extra_per_item": null,
"extra": null
},
"properties": {
"enabled": true,
"editable": true,
"external_validator": null,
"required": true,
"preserve_whitespace": null,
"unicode": null
},
"messages": {
"external_validator_failed": {
"message": "The input failed the external validator.",
"i18n": "validation_dict.external"
},
"required_not_found": {
"message": "Input is required but no input given.",
"i18n": "validation_dict.required"
},
"unknown_selection": {
"message":"You selected on option not on the menu",
"i18n": "validation_dict.option_not_available"
}
}
}
````
-------------------------- COPY RADIOFIELD -----------------------------------
````
"[name_of_field]": {
"type":"RadioField",
"widget": {
"id": "[name_of_field]",
"title_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].title",
"description_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].description",
"alternate_name": "[name_of_field]",
"css_class": null,
"hidden": null,
"items": "[method_to_create_group|null]",
"select_first_item": true,
"extra_per_item": null,
"extra": null
},
"properties": {
"enabled": true,
"editable": true,
"external_validator": null,
"required": true,
"preserve_whitespace": null,
"unicode": null
},
"messages": {
"external_validator_failed": {
"message": "The input failed the external validator.",
"i18n": "validation_dict.external"
},
"required_not_found": {
"message": "Input is required but no input given.",
"i18n": "validation_dict.required"
},
"unknown_selection": {
"message":"You selected on option not on the menu",
"i18n": "validation_dict.option_not_available"
}
}
}
````
------------------------- COPY LISTFIELD -------------------------------------
````
"[name_of_field]": {
"type":"ListField",
"widget": {
"id": "[name_of_field]",
"title_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].title",
"description_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].description",
"alternate_name": "[name_of_field]",
"default_value": null,
"css_class": null,
"hidden": null,
"items": "[method_to_get_options]",
"size": null,
"extra": null,
"extra_per_item": null
},
"properties": {
"enabled": true,
"editable": true,
"external_validator": null,
"required": true,
"unicode": null
},
"message": {
"external_validator_failed": {
"message": "The input failed the external validator.",
"i18n": "validation_dict.external"
},
"required_not_found": {
"message": "Input is required but no input given.",
"i18n": "validation_dict.required"
},
"unknown_selection": {
"message":"You selected on option not on the menu",
"i18n": "validation_dict.option_not_available"
}
}
}
````
------------------------- COPY MULTILISTFIELD --------------------------------
````
"[name_of_field]": {
"type":"MultiListField",
"widget": {
"id": "[name_of_field]",
"title_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].title",
"description_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].description",
"alternate_name": "[name_of_field]",
"default_value": null,
"css_class": null,
"hidden": null,
"items": null,
"size": null,
"view_separator": null,
"extra": null,
"extra_per_item": null
},
"properties": {
"enabled": true,
"editable": true,
"external_validator": null,
"required": true,
"unicode": null
},
"message": {
"external_validator_failed": {
"message": "The input failed the external validator.",
"i18n": "validation_dict.external"
},
"required_not_found": {
"message": "Input required but not found.",
"i18n": "validation_dict.required"
},
"unknown_selection": {
"message": "You selected an item that was not in the list.",
"i18n": "validation_dict.unknown_selection"
}
}
}
````
---------------------- COPY PARALLELLISTFIELD --------------------------------
````
[not used yet]
````
------------------------ COPY DATETIMEFIELD ----------------------------------
````
"[name_of_field]": {
"type":"DateTimeField",
"widget": {
"id": "[name_of_field]",
"title_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].title",
"description_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].description",
"alternate_name": "[name_of_field]",
"default_value": null,
"css_class": null,
"hidden": null,
"default_to_know": null,
"data_separator": null,
"time_separator": null,
"input_style": null,
"input_order": null,
"display_date_only": true,
"am_pm time style": null,
"display_timezone": null,
"hide_day": null,
"hidden_day_is_last_day_of_the_month": null
},
"properties": {
"enabled": true,
"editable": true,
"external_validator": null,
"required": true,
"start_datetime": null,
"end_datetime": null,
"allow_empty_datetime": null
},
"message": {
"external_validator_failed": {
"message": "The input failed the external validator.",
"i18n": "validation_dict.external"
},
"required_not_found": {
"message": "Input is required but no input given.",
"i18n": "validation_dict.required"
},
"not_datetime": {
"message": "You did not enter a valid date and time.",
"i18n": "validation_dict.not_valid_datetime"
},
"datetime_out_of_range": {
"message": "The date and time you entered were out of range.",
"i18n": "validation_dict.out_of_range_datetime"
}
}
},
````
------------------------ COPY TEXTAREAFIELD ----------------------------------
````
"[name_of_field]": {
"type":"TextareaField",
"widget": {
"id": "[name_of_field]",
"title_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].title",
"description_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].description",
"alternate_name": "[name_of_field]",
"css_class": null,
"hidden": null,
"width":null,
"height":null,
"extra":null
},
"properties": {
"enabled": true,
"editable": true,
"external_validator": null,
"required": true,
"preserve_whitespace": null,
"unicode": null,
"maximum_lines": null,
"maximum_length_of_line": null,
"maximum_characters": null
},
"message": {
"external_validator_failed": {
"message": "The input failed the external validator.",
"i18n": "validation_dict.external"
},
"required_not_found": {
"message": "Input required but not found.",
"i18n": "validation_dict.required"
},
"too_many_lines": {
"message": "You have entered too many lines.",
"i18n": "validation_dict.too_many_lines"
},
"line_too_long": {
"message": "One or more lines you have entered are too long.",
"i18n": "validation_dict.too_long_lines"
},
"too_long": {
"message": "You have entered too many characters.",
"i18n": "validation_dict.too_many_chars"
}
}
}
````
------------------------- COPY EMAILFIELD ------------------------------------
````
"[name_of_field]": {
"type":"EmailField",
"widget": {
"id": "[name_of_field]",
"title_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].title",
"description_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].description",
"alternate_name": "[name_of_field]",
"default_value": null,
"css_class": null,
"hidden": null,
"display_width": null,
"maximum_input": null,
"extra": null
},
"properties": {
"enabled": true,
"editable": true,
"external_validator": null,
"required": true,
"preserve_whitespace": null,
"unicode": null,
"maximum_length": null,
"truncate": null
},
"message": {
"external_validator_failed": {
"message": "Password and confirm don't match.",
"i18n": "validation_dict.external"
},
"required_not_found": {
"message": "Input required but not found.",
"i18n": "validation_dict.required"
},
"too_long": {
"message": "Too much input give.",
"i18n": "validation_dict.too_much_input"
},
"not_email": {
"message": "You did not enter an email address.",
"i18n": "validation_dict.not_email"
}
}
},
````
------------------------- COPY IMAGEFIELD ------------------------------------
````
"[name_of_field]": {
"type":"ImageField",
"widget": {
"id": "[name_of_field]",
"title_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].title",
"description_i18n": "portal_type_dict.[name_of_portal_type]_dict.field_dict.[name_of_field].description",
"alternate_name": "[name_of_field]",
"default_value": null,
"css_class": null,
"hidden": null,
"display_width": null,
"maximum_input": null,
"extra": null,
"image_display": null,
"image_format": null,
"image_quality": null,
"image_preconverted_only": null
},
"properties": {
"enabled": true,
"editable": true,
"external_validator": null,
"required": true,
"preserve_whitespace": null,
"unicode": null,
"maximum_length": null,
"truncate": null
},
"message": {
"external_validator_failed": {
"message": "The input failed the external validator.",
"i18n": "validation_dict.external"
},
"required_not_found": {
"message": "Input required but not found.",
"i18n": "validation_dict.required"
},
"too_long": {
"message": "Too much input give.",
"i18n": "validation_dict.too_much_input"
}
}
}
````
==============================================================================
2. API Elements
==============================================================================
This API shows how to create elements.
------------------------------------------------------------------------------
TOC
------------------------------------------------------------------------------
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
------------------------------------------------------------------------------
Why generate in memory?
------------------------------------------------------------------------------
JQM is easy to use because the syntax is straighforward an JQM creates complex
element structurs for you. For example, take an input button:
````
<input type="text" value="foo" id="some" name="same" data-clear-btn="true" />
````
JQM will parse this button and generate the following:
````
<div class="ui-input-text ui-body-inherit ui-corner-all ui-shadow-inset ui-input-has-clear">
<input type="text" data-clear-btn="true" name="same" id="some" value="foo">
<a title="Clear text" class="ui-input-clear ui-btn ui-icon-delete ui-btn-icon-notext ui-corner-all" href="#">Clear text</a>
</div>
````
which makes little difference for one element on desktop, but accessing and
repainting the DOM for every element on a page will slow things down -
especially on mobile.
Therefore the idea was to provide a JSON API to read in and generate all
elements enhanced (2nd version) in memory before appending to DOM.
------------------------------------------------------------------------------
How does the renderer work?
------------------------------------------------------------------------------
The renderer is a loop, which you can feed elements to generate. The parent
element is always created as an object, which contains a child_selector to
which children are appended to. This method runs over all elements until
the full page or content section is assembled and then inserts it into the DOM.
The renderer will also run all queries on the storage, which are triggered
by providing a dyno object (see below). If no query is to be made, all query
related steps are skipped.
The processing loop looks like this:
(a) fetch content configuration to render
(b) if dyno > if sample > test for sample data, load + save if sample needed
(c) if dyno > if total query > run total query
(d) if dyno > run query
(e) render element and call mappings and renderer on child elements
------------------------------------------------------------------------------
Elements
------------------------------------------------------------------------------
Elements are generated through the "factory.element" method.
<!>
Note we don't generate elements as strings, because the page is assembled in
a document fragment (document.createDocumentFragment()), which are containers
in memory. The advantages of containers:
- can be as complex as need be
- can be nested
- are queryable using querySelector/querySelectorAll
- contents can be looped
This way we are building sort of a "shadow-dom", which we can set all
relevant things on BEFORE inserting it into the page.
<!>
An element can consist of up to 5 parts:
````
{
"type": [type of element],
"direct": { [properties directly settable, like id or className] },
"attributes": { [attributes settable using setAttribute] },
"logic": { [text & anything requiring logic, like don't set if null....] }
"children": [children of this element]
}
````
Example:
````
{
"type": "input",
"direct": {
"id": "foo",
"name": "foo",
"className":"ui-some-class translate action"
},
"attributes": {
"type": "button",
"data-icon": "star",
"data-iconpos": null,
"data-i18n": [value]global_dict.foo",
"data-action": "foo_it"
},
"logic": {
"data-baz": some_condition ? true : null
}
}
````
We are using this structure, because direct and attributes can be set without
questioning, so there is no need to evaluate every attribute when assembling
an element.
NOTES:
- To make an element translateable it MUST have the translate class!
- Interactions (form submit, change language, pagination, [your_action] are
triggered through actions. This requires the class "action" and an
attribute specifying the action to take on the respective link, input or
select element.
- Logic includes a strict switch, so not everything you specify will be
evaluated!!!
------------------------------------------------------------------------------
Element "Logic" Options
------------------------------------------------------------------------------
These options which will be evaluated include:
> "disabled":
> "id":
> "href":
> "rows":
> "cols":
> "name":
> "value":
> "data-": [all data-attributes!]
> "role":
> "type":
> "rel":
> "alt":
> "src":
> "readonly":
> "size":
> "colspan":
> "rowspan":
> "text": [appends text node]
> "img": [appends image][depreciated?]
> "options": [appends select options]
> "extra": [sets attribute][depreciated?]
The logic object has become quite complex as it needs to contain a lot
of special handlers. These are:
> "wrapper_class_list": [string]
Classes to set on a wrapper element > use for all inputs if needed!
> "setParam": [["text", "price"], ["text", "currency"]]
Set property from data-record, works accumulative (+= vs =)
> "wrap": [true]
Wrap form elements in a <div clas="ui-fieldcontain">. For custom form inputs
> "label_i18n": [pointer]
Text pointer for custom form elements
> "title_i18n": [pointer]
Text pointer for custom form element fieldcontain title
> "options": [{"value": "en-EN", "text_i18n":"global_dict.english", "selected": true}]
Options to render for a select element
> "clear": [true]
Add clear button to text input
> "action": {"action":"search", "icon": "search", "text_i18n": "global_dict.search_items"}
Add action button with icon "search" and action "search" to text input
> "action": [string] [depreciated?]
Add action foo to element.
> "add_label": [true]
Add a label to a custom form element
> "plain_link": [true]
Don't make this link into a button, just add external link flag.
> "skip": [false][don't use]
By default custom form fields are not mapped. This forces through mapping.
================ used in slapos-ui/slapos-ui offline ======================
> "set_reference": [true][don't use]
Set the parent dyno-id (reference) as a data-refernce
> "set_value": [field_name][don't use]
Set value to the field value of record
> "setters": ["data-method", "href"][don't use]
Set these properties of the element base on lookupValue. This is needed
because of the JSON response structure returned by SlapOS/ERP5.
> "lookupValue": [["_actions", "destroy", "method"], ["_actions", "destroy", "href"]]
Set setters data-method to value at path _actions.destroy.method
------------------------------------------------------------------------------
Form Elements
------------------------------------------------------------------------------
Form elements (input, select, textarea) are rendered in a separate method
(factory.formElement), athough the syntax is the same as for regular element.
Example:
````
{
"type": "input",
"direct": {
"id": "foo",
"name": "foo",
"className":"ui-some-class translate action"
},
"attributes": {
"type": "button",
"data-icon": "star",
"data-iconpos": null,
"data-i18n": [value]global_dict.foo",
"data-action": "foo_it"
},
"logic": {
"data-baz": some_condition ? true : null,
"wrap": false,
"add_label": false
}
}
````
A form element can have a wrapping field container (<div class.ui-fieldcontain>)
and a label. If generating forms from dynamic data these will always be
included out of the box.
On custom fields, both can be blocked using [logic][add_label][wrap] options.
However this should only be done outside of forms, because the form layout
is set to using labels and wrappers.
<!>
When generating custom elements, be sure to set the element type on
the attribute object to prevent select elements from ending up with a type
attribute.
<!>
------------------------------------------------------------------------------
JQM Widgets
------------------------------------------------------------------------------
The elements JQM generates are called widgets. Not all widgets are implemented
in the renderer. Also, some widgets are modified, like the table widget
(currently inside extensions.js/extensions.css).
Widgets have the following structure:
Example:
````
{
"generate": "widget",
"type": [name_of_widget]
"property_dict: {
[properties of a widget - see below]
},
"children": [[child elements of a widget]]
}
````
Widgets can contain dynamic data, which will be appended to the children
array.
The following widgets can be generated (along with properties supported):
--------------------------- CONTROLBAR -------------------------------------
The controlbar widget is a wrapper widget and only used to wrap contents
either inside a <div> or document fragment.
Example:
````
{
"generate": "widget",
"type": "controlbar",
"property_dict" : {
"slot": null,
"class_list": [string],
"wrap": null,
"target": null,
"reference": null,
"persist": null
},
"children": []
}
````
NOTES:
> [optional] class_list > string of classes to add to the controlbar.
> [optional] wrap > "fragment" will wrap in documentFragment,
everything else in <div>
> [optional] slot > if used inside a table, this denotes slot to
add element to (slot defined on the table)
> [optional] reference > set reference of parent dyno on <div>
> [optional] persist > don't replace this element when updating parent
The controlbar will return an object with the following properties:
````
{
"fragment": [container_element],
"child_selector": [selector to append children to]
"add_on_dom": [boolean to add on dom or not]
}
````
--------------------------- PAGE -------------------------------------
The page widget is a little special, because it contains a view_dict
instead of children. The view_dict contains the different views available
for a page level.
Example:
````
{
"generate": "widget",
"type": "page",
"property_dict" : {
"title_i18n": null,
"theme": null,
"create": null,
"data_url": null,
"fix_header": null,
"fix_footer": null,
"fragment_list:" null
},
"view_dict": {
"default": [{[element(s)_to_render]}],
[name_of_view]: [{[element(s)_to_render]}]
}
}
````
NOTES:
> title_i18n > pointer to page title
> [optional] theme > theme to set on page
> create > whether this is a new page or only an update
> data_url > url of page set on breadcrumbs (internally)
> [optional] fix_header > flag for class string constructor
> [optional] fix_footer > flag for class string constructor
> fragment_list > link fragments to build breadcrumbs
The page will return an object with the following properties:
````
{
"fragment": [container_element],
"child_selector": [selector to append children to],
"is_page": [boolean to denote a page]
}
````
------------------------ COLLAPSIBLE SET/TABS --------------------------------
[still broken/not implemented]
---------------------------- COLLAPSIBLE ------------------------------------
Example:
````
{
"generate": "widget",
"type": "collapsible",
"property_dict" : {
"text_i18n": null,
"collapsed": null,
"inset": null,
"mini": null,
"theme": null,
"content_theme": null,
"class_list:" null,
"expanded_icon": null,
"expanded_icon": null,
"form": null,
"href": null,
"title_i18n": null
"form_config": null,
"iconpos": null
},
"children": []
}
````
NOTES:
> title_i18n > pointer to page title
> [optional] theme > theme to set on page
> [optional] collapsed > show collapsed by default
> [optional] mini > show small version
> [optional] content_theme > theme for content section
> [optional] class_list > custom classes to set
> [optional] expanded_icon > expanded icon (default "minus")
> [optional] collasped_icon > collapsed icon (default "plus")
> [optional] form > contains a form [not implemented]
> [optional] form_config > form configuration [not implemented]
> [optional] href > href to set on the hide/show toggle
> [optional] title_i18n > pointer to hide/show toggle hint
> [optional] iconpos > position of icons
The collapsible will return an object with the following properties:
````
{
"fragment": [container_element],
"child_selector": [selector to append children to]
}
````
------------------------------ HEADER ----------------------------------------
The header should always be a global element, because it can be shown first
and is automatically updated with every page change.
The options for a header are somewhat complicated, because both title and
images need to be supported along with button controlgroups on both sides.
While the controlgroups are set upfront, title and image must be added
dynamically at the correct position. And JQM only supports one button per
side by default.
The button controlgroups can contain any type of links and select elements.
The header may also have subheaders, set as a child element (see example).
<!>
Note: More than 2 buttons per controlgroup should not be used
<!>
To add a controlgroup with buttons, simply set the children array to this:
````
children: [{
"generate": "widget",
"type": "controlgroup",
"property_dict": {
"direction": "horizontal"
},
"children": [ [buttons here] ]
}]
````
Example of header:
````
{
"generate": "widget",
"type": "header",
"property_dict" : {
"id": null,
"image": null,
"data_url": null,
"fixed": null,
"class_list": null,
"theme": null,
"class_list:" null,
"reference": null,
"title_i18n": null,
"section_list": null,
"add_content": null
},
children: [{
"generate": "widget",
"type": "controlbar",
"property_dict": {
"class_list": "ui-subheader"
},
"children": []
}]
}
````
NOTES:
> [optional] id > id to set on global-header (= default)
> [optional] image > src of image to load as center image
> [optional] data_url > used to generate id
> [optional] fixed > fixed header or not
> [optional] class_list > custom classes to set
> [optional] theme > theme to set on the header
> [optional] reference > reference of parent dyno to set [don't use]
> title_i18n > pointer to title text
> [optional] section_list > describe sections eg.["first", "last"]
> [optional] add_content > integer, inject title/image after this element
The handling with add_content and section_list still is bad. A scheme would
proably be better.
The header return the following object (also to complex):
````
{
"fragment": [container_element],
"child_selector": [selector to append children to]
"target": [method to wrap children in controlbar],
"target_selector": ["first"|"last" fragment child to add button group to],
"spec": {
"img": [boolean],
"src": [image scr],
"title_i18n": [image alt lookup pointer],
"section_list": ["scheme" of header],
"add_content": [position where to inject element]
}
````
--------------------------- CONTROLGROUP -------------------------------------
A simple widget. The only exception is the Radio/Checkbox field creating a
group of radios/checkboxes, which will be rendered as controlgroup requiring
a custom legend/label element. The configuration is only passed internally
though.
In general, the controlgroup will take all type of link and input/select
elements and render them accordingly.
Example:
````
{
"generate": "widget",
"type": "controlgroup",
"property_dict" : {
"id": "null",
"direction": null,
"class_list": null,
"mini": null,
"persist": null,
"label": {
"title_i18n": null,
"text_i18n": null
},
"theme": null,
"map_children:" null
},
children: []
}
````
NOTES:
> [optional] id > id to set on the controlgroup [don't use]
> [optional] direction > force "horizontal"
> [optional] class_list > custom classes to set
> [optional] theme > theme to set on the controlgroup
> [optional] mini > render as mini widget
> [optional] persist > don't remove on update of parent gadget
> [optional] map_children > method to call before generating children
> [optional] label > configuration of label object (only used with
group of radio/checkboxes, used internally)
To generate children, the controlgroup will return a generator function
analogous to listview, table and other more complex elements.
The controlgroup will return the following response object:
````
{
"fragment": [container_element],
"child_selector": [selector to append children to],
"child_constructor": [method to call to generate children],
"child_mapper": [method to call before generating children][> map_children!]
}
````
------------------------------ POPUP -----------------------------------------
Because excessive use of popups slow down an application popups and panels
can be used globally with content being swept in and out.
This is done by adding an action to the button triggering the popup to open
(along with a class "action") and adding a custom action pointing to the
file to load. For example:
````
{
"type":"a",
"direct": {"href": "#global-user", "className":"responsive translate action"},
"attributes": {"data-rel":"panel", "data-action":"set_login", "data-i18n":"global_dict.login"}
}
````
and the action inside map.handlers
````
"set_search": function (obj) {
factory.util.setDynamicPointer(obj, "ui_panel_detail_search");
}
````
This will set a content pointer to the file "ui_panel_detail_search.json",
which will be loaded into the popup before the popup opens.
The content will remain in the popup until a new pointer is called, so
rendered content will not be rendered again.
Example:
````
{
"generate": "widget",
"type": "popup",
"property_dict" : {
"id": "null",
"data_url": null,
"class_list": null,
"theme": null,
"transition": null,
"overlay_theme": null,
"shadow": null,
"tolerance": null,
"position": null
},
children: []
}
````
NOTES:
> [optional] id > if not specified defaults to global-header or uuid
> [optional] data_url > used to specifiy the id of local popups
> [optional] class_list > custom classes to set
> [optional] theme > theme to set on the controlgroup
> [optional] transition > transition to use when opening the popup
> [optional] overlay_theme > apply background overlay to popup
> [optional] shadow > method to call before generating children
> [optional] tolerance > minimum boudary from side of screen
> [optional] position > position to window (default) or clicked element.
The controlgroup will return the following response object:
````
{
"fragment": [container_element],
"child_selector": [selector to append children to],
"placeholder": [div-element to insert at position of popup (by JQM)]
}
````
------------------------------ FOOTER ----------------------------------------
More or less analogous to the header, only it can contain different types of
elements without the positioning order.
Example:
````
{
"generate": "widget",
"type": "popup",
"property_dict" : {
"id": "null",
"data_url": null,
"class_list": null,
"theme": null,
"fixed": null,
"overlay_theme": null,
"shadow": null,
"tolerance": null,
"position": null
},
children: []
}
````
NOTES:
> [optional] id > if not specified defaults to global-header or uuid
> [optional] data_url > used to specifiy the id of local popups
> [optional] class_list > custom classes to set
> [optional] theme > theme to set on the controlgroup
> [optional] fixed > the footer should be fixed
> [optional] reference > reference of parent dyno
The controlgroup will return the following response object:
````
{
"fragment": [container_element],
"child_selector": [selector to append children to]
}
````
------------------------------ PANEL -----------------------------------------
The panel behaves similar to the popup. It can be set globally and content
can be swapped in and out based on an action declared on the button toggling
the panel.
Panel Example:
````
{
"generate": "widget",
"type": "popup",
"property_dict" : {
"id": "null",
"data_url": null,
"class_list": null,
"theme": null,
"position": null
"reference": null,
"local": null,
"close": null
},
children: []
}
````
NOTES:
> [optional] id > if not specified defaults to global-panel or uuid
> [optional] data_url > used to specifiy the id of local panels
> [optional] class_list > custom classes to set
> [optional] theme > theme to set on the footer
> [optional] position > whether to add a panel left or right side
> [optional] reference > reference to parent gadget
> [optional] local > panel is local to page (vs global)
> [optional] close > panel should have a close button on mobile
The footer will return the following response object:
````
{
"fragment": [container_element],
"child_selector": [selector to append children to]
"target": [wrapper for child element to be generated]
"target_selector": [fragmentChild to append panel to],
"closer": [close button
"add_on_page": spec.local
}
````
------------------------------ FORM ------------------------------------------
This widget will create a form container. There are a lot of things probably
not needed anymore, but currently still in.
Example:
````
{
"generate": "widget",
"type": "form",
"property_dict" : {
"id": "null",
"update": null,
"reference": null,
"class_list": null,
"block_identifier": null,
"secure": null,
"secure_hash": null,
"captcha": null,
"reverse": null,
"map_children": null
},
children: []
}
````
NOTES:
> [optional] id > if not specified, form will get uuid
> [optional] update > update will only create a fragment
> [optional] reference > reference of parent gadget
> [optional] class_list > string of classes to add to the form
> [optional] block_identifier > [don't use] custom parameter
> [optional] secure > [don't use] custom parameter to secure
> [optional] secure_hash > [don't use] custom parameter
> [optional] captcha > [don't use] add captcha to form
> [optional] reverse > [don't use] needed for captcha
> [optional] map_children > run this method over all children
The form will return the following response object:
````
{
"fragment": [container_element],
"child_selector": [selector to append children to],
"child_mapper": [method to run over children]
"placeholder": [captcha]
"target": [wrapper for all form children]
"target_selector": [selector for wrapper ~ firstElementChild/firstChild]
"reverse": [boolean to build form reverse]
}
````
---------------------------- CAROUSEL ----------------------------------------
This widget is self-made and still buggy. Should work for basic usage though.
Can be used with static and dynamic data (mapping done with listItem method,
because the base element is "<li>").
Note that children will be generated as items (see below). Items are:
````
{"type": "item", "content": <rendered HTML> }
````
Example:
````
{
"generate": "widget",
"type": "carousel",
"property_dict" : {
"handles": "null",
"thumbnails": null,
"controller_id": null,
"id": null,
"class_list": null,
"shadow": null,
"inset": null,
"corners": null,
"theme": null,
"theme": null,
"total_rows": null,
"length": null,
"map_children": null,
"dynamic": null,
"reference": null
},
children: []
}
````
NOTES:
> [optional] handles > add left/right buttons to carousel
> [optional] thumbnails > create thumbnail mode of carousel
> [optional] controller_id > id of the element containing enhanced radios
> [optional] id > id of the carousel
> [optional] class_list > custom classes to add to wrapper
> [optional] inset > add inset to carousel
> [optional] corners > add corners to carousel
> [optional] theme > theme for carousel
> [optional] total_rows > [internal] total number of items
> [optional] length > [internal] total number of items fallback
> [optional] map_children > method to run over children,
> [optional] dynamic > slides will be generated from dynamic data
> [optional] reference > reference of parent dyno
The carousel will return the following response object:
````
{
"fragment": [container_element],
"child_selector": [selector to append children to],
"child_constructor": [method to generate children],
"child_mapper": [method to run over children before generating],
"base": [base element of children]
}
````
---------------------------- LISTVIEW ----------------------------------------
The listview can be used with dynamic and static data (mapping done through
method "listItem", which converts {data per item} into {config per item} which
the generator then converts into HTML (details see [API Portal Type Data 1.7]
(www)).
Note that children will be generated as items (see below). Items are:
````
{"type": "item", "content": <rendered HTML> }
````
Also, there are special options and content elements settable on a listview
which receives data from a query. These are shown in the items section (below)
Example:
````
{
"generate": "widget",
"type": "listview",
"property_dict" : {
"filter": null,
"update": "null",
"input": null,
"text_i18n": null,
"numbered": null,
"id": null,
"dynamic": null,
"inset": null,
"corners": null,
"reference": null,
"map_children": null,
"class_list": null
},
children: []
}
````
NOTES:
> [optional] filter > auto generate a filter to search records
> [optional] update > if true only creates fragment, not <ul>
> [optional] input > id of related search input field
> [optional] text_i18n > pointer to related search input field
> [optional] numbered > create a numbered list
> [optional] id > id to set on <ul>
> [optional] dynamic > list items will be generated dynamically
> [optional] inset > set inset on the listview
> [optional] corners > set corners on the listview
> [optional] reference > reference of parent dyno
> [optional] map_children > method to run over children
> [optional] class_list > string of classes to set on <ul>
The listview will return the following response object:
````
{
"fragment": [container_element],
"child_selector": [selector to append children to],
"child_constructor": [method to generate children],
"child_mapper": [method to run over children before generating],
"base": [base element of children]
}
````
----------------------------- TABLE ------------------------------------------
The table widget is heavily customized compared to the original JQM table. In
columntoggle mode you can suppress the toggle popup (too much unused UI).
Also a table can be wrapped with top and bottom grids, which each can contain
up to 5 items. To add an item to a table grid, specify the number of grid slots
and then declare controlbar widgets with a property slot:[number] of where to
put the element.
This way a table can have button menus, searchbar, captions, pagination inside
it's grid-wrappers.
Note that children will be generated as items (see below). Items are:
````
{"type": "item", "content": <rendered HTML> }
````
Also, there are special options and content elements settable on a listview
which receives data from a query (see below):
Example:
````
{
"generate": "widget",
"type": "listview",
"property_dict" : {
"inset": null,
"filter": null,
"input": null
"mode": null,
"toggle_popup": null,
"wrap": null,
"top_grid": null,
"bottom_grid": null
"sorting": null,
"dynamic": null,
"map_children": null,
"update": null
"class_list":null,
"id": null,
"scheme": null
},
children: []
}
````
NOTES:
> [optional] inset > set inset on the table
> [optional] filter > auto generate a filter to search records
> [optional] input > id of related search input field_name
> [optional] mode > set to columntoggle to get a listbox
> [optional] toggle_popup > set false to suppress popup
> [optional] wrap > wrapper (top/bottom/both)
> [optional] top_grid > number of slots in top grid
> [optional] bottom_grid > number of slots in bottom grid
> [optional] sorting > add sorting buttons on cells with data-sort="true"
> [optional] dynamic > children will be generated from dynamic data
> [optional] reference > reference of parent dyno
> [optional] map_children > method to run over children
> [optional] class_list > string of classes to set on <ul>
> [optional] update > will only generate a wrapper fragment
> [optional] id > id of the table
> [optional] scheme > scheme used to generate table header
The table will return the following response object:
````
{
"fragment": [container element <table>/fragment],
"child_selector": [child selector <tbody>/fragment],
"child_constructor": [method to generate children],
"child_mapper": [mapper to run over data before calling child_constructor],
"base": [base element tr],
"count": [counter used for no-items (need column count)
}
````
------------------------ LINK BUTTONS ----------------------------------------
JQM does no longer render link buttons and asks users to ask classes themselves.
To make a link button, use the following template:
````
{
"type": "a",
"direct": {
"className": "ui-btn ui-btn-icon-[iconposition] ui-icon-[icon] ui-shadow ui-corner-all ui-theme-inherit"
"href": "#"
},
"attributes": {"data-i18n": [dict]}
}
````
------------------------------------------------------------------------------
Dynos
------------------------------------------------------------------------------
Dynos are the queries to run on the existing data. At some point the dyno will
be integrated into a widget, so it can be run for a widget and inherit to all
child elements.
At this point however, this is not possible so dynos need to be stand-alone
elements with their own type.
Here is a complex example with the hopefully complete API below.
````
{
"portal_type_source": "Service",
"portal_type_title": "service",
"portal_type_mapper": "installed_services",
"initial_query": {"include_docs": true, "limit":[0,8]},
"view": "web_view",
"property_dict": {
"initial_query_url_identifier": "reference_computer",
"dynamic_children": [0],
"wrap_gadget": 2,
"link": true,
"link_identifier": "_id",
"link_core": "couscous/bar"
"caption": {
"slot": 1,
"text_i18n":"portal_type_dict.software_dict.text_dict.installed_services"
},
"pagination": {
"slot": 2,
"option_list": [
{"value": "8", "text_i18n":"portal_type_dict.invoice_dict.text_dict.8"},
{"value": "16", "text_i18n":"portal_type_dict.invoice_dict.text_dict.16"},
{"value": "32", "text_i18n":"portal_type_dict.invoice_dict.text_dict.32"},
{"value": "64", "text_i18n":"portal_type_dict.invoice_dict.text_dict.64"}
]
},
"no_items": {
"message": "No Test Page found.",
"message_i18n": "portal_type_dict.test_page_dict.text_dict.empty"
},
"search": {
"text_i18n": "portal_type_dict.test_page_dict.text_dict.search",
"info_list": ["records", "filter"]
},
"allow_new": true,
"force_new": false,
"skip_total_records": true,
"direct_map": false,
"submit_to": "#home"
},
"scheme":[
{
"position": "header",
"fieldlist": [
{"field": "image_url", "show": true, "priority": 4},
{"field": "title", "show": true, "priority": 5},
{"field": "version", "show": true, "priority": 2},
{"custom": true, "text": "Status", "text_i18n": "portal_type_dict.software_dict.text_dict.status", "show": true, "priority": 5},
{"field": "usage", "show": true, "priority": 3}
]
},
{
"position": "body",
"fieldlist": [
{"field": "image_url", "show": true, "priority": 4},
{"field": "title", "show": true, "priority": 5},
{"field": "version", "show": true, "priority": 2},
{"field": true, "show": true, "priority": 5, "status": true},
{"custom": "usage", "show": true, "priority": 3}
]
}
],
"children": []
}
````
NOTES:
> "portal_type_source": [string]
This is the name of the portal type to use inside the storage. Files will
be stored and searched with whatever is specified here (example "Web Page")
> "portal_type_title": [string]
The title to use in all references to this portal type (example: "web_page")
> "portal_type_mapper": [string][depreciated - SLAPOS UI only]
A method to run to assemble custom queries (now in ERP5 storage)
> "initial_query": [object]
Storage query object to run. Will be merged with queries passed through
the URL (latest branches) only.
> "view": [string][depreciate?]
Necessary for ERP5 storage, unused otherwise
> "property_dict": [object]
More properties... probably everything will be in here at some point
> "initial_query_url_identifier": [column]
If the url is #foo/1234 and url_identifier is set to _id, the storage
will query: portal type = portal_type_source AND _id = 1234!
> "dynamic_children": [array][depreciated]
The query result should be passed to the [n] element. Now using
dynamic: true on all children, that want results
> "wrap_gadget": [integer]
Create a wrapping <div> recommended. 1 = 100% width, 2 = 50%, 3 = 33%
> "link": [boolean]
Records should have links (works on listview and tables)
> "link_identifier": [column]
Use this record field as link identifier (default _id)
> "link_core": [string]
Use this link core instead of the current url location
> "caption": [object]
Add a caption (to listview and tables (specifiy slot, otherwise it
will follow the table.))
> "check": [boolean]
Tables rows and list items will have a checkbox.
> "radio": [boolean]
Table rows and list items will have a radio button.
> "mergable": [boolean]
Table columns should be mergable
> "sortable": [boolean]
Table rows and list items should be sortable.
> "pagination": [object]
Add pagination (to listview or tables (specifiy slot, otherwise it
will follow the table.). You can specifiy the options of the select
as well as the buttons to display. class_list is also supported to
add custom css. This will probably end up as a widget at some point.
<!>
This should work out of the box. Nothing to do!
<!>
> "search": [object]
Add a search field to search the records displayed (listview/table).
Should be preferred over declaring data-filter or providing a filter
handmade.
The info_list object sets the info to be displayed. Possible values are
"search" (results), "selected" (selected records), "sorted" (sorting
criteria).
<!>
This works out of the box. Nothing to do
<!>
> "no_items": [object]
Provide a custom handler in case the query returns no results.
> "submit_to": [string]
Page to go to after a form submission/action. __id__ will be replaced
with the respective element id
> "allow_new": [boolean]
No records will show an empty record (form only)
> "force_new": [boolean]
Don't query, just show empty record (form only). Use for new records.
> "skip_total_records": [boolean]
Don't query for total records (when fetching single item or new records
for example).
> "direct_map": [boolean]
Allows to directly map the record fields into the HTML using the
setParam method in logic.
> "scheme": [array]
This tells the renderer how to convert the query results. Every widget
can have a scheme. Currently there are schemes for listview, tables, forms
and carousel.
A scheme will always consist of a "position" identifier:
> table: "header", "body", "footer" of a table
> listview: "left", "center", "right" of a list item
> form: "left", "right" (50%) and "center": 100%
> carousel: "content"
And a field_list array, which contains objects with the fields to add
````
{"field": "title"}
````
The "field" key tells the renderer to use a field from a record. You
can of course add custom fields or normal HTML objects to render along
within a form. See the tutorial for details ([Create App](www)).
Note that a table widget may have additional properties on a field. These
are:
> "priority": [integer]
1 = high priority to show, 7 = low priority to show
> "persist": [boolean]
always show column
> "sort": [boolean]
Create a sortable button
> "merge": [boolean]
Don't show this column. It will be merged.
> "mergeWith": [column]
Column to merge with.
> "merge_text_i18n": [string]
Pointer to text to display on merged column
Once the query is run, the results are mapped to the ALLDOCS response format,
so you can do GET, ALLDOCS, ALLDOCS + select_list and will always receive the
same type of response object.
<!>
Dynamic data will be wrapped inside a <div> with the class "dyno" and a UUID
This UUID will be passed as data-reference to all children. Also, the dyno
will have a state object declared on itself, so you can do:
````
document.getElementByID(reference).state
````
to retrieve configuration and query information of a dyno. This is the way
pagination and other built-in widgets work. On clicking a pagination button
eg. "next", the data-reference is used to fetch the dyno. In it's state we
modify the current query limit and then trigger an update of a gadget by
passing state back to the renderer.
------------------------------------------------------------------------------
Items
------------------------------------------------------------------------------
Items are a special type of object as they are the result of dynamic data
being run through a mapping method and child constructor.
To see how data returned is converted into items, check [API portal type data
> rendering](www).
==============================================================================
3. Modules
==============================================================================
This API shows how to add modules.
------------------------------------------------------------------------------
TOC
------------------------------------------------------------------------------
1. Modules have nothing to do with require!
2. Module API
3. Existing modules
4. Plugin custom options
------------------------------------------------------------------------------
1. Modules have nothing to do with require
------------------------------------------------------------------------------
Modules are needed whenever plugins need initialization "by hand". For example
JIO will not do anything until "createJIO" is called while jQuery will be
available simply by adding the file to index.html.
This is the reason, we also have modules for all plugins that need custom
initialization.
Currently initialization only works in application init, bcause modules are
no longer part of the rendering loop. They will be added back again, so that
specific portal types can request specific actions for example.
Modules will be available through the internal app object.
------------------------------------------------------------------------------
2. Module API
------------------------------------------------------------------------------
Two examples (jio and i18n initialization):
EXAMPLE: defining JIO = storage
````
{
"type": "jIO",
"set_on": "storage_dict",
"initializer": "createJIO",
"modernizr": ["blobconstructor", "filereader"],
"property_dict": {
"name_dict": {
"gadgets": "gadgets",
"configuration": "configuration",
"data_type": "portal_type",
"settings": "settings"
},
"storage": true,
"force_sync": true,
"force_field_definitions": true,
"skip_total_records": true
},
"scheme": [{
"property_dict": {
"type": "local",
"username": "slapos",
"application_name": "settings"}
}, {
"set_name": "items",
"property_dict": {
"type": "replicate",
"master": 0,
"storage_list": [{
"type":"erp5",
"username":"slapos",
"url": "https://slapos.vifib.com/hateoas",
"application_name": "items"
}, {
"type": "local",
"username": "slapos",
"application_name": "items"
}]
}
}],
"children": []
}
````
EXAMPLE: defining i18n = translations
````
{
"type": "i18n",
"set_on": "lang_dict",
"initializer": "init",
"handler": "language",
"property_dict": {
"use_browser_language": false
},
"scheme": [{
"property_dict": {
"lng": "fr-FR",
"load": "current",
"fallbackLng": "en-EN",
"resGetPath": "lang/__lng__/__ns__.json",
"ns": "dict",
"getAsync": false
}
}],
"children": []
}
````
All plugins have to follow the following API:
API:
"type":
Fallback name of the plugin if neither scheme > set_name or
scheme > property_dict > application_name are defined.
"modernizr": [optional][requires modernizr.js in index.html]
It is possible to define an array with features to test for before
intializing a plugin. For example, jIO requires "blobConstructor", which
the app will test for before intializing. Throws an error if not
available.
"set_on": [optional]
Name of object the plugin will be set on internally. "foo", will result
in app.foo = [plugin object].
"initializer":
Name of method to call to initialize the plugin
"handler" [optional][should not be used]
Custom handler for plugins, in case some properties can only be set at
runtime (eg. force_browser_language for i18n). Would be nice to do this
more generic.
"property_dict":
Plugin specifc properties. For storage see [API storage](), for translations
[API i18n]()
"scheme":
An array with plugin instances to generate. For example, storage.json will
include two schemes to setup 2 instances of JIO. The properties inside
a scheme are the plugin initialization properties (see API plugin aswell)
"children": [optional][not implemented]
Currently not used. It should at some point be possible to specify
plugin specific content (eg. translation button) and target to inject.
------------------------------------------------------------------------------
3. Existing Modules (plugins)
------------------------------------------------------------------------------
Currently the following modules are used inside the renderer:
(1) JIO - storage
(2) i18next - translations
(3) validval - form validation
The module syntax can also be used to add custom property objects to the
application, for example:
````
{
"type": "path_dict",
"property_dict": {
"data": "data/",
"home": "#home"
}
},
{
"type": "status_dict",
"property_dict": {
"loader": true,
"loader_theme": "slapos-black"
}
}
````
The first module will set a "path_dict" object on app while
the second module defines a "status_dict". Both will be depreciated at some
point and handled through defaults (path) and widgets (loader)
------------------------------------------------------------------------------
4. Plugin Custom Options
------------------------------------------------------------------------------
-------------------------------- JIO ---------------------------------------
JIO is initialized with it's normal configuration (see documentation at
[jio website]().
Additional properties used internally:
> "storage": [depreciated?]
Flag telling renderer this is a storage. Should work without.
> "skip_total_records": [optional]
Prevent querying for total records on all queries. Normally a query will
trigger two queries (total & query). This flag skips the query for total
records. The same option exists on a specific query, so it can be set
where needed.
> "force_field_definitions": [optional]
Always use the "select_list" option on all queries. Normally not
specifying "select_list" would return all fields of a record
while "select_list": ["foo"] would only return column "foo". This option
forces select_list with values being set from the specific
form falling back to all fields defined on a portal_type.
> "force_sync": [not implemented]
Force synchronization on all queries. Only used when working with
(hacked) replicate storage and not implemented yet.
> "allow_sample_data": [optional]
Use this option to add dummy data to an application. This will activate
two steps in rendering loops for all queries. First is a check for a
record with portal_type being queried. If no record is found, it will
load and store a sample file in (specified, but should be loca-)storage
before running the actual query.
This still needs a flag per portal_type to indicate that sample was
loaded to prevent re-testing on subsequent queries.
> "name_dict":
A dictionary of strings to use internally. Currently necessary, but will
be removed at some point.
Scheme-specific API:
The following are JIO specific overrides which are used.
> "set_name": [replicate storage]
A replicate storage does not have a name (only substorages have). Use
this option to set an internal name on replicate storages.
> "master": [replicate storage]
In the slapos-offline branch, I set a master-slave relationship because
for exapmle, on a POST I need to have the id of an object generated by
the master storage (ERP5) and then run the jio command on the slave
storage (localStorage). This option declares which storage is the master
(with 0 being the first storage in schemes).
Example: defining JIO = storage
````
[{
"type": "jIO",
"set_on": "storage_dict",
"initializer": "createJIO",
"property_dict": {
"name_dict": {
"gadgets": "gadgets",
"configuration": "configuration",
"data_type": "portal_type",
"settings": "settings"
},
"storage": true,
"allow_sample_data": true,
"force_sync": true,
"force_field_definitions": true
"skip_total_records": true
},
"scheme": [{
"property_dict": {
"type": "local",
"username": "slapos",
"application_name": "settings"}
}, {
"set_name": "items",
"property_dict": {
"type": "replicate",
"master": 0,
"storage_list": [{
"type":"erp5",
"username":"slapos",
"url": "https://slapos.vifib.com/hateoas",
"application_name": "items"
}, {
"type": "local",
"username": "slapos",
"application_name": "items"
}]
}
}],
"children": []
}]
````
-------------------------------- i18n ---------------------------------------
i18n is initialized with a default configuration like this (Details, see
[i18next website](www.i18next.com)):
````
{
"use_browser_language": true,
"lng": "en-EN",
"load": "current",
"fallbackLng": "en-EN",
"resGetPath": "lang/__lng__/__ns__.json",
"ns": "dict",
"getAsync": false
}
````
NOTES:
> "use_browser_language": [boolean]
The browser language is evaluated on loading the plugin
> "lng":[string]
Initial language
> "load":[string]
Only load the current langauge or all languages
> "fallbackLng": [string]
Fallback language to use in case default language is not available
> "resGetPath": [string]
Path where to find the language dict files
> "ns": [string]
Namespace for the language dict files
> "getAsync": [boolean]
Set to false, because currently we have to wait for the file to load
in order to translate inital content text elements. Should eventually
be attached to a then() step.
Additional properties used internally:
> "force_browser_language": [boolean]
Normally the default language (lng) is used. Forcing the browser language
will result in setting the default language to the browser language and
using this language as default instead.
==============================================================================
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.
==============================================================================
7. Creating a simple Application
==============================================================================
This is a short tutorial showing how to set up a two page application,
generate custom content in a popup and create a new record.
------------------------------------------------------------------------------
TOC
------------------------------------------------------------------------------
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
------------------------------------------------------------------------------
1. Basics
------------------------------------------------------------------------------
Applications will be single page applications, which means you only have to
provide a skeleton for the first page. It will be rendered by the generator.
The page (currently) must include all JS/CSS files required as well as your
starting jQuery Mobile page with empty content container.
A sample index.html page might look like this (notes below):
````
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no" />
<title></title>
<link rel="stylesheet" href="ext/libs/jquery-mobile/jquery-mobile.css">
<link rel="stylesheet" href="ext/libs/jquery-mobile/extensions.css">
<link rel="stylesheet" href="ext/plugins/fontawesome/MOD_fontawesome.css">
<link rel="stylesheet" href="css/themes.css">
<link rel="stylesheet" href="css/css.css">
<!-- JQM -->
<script type="text/javascript" src="ext/libs/jquery/jquery.js"></script>
<script type="text/javascript" src="ext/libs/jquery-mobile/jquery-mobile.js"></script>
<script type="text/javascript" src="ext/libs/jquery-mobile/extensions.js"></script>
<!-- JIO -->
<script type="text/javascript" src="ext/libs/jio/URI.js"></script>
<script type="text/javascript" src="ext/libs/jio/sha256.amd.js"></script>
<script type="text/javascript" src="ext/libs/jio/rsvp-custom.js"></script>
<script type="text/javascript" src="ext/libs/jio/jio.js"></script>
<script type="text/javascript" src="ext/libs/jio/localstorage.js"></script>
<script type="text/javascript" src="ext/libs/uritemplate/uritemplate-min.js"></script>
<!-- 3rd party plugins -->
<script type="text/javascript" src="ext/plugins/validval/jquery.validVal.js"></script>
<script type="text/javascript" src="ext/plugins/i18next/i18next.js"></script>
<!-- stuff happens here -->
<script type="text/javascript" data-storage="data/storages.json" data-config="data/global.json" src="js/erp5_loader.js"></script>
</head>
<body>
<div data-url="home" data-role="page" data-theme="slapos-white">
<div class="ui-content">
<!-- nothing to see here -->
</div>
</div>
</body>
</html>
````
Notes:
> CSS
First include all necessary CSS files. The example uses jQuery Mobile
with some extensions (custom widgets), custom themes, a customized version
to support Fontawesome icons in jQuery Mobile as well as some custom css
> JS Jquery, JIO
After that include core JS files in the correct order. jQuery Mobile and
extensions require jQuery, while localstorage requires jio, rsvp, etc.
> JS 3rd party
Then add 3rd party plugins used. In the generator we use i18n by default
so add the plugin along with a form validation plugin (validval) here
> Renderer
Finally add the renderer file.
<!>
The renderer must include two data-attributes:
data-storage: path to the storage modul definition, initializes jio
data-global: path to the global application configuration
Both files are currently mandatory
<!>
------------------------------------------------------------------------------
2. Defining Storage
------------------------------------------------------------------------------
storage.json is loaded using the "data-storage" attribute set on index.html
The "storage" is rendered as a module (method: app.init.config), so at
this point it can only be done on application initialization (see also
[API Module](www)). Storage sets up JIO as application storage and looks
like this (Notes below):
EXAMPLE: storage.json
````
[{
"type": "jIO",
"set_on": "storage_dict",
"initializer": "createJIO",
"property_dict": {
"name_dict": {
"gadgets": "gadgets",
"configuration": "configuration",
"data_type": "portal_type",
"settings": "settings"
},
"storage": true,
"allow_sample_data": true,
"force_sync": true,
"force_field_definitions": true
},
"scheme": [{
"property_dict": {
"type": "local",
"username": "slapos",
"application_name": "settings"}
}, {
"property_dict": {
"type": "local",
"username": "slapos",
"application_name": "items"}
}],
"children": []
}]
````
NOTES:
> the storage module is set to be available on app.storage_dict
to initialize "createJIO" is called
> property_dict includes configuration options for how the storage is
used internally (see [API storage](www) for details
> scheme includes the storages to set, so in this case we create two JIOs,
one is named "settings", one is called "items". Both storages reside
in localStorage.
> the storage module must be wrapped in an array (will be default for all
elements at some point)
<!>
To see other examples as well and the API for the storage properties
see [API storage](www) and [API module](www)
<!>
------------------------------------------------------------------------------
3. Defining Global Elements
------------------------------------------------------------------------------
config.json is loaded using the "data-config" attribute set on index.html and
contains 3rd party plugin declaration, internally used property_dicts and/or
elements that should be available globally (header/footer/panels...).
This is a short and explained example (Notes below):
EXAMPLE: global.json from slapos-ui-offline branch
````
[
{
"type": "i18n",
"set_on": "lang_dict",
"initializer": "init",
"handler": "language",
"property_dict": {
"use_browser_language": false
},
"scheme": [{
"property_dict": {
"lng": "en-EN",
"load": "current",
"fallbackLng": "en-EN",
"resGetPath": "lang/__lng__/__ns__.json",
"ns": "dict",
"getAsync": false
}
}],
"children": []
},
{
"type": "status_dict",
"property_dict": {
"loader": true,
"loader_theme": "slapos-black"
}
},
{
"type": "path_dict",
"property_dict": {
"data": "data/",
"home": "#dashboard"
}
},
{
"generate": "widget",
"type": "controlbar",
"property_dict": {
"wrap": "fragment",
"target": "document"
},
"children": [{
"generate": "widget",
"type": "panel",
"property_dict": {
"theme": "slapos-black",
"close": true
},
"children": [{
"generate": "widget",
"type": "listview",
"property_dict": {
"class_list": null,
"theme": "slapos-black",
"filter":"true",
"input": "#global-search",
"map_children": "listItem"
},
"children": [
{"type": "item", "href": "#dashboard", "left": {"icon": "home"}, "center": {"text": [{"type": "h1", "text": "Home", "text_i18n": "global_dict.home"}, {"type": "p", "text": "Your Dashboard", "text_i18n": "global_dict.dashboard"}]}},
{"type": "item", "href": "#person_module", "left": {"icon":"user"}, "center": {"text": [{"type": "h1", "text": "My Account", "text_i18n": "global_dict.person"}, {"type": "p", "text": "Administration of your personal info", "text_i18n": "global_dict.person_subtitle"}]}},
{"type": "item", "href": "#invoice_module", "left": {"icon":"file-text"}, "center": {"text": [{"type":"h1", "text": "My Invoices", "text_i18n": "global_dict.invoices"}, {"type": "p", "text": "Administration of your invoices", "text_i18n":"global_dict.invoices_subtitle"}]}},
{"type": "item", "href": "#computer_module", "left": {"icon":"hdd-o"}, "center": {"text": [{"type":"h1", "text": "My Servers", "text_i18n": "global_dict.servers"}, {"type":"p", "text": "Administration of your Servers", "text_i18n": "global_dict.servers_subtitle"}]}},
{"type": "item", "href": "#service_module", "left": {"icon":"cogs"}, "center": {"text": [{"type": "h1","text": "My Services", "text_i18n":"global_dict.services"},{"type": "p", "text": "Administration of your Services", "text_i18n":"global_dict.services_subtitle"}]}},
{"type": "item", "href": "#network_module", "left": {"icon":"sitemap"}, "center": {"text":[{"type": "h1", "text": "My Networks", "text_i18n": "global_dict.networks"},{"type": "p", "text":"Administration of your networks", "text_i18n":"global_dict.networks_subtitle"}]}},
{"type": "item", "href": "#monitoring", "left": {"icon":"bar-chart-o"}, "center": {"text":[{"type":"h1", "text": "Monitoring", "text_i18n":"global_dict.monitoring"},{"type":"p", "text": "Server Status Reports", "text_i18n": "global_dict.monitoring_subtitle"}]}},
{"type": "item", "href": "#ticket_module", "left": {"icon":"question"}, "center": {"text": [{"type": "h1", "text": "Help", "text_i18n": "global_dict.help"},{"type": "p", "text": "Contact Customer Support", "text_i18n": "global_dict.help_subtitle"}]}},
{"type": "item", "href": "#about", "left": {"icon": "book"}, "center": {"text": [{"type": "h1", "text": "About", "text_i18n": "global_dict.about"}, {"type": "p", "text": "Request further information", "text_i18n": "global_dict.about_subtitle"}]}}
]
}, {
"generate": "widget",
"type": "listview",
"property_dict": {
"class_list": null,
"theme": "slapos-black",
"map_children": "listItem"
},
"children": [
{"type": "divider", "center": {"text": [{"type": "h1", "text": "Development", "text_i18n":"global_dict.dev"}]}}
]
}, {
"generate": "widget",
"type": "collapsible",
"property_dict": {
"theme": "slapos-black",
"content_theme": "slapos-black",
"text": "Test",
"text_i18n": "global_dict.test",
"inset": false,
"collapsed_icon": "dashboard"
},
"children": [{
"generate": "widget",
"type": "listview",
"property_dict": {
"class_list": null,
"theme": "slapos-black",
"map_children": "listItem"
},
"children": [
{"type": "item", "href": "#test_page_module", "left": {"icon": "file-o"}, "center": {"text": [{"type": "h1", "text": "Pages", "text_i18n": "global_dict.pages"}, {"type": "p", "text": "Create and run test pages locally", "text_i18n": "global_dict.test_pages"}]}}
]
}]
}, {
"type": "a",
"direct": {"className": "unenhanced ui-btn", "href":"http://nexedi.com", "external": true },
"attributes": {"data-i18n": "global_dict.nexedi"},
"logic": {"text": "Nexedi 2013"}
}]
}, {
"generate": "widget",
"type": "popup",
"property_dict":{
"class_list": "popup single ui-content",
"theme": "slapos-white",
"shadow": true,
"overlay_theme": "slapos-black"
}
}, {
"generate": "widget",
"type": "header",
"property_dict": {
"theme": "slapos-white",
"fixed": true,
"title": "",
"title_i18n": "global_dict.slapos"
},
"children": [{
"generate": "widget",
"type": "controlgroup",
"property_dict": {
"direction": "horizontal"
},
"children": [
{"type":"a", "direct": {"href": "#global-panel", "className":"responsive translate"}, "attributes": {"data-rel":"panel", "data-icon":"bars", "data-i18n":"global_dict.menu"},"logic": {"text":"Menu"}}
]
},{
"generate": "widget",
"type": "controlgroup",
"property_dict": {
"direction": "horizontal"
},
"children": [
{"type":"a", "direct": {"href": "#global-popup", "className":"responsive action"}, "attributes": {"data-depend":"login_state", "data-rel":"popup", "data-action":"login", "data-icon":"user", "data-i18n":"global_dict.login"}, "logic": {"text":"Login"}}
]
}]
}]
}
]
````
NOTES:
> The first module rendered is the initialization of the i18n translation
plugin (for details, see [API plugins i18n](www))
> Second, a property object is declared, called "path_dict". This will
made available internally as app.path_dict. It includes default paths
(and will be depreciated at some point)
> This is followed by another property_dict which includes the jQuery Mobile
loader properties (and will also be depricated at some point)
<!>
> After declaring modules, the global elements are defined. Global elements
are generated as children of a controlbar widget, with properties wrap =
fragment (wrap in a documentFragment vs a <div>) and target being set to
document (where to append the content).
> The content to generate includes a global jQuery Mobile header with
two button groups left and right and an image instead of a title.
For details on jQuery Mobile widgets, see [API widgets]
> The fragment containing all content is appended to the target = Document
as the first paint (quickest stuff to show by the browser)
> The rest of the first page is generated next
<!>
------------------------------------------------------------------------------
4. Defining Home Page
------------------------------------------------------------------------------
At this point all plugins should be initialized and internal handler will
call the internal parsePage method to determine which page to load and render.
Parse page will fallback to the page id specified in index.html (#home) and
will try to fetch and store home.json.
The home page definition might look like this (Notes below):
````
{
"property_dict": {},
"children": [
{
"generate": "widget",
"type": "page",
"property_dict": {
"title_i18n": "global_dict.page_title",
"theme": "slapos-white"
},
"view_dict": {
"default": [{"href": "foo_overview"}]
}
}
]
}
````
NOTES:
> The page widget is the only widget with a view_dict (see [API widgets](www))
> We only have a default view whose contents will be loaded.
> Contents for this view are in the file foo_module.json. It is also possible
to add plain content to render here (see [API navigation](www)).
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
START TUTORIAL
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
TODO:
- Display a new portal type.
- Generate a new page module and add it to the app.
- Set portal type definition the portal type definition
- Make first page level gadget showing listbox
- Make second page level gadget adding new record
- Make second page level gadget modifying record and triggering action
TIME:
- 2hrs
FIXES:
- The tutorial is based on the slapos-ui-offline branch. Some fixes had
to be made to allow working with sample data, so do the following:
> clone slapos-ui-offline
> pick commits from slapos-ui-offline-tutorial labeled TUTORIAL-FIX
TIPS:
- you can use slapos-ui-offline data > test_page_[xxx].json as templates
- the steps below include commits doing what you should do on the
slapos-ui-offline-tutorial branch, so if you get stuck, pull in the
mentioned commit and see how it's done.
- Validate all files with http://jsonlint.com/ to save time debugging.
------------------------------------------------------------------------------
5. Define New Page Module
------------------------------------------------------------------------------
> Let's do a portal type named "agents". Create a new page architecture file
similar to the one in point 4 above. Copy and name it "agent_module.json".
(Use test_page_module as a template if you want)
> The file should have 2 page levels => 2 page widgets.
> The first widget should only have a "default" view.
> The 2nd page widget should have a "default" widget and an "add" view.
> Add <p> tags to all views explaining what the views can do
(= view all agents, view profile/issue licensce, register new agent...).
Add class of "ui-content-element" to the <p> tags to make them look
nice.
> Add translations to the lang/[language]/dict.json. Translations should
go into the portal_type_dict, where you make a new "agent_dict", with
the first object being a (generic) text_dict. Add your translations there.
> To finish this part, open the global.json and find the listview with
the hardcoded "items" inside the panel widget and add a new item
with href pointing to #agent_module. Add a link headline and description
translation pointers, translations and pick a icon from
http://fontawesome.io/icons/
> Since we only have <p> tags in our view, everything should work right away
> Refresh your browser and check for agents in the side panel (I puth it
in the collapsible on the bottom of the panel). Click on the link.
COMMIT [Tutorial: first commit. Add new portal type page widget]
> Now add widgets to your page widget views.
> In our first level page widget after the <p> tag, add an object
{"href": "agent_overview"}. On the 2nd page widget in the default view,
add {"href": "agent_view"} and in the "add" view, append
{"href": "agent_new"}. These will be our three gadgets.
COMMIT [data: added references to gadgets]
> Before we create the views, we need to define the portal_type data and sample
------------------------------------------------------------------------------
6. Define New Portal Type & Sample
------------------------------------------------------------------------------
> Create two new files "agent_fieldlist.json" and "agent_sample.json".
> Add some fields to the agent_fieldlist. We don't do anything fancy, so:
(0) StringField _id
(1) StringField agent_name
(2) StringField code_name > set this field to required: false
(3) Checkbox active_duty
(4) TextareaField profile_information
(5) ListField residence_country
> Don't forget to add translations in the language files. These should now
go on portal_type_dict.agent_dict.field_list similar to the other
portal_types.
> The ListField "residence_country" items property should point to a
getCountries.json, which should be loaded when the field should be
displayed the first time.
> getCoutries.json should include this:
[
{"text_i18n":null, "class":"translate", "value":"", "selected":true},
{"text_i18n":"portal_type_dict.agent_dict.text_dict.france", "class":"translate", "value":"fr"},
{"text_i18n":"portal_type_dict.agent_dict.text_dict.uk", "class":"translate", "value":"uk"},
{"text_i18n":"portal_type_dict.agent_dict.text_dict.belgium", "class":"translate", "value":"be"},
{"text_i18n":"portal_type_dict.agent_dict.text_dict.usa", "class":"translate", "value":"us"}
]
> Add the respective translations in the language file, too (on text_dict).
> In the sample file, create 5 agents... how about James Bond, Clever&Smart,
Spy&Spy, TinTin and OSS117
> If you are lazy, copy the sample.json from the commit.
COMMIT [data: added fieldlist, sample data and translations]
------------------------------------------------------------------------------
7. Create first gadget showing listbox with pagination
------------------------------------------------------------------------------
> Create a file named "agent_overview.json". We will show agents in a listbox.
You may use test_page_overview.json as a template.
> Query for portal type source "Agent", portal type title "agent".
Set portal_type_fields to "agent_fieldlist.json" (not longer needed in
latest branch).
> Display 3 records in your inital query, so we can try pagination.
Make the records link-able and set the _id field as link_identifier.
> To allow loading of sample data, add "allow_sample_data": true after the
initial query.
> Add pagination and a search to your query property_dict.
> wrap the gadget : 1, which should make it 50% width (this is still buggy)
> add a caption to your listbox. Don't forget to add translations for search,
caption and pagination. Set pagination to 3,6,12,24 records.
> define a scheme for the table. We will only show agent name, active_duty and
residence country in the listbox.
> Add children to the query. First set the table for dynamic data. Make
sure you set the following properties:
> dynamic: true > this table has dynamic data
> map_children: tableItem > map data into items
> class_list: "table-stroke ui-responsive"
> mode: columntoggle > JQM property
> wrap: both > create top&bottom wrapper
> top_grid: 1 > 1 slot in top wrapper
> bottom_grid: 1 > 1 slot in bottom wrapper
> toggle_popup: false > block JQM toggle popup
> You can play with the slot and top/bottom-grid. Caption, search and
pagination can be added to the wrapper by including a slot. So slot=2
in a top-grid: 1, bottom-grid: 3, would be the middle slot in the bottom.
> Before (!) the table add a link button which leads to #agent_module/add
with link text "New Agent". After the table does not work in this branch,
when updating the table...
> As JQM does not enhance links, add these classes to the link yourself:
"translate ui-btn ui-btn-icon-right ui-icon-edit ui-shadow ui-btn-inline ui-btn-slapos-black ui-link ui-corner-all"
> If you refresh the page, it should show the button a search field, the
table with your agents, caption and pagination bar. Both the search
field and pagination should work out of the box.
> You can also click on the table rows to open a profile page,
which is still empty. We do this below.
COMMIT [Fourth commit: added portal type overview with search...]
------------------------------------------------------------------------------
8. Create second gadget showing showing a form to add a new record
------------------------------------------------------------------------------
> We will now make a form to add new records. You may use the
test_page_new.json file as a template.
> Create a new file named "agent_new.json". Make sure your "add" view in
your "agent_module.json" second page widget points to this href.
> In the new file create another query. The first parts are the same, so
set portal_type_source to "Agent", portal_type_title to "agent" and
portal_type_fields to "agent_fieldlist.json"
> Make sure you add "allow_sample_data": true, otherwise on <refresh> you
will have no sample data in localstorage as it will only be loaded through
the overview page!!
> The inital query now should only return 1 record, so set the limit to 0,1
> The gadget should wrap_gadget: 2 again to be fullscreen
> Set a "submit_to" in property_dict pointing to #agent_module/__id__. This
will open the new agents profile once the form is submitted and the record
created.
> Add a "force_new": true to the property_dict to return an empty record.
> This time we will display a form. Create a scheme, with one section
"left" and "fields": "agent_name" and "residence_country".
> Overwrite the "residence_country" to be not required by adding:
"overrides": {"properties": {"required": null}}
to the field.
> Now add the children, First a form. The form should have the following properties
> dynamic: true > form should handle dynamic data
> map_children: formItem > mapper to create nice form fields
> editable: true > should do nothing...
> class_list: responsive > should also do nothing...
> secure: default > adds a timestamp, try if it work without
> secret_hash: foo > not really a secret...
> public_key: 123 > public
I always copy&paste. Something to remove in the latest version...
> As child of the form we need a submit button. Put it inside a controlgroup
widget. The submit button should have it's type set in attributes (!). It
also must have a class of "action" and an action called "new" set in
attributes "data-action". To translate the value of a input button, set
"data-i18n": "[value]pointer".
> The data-action "new" is a default action which will create a new record
by either calling PUT or POST (depending on whether an id field is present)
> That's it. When you click on your "Add Agent" button on the overview page,
you should reach the new page including your form.
> Try to submit and see if validation works. Try to submit with only the
agent_name. It should work and you should be forwarded to the new agents
profile page, which we do next.
> Go back on the overview page. Your new agent should be searchable in the
listbox.
COMMIT [Tutorial: fifth commit. Add new record creation]
------------------------------------------------------------------------------
9. Create third gadget showing single record with custom action
------------------------------------------------------------------------------
> We will now display an agents profile and provide a button to trigger
a custom action. You can work with the "test_page_view.json" file as a
template.
> The profile will just consist of a from showing all fields. We will disable
one field and provide an update button along with a custom action to
generate a licence-to-kill for our agent to be shown in the global-popup.
> We start with a query again, so set "Agent", "agent", and
"agent_fieldlist" as portal_type_source|title|fields as before.
> Make sure you add "allow_sample_data": true, otherwise on <refresh> you
will have no sample data in localstorage as it will only be loaded through
the overview page!
> The inital query should return a single record but this time we don't
force_new. Set wrap_gadget: 2 and don't set a submit_to.
> Create a scheme with left/right and center sections. Put "_id", "agent_name"
and "code_name" in the left section, "residence_country" and "active_duty"
in the right section and the "profile_information" in the center.
> As children of the query, copy&paste the form from the previous page and
controlgroup from the previous page.
> Update the button translation and add a 2nd button. The 2nd button should
be a link element whose class must include "action" and "attributes"
"data-action" set to "issue_licencse". You can copy all the classes from
above and exchange the icon to something if you want.
COMMIT [Tutorial: sixth commit. Add record view and update]
> Now let's add a custom action. We want to call the action which should
do generate a licence-to-kill token and display it in the global-popup.
> We need to modify our link button. Change the href to "href": "#global-popup"
to tell JQM to open the popup. In attributes, add the pointer for JQM
"data-rel": "popup".
> Clicking on the button will now open the popup an run our custom-action
"issue_licence". Time to write a few lines of JavaScript
> Custom actions currently have to be added to the main js code by hand.
At some point these should be loadable files. Open erp5_loader.js and
search for "map.actions ". This is the object containing all actions
like paginate, search...).
> Add your own action:
````
"issue_licence": function (obj) {
console.log(obj);
factory.util.setPopupPointer(obj, "issue");
},
````
> This will set a pointer to load "issue.json" when the popup opens.
If you check the action object "obj", you will see you get back the
clicked element. There are two types of "obj". If you click on a popup
or panel button you get a reference to that popup or panel. Otherwise
you will get a reference and state of the parent dyno including all
current configuration options and query parameters.
> Create a new file, name it issue.json and add:
````
{
"children": [
{"type": "p", "logic": {"text": "Licence to kill 4563d413 issued"}}
]
}
````
> NOTE: this way, you can load static files into the popup
> Alternatively to post to a server, you should modify the link to NOT
open the popup. Instead provide a data-method=post and the href you want
to post to. Not opening the popup will cause the "obj" to contain
a reference to the parent gadget including all query and config parameters
so it would be easy to build a custom URL to from the href on the link
and whatever is stored on the gadget.
> For an example, have a look at the action "destroy_installation" in
erp5_loader.js.
> Post to the url you want and you will get back an answer. To put this
into the popup you will have to hack a little bit:
> Assemble the content you want to display.
> Then do something like this in your issue_licence next then() step
(similar to generateDynamicContent method)
````
return RSVP.resolve([your content as documentFragment]))
.then(function (content) {
// translate
map.actions.translateNodeList(content);
// append
var element = document.getElementById("global-popup")
element.innerHTML = "";
element.appendChild(content);
})
.then(function () {
// enhance...
$el = $(element).enhanceWithin();
// TODO: un-jQuery eventually... show the popup
switch ($el.data("role")) {
case "popup":
$el.popup("reposition", {"positionto": "window"}).popup("open");
break;
}
})
.fail(app.util.error);
````
> That's it. Thanks for lasting through to here.
COMMIT [Seventh commit. Add custom action]
------------------------------------------------------------------------------
END
------------------------------------------------------------------------------
{
"_links": {
"object_view": [
{"href": "http://10.0.109.248:12002/erp5/person_module/20130611-1F796/Person_view?portal_skin=Hal", "name": "View"},
{"href": "http://10.0.109.248:12002/erp5/person_module/20130611-1F796/Person_viewDetails?portal_skin=Hal", "name": "Details"},
{"href": "http://10.0.109.248:12002/erp5/person_module/20130611-1F796/Person_viewAssignment?portal_skin=Hal", "name": "Assignments"},
{"href": "http://10.0.109.248:12002/erp5/person_module/20130611-1F796/Entity_viewAccountingTransactionList?portal_skin=Hal", "name": "Transactions"},
{"href": "http://10.0.109.248:12002/erp5/person_module/20130611-1F796/Person_viewFinancialInformationList?portal_skin=Hal", "name": "Financial Information"},
{"href": "http://10.0.109.248:12002/erp5/person_module/20130611-1F796/Base_viewDocumentList?portal_skin=Hal", "name": "Documents"},
{"href": "http://10.0.109.248:12002/erp5/person_module/20130611-1F796/Base_viewEventList?portal_skin=Hal", "name": "Events"},
{"href": "http://10.0.109.248:12002/erp5/person_module/20130611-1F796/Person_viewCredential?portal_skin=Hal", "name": "Credential"},
{"href": "http://10.0.109.248:12002/erp5/person_module/20130611-1F796/Base_viewContentTranslation?portal_skin=Hal", "name": "Translation"},
{"href": "http://10.0.109.248:12002/erp5/person_module/20130611-1F796/Base_viewConsistency?portal_skin=Hal", "name": "Consistency"},
{"href": "http://10.0.109.248:12002/erp5/person_module/20130611-1F796/Base_viewHistory?portal_skin=Hal", "name": "History"},
{"href": "http://10.0.109.248:12002/erp5/person_module/20130611-1F796/Base_viewMetadata?portal_skin=Hal", "name": "Metadata"}
],
"self": {"href": "http://10.0.109.248:12002/erp5/person_module/20130611-1F796/ERP5Document_getHateoas"},
"type": {"href": "http://10.0.109.248:12002/erp5/portal_types/Person/ERP5Document_getHateoas?portal_skin=Hal", "name": "Person"}
},
"title": "Person 22\u00f6"
}
[
{type: fieldlist, span: 2, gadget: bar},
{type: fieldlist, span: 1, gadget: baz},
{type: fieldlist, span: 1, gadget: bam},
{type: tabs, span: 2, tabs: [
[
{type: fieldlist: span: 1, gadget: piz},
{type: fieldlist: span: 1, gadget: paz},
{type: listbox: span: 2, gadget: puz}
], [
{type: fieldlist: span: 2, gadget: poz},
{type: listbox: span: 2, gadget: pez},
{type: listbox: span: 2, gadget: pyz}
], [
{type: fieldlist: span: 2, gadget: abc},
]
]
},
{type: listbox: span: 2, gadget: zzz}
]
Questions:
> If tabs only include "sub-layouts", we don't need to fetch the tab-gadget
configuration, which would recursively re-run the gadget loop and
fetch all gadgets. Would make syntax coherent, but requires another
HTTP request for getting the gadget configuration.
Page layout action/jumps/tasks...
> new listview API!
"portal_type_source": "Person",
"portal_type_title": "person",
"actions": {
"jump": {
"hash": null,
"items": [
{"type": "", "href":"", "title":"", "title_i18n":""}
]
},
"action": {
"hash":null,
"items": [
{"type": "", "href":"", "title":"", "title_i18n":""}
]
},
"export": {
"hash":null,
"items": [
{"type": "", "href":"", "title":"", "title_i18n":""}
]
},
"favorites": {
"hash": null,
"items": [
{"type": "divider", "texts":[{"type":"h1", "text":"-- Bug Event workflow --", "text_i18n":""}]},
{"type": "item", "count": 46, "texts":[{"type":"h1", "text":"Bug Lines to Send", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "divider", "texts":[{"type":"h1", "text":"-- Bug Workflow --", "text_i18n":""}]},
{"type": "item", "count": 278, "texts":[{"type":"h1", "text":"Open Bugs", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 12, "texts":[{"type":"h1", "text":"Bugs to Resolve", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 617, "texts":[{"type":"h1", "text":"Bugs to Follow", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 732, "texts":[{"type":"h1", "text":"Bugs Assigned to Follow", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "divider", "texts":[{"type":"h1", "text":"-- Document Ingestion Workflow --", "text_i18n":""}]},
{"type": "item", "count": 12, "texts":[{"type":"h1", "text":"Ingested Documents", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "divider", "texts":[{"type":"h1", "text":"-- Document Publication Workflow --", "text_i18n":""}]},
{"type": "item", "count": 5, "texts":[{"type":"h1", "text":"Documents to Submit", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 29, "texts":[{"type":"h1", "text":"Documents to Review", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "divider", "texts":[{"type":"h1", "text":"-- Events Workflow --", "text_i18n":""}]},
{"type": "item", "count": 560, "texts":[{"type":"h1", "text":"Planned Events to Confirm", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 10, "texts":[{"type":"h1", "text":"Confirmed Events to Generate", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "divider", "texts":[{"type":"h1", "text":"-- Inventory Workflow --", "text_i18n":""}]},
{"type": "item", "count": 7, "texts":[{"type":"h1", "text":"Inventories to validate", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "divider", "texts":[{"type":"h1", "text":"-- Order Workflow --", "text_i18n":""}]},
{"type": "item", "count": 6, "texts":[{"type":"h1", "text":"Purchase Orders to Plan", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 1, "texts":[{"type":"h1", "text":"Purchase Orders to Confirm", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 174, "texts":[{"type":"h1", "text":"Sale Orders to Plan", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 7, "texts":[{"type":"h1", "text":"Sale Orders to Order", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 9, "texts":[{"type":"h1", "text":"Sale Orders to Confirm", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 143, "texts":[{"type":"h1", "text":"Offered Sale Orders to Follow", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "divider", "texts":[{"type":"h1", "text":"-- Packing List Workflow --", "text_i18n":""}]},
{"type": "item", "count": 112, "texts":[{"type":"h1", "text":"Sale Packing List to Prepare", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 12, "texts":[{"type":"h1", "text":"Sale Packing List to Validate", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 102, "texts":[{"type":"h1", "text":"Sale Packing List to Solve", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "divider", "texts":[{"type":"h1", "text":"-- Project Workflow --", "text_i18n":""}]},
{"type": "item", "count": 1, "texts":[{"type":"h1", "text":"Project to Open", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "divider", "texts":[{"type":"h1", "text":"-- Task Report Workflow --", "text_i18n":""}]},
{"type": "item", "count": 4, "texts":[{"type":"h1", "text":"Task Reports to Follow", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "divider", "texts":[{"type":"h1", "text":"-- Task Workflow --", "text_i18n":""}]},
{"type": "item", "count": 2, "texts":[{"type":"h1", "text":"Tasks to Validate", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 1, "texts":[{"type":"h1", "text":"Tasks to Order", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "divider", "texts":[{"type":"h1", "text":"-- Validation Workflow --", "text_i18n":""}]},
{"type": "item", "count": 3689, "texts":[{"type":"h1", "text":"Persons to Validate", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 1759, "texts":[{"type":"h1", "text":"Organisations to Validate", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 123, "texts":[{"type":"h1", "text":"Products to Validate", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 106, "texts":[{"type":"h1", "text":"Services to Validate", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 5, "texts":[{"type":"h1", "text":"Sale Trade Conditions to Validate", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 5, "texts":[{"type":"h1", "text":"Transformations to Validate", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 2, "texts":[{"type":"h1", "text":"Sale Supplies to Validate", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "count": 1, "texts":[{"type":"h1", "text":"Purchase Supplies to Validate", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "divider", "texts":[{"type":"h1", "text":"-- Other --", "text_i18n":""}]},
{"type": "item", "texts":[{"type":"h1", "text":"Update Credentials", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "texts":[{"type":"h1", "text":"Undo", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "divider", "texts":[{"type":"h1", "text":"-- User --", "text_i18n":""}]},
{"type": "item", "texts":[{"type":"h1", "text":"Preferences", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]},
{"type": "item", "texts":[{"type":"h1", "text":"Log out", "text_i18n":""}], "actions":[{"type":"link", "href":"index.html"}]}
]
}
},
"controls": {
"header": [
[
{"action":"navigate","active":"true"},
{"action":"tasks", "active": "true"}
],[
{"action":"login", "active":"true"},
{"action":"home","active":"true"}
]
],
"footer": [
{"action":"jump", "active":"true"},
{"action":"add", "active":"false"},
{"action":"remove","active":"false"},
{"action":"action","active":"true"},
{"action":"export", "active":"true"}
]
},
"status": {},
"breadcrumbs": {}
}
\ No newline at end of file
==============================================================================
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