OML - globules-io/OGX.JS GitHub Wiki

OML, short for Object Markup Language, is a JSON based markup language to create complex UI interfaces with OGX.JS. OML is like a shadow DOM made with objects, where the objects are linked to HTML elements in the DOM.

Just as the DOM, you can cycle and find Objects, starting from any node and descending into the tree.

Loading and Caching

OML files can be loaded and cached using app.json and adding references to files in /oml. If you are hosting on IIS, you will need to add a MIME type for oml files of type application/json

Nodes

OML is composed of JSON Nodes identified by a name of format selector:Class. A complete node looks like this

 {"someSelector:someClass":{
      [some properties for the class]
      [a possible node]
 }}      

The selector part of the name of the node is the destination of the node on your page and Class is the Object to be created at that location. For instance, if we wanted to render a simple HTML template in a DIV of id myDiv, our node would be

 {"#myDiv:Templates.MyTemplate":{}}    

Now let's pretend that the HTML template holds other placeholders (other DIV's) and that we want to add other templates inside each of them, we could do

 {"#myDiv:Templates.MyTemplate":{
      "node:OML":{
          ".top:Templates.OtherTemplate":{},
          ".bottom:Templates.OtherTemplate":{} 
       } 
    }         
 }  

Considering that the MyTemplate HTML preloaded file, is as follow

 <div id="myDiv">
      <div class="top"></div>
      <div class="bottom"></div>
 </div>

You can of course as have other sub nodes in each template, or even other OGX Objects instead, with a more complex HTML template.

 {"#myDiv:Templates.MyTemplate":{
      "node:OML":{
          ".top:Templates.OtherTemplate":{
                "node:OML":{
                     "#some_id:Carousel":{
                          "id" : "my_carousel", 
                          "node:OML":[
                               {"default:Templates.WhateverTemplate":{
                                    "node:OML":{  ... ∞}                                         
                               }},
                               {"default:Templates.WhateverTemplate":{
                                    "node:OML":{  ... ∞}    
                               }}
                          ]
                     }
                }
           },
          ".bottom:Templates.OtherTemplate":{} 
       } 
    }         
 }  

Using

 <div id="myDiv">
      <div class="top"><div id="some_id"></div></div>
      <div class="bottom"></div>
 </div>

as MyTemplate

Sub-files

Subfiles can be merged into any OML node, by settings the value of the node:OML as simple string, or as OSE expression. Both lines produce the same result

 "node:OML":"myOMLFile"
 "node:OML":"{{oml myOMLFile}}"

Reusing the previous

 {"#myDiv:Templates.MyTemplate":{
      "node:OML":{
          ".top:OML": "subfileA",
          ".bottom:OML": "subFileB"
       } 
    }         
 }  

Selectors

Most of the time, a selector is a standard selector such as #myDiv or .myClass but there are also reserved selectors. Some objects do not require a selector, such as Popups and Windows because they are added by default to the object creating them.

Here are some OML selectors you will encounter across the framework

Create an object and attach it to an element given an id

  #someDiv:someObject 

Create an object and attach it to an element given a class

  .someClass:someObject     

Create an object and attach it to the default element generated by an Object

  default:someObject

Create a Float object given an id

  someObjectid:someObject

Create a Float object without an id 1.26.0+

  node:someObject   

Attach an existing detached Uxi from invoking uxi.detach();, to the default element

  default:Uxi

Responsive Break Point

  selector:Point

Scope Fork

  scope:Fork

OSE Script Fork

  some_OSE_script:Fork

Bind a property with a controller

  property:Bind

Origin allowed for a route

  origin:Route

Get/Tranform data

  someString:OSE

Note that if you are transforming a template with OSE but your script variables are all global, you can pass an empty string to force evaluation, such as

  ...,
  "template":"MyTemplate",
  "data:OSE":""

Get data from db (mongogx)

 someString:Database

Render an OML sub node based on the result of a promise returned by a global function

 some_global_function:Function

OML Node

  node:OML

OML file

  selector:OML

data for dynamic selectors

  data:OML

Placeholders

Some components will generate placeholders on their own, as they might have their own mechanic to deal with their placement. Let's take the example of a Popup. It will generate a single placeholder that is the body of itself. Now it would be very tedious to have to remember and write the name of the default selector per generated object. For this reason, the default selector exists.

Let's create a Popup OML node first. As popups are added to their parent, they do not require a selector and it is replaced by the id that the Popup will receive.

 {"myPopup:Popup":{
      "width":"50%", "height":"50%", "title":"Popup", "drag":true, "resize":true
 }}

We just created a blank popup that will have a width of 50% of its parent and 50% height of its parent. Let's now create this time a popup with a HTML template in it, in its default inner location (its body)

 {"myPopup:Popup":{
      "width":"50%", "height":"50%", "title":"Popup", "drag":true, "resize":true,
      "node:OML":[
             {"default:Templates.MyTemplate":{}}
       ]
 }}

Popups and Windows only generate 1 placeholder. But it's also possible to have multiple objects in a single location using a Container, to create, for instance, a popup with tabs

 {"myPopup:Popup":{
      "width":"50%", "height":"50%", "title":"Popup", "drag":true, "resize":true,
      "node:OML":[
             {"default:Container":{
                  "id":"someid", "tabs":true,
                  "node:OML":[
                         {"default:Templates.MyTemplate":{}},
                         {"default:Templates.MyOtherTemplate":{}}
                   ]                     
             }}
       ]
 }}

Here we have created a Popup with tabs that holds 2 templates. Clicking each tab will reveal the associated template.

By default, View and HTML (templates) both extend the Placeholder Class. It will look for default declarations in its nodes and replace them by the direct selector of the class. For instance, if you create a view that has a dynamic id and a template such as

 {"#something:Views.MyView":{
      "id":"#"+some_generated_id,
      "template":"MyTemplate",
      "node:OML":{
            "default .class_in_template:DynamicList":{...}
      }
  }};

Instead of writing the selector as "#"+some_generated_id+"default .class_in_template", we use default .class_in_template to refer to an element of class class_in_template

With OML you can create a virtually unlimited tree of Objects.

Inline OMLs 1.29.1+

You can also convert inline OML. Consider the following as the content of myoml.oml

 {"default:MyClass":{...}}

You can load this content from another OML file, by referring to its file name

 {".mySelector:OML" = "myoml"};

Then the default selector in the OML will be replaced to reflect the desired selector

 {".mySelector:MyClass" = {...}};

Break Points

On top of CSS media queries, you can change the allure of your application depending on the resolution. For that purpose, you need to use OML break points. Just like media queries, OML break points are defined by a low and a high value over the width of a parent. To render different OML nodes at different resolutions, set your OML break points this way:

 {"selector:Point":{
      "live":[ARRAY of ids of objects to reuse]
      "0-400":[OML node],
      "401-720":[OML node]
 }}

For more information about using Point, check out the dedicated section

Binding

You can bind controls to DynamicList to act as a filtering control. You can use as many controls per property as you need.

Binding a simple input element to a DynamicList

 {"#mylist:DynamicList":{
      ...,
      "name:Bind":{
           "object":"input[name=\"name\"]",
           "action":"filter",
           "mode":"in",
           "min_length":2
      }
 }}

Binding a Uxi to the list as control

 {"#mylist:DynamicList":{
      ...,
      "active:Bind":{
           "object:OSE":"{{uxi test_switch:Switch}}",
           "action":"filter",
           "mode":"eq"
      }
 }}

Binding multiple controls, in this case, filter a date by using a range (start_date and end_date)

 {"#mylist:DynamicList":{
      ...,
      "date:Bind":[
           {
                "object:OSE":"{{uxi start_date:Calendar}}",
                "action":"filter",
                "mode":"gte"
           },
           {
                "object:OSE":"{{uxi end_date:Calendar}}",
                "action":"filter",
                "mode":"lte"
           }
      ]
 }}

