B4J Tutorial [BANano]: Creating a CRUD app with LocalStorage Backend

Hi there

27 March 2019 Update: Now updated to use version 1.06 of UOEBANano.

Download UOEBANano
Download SD Zip Library
Download ATM Source Code

#WithBANanoYouCan

First, I am not an expert in BANano, this is just based on my experiments using it, after having asked a lot and lot of questions (and still do) about it. It's a very maverlous framework to work with and all my questions have been marked [SOLVED]. That kind of support from the developer is priceless.

Anyway, I'm exploring CRUD, case in point use of LocalStorage. There is nothing cut in stone, so this is just my implementation. It has bugs and some irritations that I need to solve too but so far it works. Im still implementing more things, for example, I want to use the built in BANAno email sending functionality to send the localstorage contents to an email address for 'backup' purposes.

So this is how the UX looks like and I will try and explain how I managed to get this real-world app working (bugs included, :)).

BANano's engine is the glue that drives ones UX to works, now creating this UX was the hard part. Using BANano is becoming easier and easier (personal opinion). I'm realising though that some lack of knowledge about how this HTML5 thing works hinders progress. So knowing CSS and the whole DOM tree stuff will help one out. So much to learn.

The app looks like this, there is nothing fancy about it and besides the client wanted it to be as simple as possible, add members, list members, search members. Pure basic stuff.

memberlisting.png


As noted abobe, the app has a navigation bar and buttons, a floating action button, a table with edit and delete, it also has a drawer...

sidebar.png


And some modal dialogs with back and next...

modal1.png


Member details 2

member2.png


Member Details 3

member3.png


One should be able to delete a member, we have a confirm dialog.

confirmdelete.png


And one should be able to search too..

membersearch.png


Yes, there are a lot of things I can improve and do differently, but hey, technology can be confusing.

So let's create this SPA.

 
Last edited:

Mashiane

Expert
Licensed User
Longtime User
Creating the app...

1. You need BANano (Compulsory)
2. You need UOEBANano - this is a UX library I have created for the community. It's open source, has some bugs and irritations. You can use any UX framework of your choice anyway. That's the nice thing about BANano, it does not have a UX dependency. Such brilliance!!
3. UOEBANano comes uncompiled, you need to compile it to a lib yourself in B4J.

Ok, let's do this...

1. Create a new UX project in B4J
2. Reference BANano and UOEBanano libraries
3. Add a new code module, let's call it pgIndex.

Main

B4X:
Sub Process_Globals
    Public BANano As BANano 'ignore
    Public App As UOEApp
    Public AppName As String = "ATM"
End Sub

Sub AppStart (Form1 As Form, Args() As String)
    'initialize banano for first use
    BANano.Initialize(AppName, AppName,1)
    'let us not use service workers
    BANano.UseServiceWorker = False
    BANano.HTML_NAME = "index.html"
    BANano.Header.Title = "ATM"
    'add needed files
    'loop through each file and add it
    modUOE.PrepareResources
    Dim fTot As Int = modUOE.Resources.Size - 1
    Dim fCnt As Int
    For fCnt = 0 To fTot
        Dim sFile As String = modUOE.Resources.GetValueAt(fCnt)
        If sFile.EndsWith(".css") Then
            BANano.Header.addcssfile(sFile)
        else if sFile.EndsWith(".js") Then
            BANano.Header.AddJavascriptFile(sFile)
        End If
    Next
    'ensure we have the latest version of the file
    '#If RELEASE
    'Dim TimeStamp As Long = DateTime.now
    'BANano.JAVASCRIPT_NAME =  $"app${TimeStamp}.js"$
    '#End If
    BANano.Build(File.DirApp)
    ExitApplication
End Sub

'Return true to allow the default exceptions handler to handle the uncaught exception.
Sub Application_Error (Error As Exception, StackTrace As String) As Boolean
    Return True
End Sub

' HERE STARTS YOUR APP
Sub ATM_Ready()
    App.Initialize(AppName,"white.black")
    App.Copyrights = $"2019 ${AppName}, All Rights Reserved, powered by TGIF Zone Inc"$
    App.TermsAndConditionsURL = ""
    App.DisclaimerURL = ""
    App.PrivacyPolicyURL = ""
    'lets open the first page
    pgIndex.Init(App)
End Sub

I'm sure by now we all know what should be in the Main module. You can leave the event of your app to be BANano so that you use BANano_Ready.

UOEBANano comes up with some css and js files. At the moment its kinda loaded as I am still experimenting so don't be alarmed. I have created a map of all needed css and js files in modUOE for a basic project. So what happens in AppStart is that that man is transversed and the js and css files are added to the BANano.Header. So you can tweak ahead, besides we are experimenting and learning...

In ATM_Ready, we just initialize our app with a few things. There is nothing much expected from there as it does not do much. If you have a terms and conditions, disclaimer and privacy policy URLS then you can just write the paths there. These are put on the UOECopyrights component just below the footer of the page. This you can turn off too.
 

Mashiane

Expert
Licensed User
Longtime User
But then what are these UOEBANano.Resources?

Taking a peek at UOEBANano.modUOE, the resource list is...

