B4J Tutorial [ABMaterial] Custom components and ABMModalSheet

Discussion in 'B4J Tutorials' started by OliverA, Jun 14, 2019.

  1. OliverA

    OliverA Expert Licensed User

    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
    Code:
    Sub ABMComp_Build(InternalPage As ABMPage, internalID As StringAs 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
    Code:
    <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):
    Code:
    <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
    Code:
    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:
    Code:
    _${internalID.Replace("-","_")}
    Issue#2 sub-issue:
    Now we have:
    Code:
    Sub ABMComp_Build(InternalPage As ABMPage, internalID As StringAs 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.
    Code:
    <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
    Code:
    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
    Code:
    Sub ABMComp_Build(InternalPage As ABMPage, internalID As StringAs 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
    Code:
    <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
    Code:
    <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
    Code:
    Sub ABMComp_Build(InternalPage As ABMPage, internalID As StringAs String
       options.Put(
    "id"$"${internalID}-internal"$)
       
    Return $"<div id="${internalID}-internal"></div><script>var _${internalID.Replace("-","_")};</script>"$
    End Sub
    Which results in
    Code:
    <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: Jun 17, 2019
  2. OliverA

    OliverA Expert Licensed User

    Last edited: Jun 15, 2019
    Johan Hormaza and Harris like this.
  3. OliverA

    OliverA Expert Licensed 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:
    Code:
    Sub ABMComp_Build(InternalPage As ABMPage, internalID As StringAs 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:
    (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
    Code:
    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
    Code:
    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
    Code:
    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
    Code:
    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
    Code:
    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
     
Loading...
  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice