B4J Tutorial [ABMaterial] B4JS - 06 The UI Connection

See for a B4JS introduction: https://www.b4x.com/android/forum/threads/abmaterial-b4js-0-9.90249/
-------------------------------------------------------------------------------

So now that we know the basics of B4JS, lets make something real using a lot of what we have learned together with some ABMaterial components.

In this tutorial, we are going to create a simple calculator where all our 'logic' happens on the browsers side.

A video of what we are going to make:

Great, so lets get started!

First we are going to create of B4JS part: the logic of the calculator. This is pretty simple and we make use of the JavaScript eval() function to do the actual calculation.
B4X:
'Class module
Sub Class_Globals
   ' use public or dim if you want to share this variable over ALL B4JS classes
   ' use private if only within this class
   Public CurrentInput As String
 
   ' to access the constants
   Public ABM As ABMaterial 'ignore
   ' so we can use an msgbox
   Public Page As ABMPage 'ignore, just to be able to run ABMPage functions
End Sub

'Initializes the object. You can NOT add parameters to this method.
'MUST be called InitializeB4JS is automatically called when using this class
Public Sub InitializeB4JS
 
End Sub

public Sub ButtonPressed(key As String) As Boolean
   Select Case key
       Case "="
           If CurrentInput <> "" Then
               CurrentInput = Page.B4JSRunInlineJavascriptMethod("evaluate", Array As String(CurrentInput))         
           End If
       Case "Del"
           If CurrentInput.Length > 0 Then
               CurrentInput = CurrentInput.SubString2(0, CurrentInput.Length - 1)
           End If     
       Case Else
           CurrentInput = CurrentInput & key
   End Select
   Dim ResultLabel As ABMLabel 'ignore
   ' use the same key as when you created it
   ResultLabel.B4JSUniqueKey = "ResultLabel"
   ' we must use the B4JSText, not the normal Text property in a B4JS class
   ResultLabel.B4JSText = CurrentInput
   ' consume the event, if any server one should exist
   Return True
End Sub

public Sub OnMouseEnter(uniqueID As String) As Boolean
   Page.B4JSRunInlineJavascriptMethod("setCSS", Array As String(uniqueID, "background-color: #cacaca !important"))
   ' consume the event, if any server one should exist
   Return True
End Sub

public Sub OnMouseLeave(uniqueID As String) As Boolean
   Page.B4JSRunInlineJavascriptMethod("setCSS", Array As String(uniqueID, "background-color: #f5f5f5 !important"))
   ' consume the event, if any server one should exist
   Return True
End Sub

#if JAVASCRIPT
function evaluate(s) {
    // so we get back a string
   return '' + eval(s);
}

function setCSS(id, val) {
    // we got the button, but we want the cell (which is its parent parent)
   $('#' + id).parent().parent().attr('style', val); 
}
#End If

Notes:

As said in a previous tutorial, when we use a B4JS class in a ABM components B4JSOn... method, it gets its own instance. This is not very practical for our calculator as the CurrentInput variable must be shared. If we don't make CurrentInput public, then each button will have its own CurrentInput.

As we don't want any communication with the server, each method we are going to call returns true: consuming the event on the browser side.

When we dim the ResultLabel label, we do not initialize it again. To remove the warning in B4J, you can just add the 'ignore after the dim. But what we MUST do, is set the B4JSUniqueKey. It must be the same as what we will set in the next part, the normal ABM Web Page.

Now we are ready to build the graphical UI part in ABMaterial and use the B4JS methods we created here.

We make some themes for our buttons and input field
B4X:
public Sub BuildTheme()
   ' start with the base theme defined in ABMShared
   theme.Initialize("pagetheme")
   theme.AddABMTheme(ABMShared.MyTheme)

   theme.AddCellTheme("border")
   theme.Cell("border").BorderColor = ABM.COLOR_BLACK
   theme.Cell("border").BorderWidth = 1
   theme.Cell("border").BorderStyle = ABM.BORDER_SOLID
   theme.Cell("border").VerticalAlign = True
   theme.Cell("border").Align = ABM.CELL_ALIGN_RIGHT

   theme.AddRowTheme("white")
   theme.Row("white").BackColor = ABM.COLOR_GREY
   theme.Row("white").BackColorIntensity = ABM.INTENSITY_LIGHTEN4

   theme.AddLabelTheme("right")
   theme.Label("right").Align = ABM.TEXTALIGN_RIGHT
     
   theme.AddLabelTheme("white")
   theme.Label("white").ForeColor = ABM.COLOR_WHITE
   theme.Label("white").Align = ABM.TEXTALIGN_CENTER
 
   theme.AddLabelTheme("black")
   theme.Label("black").ForeColor = ABM.COLOR_BLACK
   theme.Label("black").Align = ABM.TEXTALIGN_CENTER
 
   theme.AddCellTheme("white")
   theme.Cell("white").BackColor = ABM.COLOR_GREY
   theme.Cell("white").BackColorIntensity = ABM.INTENSITY_LIGHTEN4
   theme.Cell("white").VerticalAlign = True
   theme.Cell("white").Align = ABM.CELL_ALIGN_CENTER
   theme.Cell("white").Clickable = True
 
   theme.AddCellTheme("black")
   theme.Cell("black").BackColor = ABM.COLOR_BLACK
   theme.Cell("black").VerticalAlign = True
   theme.Cell("black").Align = ABM.CELL_ALIGN_CENTER
   theme.Cell("black").Clickable = True
 
   theme.AddCellTheme("green")
   theme.Cell("green").BackColor = ABM.COLOR_GREEN
   theme.Cell("green").VerticalAlign = True
   theme.Cell("green").Align = ABM.CELL_ALIGN_CENTER 
   theme.Cell("green").Clickable = True
End Sub

In BuildPage() we create our grid layout
B4X:
public Sub BuildPage()
   ' initialize the theme
   BuildTheme
 
   ' initialize this page using our theme
   page.InitializeWithTheme(Name, "/ws/" & ABMShared.AppName & "/" & Name, False, ABMShared.SessionMaxInactiveIntervalSeconds, theme)
   page.ShowLoader=True
   page.PageHTMLName = "index.html"
   page.PageTitle = "Template"
   page.PageDescription = "Template"
   page.PageKeywords = ""
   page.PageSiteMapPriority = ""
   page.PageSiteMapFrequency = ABM.SITEMAP_FREQ_YEARLY
     
   page.ShowConnectedIndicator = True
         
   ' create the page grid
   page.AddRows(1,True,"").AddCells12(1,"")
   page.AddRowsM(1,True, 0,0,"").AddCellsOSMP(1,0,0,0,12,12,12,0,0,0,0,"border")
   page.AddRowsM(4,True, 0,0,"white").AddCellsOSMP(4,0,0,0,3,3,3,0,0,0,0,"")
   page.AddRowsM(1,True, 0,0,"").AddCellsOSMP(1,0,0,0,12,12,12,0,0,0,0,"")
   page.AddRows(5,True,"").AddCells12(1,"")
 
   page.BuildGrid 'IMPORTANT once you loaded the complete grid AND before you start adding components
End Sub

And in ConnectPage() we build the calculator
B4X:
public Sub ConnectPage()
   Dim ResultLabel As ABMLabel
   ResultLabel.Initialize(page, "ResultLabel", "", ABM.SIZE_H4, True, "right")
   ' we are going to use this component on the B4JS side, so give it a UNIQUE key
   ResultLabel.B4JSUniqueKey = "ResultLabel"
   ResultLabel.PaddingRight = "10px"
   page.Cell(2,1).AddComponent(ResultLabel)
 
   ' setting a fixed height to the cell
   page.Cell(2, 1).SetFixedHeight(90, False)
 
   ' a list with all the buttons so we can easily iterate through them to build the buttons
   Dim Buttons As List = Array As String("7", "8", "9", "/", "4", "5", "6", "*", "1", "2", "3", "-", ".", "0", "Del", "+", "=")
   Dim ButtonPos As Int = 0
 
   For x = 1 To 4
       For y = 1 To 4
           ' we use just a label for the 'button'
           Dim btn As ABMLabel         
           ' as we will raise events from the cell this time, we must give it also a UNIQUE key
           page.Cell(2+x, y).B4JSUniqueKey = "btn" & ButtonPos
           If y < 4 Then
               ' the first three button (white) of the row
               btn.Initialize(page, "" & ButtonPos, Buttons.Get(ButtonPos), ABM.SIZE_H4, True, "black") 
               btn.IsTextSelectable = False
               ' setting the white theme
               page.Cell(2+x, y).UseTheme("white")
               ' attaching our B4JS methods to the Cell events.  We pass the labels ID that we can then use in the Javascript SetCSS method
               page.Cell(2+x, y).B4JSOnMouseEnter("B4JSCalculator", "OnMouseEnter", Array As String("btn" & ButtonPos))
               page.Cell(2+x, y).B4JSOnMouseLeave("B4JSCalculator", "OnMouseLeave", Array As String("btn" & ButtonPos))                             
           Else
               ' the last button in the row (black).  We don't set a hover effect on theù
               btn.Initialize(page, "" & ButtonPos, Buttons.Get(ButtonPos), ABM.SIZE_H4, True, "white")
               btn.IsTextSelectable = False
               page.Cell(2+x, y).UseTheme("black")
           End If
           ' all the cells have a click event and we pass the labels text to the B4JS function to use in the Select Case
           page.Cell(2+x, y).B4JSOnClick("B4JSCalculator", "ButtonPressed", Array As String(Buttons.Get(ButtonPos)))
           ' also setting a fixed height
           page.Cell(2+x, y).SetFixedHeight(90, False)
           ' add we add the component as an Array component
           page.Cell(2+x, y).AddArrayComponent(btn, "btn")
           ' next button
           ButtonPos = ButtonPos + 1
       Next
   Next 
   ' finally we also add our last button, the =
   Dim btn As ABMLabel
   ' setting the UNIQUE key
   page.Cell(7, 1).B4JSUniqueKey = "btn" & ButtonPos
 
   btn.Initialize(page, "" & ButtonPos, Buttons.Get(ButtonPos), ABM.SIZE_H4, True, "white")
   btn.IsTextSelectable = False
   ' using the green theme
   page.Cell(7, 1).UseTheme("green")
 
   ' also using the ButtonPressed function from B4JS
   page.Cell(7, 1).B4JSOnClick("B4JSCalculator", "ButtonPressed", Array As String(Buttons.Get(ButtonPos)))
   ' setting the height
   page.Cell(7, 1).SetFixedHeight(90, False)
   ' and finally adding it as an Array component
   page.Cell(7, 1).AddArrayComponent(btn, "btn")
 
   ' and just to be sure if our server is still synced with the browser when we need it, lets show an msgbox
   Dim btnServerResult As ABMButton
   btnServerResult.InitializeFlat(page, "btnServerResult", "", "", "Hey, server what is my current result?", "")
   page.Cell(9, 1).AddComponent(btnServerResult)
 
   ' refresh the page
   page.Refresh 
   ' Tell the browser we finished loading
   page.FinishedLoading
   ' restoring the navigation bar position
   page.RestoreNavigationBarPosition
End Sub

Finally, we add the msgbox to the button on the server so we can prove our server can still receive the current browser situation of the ABMInput field when needed.
B4X:
Sub btnServerResult_Clicked(Target As String)
   Dim ResultLabel As ABMLabel = page.Component("ResultLabel")
   page.Msgbox("msgbox", "Your complex calculation is now " & ResultLabel.Text, "Result", "OK", False, ABM.MSGBOX_POS_CENTER_CENTER, "")
End Sub

I've added a lot of comments in the source code so it will be easier to follow.

I've thought a long time on how we could connect the ABM UI in a clean way with the B4JS classes and I'm satisfied with the result. Some may argue why the introduction of B4JSUniqueKey and not just using the ID but it was a real necessity. ABM is actually very smart in how IDs work. It keeps track of its parents and makes it unique if needed. However, as B4JS is 'compiled' it doesn't has this information when running.

For example it is quite possible that at compile time some components don't even exist yet. (actually, most of them don't as they are created in ConnectPage()). So the link between the B4JS component and its future ABM counterpart must be done by you, the programmer.

Another thing you could ask is why having .Text and .B4JSText, why can't the same be used. In theorie there wouldn't be a problem with that, except an ABMComponent has a lot more properties and methods than what B4JS can do. To distinguish which properties are available in B4JS, I gave them a prefix.

It is impossible to convert all ABM Components properties and methods. Gradually, some may be added but it is never the intention to convert them all to B4JS. Frankly, it would be an impossible task. ABM is so huge (took me over 2 years day and night to get where we are now). Other components will also be 'converted' in the future too, but they will be done on a 'on-need' base.

I'll try to make a download of ABM 4.25 by the end of next week so the donators can have a go with B4JS very soon. :)

For the ones interested in the relevant Javascript source code of our B4JS class:
B4X:
var _currentinput="";
var _abm;
var _page;

function b4js_b4jscalculator() {
     var self;
     this.initializeb4js=function(){
          self=this;
          try {
          }
          catch(err) {
               console.log(err.message + ' ' + err.stack);
          }
     };
     this.buttonpressed=function(_key){
          try {
               switch ("" + _key) {
                    case "" + "=":
                         if (_currentinput!="") {
                              _currentinput = evaluate(_currentinput);
                         }
                         break;
                    case "" + "Del":
                         if (_currentinput.length>0) {
                              _currentinput = _currentinput.substring(0,_currentinput.length-1);
                         }
                         break;
                    default:
                         _currentinput = _currentinput+_key;
                         break;
               }
               var _resultlabel={};
               _resultlabel.b4jsvar=$('[data-b4js="resultlabel"]');
               _resultlabel.b4jsvar.html(b4js_buildtext(_currentinput, false));
               return true;
          }
          catch(err) {
               console.log(err.message + ' ' + err.stack);
          }
     };
     this.onmouseenter=function(_uniqueid){
          try {
               setCSS(_uniqueid, "background-color: #cacaca !important");
               return true;
          }
          catch(err) {
               console.log(err.message + ' ' + err.stack);
          }
     };
     this.onmouseleave=function(_uniqueid){
          try {
               setCSS(_uniqueid, "background-color: #f5f5f5 !important");
               return true;
          }
          catch(err) {
               console.log(err.message + ' ' + err.stack);
          }
     };
};

function evaluate(s) {
    // so we get back a string
   return '' + eval(s);
}
function setCSS(id, val) {
    // we got the button, but we want the cell (which is its parent parent)
   $('#' + id).parent().parent().attr('style', val);   
}

Alwaysbusy
 
Last edited:
Top