B4X:
Sub PrepareResources
    Resources.Initialize
    Resources.clear
    AddResource("materialfont.css")
    AddResource("materialize.min.css")
    AddResource("preloader.css")
    AddResource("jquery-3.3.1.min.js")
    AddResource("jquery.timeago.min.js")
    AddResource("materialize.min.js")
    '*** add other css and js files after this
    AddResource("all.min.css")
    AddResource("all.min.js")
    AddResource("nouislider.css")
    AddResource("nouislider.min.js")
    AddResource("jquery.sweet-modal.min.css")
    AddResource("jquery.sweet-modal.min.js")
    'can save elements
    AddResource("Blob.min.js")
    AddResource("canvas-toBlob.js")
    AddResource("dom-to-image.min.js")
    AddResource("FileSaver.min.js")
    AddResource("printThis.js")
    'toolbar
    AddResource("jquery.toolbar.css")
    AddResource("jquery.toolbar.min.js")
    'billboard chart
    AddResource("billboard.min.css")
    AddResource("billboard.pkgd.min.js")
    'export
    AddResource("tableHTMLExport.js")
End Sub

Sub AddResource(sFile As String)
    Resources.Put(sFile,sFile)
End Sub

Besides the required css and js files for the UX lib, I use sweetmodal for confirm and alert boxes, Im experimenting with saving elements do images and also printing elements directly, there is a toolbar component I'm curious about and will definately add the D3 based charting framework in some future. So there is it.

Ok, lets continue...
 

Mashiane

Expert
Licensed User
Longtime User
pgIndex.Header

In Main.ATM_Ready we call pgIndex.Init, thats the code that builds the UX page as it is, adding modals, alerts, toasts, floating action buttons etc. The methodolody of the UOEBANano library is:

1. Create your UX.
2. Link events to elements using BANano
3. No abstract designer elements (you code the ux)

We need 3 modals for this exercise and a table. As these will be accessible anywhere on the page, lets define them as global.

B4X:
'Static code module
Sub Process_Globals
    Public BANano As BANano 'ignore
    Public App As UOEApp
    Public Page As UOEPage
    Private member As UOEModal
    Private member2 As UOEModal
    Private member3 As UOEModal
    Private tbl As UOETable
    Private action As String
End Sub

The next stages will be a bit by bit process.

Let's create the navigation bar and drawer

Navigation Bar

The navigation bar has a drawer and some buttons..

navbar.png



B4X:
'create the page
Sub Init(thisApp As UOEApp)
    App = thisApp
    'initialize the page, centered logo
    Page.Initialize(App,"pg1",False,"ATM Membership App",App.EnumLogoPosition.center,True,False,"","")
    Page.NavBar.AddImageL("logo","assets/logo.png",True,"40","40","1")
    Page.NavBar.AddIcon("search","mdi-search","#",False,App.EnumAlignment.right, App.EnumVisibility.visible, "")
    Page.NavBar.AddIcon("refresh","mdi-autorenew","#",False,App.EnumAlignment.right, App.EnumVisibility.visible, "")

There are a could of things that happen here. Whilst creating the navigation bar, we also indicate that the page container should not be centered. We have a logo that is centered. A logo can be aligned left, center or right.

We add an image to the left of the navigation bar and specify its aspects with AddImageL, we add two icons for search and refresh to the right of the navigation. When search is clicked, a search modal should appear and when refresh is clicked, we want the table contents to be refreshed.

To handle all the events of the page, we have defined a single event handler and parse the event id to get what we need to do. This is called indexEvents.

B4X:
Sub indexEvents(event As BANanoEvent)
    'stop bubbling of events
    event.StopPropagation
    'get the event id
    Dim thisEvent As String = event.ID
    Log(thisEvent)
    Dim ePrefix As String = App.MvField(thisEvent,1,"-")
    Select Case ePrefix
        Case "search"
            SearchMembers
        Case "searchmember"
            SearchMembers
        Case "refreshmember"
            CreateTable
 

Mashiane

Expert
Licensed User
Longtime User
Creating the drawer

On small devices, the navigation bar buttons usually hide themselves, providing a drawer for the same functionality is better.

drawer.png


As noted, we have functionality to add a member, search members and refresh. The back up functionality is still a work in progress and will use the BANAno built in functionality to send an email of the local storage contents.

Let's look at the code...

B4X:
Page.NavBar.Drawer.DrawerWidth = 350
    'add the userview
    Page.NavBar.Drawer.AddUserView("ATM","[email protected]","assets/logo.png","assets/background.png")
    Page.NavBar.Drawer.AddHeader("addmember","mdi-add_circle_outline","Add Member","",False,"","","green.transparent",True)
    Page.NavBar.Drawer.AddHeader("searchmember","mdi-search","Search Members","",False,"","","",True)
    Page.NavBar.Drawer.AddHeader("refreshmember","mdi-autorenew","Refresh","",False,"","","",True)
    Page.NavBar.Drawer.AddHeader("backup","mdi-backup","Back Up","",False,"","","",True)

Automatically, as these are clickable things, event handlers will be added when the page is created by UOEBANano, thus creating the BANano HandleEvents functionality. As each element in a page needs to have its own unique element id, as much as search, add, refresh do the same things as per navigation bar, these need to have different ids. Having the same id also could create multiple event handlers.
 

Mashiane

Expert
Licensed User
Longtime User
Creating the footer

footer.png


This footer has a copyright at the bottom. A footer can be visible and not visible on a page. A footer just like Page.Content is just a container that you can add rows and columns to. So here we tell the Page that it needs to have copyrights, we need to fix the footer, the container that will house elements in the footer should not be centered.

We then add rows to the footer and then add a paragraph to it. The copyrights details you will remember were defined in Main.ATM_Ready.

B4X:
'footer
    Page.HasCopyright = True
    Page.FixedFooter = True
    Page.HasFooter = True
    Page.Footer.CenterInPage = False
    Page.footer.AddRowsM(1,0,0, "", "").AddColumnsSP(1,12,12,12,10,10, "", "")
    Page.Footer.AddParagraph(1,1,"","Developed by Mashy, [email protected]","","")
    'add a tooltip
    Page.AddToolTip("newmember","Add a new member",App.EnumTooltipPos.Left,50)
    'add fab
    Page.Content.AddFab1("newmember","","mdi-add","","white.green","pulse")

We also want to provide functionality for a user to select an action button that will enable one to add new members. We do so with AddFab1
When a user hovers over the fixed action button, we want a tooltip to be shown, thus we add the tooltip BEFORE we add the component on the page. Because this button is important to add members for the app, we want it to pulse, this adding the "pulse" class to it.

There are some themes that are built inside UOEBANAno that you can use. Although the theming for UOEBANano is imcomplete, for basic things like buttons etc, it seems to work well. Also the event handler for this FAB is created when the Page.Create method is being executed, AFTER all the elements of the page are added to the page grid. The universal event handler, indexEvents is being used.
 

Mashiane

Expert
Licensed User
Longtime User
Creating the page grid to house components.

B4X:
'define grid for the page
    Page.Content.AddRows(2,"", "").AddColumnsOSMP(1,0,0,0,12,12,12,0,0,20,20,"","center-align")
    Page.Content.AddRows(5,"", "").AddColumnsOSMP(1,0,0,0,12,12,12,0,0,20,20,"","")
    'add other page contents
    Dim lbl As UOELabel
    lbl.Initialize(App,"lblHead",App.EnumLabelSize.span,"","","")
    lbl.AddColor("{B}African{NBSP}{/B}",App.EnumColor.BLACK)
    lbl.AddColor("{B}Transformation{NBSP}{/B}",App.EnumColor.blue)
    lbl.AddColor("{B}Movement{NBSP}{/B}",App.EnumColor.green)
    Page.Content.AddLabel(2,1,lbl)
    
    'Page.content.AddImage1(3,1,"mainlogo","assets/mainlogo.png","Main Logo",False,App.EnumZDepth.zdepth_2,True,True,False,"300","500",App.EnumAlignment.CenterDiv,"")
    
    
    
    Member1Modal
    Member2Modal
    Member3Modal
    'create the boxy
    Page.Create
    'add a table of contents: this should update already existing content
    CreateTable
    action = "create"
    'update settings so that labels of group are set properly.
    member.Content.setColumnPadding1(1,1,0,0,10,0)
    member2.Content.setColumnPadding1(2,1,0,0,10,0)
End Sub

If you have used ABM before the AddRows and AddColumnOSMP methods should not be a surprise. The same methodolody has been followed, thus ensuring a smooth transition to this as possible. It is however true that UOEBANano is not affiliated or supported by ABM and UOEBANano is NOT ABM, but indeed is inspired by ABM, by the community for the community. Its open source, meaning its yours too. ;)

Anyway, in most components that use the Container, these have been defined as .Content.

At R2C1 we add a label with some colors. BANano has a .SF function to do that, this is just my flavour of doing things. We add 3 modals to the page, execute a Page.Create which generates all the HTML needed for the elements and then inject the table with CreateTable and then padd some modal sheet elements. We do these after the page is created because such padding used BANAno.<>.SetStyle methods.

CreatePage creates the table and injects data to it from LocalStorage so each time it runs, it replaces an html element on the page RC, thus also created after the page is created because it used BANano.SetHTML methods

The most important call after defining your components is Page.Create. After it, usually one has to link events to the elements with BindEvents but because the table is dynamic and has its own events (edit & delete), I am only binding events after the table is created.
 

Mashiane

Expert
Licensed User
Longtime User
Lets look at the modal definitions

MemberModal1, MemberModal2 and MemberModal3

These subs create the modal sheets that the membership details should be captured in. As the code for these modals is closely related and the same, only 1 will be discussed here..

MemberModal1

