B4J Code Snippet [ABMaterial] Making an ABMCombo MultiSelect with ABMCheckboxes

Mashiane

Expert
Licensed User
Ola

So i am sitting and thinking... I need some multi-check box functionality in my combo box. I need to return the selected items and be able to set each item to be checked. Then it dawns on me. One of the nice things about the ABMCombo is the functionality to add ABMContainers to it. So it should be possible to add a container with a checkbox as an item. This basically is a nicer alternative for what I intend doing. I can trap the combo click event, get the selected items and process what I need to do.

ABMCombo.gif


So, I need a container that has a checkbox, I should be able to set the checkbox on and off on addition.

So I add this to my ABMShared..

B4X:
'add a checkbox item to combo
Sub ABMCheckBoxItem(page As ABMPage, id As String, text As String, checked As Boolean) As ABMContainer
    Dim ItemCont As ABMContainer
    ItemCont.Initialize(page, id, "")
    ItemCont.AddRowsM(1,False,0,20, "").AddCellsOSMP(1,0,0,0,12,12,12,0,0,10,10,"")
    ItemCont.BuildGrid 'IMPORTANT once you loaded the complete grid AND before you start adding components
 
    Dim chk As ABMCheckbox
    chk.Initialize(page, id,text,checked,"")
    ItemCont.Cell(1,1).AddComponent(chk)
    Return ItemCont
End Sub
Here I create a container with a checkbox, specifiying the id and text of the checkbox.

So in my connectpage, I create a combo and add some check boxes to it.

B4X:
Dim asel As ABMCombo
    asel.Initialize(page,"asel","Resources","650","")
    asel.AddItem("opt1","Option 1",ABMShared.ABMCheckBoxItem(page,"opt1","Option 1",True))
    asel.AddItem("opt2","Option 2",ABMShared.ABMCheckBoxItem(page,"opt2","Option 2",False))
    asel.AddItem("opt3","Option 3",ABMShared.ABMCheckBoxItem(page,"opt3","Option 3",True))
    asel.AddItem("opt4","Option 4",ABMShared.ABMCheckBoxItem(page,"opt4","Option 4",False))
    asel.AddItem("opt5","Option 5",ABMShared.ABMCheckBoxItem(page,"opt5","Option 5",False))
    page.Cell(2,1).AddComponent(asel)
Now, when the combo box is selected, I need to get the items that are checked in the combobox. So I add a click event. Off course, returning the selected items can be done anywhere in my code. The returned object is a list with the ids of each of the items selected.

B4X:
Sub asel_Clicked(itemId As String)
    Dim lst As List = ABMShared.ABMComboGetChecked(page,"asel")
    Dim out As String = ABMShared.List2JSON(lst)
    page.Msgbox("",out,"asel checked","ok",False,"","")
End Sub
As noted above, I have called ABMComboGetChecked passing the combo box name to it. To be able to do this I needed to write some JQuery.

B4X:
'get checked from combobox
Sub ABMComboGetChecked(page As ABMPage, id As String) As List
    Dim ulKey As String = $"dropdown${id}"$
    Dim script As String = $"var selected = [];
$('#${ulKey} input:checked').each(function() {
    selected.push($(this).attr('id'));
});
var myJSON = selected.toString();
return myJSON;"$
Dim f As Future = page.ws.EvalWithResult(script,Null)
Dim sout As String = f.value
Dim items() As String = StrParse(",",sout)
Dim lstItems As List
lstItems.Initialize
For Each strItem As String In items
    strItem = MvField(strItem,1,"-")
    lstItems.Add(strItem)
Next
Return lstItems
End Sub
When you right click on any HTML element on your page, you can see how its created. So the UL (unordered list) that stores the combo items is called dropdown + id of your component. The checkboxes are somewhere in that html element, so I call jquery to return all checked input ids inside the UL. Remember from this, the ABM dropdown is not a SELECT component but a UL.

Each element id is suffixed with -id+'input' being for example opt1-opt1input, so we need to return opt1, thus the mvfield function. This ends up being shown on the msgbox in the click event.

So, to be able to check anything in the combo, I also needed another method.

B4X:
Sub iif(Expression2Evaluate As String, ReturnIfTrue As Object, ReturnIfFalse As Object) As Object
    If Expression2Evaluate = True Then Return ReturnIfTrue Else Return ReturnIfFalse
End Sub

'set checked
Sub ABMComboSetChecked(page As ABMPage, compid As String, id As String, bChecked As Boolean)
    If id.length = 0 Then Return
    If id.StartsWith("[") And id.EndsWith("]") Then
        Dim lst As List = Json2List(id)
        id = MvFromList(lst,",")
    End If
    Dim parentID As String = $"dropdown${compid}"$
    'ability to set multiple items at once
    Dim spItems() As String = StrParse(",",id)
    Dim sbCode As StringBuilder
    sbCode.Initialize
    For Each strItem As String In spItems
        id = strItem
        Dim itmKey As String = $"${id}-${id}input"$
        Dim sChecked As String = iif(bChecked=True,"true","false")
        Dim script As String = $"$('#${parentID}').find('#${itmKey}').prop('checked', ${sChecked});"$
        sbCode.Append(script).Append(CRLF)
    Next
    page.ws.Eval(sbCode.tostring,Null)
    page.ws.flush
End Sub

Sub MvFromList(lst As List, Delim As String) As String
    Dim lTot As Int
    Dim lCnt As Int
    Dim lStr As StringBuilder
    lStr.Initialize
    lTot = lst.Size - 1
    For lCnt = 0 To lTot
        lStr.Append(lst.Get(lCnt))
        If lCnt <> lTot Then lStr.Append(Delim)
    Next
    Return lStr.tostring
End Sub
This I can call whenever I need to.