Scope & Fork

The scope node is a fork that redirects the OML flow depending on the end user scope. For instance, if your application supports multiple user types, and that some parts of your application is restricted to a certain scope (user type), then you can you the node scope redirect to the proper node.

 {"scope:Fork":{
      "admin":{
           [OML]
      },
      "support":{
           [OML]
      }
 }}

When this node is reached, it will fork towards the admin node if the user has the admin scope, or support if the user has the support scope.

Scope Expression 1.11.0+

Note that the scope can also be declared as a scope expression. In this case the scope is computed based on the current scope and the expression that is passed. It then returns the first matching expression

 {"scope:Fork":{
      "permA|permC":{
           [OML]
      },
      "(permA|permB)+permC":{
           [OML]
      }
 }}

OSE Scripts 1.11.0+

You can also use Fork as a condition where you can pass an OSE script and an object, such as

 {"{{$has_comments}}:Fork":{
      "data":{"has_comments":true},
      "values":{
           "true:Boolean":[OML A],
           "false:Boolean":[OML B]
      }
 }}

You can use :Boolean or :Number to convert the value from a string

Function 1.17.0+

The Function node behave like a Fork but is intended to use with Promises. Here MyFunction must be a globally available function (name/path) that must return a Promise

  {"MyFunction:Function":{
       "id" : "myFnc",
       "data" : {...},
       "success" : OML,
       "error" : OTHER_OML
  }}

Here's a dummy function available globally

  function MyFunction(args){
       let promise = new Promise(function(success, error){...});
       return promise;
  }

To retrieve the result passed to each callback (success or error), you can then use down the OML tree, the OSE tag

  "data:OSE": "{{result MyFunction}}"

Note that once the result is retrieved once, it is removed from memory and not accessible anymore

Accessing Data

If some of your nodes generate by themselves other OML node dynamically, such as with a DynamicList, you can still access the relative objects of the list and their properties by using {{&item}} to target the current element of the list. Note that it relies on the as property of the config of the DynamicList, which by default is "item".

In this example, we create, from a list of objects, a DynamicList that creates one Carousel per item of its list, and 2 views per Carousel. The second view of the Carousel will have another DynamicList based on a sub-list of the current item in the first DynamicList.

 {
      "#subscriptions > .list:DynamicList":{
           "id":"subscription_list",
           "key":"_id",
           "as":"item",
           "scroll":true,
           "display":{                               
                "oml":{                                    
                     "default:Carousel":{
                          "dots":false,
                          "node:OML":[
                                {"default:Templates.Subscription":{
                                     "data:OSE":"{{&item}}"
                                }},
                                {"default:Templates.Settings":{
                                     "id:OSE":"#settings_{{&item._id}}",
                                     "data:OSE":"{{&item}}",
                                     "node:OML":{
                                          "#settings_{{&item._id}} .list:DynamicList":{
                                               "id:OSE":"subscription_{{&item._id}}",
                                               "key":"_id",   
                                               "as":"renewal",                                               
                                               "display":{
                                                    "template":"Renewal",
                                                    "css":"renewal"
                                                },
                                               "list:OSE":"{{&renewal.subList}}"
                                           }
                                      }                                  
                                }}
                          ]
                    }
                }                               
           }
      }
 }                 

We use {{&OBJECT_ID}} to refer to another object, in our case {{&subscription_list}} which refers to the DynamicList of id "subscription_list". We also use {{&OBJECT_ID.$}} or {{&subscription_list.$}} to refer to the current item of that list.

Also note that the first DynamicList id:"subscription_list" isn't passed any data. We would pass some data to that list later on and it would render all the objects based on that oml.

Dynamic Selectors 1.9.0+