B4X:
Sub Member1Modal
    '***
    member.Initialize(App,"aemember","")
    member.fixedFooter = True
    member.dismissible = False
    'process the header
    member.Header.AddRows(1,"white.lightblue","").AddColumnsOSMP(1,0,0,0,2,2,2,10,0,10,10,"","").AddColumnsOSMP(1,0,0,0,10,10,10,10,0,10,10,"","")
    member.Header.AddImage1(1,1,"","assets/logo.png","logo",True,App.EnumZDepth.zdepth_3,True,True,False,"50px","50px","","") 
    member.Header.AddH4(1,2,"h4","Member [1/3]","","")
    member.Content.AddRowsM(3,5,5,"","").AddColumnsOSMP(2,0,0,0,12,6,6,0,10,0,0,"","")
    'category
    Dim category As UOERadioGroup
    category.Initialize(App,"category","Category of Membership",False,"")
    category.AddItem("itemnew","N","New",True,True,True,True)
    category.AddItem("itemrenewal","R","Renewal",True,True,True,False)
    member.Content.AddRadioGroup(1,2,category)
    member.Content.AddParagraph(1,1,"","Category of Membership","","")
    'add title
    member.Content.AddTextBox(2,1,"title","","Title","","",0,"","")
    'add surname
    member.Content.AddTextBox(2,2,"surname","","Surname","","",0,"","")
    'add fullnames
    member.Content.AddTextBox(3,1,"fullnames","","Full Name(s)","","",0,"","")
    'add id number
    member.Content.AddTelephone(3,2,"idnumber","","Identity Number","","",0,"","")
    'update fields to use in updates
    Dim flds As List = member.fields
    flds.Initialize
    flds.clear
    flds.AddAll(Array As String("category", "title", "surname", "fullnames", "idnumber"))
    'add footer
    member.Footer.AddRows(1,"","").AddColumns12(1,"","")
    'add footer buttons
    member.Footer.AddRaisedButton(1,1,"btnCancelMember","Cancel","","","white.red","")
    member.Footer.AddRaisedButton(1,1,"btnSaveMember1","Next","","","white.green","")
    Page.Content.AddModal(member)
    
End Sub

In globals we defined member as an UOEModal component. What we do in this sub is just ensure that the needed components are existing.
The modal should have a fixed footer and cannot be dismissed. We create a header, content and footer with some contents for the UX.

The header, content and footer of the modal are instances of UOEContainer just like Page.Content is an instance of UOEContainer. Where everything happens.

1. We add a grid in the header and add a logo and a heading.
2. We add a grid to the content too and then add some elements to the modal.content, e.g. radiogroup, textboxes etc.
3. Also on the footer, we add a grid and then add the save and cancel buttons for Cancel and Next.

Clearing the modal contents

Clearing modal contents for all three modals are ClearModal1, ClearModal2 and ClearModal3 respectively. For the first modal..

B4X:
'clear the contents of the modal
Sub ClearMember1
    Page.setTextBox("title","")
    Page.setTextBox("surname","")
    Page.setTextBox("fullnames","")
    Page.setTextBox("idnumber","")
    Page.SetRadio("category", "N")
End Sub

Getting the modal contents

Getting the modal contents for all three modals are GetMember1, GetMember2 and GetMember3 respectively. For each modal, the element details are read and validated, if anything required is missing a 'red' toast message is provided to nudge the end user. When all the details are read, the details are saved on a temporal map object called 'member' in local storage. This 'member' item is updated each time a user navigates between the screen with Back and Next.

B4X:
'get saved details, check validity
Sub GetMember1 As Boolean
    Dim scategory As String = Page.GetRadio("category")
    Dim stitle As String = Page.GetTextbox("title")
    Dim ssurname As String = Page.GetTextbox("surname")
    Dim sfullname As String = Page.GetTextbox("fullnames")
    Dim sidnumber As String = Page.GetTextbox("idnumber")
    'tell user if blank
    If scategory <> "N" Then
        If scategory <> "R" Then
            Page.ToastError("The category has not been selected, please select a category!")
            Return False
        End If
    End If
    
    If stitle.Length = 0 Then
        Page.ToastError("The title cannot be blank, please specify a title!")
        Return False
    End If
    If ssurname.Length = 0 Then
        Page.ToastError("The surname cannot be blank, please specify a surname!")
        Return False
    End If
    If sfullname.Length = 0 Then
        Page.ToastError("The fullnames cannot be blank, please specify the fullnames!")
        Return False
    End If
    If sidnumber.Length = 0 Then
        Page.ToastError("The id number cannot be blank, please specify an id number!")
        Return False
    End If
    
    'get saved member
    Dim sMember As String = Page.GetLocalStorage("member")
    If sMember = "" Then
        Dim m1 As Map = CreateMap("category" : scategory, "title" : stitle, "surname" : ssurname, "fullnames" : sfullname, "idnumber" : sidnumber)
        sMember = App.Map2Json(m1)
    Else
        Dim m1 As Map = App.Json2Map(sMember)
        m1.Put("category", scategory)
        m1.Put("title", stitle)
        m1.Put("surname", ssurname)
        m1.Put("fullnames", sfullname)
        m1.Put("idnumber", sidnumber)
        sMember = App.Map2Json(m1)
    End If   
    'save to localstorage
    BANano.SetLocalStorage("member", sMember)
    Return True
End Sub

Setting the modal contents

Setting the modal contents happens when a member details are read from the temporal 'member' local storage object and then displayed to the screen. The member object is parsed and then each element shown where its supposed.

The setters for all three modals are ReadMember1, ReadMember2 and ReadMember3 respectively for each modal.

B4X:
Sub ReadMember1(strMember As String)
    If strMember = "" Then Return
    'convert the member to a map
    Dim mm As Map = App.Json2Map(strMember)
    Dim stitle As String = mm.Get("title")
    Dim ssurname As String = mm.Get("surname")
    Dim sfullnames As String = mm.Get("fullnames")
    Dim sidnumber As String = mm.Get("idnumber")
    Dim scategory As String = mm.Get("category")
    
    Page.SetRadio("category", scategory)
    Page.setTextbox("title", stitle)
    Page.setTextbox("surname", ssurname)
    Page.setTextbox("fullnames", sfullnames)
    Page.setTextbox("idnumber", sidnumber)
End Sub

The 'member' object is saved to localstorage as a JSON object and has to be parses to get the various keys.
 

Mashiane

Expert
Licensed User
Longtime User
A look at the table. CreateTable

The main universal method for the table is the sub called CreateTable. What this does is to create the table structure, read the contents from local storage of all saved members and then displays them on the table. When the table is created, then its injected into the page, AFTER all other components are already added and then events are linked to the page for all elements needing events and not defined internally by UOEBANano after Page.Create.

B4X:
'refresh the table
Sub CreateTable
    'table of members
    tbl.Initialize(App,"tblmembers","")
    tbl.CanExport = False
    tbl.HasBorder = True
    tbl.AddHeadingsFields(Array As String("category","surname","fullnames","idnumber","gender","cellphone","email","edit","delete"))
    tbl.AddHeadings(Array As String("Category","Surname","Fullnames","ID #","Gender","Cellphone #","Email","Edit","Delete"))
    tbl.AddHeadingsThemes(Array As String(App.theme,App.theme,App.theme,App.theme,App.theme,App.theme,App.theme,App.theme,App.theme))
    tbl.AddHeadingsAlignment(Array As String("","","","","","","",App.EnumAlignment.Center,App.EnumAlignment.Center))
    tbl.AddHeadingsWidths(Array As String("","","","","","","","60","60"))
    'load saved records from localstorage
    Dim mCnt As Int =  0
    'get the field names from the table, our records as json format
    Dim flds As List = tbl.Fields
    'read the saved members from localstorage
    Dim strMembers As String = Page.GetLocalStorage("members")
    If strMembers = "" Then
    Else
        'convert existing members to a list
        Dim lstM As List = App.Json2List(strMembers)
        'each member is a json string
        For Each strMember As String In lstM
            'each member record is a json string, convert this to a map
            Dim memberM As Map = App.Json2Map(strMember)
            'the id number is the key
            Dim sidnumber As String = memberM.Get("idnumber")
            'extract the values needed
            Dim memberR As List
            memberR.Initialize
            memberR.clear
            'loop through each fld
            Dim fldTot As Int = flds.Size - 1
            Dim fldCnt As Int
            For fldCnt = 0 To fldTot
                Dim fldStr As String = flds.Get(fldCnt)
                Dim hasFld As Boolean = memberM.ContainsKey(fldStr)
                'only process the fields we want to show on the table
                If hasFld Then
                    Dim fldVal As String = memberM.Get(fldStr)
                    memberR.Add(fldVal)
                End If
            Next
            'add the edit button, get their settings first to theme the buttons
            Dim hdrEdt As UOETableHeading = tbl.getTableHeading("edit")
            Dim hdrDel As UOETableHeading = tbl.getTableHeading("delete")
            'define the buttons, the id is the key for each record, include it for each button
            Dim btnEdt As UOEButton
            Dim eKey As String = $"editrow-${mCnt}-${sidnumber}"$
            Dim dKey As String = $"deleterow-${mCnt}-${sidnumber}"$
            'define a floating button for edit/delete
            btnEdt = Page.Content.getFloatingButton(eKey,"mdi-create",hdrEdt.Theme,hdrEdt.ClassName)
            Dim btnDel As UOEButton
            btnDel = Page.Content.getFloatingButton(dKey,"mdi-delete_forever",hdrDel.Theme,hdrDel.ClassName)
            'add buttons to record
            memberR.Add(btnEdt.ToString)
            memberR.Add(btnDel.ToString)
            'add click events for edit / delete
            App.AddEvent(eKey, "click")
            App.AddEvent(dKey, "click")
            'add row to table
            tbl.AddRow(memberR,Null,Null)
            'increment table row
            mCnt = mCnt + 1
        Next
    End If
    'update the container with the new table
    Page.Content.ReplaceRC(3,1,tbl.ToString)
    'relink the events for the page, if we were not running CreateTable, this should be added directly under page.create
    App.BindEvents("indexEvents", Me)
End Sub

As member records are saved as JSON objects in localstorage, each member has its own field definition. One can read some of this content to display on a table. To tell the table which fields to processes, we add fields to the table with AddHeadingFields. We would recommended this to be the first thing one does.

Then the table heading titles are added with AddHeadings, then Themes, then Alignment and Widths. Then, we read the saved 'members' list from localstorage, loop through each one, determine which fields are displayed on the table as defined by AddHeadingFields and then create each member record as a List object. We read the theme for the edit / delete columns from their specified themes with AddHeadingThemes and apply the same theme for Edit/Delete buttons.

Each edit/delete button is created per member being added using the member idnumber as a unique key, buy defining each button with .getFloatingButton. This just generates the required HTML for the button.

When all the member details are added to the memberR list item, then each edit/delete is linked to an event with .AddEvent. AddEvent does nothing else other than just keeping a record of all the elements that need to be having events. After all the members are added to the page, events are bound to the controls with .BindEvents

This is depicted like this..

B4X:
'add an event to the collection
Sub AddEvent(elID As String, elEvent As String)
    elID = elID.ToLowerCase
    elID = elID.Trim
    elEvent = elEvent.trim
    If elID.Length > 0 And elEvent.Length > 0 Then
        Events.Put(elID,elEvent)
        Dim em As List
        em.Initialize
        em.AddAll(Array As String("icon","img","badge","title","badge"))
        For Each strsuffix As String In em
            Dim strkey As String = $"${elID}-${strsuffix}"$
            Events.Put(strkey,elEvent)
        Next
    End If   
End Sub

B4X:
'bind the events
Sub BindEvents(elMethod As String, EventHandler As Object)
    Dim mTot As Int = Events.Size - 1
    Dim mCnt As Int
    For mCnt = 0 To mTot
        Dim elID As String = Events.GetKeyAt(mCnt)
        Dim elEvent As String = Events.Get(elID)
        elID = elID.Trim
        elEvent = elEvent.trim
        If elID.Length > 0 And elEvent.Length > 0 Then
            Dim btnEvent As String = $"${elID}_${elEvent}"$
            Dim elKey As String = $"#${elID}"$
            'these call a universal method
            If elMethod.Length > 0 Then
                btnEvent = elMethod.tolowercase
            End If
            If EventHandler <> Null Then
                Dim elm As BANanoElement = BANano.GetElement(elKey)
                elm.Off(elEvent)
                elm.HandleEvents(elEvent, EventHandler, btnEvent)
            End If
        End If
    Next
End Sub

By default, all events defined like this in an UOEBANano project will use HandleEvents, but then you can turn an event off as done here.

 

Mashiane

Expert
Licensed User
Longtime User
Saving Member Details

The third modal has a SAVE button whilst others have NEXT, that is where the save happens. When scrolling back and forth in the modals, one can change details etc. On each Next click, the member details are read and validated and then saved to a temporal local storage object named 'member'.

The button linked to saving all member details is btnsavemember3, this executes the CreateUpdate sub.

This all gets done inside the indexEvents sup.

B4X:
Sub indexEvents(event As BANanoEvent)
    'stop bubbling of events
    event.StopPropagation
    'get the event id
    Dim thisEvent As String = event.ID
    Log(thisEvent)
    Dim ePrefix As String = App.MvField(thisEvent,1,"-")
    Select Case ePrefix
        Case "search"
            SearchMembers
        Case "searchmember"
            SearchMembers
        Case "refreshmember"
            CreateTable
        Case "refresh"
            'refresh the table
            CreateTable
        Case "search"
            'search for a member
        Case "editrow"
            Dim rowPos As String = App.mvfield(thisEvent,2,"-")
            Dim rowKey As String = App.MvField(thisEvent,3,"-")
            action = "update"
            ClearMember1
            ClearMember2
            ClearMember3
            Dim strMember As String = ReadMember(rowKey)
            Page.SetLocalStorage("member", strMember)
            ReadMember1(strMember)
            ReadMember2(strMember)
            ReadMember3(strMember)
            'refresh controls
            Page.refresh
            member.Open
        Case "deleterow"
            Dim rowPos As String = App.mvfield(thisEvent,2,"-")
            Dim rowKey As String = App.MvField(thisEvent,3,"-")
            'ask the user
            'save this memberid for later retrieval
            Page.SetLocalStorage("memberid", rowKey)
            'save the table row for later retrieval
            Page.SetLocalStorage("index", rowPos)
            'call a method existing in this application
            '_banano_atm_pgindex.deletemember();
            Dim onYes As String = Page.CallMethod("atm","pgindex","deletemember();")
            Page.SweetModalConfirm($"Confirm Delete: ${rowKey}"$, _
            "Are you sure that you want to delete this record? You will not be able to undo your changes.", onYes, "")
            
        Case "btncancelmember3"
            member3.close
            member2.Open
        Case "btnsavemember3"
            Dim bDo As Boolean = GetMember3
            If bDo Then
                CreateUpdate
                action = "create"
                Page.ToastSuccess("Member updated succesfully!")
                member3.close
            End If
        Case "btnsavemember2"
            Dim bDo As Boolean = GetMember2
            If bDo Then
                'goto address
                member2.close
                member3.open
            End If
        Case "btncancelmember2"
            member2.Close
            member.open
        Case "btncancelmember"
            member.close
        Case "btnsavemember1"
            'a member is being saved
            Dim bDo As Boolean = GetMember1
            If bDo Then
                member.close
                member2.open
            End If
        Case "addmember"
            'add a new member, create the modal
            action = "create"
            ClearMember1
            ClearMember2
            ClearMember3
            Page.refresh
            member.Open
        Case "newmember"
            action = "create"
            ClearMember1
            ClearMember2
            ClearMember3
            Page.refresh
            member.open
        Case "backup"
            'read members and send email of records
    End Select
End Sub

CreateUpdate sub, reads the temporal saved 'member' object from localstorage and also reads the existing 'members' list from localstorage. Using the members idnumber as a key, the existing members are compared and if a match of an idnumber is found, that particular record is updated, if not a new member record is added to the list. This list is then saved back to localstorage as a 'members' object, a list of JSON objects that are internally maps with each member record.

