B4J Tutorial [ABMaterial] Custom components and ABMModalSheet

This is not a tutorial on how to create custom components for ABMaterial. It is more a "what to watch out for when creating custom components that need to work with an ABMModalSheet". This is also not a definitive guide on this subject, just a sharing of issues/resolutions I came across while 1) implementing my own version of a JustGage custom component based on the one already found in the source of the Demo and 2) trying to build a new custom component on Leaflet.

Issue #1: Neither the JustGage component, nor the Leaflet component would show up the modal sheet. Doing some research, this seems to be a common problem (graphical items not properly showing up on a modal sheet) and there seem to be resolutions for both JustGage (set width and height of the DIV that contains the JustGage component) and Leaflet (call invalidateSize() AFTER the modal is up, with various solutions on how to approach the issue found). I tried and tried to get the suggested resolutions working. And it was never going to work with using the standard "ABM Custom Component" found under Project -> Add New Module -> Class Module, which is issue #2.

Issue #2: Let me explain. When adding this class module, the following method is used to "build" your component
B4X:
Sub ABMComp_Build(InternalPage As ABMPage, internalID As String) As String
   Return $"<div id="${internalID}"></div><script>var _${internalID};</script>"$
End Sub
So if you have a custom component and you give it the id "MyFirstGauge", then this will spit out
B4X:
<div id="myfirstgauge"></div><script>var _myfirstgauge;<script>
Note that the internalid is your id lowercased. This generated code works great for components that you add at the page level but breaks down totally when you add a component to a modal sheet. You have three areas that you can add components to in a modal sheet: header, content, and footer section. Let's say you've created a modal sheet with the id "MyFirstModal", then the id's for those sections are: myfirstmodal-header, myfirstmodal-content and myfirstmodal-footer. Depending on where you add your "MyFirstGauge" component, your component's internal id will either be myfirstmodal-header-myfirstgauge, myfirstmodal-content-myfirstgauge, and myfirstmodal-footer-myfirstgauge. Anyone notice the issue with this? I did not at first, since I was working hard on issue#1. After being unsuccessful for hours, I finally happened to see this (I placed my component in the content section):
B4X:
<script>var _myfirstmodal-content-myfirstgauge;<script>
See the issue here? Ok, in order to display the gauge, I had the following code in ABMComp_FirstRun
B4X:
Dim script As String = $"_${internalID} = new JustGage(${optionsJSON});"$
InternalPage.ws.Eval(script, Null)
Where optionsJSON is just a JSON string with options to pass on to JustGage. See the issue? Anyone with JavaScript experience should. The variable uses a non-allowed character, the dash (-). Therefore, nothing works. So my solution for this showstopper is this SmartString fragment instead for the variable name:
B4X:
_${internalID.Replace("-","_")}
Issue#2 sub-issue:
Now we have:
B4X:
Sub ABMComp_Build(InternalPage As ABMPage, internalID As String) As String
   Return $"<div id="${internalID}"></div><script>var _${internalID.Replace("-","_")};</script>"$
End Sub
This introduces another subtle issue. When looking at the resulting DOM, you'll notice that there are two DIV's with the same id. So, if you used MyFirstGauge as your id, you'll actually get two DIV's with that id.
B4X:
<div id="myfirstmodal-content-myfirstgauge" style="" class="  only-print  ">
<div id="myfirstmodal-content-myfirstgauge">
</div>
<script>var _myfirstmodal_content_myfirstgauge;</script>
</div>
One is for the ABMComp component that is initialized in the Initialize method of your custom component
B4X:
Public Sub Initialize(InternalPage As ABMPage, ID As String)
   Dim CSS As String = $""$
   ABMComp.Initialize("ABMComp", Me, InternalPage, ID, CSS)      
End Sub
and one is for the DIV that is set up in the ABMComp_Build method. To properly set up your JavaScript components, you will most likely pass on the internalID to a JavaScript method. Here's an edited version of my JustGage code
B4X:
Sub ABMComp_Build(InternalPage As ABMPage, internalID As String) As String
   options.Put("id", $"${internalID}"$)
   Return $"<div id="${internalID}"></div><script>var _${internalID.Replace("-","_")};</script>"$
End Sub

Sub ABMComp_FirstRun(InternalPage As ABMPage, internalID As String)   Dim JSON As JSONGenerator
   JSON.Initialize(options)
   Dim optionsJSON As String = JSON.ToPrettyString(2)

   Dim script As String = $"_${internalID.Replace("-","_")} = new JustGage(${optionsJSON});"$
   InternalPage.ws.Eval(script, Null)End Sub
The (may not be a) problem is that the Gauge is "bound" to the first DIV, not the DIV that you set up in the Build method above.
So you get
B4X:
<div id="myfirstmodal-content-myfirstgauge" style="" class="  only-print  ">
   <svg ....>
   //all the JustGage DOM stuff added here
   <div id="myfirstmodal-content-myfirstgauge">
   </div>
   <script>var _myfirstmodal_content_myfirstgauge;</script>
</div>
instead of
B4X:
<div id="myfirstmodal-content-myfirstgauge" style="" class="  only-print  ">
   <div id="myfirstmodal-content-myfirstgauge">
      <svg....>
      //all the JustGage DOM stuff added here
   </div>
   <script>var _myfirstmodal_content_myfirstgauge;</script>
</div>
The workaround for this is that I just append a "-internal" to the internalID to create a new DIV id. Update code
B4X:
Sub ABMComp_Build(InternalPage As ABMPage, internalID As String) As String
   options.Put("id", $"${internalID}-internal"$)
   Return $"<div id="${internalID}-internal"></div><script>var _${internalID.Replace("-","_")};</script>"$
End Sub
Which results in
B4X:
<div id="myfirstmodal-content-myfirstgauge" style="" class="  only-print  ">
   <div id="myfirstmodal-content-myfirstgauge-internal">
      <svg....>
      //all the JustGage DOM stuff added here
   </div>
   <script>var _myfirstmodal_content_myfirstgauge;</script>
</div>

Now back to issue#1: (continued in another post)

Links:
JustGage: https://github.com/toorshia/justgage
Leaflet: https://leafletjs.com/
 
Last edited:

OliverA

Expert
Licensed User
Longtime User
Last edited:

OliverA

Expert
Licensed User
Longtime User
Issue#1

A) JustGage.js: For some reason, even after taking care of issue #2, JustGage still did not show a Gauge. Looking more into the ins and outs of how the JustGage worked, I realized that a change in how @alwaysbusy's sample code worked caused it. The sample code has relativeGaugeSize set to True, but I changed that to False, since, at least for me, that produced better results. This one change though, broke things when using JustGage with a modal sheet. With a modal sheet, when relativeGaugeSize is set to False, then one must supply a width and height. When relativeGaugeSize is set to True, the Gauge displays without issues (well, except not quite the size you expect).

B) Leaflet.js: This one is a little trickier. In either modal or non-modal usage, the DIV that acts as the container for needs to have a width and height set up:
B4X:
Sub ABMComp_Build(InternalPage As ABMPage, internalID As String) As String
   Return $"<div id="${internalID} style="height: ${height}; width: ${width};" ></div><script>var _${internalID.Replace("-","_")};</script>"$
End Sub
Where height and width can be passed to the component via the initialization routine (this is just one way. Feel free to implement your own). Even with this, in a modal sheet the map does not show up at all or just barely. The issue seems to be:
when the map is created, the container width/height for your `map-canvas' element has not yet been adjusted to the width/height of the modal dialog. This causes the map size to be incorrect (smaller) than what it should be
(Source: https://stackoverflow.com/questions/20400713/leaflet-map-not-showing-properly-in-bootstrap-3-0-modal). The solution: call Leaflet's invalidateSize() after the modal sheet has settled down. So the first solution I came up with was to implement the code shown in the link as part of the ABMComp_FirstRun method
Solution#1 code
B4X:
Sub ABMComp_FirstRun(InternalPage As ABMPage, internalID As String)
   Dim script As String = $"
_${internalID.Replace("-","_")} = L.map('${internalID}-internal', {
   center: [51.505, -0.09],
   zoom: 13
});
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {}).addTo(_${internalID.Replace("-","_")});
setTimeout(function () {_${internalID.Replace("-","_")}.invalidateSize();}, 500);  '<------- This does the trick!!!!
"$
   InternalPage.ws.Eval(script, Null)End Sub
And everything worked!!!! Except even on non-modal pages, this is invalidating the size even though it is not necessary. At that time I also ran into this forum post: https://www.b4x.com/android/forum/t...map-and-modalsheet-problem.88677/#post-561049. With this solution, the timer and the code to call the invalidation would be in the page code and therefore we can just use it when we are showing a modal sheet. The timer tick routine looks like this
Solution#2 code
B4X:
Sub mapTimer_Tick
   mapTimer.Enabled = False
   Dim modal As ABMModalSheet = page.ModalSheet("myfirstmodal")
   Dim myMap As ABMCustomComponent= ABM.CastABMComponent(modal.Content.Cell(1,1).Component("myfirstmap"))
   If myMap <> Null And myMap.IsInitialized Then
       page.ws.Eval($"_${myMap.ID.Replace("-","_")}.invalidateSize();"$, Null)
       page.ws.Flush
   End If
End Sub
Solution#3
In the end I settled for another solution. When the modal finishes displaying, a page_ModalSheetReady event is raised. So I removed the setTimout line in ABMComp_FirstRun
B4X:
Sub ABMComp_FirstRun(InternalPage As ABMPage, internalID As String)
   Dim script As String = $"
_${internalID.Replace("-","_")} = L.map('${internalID}-internal', {
   center: [51.505, -0.09],
   zoom: 13
});
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {}).addTo(_${internalID.Replace("-","_")});
"$
   InternalPage.ws.Eval(script, Null)End Sub
I code ABMComp_Refresh as follows
B4X:
Sub ABMComp_Refresh(InternalPage As ABMPage, internalID As String)
   Dim script As String = $"_${internalID.Replace("-","_")}.invalidateSize();"$
   InternalPage.ws.Eval(script, Null)
End Sub
and the page class modules page_ModalSheetReady looks like this
B4X:
Sub page_ModalSheetReady(Target As String)
   Log("Modal ready: " & Target)
   Select Case Target.ToLowerCase
       Case "myfirstmodal"
           Dim modal As ABMModalSheet = page.ModalSheet("mapmodal")
           Dim myMap As ABMCustomComponent = modal.Content.Cell(1,1).Component("myfirstmap")
           myMap.Refresh
       Case Else
           Log("Unknow modal sheet: " & Target)
   End Select
End Sub
Now, after the modal sheet is ready, the custom component is refreshed. No need for timers, no need to guess how long the timer should wait for a modal sheet to be ready.

In conclusion:

A) Out of the box, the ABMCustomComponent templates do not work well for components that are in containers such as a modal sheet. The forum does seem to contain the answers, but I only found them after spending hours on figuring out what is going on (and yes, I used the forum trying to find the answers). As of posting the first part, I found two posts that pretty much explain the issue and offer solutions. The first I already posted above and I found it because I was looking for something else and I thought it involved charting. The second post pretty much shows you how to set up a custom component with all the changes necessary to make it work in a container. I would have loved to find this one days ago, but it never showed up in my searches. The only reason I found it is because I was looking for an example on how to do a callback to ABM from JavaScript that involves ABM's page_ParseEvent:
1) https://www.b4x.com/android/forum/t...frappechart-on-a-container.90862/#post-574427
2) https://www.b4x.com/android/forum/t...he-input-abmaterial-events.94646/#post-599511

B) When things don't work in containers (such as modal sheets), understanding how a JavaScript component works and what is required of it will be necessary. Your search engine is your friend. JavaScript at that point will be a must. Understanding HTML's DOM will be a must. Multiple solutions to your problem may present themselves to you (as with the Leaflet.js example above) and understanding more of the ABM framework will influence your decision on implementation (with my point of view being to let ABM handle as much of the lifting as possible).

Happy ABM'ing
 
Top