Dynamic selectors can also be resolved using the data:OML property. For instance, if you dynamically create an id and want to target the HTML element created dynamically within OML, you can target any property with data:OML.

 {'#mydiv > .something:Views.MyViews':{
     id:'#myview_'+__someobject.id,
     'node:OML':{
          '#myview_{{$id}}:Component':{
               'data:OML':__someobject
          }
     }
 }}

The same result can be achieved by pre-replacing values using OGX.Templater.jmake without using data:OML.

Note that the data:OML property has to be passed inside the OML node, and that Component is just generic naming used for the sake of the example and is not actually a valid object.

Creation

OML nodes can be added at anytime! Every UI Object in OGX.JS that extend the Uxi Class can add node to itself. To create a tree of objects from, for instance, a View

  OGX.OML.render(this, [OML node]);

Using OML vs Object

The purpose of OML nodes, is to render a tree of information through different components, where each sub component inherit the status of its parent. If you wish to only create one object that does not have sub-objects (as nodes), then you should use

 let o = OGX.Object.create(Class, Config);    

In this case the object will be instantiated but its constructor construct will not be fired and the object won't be added to any parent. You can do that manually.

 someObj.add(o);
 o.construct(someData);

Now, to create, fire construct and add to a parent automatically, use (from any Uxi)

 this.create(Class, Config);

If you want to render a tree of objects with OML, where sub nodes can also have sub nodes and are all added to their respective parent and have their constructor construct fired, then you have to use

 OGX.OML.render(this, OML);

Destroy

Any Object created from a Uxi node do not need to be destroyed, it is handled automatically from route to route. Objects created as stand alone and never attached to another Uxi node need to be destroyed. This is done by doing

 myObject.destroy();

Renaming a node

Since 1.8.0 the method replaceNodeName has been deprecated and replaced by rename

You can also rename a node using the rename method.

 OGX.OML.rename(_NODE_, _REPLACE_, _SEARCH_);

Note that _SEARCH_ defaults to default as per OML default, hence passed last. Also note that _SEARCH_ and _REPLACE_ can also be of type Array. In this case, both _SEARCH_ and _REPLACE_ are expected to be of type Array. Consider this node:

 let myNode = {
      '.someclass .otherclass:Roulette':{
           ...
      } 
 };

If you want to rename .someclass .otherclass with #someid .otherclass, do

 let newNode = OGX.OML.rename(myNode, '#someid .otherclass', '.someclass .otherclass:Roulette');

Getting node by id 1.8.3+

You can lookup a node given its id, the node can be nested as deep as you want

 let myNode = {
      ..., 
      'node:OML';
           '.someclass .otherclass:Roulette':{
                id:'my_roulette'
           } 
       }
 };

let obj = OGX.OML.getNodeById(myNode, 'my_roulette');

Getting a node class 1.29.0+

let node = {'default:Views.MyViews':{...}};

const cls = OGX.OML.getNodeClass(node); //Views.MyViews

Testing if a value is OML 1.34.0+

let is_oml = OGX.OML.isOML('hello');   //false
let is_oml = OGX.OML.isOML('{".selector:Html:{"html":"hello"}}');   //true    

Using OML sub files

If the routing node of your app.json file is being too big or you just want to organize things better, you can merge OML data to the routing, as such

In myview.oml

  {"#myId:SomeObject":{...}}

In the route to get to that view, in app.json

  {
      "routing": {
           "routes": {
                  "mystage/myroute":{
                       "oml":"myview"
                   }
            }
       }
  }

Options 1.30.0+

options are passed to OML when creating an instance of Core via new App() and are exposed as getters/setters.

max_render_time

The maximum time it should take the engine to render an OML script, expressed in int. Get or Set this value using

 const time = OGX.OML.maxRenderTime();
 OGX.OML.maxRenderTime(300);

max_depth

The maximum (recursive) depth the engine should look to resolve OSE scripts. Recommended default is 2 but then it depends on your code. Get or Set this value using

 const max = OGX.OML.maxDepth(_value_or_nothing);
 OGX.OML.maxDepth(4);
⚠️ **GitHub.com Fallback** ⚠️