B4X:
'update list of members
Sub CreateUpdate
    Dim hasMembers As Boolean = False
    Dim lstM As List
    lstM.Initialize
    lstM.clear
    'get last member
    Dim smember As String = Page.GetLocalStorage("member")
    'get a list of members
    Dim lmember As String = Page.GetLocalStorage("members")
    'members exist, add to existing
    If lmember <> "" Then
        'there are existing members
        lstM = App.Json2List(lmember)
        hasMembers = True
    End If
    If hasMembers Then
        'read the id of this member
        Dim thisMember As Map = App.Json2Map(smember)
        Dim sidnumber As String = thisMember.Get("idnumber")
        Dim mTot As Int = lstM.Size - 1
        Dim mCnt As Int
        Dim mFound As Boolean = False
        For mCnt = 0 To mTot
            Dim savedMember As String = lstM.Get(mCnt)
            Dim savedMemberM As Map = App.Json2Map(savedMember)
            Dim savedid As String = savedMemberM.Get("idnumber")
            If savedid = sidnumber Then
                lstM.Set(mCnt,smember)
                mFound = True
                Exit
            End If
        Next
        If mFound = False Then
            lstM.Add(smember)
        End If
    Else
        lstM.Add(smember)
    End If
    Dim allMembers As String = App.List2Json(lstM)
    Page.SetLocalStorage("members", allMembers)
    Page.SetLocalStorage("member", "")
    'create the table
    CreateTable
End Sub
 

Mashiane

Expert
Licensed User
Longtime User
Editing Member Details

To edit details of a member, an edit button is provided per member, each edit button is linked to the idnumber of the member. When edit is clicked on a member, as that is linked to each member, we are able to pick up which member's id number was selected from the multivalue field linked to the event.

The edit button is linked to a click event and then we linked the event on CreateTable per member row.

B4X:
Case "editrow"
            Dim rowPos As String = App.mvfield(thisEvent,2,"-")
            Dim rowKey As String = App.MvField(thisEvent,3,"-")
            action = "update"
            ClearMember1
            ClearMember2
            ClearMember3
            Dim strMember As String = ReadMember(rowKey)
            Page.SetLocalStorage("member", strMember)
            ReadMember1(strMember)
            ReadMember2(strMember)
            ReadMember3(strMember)
            'refresh controls
            Page.refresh
            member.Open

So, what this does is, get the member idnumber from the eventid, clear all the modals from 1-3, read the member using ReadMember from local storage and then save the active member to the 'member' object in localstorage and then use the setter methods ReadMember1 - 3 and then displays the first modal using .Open.

ReadMember

This gets passed the idnumber to search for from already saved 'members' in localstorage. This loops through each record on the list, finds the one matching the idnumber specified and then returns it.

B4X:
'read a member from localstorange
Sub ReadMember(sid As String) As String
    'get the existing members
    Dim strMembers As String = Page.GetLocalStorage("members")
    If strMembers = "" Then
        Return "{}"
    Else
        'convert existing members to a list
        Dim lstM As List = App.Json2List(strMembers)
        'each member is a json string
        Dim mTot As Int = lstM.Size - 1
        Dim mCnt As Int
        For mCnt = 0 To mTot
            Dim strMember As String = lstM.Get(mCnt)
            Dim memberM As Map = App.Json2Map(strMember)
            'the id number is the key
            Dim sidnumber As String = memberM.Get("idnumber")
            If sidnumber = sid Then
                Return strMember
            End If
        Next
    End If
    Return "{}"
End Sub

Each time you update the member details are click Next, this 'member' object is updated in localstorage and when the last button is clicked to Save the member, CreateUpdate is ran to read the temporal 'member' local storage object and update the 'members' local storage object.
 
Last edited:

Mashiane

Expert
Licensed User
Longtime User
Deleting a Member

To delete a member, one has to click the delete button close to the member they wish to delete. For each member row, this is linked to a button with an event id prefixed by delete row (remember on CreateTable)

B4X:
Case "deleterow"
            Dim rowPos As String = App.mvfield(thisEvent,2,"-")
            Dim rowKey As String = App.MvField(thisEvent,3,"-")
            'ask the user
            'save this memberid for later retrieval
            Page.SetLocalStorage("memberid", rowKey)
            'save the table row for later retrieval
            Page.SetLocalStorage("index", rowPos)
            'call a method existing in this application
            '_banano_atm_pgindex.deletemember();
            Dim onYes As String = Page.CallMethod("atm","pgindex","deletemember();")
            Page.SweetModalConfirm($"Confirm Delete: ${rowKey}"$, _
            "Are you sure that you want to delete this record? You will not be able to undo your changes.", onYes, "")

Clicking the delete button activates through BANano the linked event indexEvents method and then the eventid is parsed to execute delete row.

The rowKey, being the idnumber of the member to be deleted is read from the event event, a prompt is provided to the user to confirm..

dodelete.png


using the idnumber of the member, when a user selects Yes, then the DeleteMember sub is called. The idnumber of the member to be deleted is saved as 'memberid' to localstorage and DeleteMember uses that to delete the member from the existing list of 'members' in localstorage.

NB: There is a new way in BANano to execute this type of functionality. Havent got my head around it yet.