B4X:
ABMShared.ABMComboSetChecked(page, "cboItems"", "opt5",True)
As each element in a page needs a unique name, there can only be one opt5. This also creates the proper name for the element and then runs a JQuery script to check the element.

Should you wish to check elements in ConnectPage, ensure you do so after page.refresh.

Happy coding.

PS: In Summary, just to show how I have currently implemented this...

1. I build my normal modal sheet the same way it has been and add an ABMCombo in it.
2. On Add / Edit, I show the modal and clear the contents of the controls and refresh the ones I need where necessary. As an example, to refresh my combo /w checkboxes combo I call this method after I have called .ShowModalSheet. This selects records form a database and loads them to the combo, all the items are un-checked. This is both applicable for Add / Edit CRUD functions.

B4X:
'Refresh the contents of the ABMCombo at runtime.
Private Sub RefreshOnLoad_cboforums()
    'Get access to the component in the page.
    Dim msgenpeople As ABMModalSheet = page.ModalSheet("msgenpeople")
    Dim cboforums As ABMCombo = msgenpeople.Content.Component("cboforums")
    'Clear the ABMCombo items
    cboforums.Clear
    'Define list details to load to the combo
    Dim results As List
    Dim resCnt As Int
    Dim resTot As Int
    Dim resMap As Map
    'variable to hold the source field
    Dim id As String
    'variable to hold the description field
    Dim Name As String
    'Read arguments from LocalStorage (if any)
    'Add a spinner to the page
    'Get connection from current pool if MySQL/MSSQL
    Dim jSQL As SQL = ABMShared.SQLGet
    'Get the records as a list of maps from the db
    results = ABMShared.SQLExecuteMaps(jSQL,"select id,name from Forums  order by name", Null)
    'Close the connection to the database
    ABMShared.SQLClose(jSQL)
    'Loop throught each record read and process it
    resTot = results.size - 1
    For resCnt = 0 To resTot
        'Get the record map
        resMap = results.get(resCnt)
        'process the id field
        id = resMap.get("id")
        Name = resMap.get("name")
        cboforums.AddItem(id, Name, ABMShared.ABMCheckBoxItem(page, id, Name, False))
    Next
    'Refresh the ABMCombo contents
    cboforums.Refresh
End Sub
3. When a record is added / updated, I save the contents to a map which is used on my inserts database calls. To read the contents of the checked combo items, I call..

B4X:
Dim lstcboforums As List = ABMShared.ABMComboGetCheckedModal(page, "msgenpeople", "cboforums")
    Dim outcboforums As String = ABMShared.List2JSON(lstcboforums)
    pMap.put("forums",outcboforums)
The contents of outcboforums will be like ["1","2","4"] which will be ids of the checked items, this is put on the pMap map object and saved to the db.

4. When a record is selected for Edit i.e. ShowModal and view current record for editing, I read the db record to a map and then for the combo / w check, after it has been refreshed, just check the relevent items for this record.

B4X:
Dim cboforumsContents As String
    cboforumsContents = pMap.getdefault("forums","")
    ABMShared.ABMComboUnCheckAllModal(page, "msgenpeople","cboforums")
    ABMShared.ABMComboSetCheckedModal(page, "msgenpeople","cboforums", cboforumsContents,True)
 
Last edited:

Mashiane

Expert
Licensed User
@liulifeng77 , these are custom methods i wrote, here you go.

B4X:
Sub List2JSON(lst As List) As String
    Dim sOut As String
    Dim jsonGen As JSONGenerator
    jsonGen.Initialize2(lst)
    sOut = jsonGen.ToString
    Return sOut
End Sub
Sub StrParse(Delimiter As String, MV As String) As String()
    Delimiter = FixDelimiter(Delimiter)
    Return Regex.Split(Delimiter, MV)
End Sub
B4X:
Sub FixDelimiter(sValue As String) As String
    If sValue = "|" Then sValue = "\|"
    If sValue = "." Then sValue = "\."
    If sValue = "\" Then sValue = "\\"
    If sValue = "^" Then sValue = "\^"
    If sValue = "$" Then sValue = "\$"
    If sValue = "?" Then sValue = "\?"
    If sValue = "*" Then sValue = "\*"
    If sValue = "+" Then sValue = "\+"
    If sValue = "(" Then sValue = "\("
    If sValue = ")" Then sValue = "\)"
    If sValue = "[" Then sValue = "\["
    If sValue = "{" Then sValue = "\{"
    If sValue = ";" Then sValue = "\;"
    Return sValue
End Sub
B4X:
Sub MvField(sValue As String, iPosition As Int, Delimiter As String) As String
    If sValue.Length = 0 Then Return ""
    Dim xPos As Int: xPos = sValue.IndexOf(Delimiter)
    If xPos = -1 Then Return sValue
    Dim mValues() As String
    Dim tValues As Int
    Delimiter = FixDelimiter(Delimiter)
    If sValue.EndsWith(Delimiter) Then sValue = sValue & " "
    mValues = Regex.split(Delimiter, sValue)
    tValues = mValues.Length -1
    Select Case iPosition
        Case -1
            Return mValues(tValues)
        Case -2
            Return mValues(tValues - 1)
        Case Else
            iPosition = iPosition - 1
            If iPosition <= -1 Then Return mValues(tValues)
            If iPosition > tValues Then Return ""
            Return mValues(iPosition)
    End Select
End Sub
 

Mashiane

Expert
Licensed User
If you place the combobox in a modal sheet, you need to use the method below, first uncheck all the items and then check the items you want. The mdlName is the name of your modal sheet, pref lowercase. You can right click on on the html element and 'inspect element' to see how this was defined by ABM.

B4X:
'unchech all if combo on page
Sub ABMComboUnCheckAll(page As ABMPage, id As String)
    Dim ulKey As String = $"dropdown${id}"$
    Dim script As String = $"$('#${ulKey}').find('input[type=checkbox]:checked').removeAttr('checked');"$
    page.ws.Eval(script,Null)
    page.ws.Flush
End Sub

'unchech all if combo on modal content
Sub ABMComboUnCheckAllModal(page As ABMPage, mdlName as string, id As String)
    Dim ulKey As String = $"dropdown${mdlName}-content-${id}"$
    Dim script As String = $"$('#${ulKey}').find('input[type=checkbox]:checked').removeAttr('checked');"$
    page.ws.Eval(script,Null)
    page.ws.Flush
End Sub

'get checked if combo on modal content
Sub ABMComboGetCheckedModal(page As ABMPage, mdlName As String, id As String) As List
    Dim ulKey As String = $"dropdown${mdlName}-content-${id}"$
    Dim script As String = $"var selected = [];
    $('#${ulKey} input:checked').each(function() {
    selected.push($(this).attr('id'));
    });
    var myJSON = selected.toString();
    return myJSON;"$
    Dim f As Future = page.ws.EvalWithResult(script,Null)
    Dim sout As String = f.value
    Dim items() As String = StrParse(",",sout)
    Dim lstItems As List
    lstItems.Initialize
    For Each strItem As String In items
        strItem = MvField(strItem,1,"-")
        lstItems.Add(strItem)
    Next
    Return lstItems
End Sub
 

Mashiane

Expert
Licensed User
NB: For this to work, ShowModal should have been called first before setting its control contents.

To be able to set the checked items on the modal.

e. ABMComboSetCheckedModal(page, "mdlName", "cboBox","opt1,opt3", True) to set all the ids

B4X:
'set checked items on a page
Sub ABMComboSetCheckedModal(page As ABMPage, mdlName As String, compid As String, id As String, bChecked As Boolean)
    If id.length = 0 Then Return
    If id.StartsWith("[") And id.EndsWith("]") Then
        Dim lst As List = Json2List(id)
        id = MvFromList(lst,",")
    End If
    Dim parentID As String = $"dropdown${mdlName}-content-${compid}"$
    'ability to set multiple items at once
    Dim spItems() As String = StrParse(",",id)
    Dim sbCode As StringBuilder
    sbCode.Initialize
    For Each strItem As String In spItems
        id = strItem
        Dim itmKey As String = $"${id}-${id}input"$
        Dim sChecked As String = iif(bChecked=True,"true","false")
        Dim script As String = $"$('#${parentID}').find('#${itmKey}').prop('checked', ${sChecked});"$
        sbCode.Append(script).Append(CRLF)
    Next
    page.ws.Eval(sbCode.tostring,Null)
    page.ws.flush
End Sub

[/COD
 
Last edited:

liulifeng77

Active Member
Licensed User
nice,I like it!。by the way, a weird problem.,when I clicked the item, sub XX_clicked(itemid as string) will be fired twice.
B4X:
Sub choice_Clicked(itemId As String)
    selected_class.Clear
    selected_class = ABMShared.ABMComboGetChecked(page,"choice")
'    Dim out As String = ABMShared.List2JSON(selected_class)
'    page.Msgbox("",out,"asel checked","ok",False,"","")
    Log("check")
    'show_tab_message
End Sub
 
Last edited:

Mashiane

Expert
Licensed User
@liulifeng77 , you are right. I tested what you are saying and just like I expected, the checkbox click event and then the combo box click event are being fired one after another. I guess the checkbox click bubbles up to the next event in line, thus the firing twice.

As you are aware, I did an "element inspection" which gives the HTML content of the page/component. This is how ABM has defined the checkbox and combo to have those events fired. I guess if there was a way to turn an event off it could work, but then again, I don't know what other consequences it will have. I'm not prepared to find out.

1. see <li> <div onclick = "comboclickarray(..." and further down "checkboxclickarray..." on input

Finally, in my case I am not trapping the combo click event as I just want to return the selected items. That was the point initially and I am not going any further than that.

Perhaps in your case, you can have yourself a button that one can click to process your selection and not trap the combo_click event at all, I see you assign that to a list. Or else play around with jQuery and see where it can take you.

CheckComboClick.png


PS: I will request AB to have a look at it.
 
Last edited:

Mashiane

Expert
Licensed User
As per post number 1, the items in my ComboCheckBox component are loaded from the Forums table. I want when the contact details are listed, the forum a person belongs to gets listed as a chip.

ComboChecked2Chips.png


When loading records to a table, the records to display are filtered, an executemaps is ran and each record is returned as a map. So for each record processed, in the forums table I run

B4X:
Dim forums As String = resMap.GetDefault("forums", "")
        If forums = "" Then
            rCellValues.Add("{NBSP}")
        Else
            Dim abmc As ABMContainer = ABMShared.ABMComboChecked2Chips(page,resMap,"forums","Forums","id","name")
            rCellValues.Add(abmc)
        End If
        'Add theme to the cell
        rCellThemes.Add("nocolor")
What ABMComboChecked2Chips does is: for the selected record, the complete map is passed to it, the field to read in the current record is forums, this will return a list of all forums the user belongs to as ["1","2"] as per previonsly checked forums from the combocheck box, depending on what was saved in the database as selected by end user.

i.e then for each of the selected forums in [1,2], go to the "Forums" table", use a field named "id" to return the contents of the "name" field.

B4X:
'convert combo checked items to chips
Sub ABMComboChecked2Chips(pg As ABMPage, rMap As Map, sFieldName As String,sSourceTable As String,sSourceIDField As String,ssourcedescriptionfield As String) As ABMContainer
    'read the id of the record
    Dim sid As String = rMap.GetDefault("id","")
    'initialize the container
    Dim ItemCont As ABMContainer
    ItemCont.Initialize(pg, "combo2chip" & sid, "")
    ItemCont.IsResponsiveContainer = True
    ItemCont.AddRowsM(1,False,0,0, "").AddCellsOSMP(1,0,0,0,12,12,12,0,0,0,0,"")
    ItemCont.BuildGrid 'IMPORTANT once you loaded the complete grid AND before you start adding components
   
    Dim msFieldName As String = rMap.GetDefault(sFieldName.ToLowerCase,"")
   
    'open the connection to the database
    Dim jSQL As SQL = SQLGet
    'convert the checked items to a list
    Dim itmItems As List = Json2List(msFieldName)
    For Each chkItem As String In itmItems
        Dim chkName As String = SQLSelectSingleResult(jSQL, $"select ${ssourcedescriptionfield} from ${sSourceTable} where ${sSourceIDField} = ?"$, Array As String(chkItem))
Dim sKey As String = $"combo2chip"${sid}${chkItem}"$
        Dim abmc As ABMChip
        abmc.Initialize(pg,sKey,chkName,False,"")
        abmc.Tag = chkName
        ItemCont.Cell(1,1).AddComponent(abmc)
    Next
    SQLClose(jSQL)
    Return ItemCont
End Sub
I will just now theme my chips for follow my user defined color scheme.

Ta!
 

codie01

Active Member
Licensed User
Hi Mashiane

I have my combobox inside two containers as follows:

<code>
Dim myContainer0 As ABMContainer = page.Component("section2h")
Dim myContainer1 As ABMContainer = myContainer0.Component("section2a")
Dim myCombo1 As ABMCombo = myContainer1.Component("section2aCombo0") 'Location
</code>

How do I modify your code to be access this combo object.

Thanks in advance.
 

codie01

Active Member
Licensed User
If you dont mind me asking, is your question specific to the discussions on this thread or is a general ABMaterial question?
Hi Mashine,

I have a combobox on a page that is inside two containers. How do I change your below subroutine "id" so I can point to the combobox.

Sub ABMComboGetChecked(page As ABMPage, id As String) As List

Thanks
 

Mashiane

Expert
Licensed User
<code>
Dim myContainer0 As ABMContainer = page.Component("section2h")
Dim myContainer1 As ABMContainer = myContainer0.Component("section2a")
Dim myCombo1 As ABMCombo = myContainer1.Component("section2aCombo0") 'Location
</code>

How do I modify your code to be access this combo object.
The reason I am still confused is because your question is vague. You provided the code snippet above and I am not sure whether you are trying to reproduce the code in this thread or are talking about something else. Your code also is not enough, it does not show how the components you are getting were created etc.

Perhaps just send a sample small project that I can take a look at. This for me
How do I modify your code to be access this combo object.
I honestly dont understand what do you mean.

Ta!
 

Mashiane

Expert
Licensed User
I have a combobox on a page that is inside two containers. How do I change your below subroutine "id" so I can point to the combobox.

Sub ABMComboGetChecked(page As ABMPage, id As String) As List
On the other hand, is the combobox that you have created based on the code in this thread, i.e. is it a combo with checkboxes as defined here? If so, can you please post the code so that I can see how you created it and then try and help you.

I hope you understand.
 
Top