There are workflow use cases that aren't directly connected to loading and deliveries even in a shipping company. Based on your work for them so far, Rocky Jupiter think you rock and want you to take over preferably all their other workflow management form-building. Starting with the vast contacts list that brings together all the people in the organization for a handy reference on every Jupiterian's mobile device.
The tricky bit (you wouldn't like it without a tricky bit, right?) is
that the list of people and their contact details needs to be searchable, navigable
page by page, and editable, and the form should make it easy to call, email, or map
the location of the people on the list. This task will bring together everything
that you've learned about forms and workflow scripts, and introduce you to a new
element, the actionbutton
.
The task has been chunked into manageable bits, so there's no reason to dawdle; let's get to it.
Any contact list has to start with a list of people, which can only be a reference table in the company database. Reference tables all need a declaration file, so that's what you need to get going. The table needs to include names, phone numbers, company department info, addresses, and emails, and the declaration file will reflect this.
<Reftab Name="contact" xmlns="http://schemas.mobilengine.com/reftab/v1" Push="true" Notify="true"> <Columns> <Column Name="id" Type="Text" PrimaryKey="true" NotNull="true"></Column> <Column Name="name" Type="Text" NotNull="true"></Column> <Column Name="phone" Type="Text" NotNull="true"></Column> <Column Name="category" Type="Text" NotNull="true"></Column> <Column Name="address" Type="Text" NotNull="false"></Column> <Column Name="mail" Type="Text" NotNull="true"></Column> </Columns> </Reftab>
Download an input data spreadsheet for the meat on the bones, and you're set to actually start on the form.
Now to the form. As you'd expect, it's actually just a table
control that queries the contact
reference table, and displays
it on a mobile device. It displays the addresses in linkview
s
that start up the native mapping application, as you've seen before. Apple also has APIs in place to start up the native dialler
and the native email app on an iOS device, and the table
will
take advantage of these in a second linkview
for the phone
numbers, and a mailto
control (a specialized
linkview
) for the email addresses. Everything else in the form
is standard Mobilengine table
control operating
procedures.
<form id='contacts' menuName='Search the Contacts Database' platforms='ios' xmlns='http://schemas.mobilengine.com/fls/v2'> <table id='contactsTable' record='c' recordset='{SELECT c.id, c.name, c.phone, c.category, c.address, c.mail FROM results c}'> <header> <row> <cell> <textview text='Name'/> </cell> <cell> <textview text='Call'/> </cell> <cell> <textview text='Category'/> </cell> <cell> <textview text='Find'/> </cell> <cell> <textview text='Mail'/> </cell> </row> </header> <row> <cell> <textview id='colName' text='{c.name}'/> </cell> <cell><linkview text='Call' url='{"tel:" || c.phone}'/>
</cell> <cell> <textview id='colCategory' text='{c.category}'/> </cell> <cell><linkview text='Find' url='{SELECT "http://maps.apple.com/?address=" || REPLACE(c.address, " ", "%20") address}'/>
</cell> <cell><mailto text='Send e-mail' address='{c.mail}'/>
</cell> </row> </table> </form>
Save
the form, the declaration file, and the spreadsheet in the same folder, and
publish via the mebt
. Open on the form, look at all the people,
and call, find, and email them at will.
Wait a second, this is just a wall of text that goes on and on. You'll
definitely have to make this more user-fiendly, or else no-one will use it (and
worse still, pay you for it).
Add icons to the links
The Mobilengine platform lets you upload icons along with a workflow solution, and then use those icons in one or more forms to spice up the look and feel, enhance the user experience, or just go crazy. In this part, you'll be replacing the text in the links in the form with fancy icons as a first step to making the contact list form easier to use.
Here's the icons for the phone, map, and email links. Their #34A0E8 color matches the Rocky Jupiter branding, but feel free to use your own images.
Use the
icons
.[image-name-without-extension]
syntax to pull an icon into a form
control.
<form id='contacts'...> <table id='contactsTable'...> ... <row> ... <cell> <linkviewlinkIcon='{icons.phone}'
url='{"tel:" || c.phone}'/> </cell> ... <cell> <linkviewlinkIcon='{icons.map}'
url='{SELECT "http://maps.apple.com/?address=" || REPLACE(c.address, " ", "%20") address}'/> </cell> <cell> <mailtolinkIcon='{icons.mail}'
address='{c.mail}'/> </cell> </row> </table> </form>
Make sure that you include all three referenced images in the solution folder, and upload the new version.
Well, so much for making the form easier on the eye. How about adding
functionality to find a specific person in all this jumble?
Making the list searchable and filterable
The list of contacts isn't even alphabetized in the database, and it's a scroll nightmare even to look at all the people, let alone find one. This must change.
Let's make the table
only display the contacts whose names begin
with a user-entered string, and sort the results by either their names or their
department. For the searching part, you'll reprise something you've already done when learning about data-binding, and for the sorting, it's a simple
case of an ORDER BY
clause.
<form id='contacts' menuName='Search the Contacts Database' platforms='ios' xmlns='http://schemas.mobilengine.com/fls/v2'> <textbox id='searchInput' label='Search by name' hint='Start typing'/> <segmentedbutton id='sort' label='Sort by' choices='{table key, text (1,"Name"; 2,"Category")}' keyMap='{key}' textMap='{text}'/> <table id='contactsTable' record='c' recordset='{SELECT c.id, c.name, c.phone, c.category, c.address, c.mail FROM results cWHERE c.name LIKE searchInput.text || "%" ORDER BY CASE WHEN sort.selectedKey==1 THEN name WHEN sort.selectedKey==2 THEN category END
}'> ... </table> </form>
Save, publish, and open on the mobile device. Go ahead and try out the search box.
The list is coming along nicely. Now, if you could go ahead and let the user
step through the list of results, that'd be great.
Long lists on websites have Next page and Previous page buttons so that users can acess the list in manageable bites. If you could even let them set how many items to display on a page, that would really make your users grateful.
Enter the actionbutton
, a
nifty little form element that will set or reset the value of one or more form
control properties to whatever you please.
If you make your actionbutton
increment a variable that feeds a
LIMIT clause in the table
query, you've got yourself a list
that you can navigate at the tap of a button.
<form id='contacts'...><declarations> <let id='page' shape='scalar' value='{0}'/> </declarations>
... <table id='contactsTable' record='c' recordset='{SELECT c.id, c.name, c.phone, c.category, c.address, c.mail FROM contact c WHERE c.name LIKE searchInput.text || "%" ORDER BY CASE WHEN sort.selectedKey==1 THEN name WHEN sort.selectedKey==2 THEN category ENDLIMIT (page*10), 10
}'> ...<footer> <row> <cell> <actionbutton text='Next page'> <set target='page' value='{page + 1}'/> </actionbutton> </cell> </row> </footer>
</table> </form>
The
page
variable starts off at 0
, and the
contactsTable
query displays only ten items, beginning with
page*10
(line 18). The actionbutton
in the
footer
increments the value of page
by
1 (lines 23-25). This effectively means that your user only sees the first ten
contacts when they open the form, and can page through the list in icrements of
ten contacts.
Save, publish, and open on the device.
It works, doesn't it? Only problem is, there's no way back...
Paging up and down
One-way list navigation is only slightly better than having no navigation at all. If you could keep track of where the user has currently navigated in the list, you could display a Previous page button without fear of the user being able to navigate themselves 'off' the list into negative page numbers and display non-existent contacts.
Keeping track of things is what variables do best. Have a variable query the
number of contacts that the contactsTable
is currently
displaying, and set up a condition that displays the Next
pageactionbutton
only if there exists at least one more page worth
of contacts to
display.
<form id='contacts'...> <declarations><let id="total" shape="scalar" value='{SELECT COUNT(*) FROM contact c WHERE c.name LIKE searchInput.text || "%"}'/>
<let id='page' shape='scalar' value='{0}'/> </declarations> ... <table id='contactsTable'...> ... <footer> <row><cell> <if cond='{ page > 0}'> <actionbutton text='Previous page'> <set target='page' value='{page - 1}'/> </actionbutton> </if> </cell> <cell> <if cond='{total >(page+1)*10}'> <actionbutton text='Next page'> <set target='page' value='{page + 1}'/> </actionbutton> </if> </cell>
</row> </footer> </table> </form>
The
Previous pageactionbutton
is also wrapped inside an if
, but
its cond
is quite straightforward.
Save, publish, and view as usual. Jump up and down in the list using the
actionbutton
s.
Out of sight. The user-adjustable page size you promised your users should
be a breeze at this point.
Let users set the page size
This one is actually pretty easy (not that anything so far was hard, right?): you
just need to introduce a variable to keep track of the current page size, and
set it equal to the value of a textbox
control where you ask
the user for a preferred page size.
<form id='contacts' menuName='Search the Contacts Database' platforms='ios' xmlns='http://schemas.mobilengine.com/fls/v2'> <declarations> <let id="total".../> <let id='page'.../><let id='pageSize' shape='scalar' value='{TOINT(sizeSelector.text)}'/>
</declarations> ... <table id='contactsTable' record='c' recordset='{SELECT c.id, c.name, c.phone, c.category, c.address, c.mail FROM contact c WHERE c.name LIKE searchInput.text || "%" ORDER BY CASE WHEN sort.selectedKey==1 THEN name WHEN sort.selectedKey==2 THEN category ENDLIMIT (page*pageSize), pageSize}'
> ... <footer> <row> <cell> <if cond='{ page > 0}'><actionbutton text='{"prev " || TOSTRING(pageSize)}'>
<set target='page' value='{page - 1}'/> </actionbutton> </if> </cell> <cell> <if cond='{total >(page+1)*pageSize}'><actionbutton text='{"next " || TOSTRING(pageSize)}'>
<set target='page' value='{page + 1}'/> </actionbutton> </if> </cell> </row> </footer> </table><textbox id='sizeSelector' label='Number of records on a page' text='{TOSTRING(10)}'/>
</form>
This
way, the user can have the form display as many or as few contacts at any one
time as they want, and the pager actionbutton
s will also
reflect the page size. Just make sure that you convert between strings and integers when resetting the
page size, and that you have a default page size so that the form works without
explicit user input.
Save, publish, and open on your mobile device.
Cowabunga!
If the specs for this form didn't include allowing users to edit contact details, you'd be done!
Allow user
edits? OK, you might say, why not just make the textview
s in the
contactsTable
table
textbox
es instead and be done with it?
The not-so-troublesome
reason you don't want to do that is that you can't make a linkview
user-editable, which means that you'd have to include three extra columns to display
the phone number, the address, and the email in a separate textbox
,
ruining your fancy icons setup.
The deal-breaker reason, though, is that the
starting strings for each of your textbox
es would still be coming
from the reference table and so every time the user ran a search or tapped a pager
actionbutton
, user edits would be lost. You'll have to think of
something else.
The way to have your cake, meaning functioning search and
paging, and eat it too, meaning functioning user edits, is to move the
textbox
es that store the user edits for each contact to another
form. One that triggers a workflow script on the server and updates the
contact
reference table itself with the user edits, so that no
amount of repeated queries in the contacts list form will drop those
edits.
The way to open a new form is by specifying it as the
nextForm
property of a submitbutton
control.
You could stick a submitbutton
in each row of the
contactsTable
table
. The new form could have a textbox
for each
of the fields of a contact, and the script that it triggers would update the
particular row in the contact
reference table with the input in the
textbox
es.
Seems legit in theory, let's see how it works out in practice.
A new table column for a new use case
The first hurdle is to make sure that the new form will display the details of only the specific contact that the user wants to edit.
The submitbutton
could trigger a workflow script that somehow
flags the row that had the submitbutton
. The flag could be a
specific value inserted into an optional field for the row that no other query
uses. The new form that the submitbutton
opens on the device
could query only the rows that have a specific value in this optinal field, and
so would end up with only the row you want to display.
This optional field would have to be a new column in the contact
reference table. And a new column requires a new reference table
declaration.
<Reftab Name="contact" xmlns="http://schemas.mobilengine.com/reftab/v1" Push="true" Notify="true"> <Columns> <Column Name="id" Type="Text" PrimaryKey="true" NotNull="true"></Column> <Column Name="name" Type="Text" NotNull="true"></Column> <Column Name="phone" Type="Text" NotNull="true"></Column> <Column Name="category" Type="Text" NotNull="true"></Column> <Column Name="address" Type="Text" NotNull="false"></Column> <Column Name="mail" Type="Text" NotNull="true"></Column><Column Name="edit_status" Type="Text" NotNull="false"></Column>
</Columns> </Reftab>
You'll also need to download a new input data spreadsheet identical to the previous one save for the added empty column.
All right, time for the submitbutton
that gets things going.
Actually, you'll need more than a submitbutton
. User edits
aren't all about updating existing details. A user should very well be able to
add or delete contacts as well. While you can delegate both tasks to the new
form that the submitbutton
s open, you'll have to include an
extra Add new contact button on the original contact list
form.
In keeping with the icons-for-everything approach that served you so well so far,
why not have both the 'editing' and the 'adding' submitbutton
s
have an icon in a matching color?
Click here and here to save the images in your
solution folder.
To the form: insert the submitbutton
inside the
row
template so that each contact will have one. The button
will submit the form to the server, and the form submission will trigger a workflow script that inserts a value into the
edit_status
field of the row in question. The form that the
submitbutton
opens will display rows with an
edit_status
value
As for the Add new contact button, it doesn't need to
trigger a workflow script - it will open the new editing form with blank
textbox
es - and so doesn't need to submit the form. Make it
a discardbutton
, then, no need to tax the bandwidth.
Add your icons, but don't set a nextForm
property yet, since
there's nothing to point
to.
<form id='contacts'...> ...<header> <discardbutton text=''/> </header>
<table id='contactsTable'...> ... <row><declarations> <let id='id' shape='scalar' value='{c.id}'/> </declarations> <cell> <submitbutton id='edit' labelIcon='{icons.edit}' text=''/> </cell>
... </row> ... </table> ...<discardbutton id='add' linkIcon='{icons.add}' text='Add new contact'/>
</form>
As
you can see, with the submitbutton
included, you don't need to
have a separate Submit button for the form, hence the
header
element with the
discardbutton
.
Save, publish with the icons, the new declaration file, and the new spreadsheet.
Pretty pretty.
Now for the robust functionality of those fancy new buttons.
You'll make the submitbutton
s run a script that inserts the
Editing
string into the edit_status
field
of the contact that it refers
to.
The
script is crucially a client server
one, so that it runs on the mobile client and
updates the local snapshot of the database, so the user will be able to see
their edits in the contact list. The script iterates through the submitted
table
, and updates the row, identified by traversing the
form control hierarchy using the parentControl
property that refers to, wait for it, the parent node of the control in the XML
hierarchy.
The editor form
Your user can now specify a single contact that they want to change by updating
its edit_status
column. All you need now is a second form that
will open when the user taps the pretty pencil icon (or the even prettier new
user icon).
It's going to be a very simple affair: textbox
es, mainly, with a
dropdown
for the Category field,
since it's a choice from a list of options. The interesting bit is that you
declare a variable that stores all the fields of the row that has the
Editing
value in its edit_status
field (or
whatever you specified in the contactsChecker
script). This
variable will hold null
in all its fields if there isn't such a
row, so if you data-bind its fields into the controls in the Edit
contact form, you'll end up with blank controls, which is what
you want if the user wants to add a new
contact.
<form id='editor' menuName='Edit contact' platforms='ios' xmlns='http://schemas.mobilengine.com/fls/v2'menuOrder='hidden'
> <declarations> <let id="record" shape="record" value='{SELECT c.id, c.name, c.phone, c.category, c.address, c.mail, c.edit_status FROM contact c WHERE c.edit_status=="Editing"}'/> </declarations><header></header>
<textbox id='newName' label='Name' text='{record.name}'/> <textbox id='newPhone' label='Phone' text='{record.phone}'/> <dropdown id='newCategory' label='Category' choices='{table key, text (1,"Head Office"; 2,"Colleague"; 3,"Auto Service"; 4,"Emergency")}' keyMap='{key}' textMap='{text}' selectedKey='{SELECT cat.key FROM (table key, text (1,"Head Office"; 2,"Colleague"; 3,"Auto Service"; 4,"Emergency")) cat WHERE cat.text == record.category}'/> <textbox id='newAddress' label='Address' text='{record.address}'/> <textbox id='newMail' label='Mail' text='{record.mail}'/><submitbutton id="save" text='Save' nextForm='{forms.contacts}'/> <if cond='{record IS NOT NULL}'> <submitbutton id='delete' text='Delete contact' nextForm='{forms.contacts}'/> </if> <discardbutton id='discard' text='Discard edits' nextForm='{forms.contacts}'/>
</form>
All
you need to make sure is that the form does not appear in the sidebar (line 5),
does not have any buttons in its header (line 19), and that all its
submitbutton
and discardbutton
s point back
to the contact list. The delete
discardbutton
should only appear if there is anything to
delete, that is, there is a row with a non-null edit_status
field.
Save, publish, and tap one of the pretty icons on your contact list form.
Well, it opens, doesn't it, and either displays the values from the row with the
submitbutton
, or blank controls if you tapped
Add new contact to get here. Also, the buttons at the
bottom open the contacts list back up. That's it, though. Any changes you might
have made in the controls are irrevocably lost when you close this form. What
the Edit contact form desperately needs is a workflow
script that processes its submission data.
The editor script
Now, this needs a delicate touch and will result in a slightly
complicated-looking tree of conditional statements, because you want the script
to behave differently based on which submitbutton
or
discardbutton
the user taps to close the form.
Let's break the expected behavior down, from the top:
If the user taps
Save, and there wasn't an original contact to
populate the form with, the script should insert the submission data into the
contact
reference table. If there was an original contact
that the user modified, it's an Update()
operation.
If the form was submitted with Delete contact, well, the contact will have to go, won't it?
Finally, if the user closed the form
with the Discard editsdiscardbutton
and there was an original contact to begin with,
the edit_status
column needs to be reset so that this or
another contact could be edited in the future. Remember, the Edit
contact form depends on one and only one contact having an
edit_status
of
Editing
.
client server program edits for form editor using reftab contact; { if(form.save.submitter) { if(form.record == null) { db.contact.Insert({id:guid.Generate().ToStringN(), name:form.newName.text, phone:form.newPhone.text, category:form.newCategory.selectedText, address:form.newAddress.text, mail:form.newMail.text}); } else { db.contact.Update({id:form.record.id}, {name:form.newName.text, phone:form.newPhone.text, category:form.newCategory.selectedText, address:form.newAddress.text, mail:form.newMail.text, edit_status:null}); } } if (form.delete.submitter) { db.contact.Delete({id:form.record.id}); } if (form.discard.submitter && form.record != null) { db.contact.Update({id:form.record.id}, {edit_status:null}); } }
Make
sure that the Update()
functions all reset the
edit_status
column to null
, otherwise
your user won't be able to ever modify any contact but the first one they
edited.
Save, publish, and open on the device. Make edits, delete, discard, and watch the contact list accommodate your changes in near-real time.
ERMAHGERD! YER A MAHBERLINERNJEN MAHSTER RERBERT NERNJA!
Master, your epic skills are needed to handle some conflicts in the next section! Please save the day; you are our only hope!