B4X:
'user clicked yes on confirmation, process the deletion
Sub DeleteMember
    'get the memberid to delete
    Dim memberid As String = Page.GetLocalStorage("memberid")
    'get the existing members
    Dim strMembers As String = Page.GetLocalStorage("members")
    If strMembers = "" Then
    Else
        'this will store all unmatching records
        Dim nList As List
        nList.Initialize
        nList.clear
        'convert existing members to a list
        Dim lstM As List = App.Json2List(strMembers)
        'each member is a json string
        Dim mTot As Int = lstM.Size - 1
        Dim mCnt As Int
        For mCnt = 0 To mTot
            Dim strMember As String = lstM.Get(mCnt)
            Dim memberM As Map = App.Json2Map(strMember)
            'the id number is the key
            Dim sidnumber As String = memberM.Get("idnumber")
            If sidnumber <> memberid Then
                nList.Add(strMember)
            End If
        Next
        'convert the list and save it back
        strMembers = App.List2Json(nList)
        Page.setlocalstorage("members", strMembers)
        'create the table
        CreateTable
    End If   
End Sub

This basically loops through all the members in localstorage saved list, creates a new list of only the members that dont have the idnumber being deleted and saves the list back to localstorage.
 

Mashiane

Expert
Licensed User
Longtime User
Compiling your BANano WebApp

Now that you are finished with your UX design and functionality coding, its time to let the BANano framework take over. You need to compile the web app with BANano so that you can distribute it.

Hit F5 or click the run button in the toolbar. This will compile your app to the specified directory. The generated content by BANano for File.DirApp will be the objects folder. You can open the index.html file on your browser and see the magic you created.

BANano creates these folders, assets (everything in your Files tab) that is not .css and not .js, scripts - everything that is .js and styles - everything that is .css. Also added will be your html file, manifest.json.

distribute.png


All the best...
 

Mashiane

Expert
Licensed User
Longtime User
27 March 2019 Update...

The table has been updated to make it easier to work with.

1. Add the table before Page.Create..

B4X:
'add the table at r3c1
    tbl.Initialize(App,"tblmembers","","idnumber",True,True,False)
    tbl.CanExport = False
    tbl.HasBorder = True
    tbl.AddHeadingsFields(Array As String("membernumber","category","surname","fullnames","idnumber","gender","cellphone","email","edit","delete"))
    tbl.AddHeadings(Array As String("Member #","Category","Surname","Fullnames","ID #","Gender","Cellphone #","Email","Edit","Delete"))
    tbl.AddHeadingsThemes(Array As String(App.Theme,App.theme,App.theme,App.theme,App.theme,App.theme,App.theme,App.theme,App.theme,App.theme))
    tbl.AddHeadingsAlignment(Array As String("","","","","","","","",App.EnumAlignment.Center,App.EnumAlignment.Center))
    tbl.AddHeadingsWidths(Array As String("","","","","","","","","60","60"))
    Page.Content.AddTable(3,1,tbl)

One can also add individual columns and specify each column properties. This is far more better than adding columns as per above way.

2. Loading the table from records, called after Page.Create

B4X:
'refresh the table
Sub RefreshTable
    'read the saved members from localstorage
    Dim strMembers As String = Page.GetLocalStorage("members")
    If strMembers = "" Then Return
    'convert existing members to a list
    Dim lstm As List = App.Json2List(strMembers)
    'we need maps to process
    Dim lstX As List
    lstX.Initialize
    For Each strMember As String In lstm
        Dim mapx As Map = App.Json2Map(strMember)
        lstX.Add(mapx)
    Next
    tbl.Load(Me,"pgindex",lstX,"white.black")
End Sub

This reads all saved JSON records from members key in localstorage i.e. BANAno.GetLocalStorage("members")

This is converted to a list with Json2List and then each record there (currently a string per member) is converted to a new list that has maps. Then the table.load method is used to load the available members.

3. As each member has edit/delete, a method to trap the events is created..

B4X:
'table click events
Sub tblmembers_click(e As BANanoEvent)
    e.StopPropagation
    Dim prefix As String = tbl.GetPrefix(e.ID)
    Dim rkey As String = tbl.GetKey(e.ID)
    Select Case prefix
        Case "edit"
            action = "update"
            modMember1.ClearMember1(Page)
            modMember2.ClearMember2(Page)
            modMember3.ClearMember3(Page)
            Dim strMember As String = ReadMember(rkey)
            Page.SetLocalStorage("member", strMember)
            modMember1.ReadMember1(Page,strMember)
            modMember2.ReadMember2(Page,strMember)
            modMember3.ReadMember3(Page,strMember)
            'refresh controls
            Page.refresh
            member.Open
        Case "delete"
            'ask the user
            'save this memberid for later retrieval
            Page.SetLocalStorage("memberid", rkey)
            'call a method existing in this application
            '_banano_atm_pgindex.deletemember();
            Dim onYes As String = App.CallBack("pgindex","deletemember",Array As String(""))
            Page.SweetModalConfirm($"Confirm Delete: ${rkey}"$, _
            "Are you sure that you want to delete this record? You will not be able to undo your changes.", onYes, "")
    End Select
End Sub

This table methodlogy based on how the FlowChart app works was applied for this project as a once off for a consistent code outlook.

Ta!
 
Top