...
Code Block |
---|
var totalAssets = TotalAutoValue.value + TotalBankValue.value + TotalRealEstateValue.value; BasicSummaryMsg.value = "<b>Name:</b> " + FirstName.value + " " + LastName.value + "<br/>" + "<b>Phone:</b> " + Phone.value + "<br/>" + "<b>Email:</b> " + EmailAddress.value; if (MilitaryOrCivilian.value === 'Military') { //Military DetailedSummaryMsg.value = "<b>Military Info:</b><br/>" + "Military ID: " + MilitaryID.value + "<br/>" + "Rank: " + Rank.value + "<br/>" + "CurrentTitle: " + CurrentTitle.value + "<br/>" + "Years of Service: " + YearsOfService.value + "<br/>"; } else if (MilitaryOrCivilian.value === 'Civilian') { //Civilian DetailedSummaryMsg.value = "<b>Civilian Info:</b><br/>" + "SSN: " + SSN.value + "<br/>" + "Current Employer: " + CurrentEmployer.value + "<br/>" + "Current Title: " + CurrentTitle2.value + "<br/>" + "Start Date: " + StartDate.value + "<br/>"; } FinancialSummaryMsg.value = "<b>Total Assets:</b> $" + totalAssets + "<br/>" + "Total Bank Assets: $" + TotalBankValue.value + "<br/>" + "Total Real Estate Value: $" + TotalRealEstateValue.value + "<br/>" + "Total Auto Value: $" + TotalAutoValue.value + "<br/>"; |
...
Code Block |
---|
// Converts UTC to either CST or CDT if (form.load) { var today = new Date(); var DST = 1; // If today falls outside DST period, 1 extra hr offset var Central = 5; // Minimum 5 hr offset from UTC // Is it Daylight Savings Time? // var yr = today.getFullYear(); // 2nd Sunday in March can't occur after the 14th var dst_start = new Date("March 14, "+yr+" 02:00:00"); // 1st Sunday in November can't occur after the 7th var dst_end = new Date("November 07, "+yr+" 02:00:00"); var day = dst_start.getDay(); // day of week of 14th // Calculate 2nd Sunday in March of this year dst_start.setDate(14-day); day = dst_end.getDay(); // day of the week of 7th // Calculate first Sunday in November of this year dst_end.setDate(7-day); // Does today fall inside of DST period? if (today >= dst_start && today < dst_end) { DST = 0; } // Adjust Date for Central Timezone today.setHours(today.getHours() - Central - DST); var m = today.getMonth() + 1; var d = today.getDate(); var da = today.getDay(); var y = today.getFullYear(); var h = today.getHours(); var min = today.getMinutes(); if (min < 10) { min = '0' + min;} var timezone = ['CDT', 'CST']; var dom = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; var todayStr = dom[da] + ' ' + m + '-' + d + '-' + y + ' ' + h + ':' + min + ' ' + timezone[DST]; DateTime.value = todayStr; } |
Hours
...
>= 4 and <= 6 Apart
This rule makes sure the end time is at least 4 hours great then the start time but no more then 6 hours later then the start time. Also start time must be on or after 1:00. The times must be entered in military units. TS is the name of the Time Start control and TE is the name of the Time End control.
...
Code Block |
---|
if (TS.value.length > 0 && TE.value.length > 0) { var sTime = TS.value.split(':'); var sHour = sTime[0]; var sMin = sTime[1]; var sMins = sHour * 60 + parseInt(sMin,10); var eTime = TE.value.split(':'); var eHour = eTime [0]; var eMin = eTime [1]; var eMins = eHour * 60 + parseInt(eMin,10); if ((eMins - sMins) < 4*60 || (eMins - sMins) > 6*60) { TE.valid = false; } else { TE.valid = false; } else { TE.valid = true; } } |
Times
The date control can be set to either just a date, just a time, or a combined date/time. Here are several examples of initializing a time control name Tm;
Code Block |
---|
/ Both set the control name Tm to the same time Tm.value = '8:30 pm'; // uses am/pm notation Tm.value = ' 20:00'; // uses military time // Initialize a date/time uses a "T" to separate the values DtTm.value = "5/15/2012T4:20"; // Copying values Tm2.value = Tm.value; Dt2.value = Dt.value; DtTm2.value = DtTm.value; |
Tenants, Roles, Users
have several built-in methods that enable you to access information about your form server such as the list of tenants; users in a tenant; and roles in a tenant. See built-in methods for the complete list. Some of these methods return a boolean true/false value. Others return a JSON string. Here is a sample of how to use these methods.
Code Block |
---|
*member roles tenants users */ var x; if(form.load) { eval('x=' + frevvo.listTenants()); Tenants.options = x.tenants; // Init a dropdown list of all tenants on this form server } if(tenant.value.length > 0) { // Check if a tenant already exists if (frevvo.isUniqueRoleId(role.value, tenant.value) === false) { ErrMsg.value = 'The role ' + role.value + ' already exists in tenant ' + tenant.value; } // Init dropdown with all tenant users eval('x=' + frevvo.listUsers(tenant.value)); Users.options = x.users; // Init dropdown with all tenant roles eval('x=' + frevvo.listRoles(tenant.value)); Roles.options = x.roles; // Init dropdown will all tenant roles except admin roles eval('x=' + frevvo.listRoles(tenant.value, false)); RolesNA.options = x.roles; } |
Code Block |
---|
// Verify that a role already exists in the tenant if(role.value.length > 0) { t.value = frevvo.isUniqueRoleId(role.value, tenant.value); if (frevvo.isUniqueRoleId(role.value, tenant.value) === false) { ErrMsg.value = 'The role ' + role.value + ' already exists in tenant ' + tenant.value; } } |
Here is an example of populating a dropdown list of all tenants on the form server with value is set to tenant Id and label is set to the tenant description returned in the JSON string. The frevvo.listTenants() method returns a string like this: {"tenants":["Default tenant","My Test Tenant"],"ids":["d","nancy.com"]}. The eval function parses that string into an object that we can iterate as follows:
Code Block |
---|
/*member tenants ids */ if(form.load) { TA.value = frevvo.listTenants(); var x; eval('x=' + frevvo.listTenants()); var opts = []; for (var i = 0; i < x.tenants.length; i++) { opts[i] = x.ids[i] + '=' + x.tenants[i]; } // Initialize dropdown of all tenants on the form server TenantList.options = opts; } |
Repeat Item Added
This rule executes when a new item is added to a repeat. Imagine your form contains a repeating section named Employee with name Erepeat. NOTE: that the name Erepeat is set on the Repeat control and not on the Section control. The Employee section control contains many controls such as Name, Phone, etc.. and a dropdown control labeled Manager and named M. It also contains a radio control labeled Employee Shift named ES whose options have been set to 'Day' and 'Evening'.
...
Code Block | ||
---|---|---|
| ||
/*member firstname lastname options resultSet value */ var x; if (form.load) { var baseURL = 'http://www.myhost.com:8080/database/'; // Manager Dropdown eval('x=' + http.get(baseURL + 'Manager')); var opts = ['']; // default 1st option to blank for (var i=0; i < x.resultSet.length; i++) { if (x.resultSet[i]) { opts[i+1] opts[i+1] = x.resultSet[i].lastname+ ", " + x.resultSet[i].firstname; } } // Repeat item index 0 is on the form by default M[0].options = opts; ES[0].value = 'Day'; // default the employee shift to day } |
Now when a new item gets added when a user clicks the "+" icon we can save the overhead of going back to the database to retrieve dynamic options.
Code Block |
---|
if (Erepeat.itemAdded) { var index = Erepeat.itemIndex; // which item is this in the list // No need to go back to the database for options. // We already retrieved them in form.load rule for repeat item index 0 M[index].options = M[0].options; ES[index].value = 'Day'; // default the employee shift to day } |
Info |
---|
Tables are repeats. So the same rule can be written for a table control. The name of a table's repeat is always <TableName>Repeat. For example if you name your table Children. Then the repeat is named ChildrenRepeat. |
Repeat Item Added - Collapse Other Items
This rule executes when a new item is added to a repeat. This form contains a repeating section with a templatized label. It is nice to collapse the other items when adding a new item to keep the repeat list small and grid-like. Medrepeat is the name of the repeat control. Medication is the name of the section control inside the repeat.
...
Code Block |
---|
if (Medrepeat.itemAdded) { var index = Medrepeat.itemIndex; for (var i = 0; i < Medication.value.length; i++) { if (i !== index) { Medication[i].expanded = false; } else { Medication[i].expanded = true; } } } |
Repeat Dynamic Min/Max
It is not currently possible to change a repeat control's min and max properties in a business rule. This feature may be added in a future release. One solution is to use hidden section controls rather than a repeat control. For example imagine an airline reservation form where the number of traveler detail rows displayed and required is based on the number of airline tickets purchased. This example form allows a maximum of 3 ticket purchases. So, first add 3 section controls for traveler details. It is important to make sure that you name the travel detail section controls uniquely -- Traveler1 is the name of the first section, Traveler2 is the name of the second section, etc.. Next uncheck the visible and required property on all 3 section controls. Then add a business rule that makes the number of sections visible and required based on the number of purchased airline tickets.
Code Block |
---|
Traveler1.visible = false; Traveler2.visible = false; Traveler3.visible = false; Traveler1.required = false; Traveler2.required = false; Traveler3.required = false; for (var i=0; i < NumTickets.value; i++) { if (i >= 0) { Traveler1.visible = true; Traveler1.required = true; } if (i >= 1) { Traveler2.visible = true; Traveler2.required = true; } if (i >= 2) { Traveler3.visible = true; Traveler3.required = true; } } |
Tables
Tables are identical to repeat controls when referenced in business rules. Tables are a grid layout of repeating items. All the rule examples in this chapter that discuss repeats apply also to tables. The one important note is that you cannot explicitly name the repeat control inside your table. The repeat control inside a table is automatically named as <TableName>Repeat. For example a table named Expense automatically has a repeat named ExpenseRepeat. The rule ExpenseRepeat.itemAdded and ExpenseRepeat.itemIndex references an item added to your table and that item's index respectively.
...
Note | ||
---|---|---|
A section in a repeat will behave differently than a table row when using a rule to set the valid property. For Example, drag and drop a table control into your form. Name it T. Then drag and drop a section into the same form. Name it S. Add a control to the section. Name the control C. Drop the section into a repeat named R. Add this rule to the form.
The section header shows "Invalid value" as a message while the table row shows no such indication signifying an invalid row. |
form.load
Rules can be used to initialize field values. This is a very useful feature and is often used to dynamically populate dropdown options from a database. Rules using form.load are triggered when a form first loads and when a workflow is loaded from a task list.
...
Code Block |
---|
//1st Rule - Default Items if (form.load) { // The form contains two repeat items by default. if (department.value === 'Marketing') { Managers[0].options = ['Joe', 'Mary', 'Stan', 'Cindy']; Managers[1].options = ['Joe', 'Mary', 'Stan', 'Cindy']; } if (department.value === 'Sales') { Managers[0].options = ['Lou', 'George', 'Wendy']; Managers[1].options = ['Lou', 'George', 'Wendy']; } } //2nd Rule - Items Added if (Erepeat.itemAdded) { var index = Erepeat.itemIndex; // which item is this in the list ES[index].value = 'Day'; // default the employee shift to day // Use options already selected in form.load rule Managers[index].options = Managers[0].options; } |
This rule is useful in a workflow where you want to make a the tab named Review visible only for the workflow activity named Manager Review. See Built-in Data for more details on _data.getParameter and flow.activity.name.
Code Block |
---|
if (form.load) { if (_data.getParameter('flow.activity.name') === 'Manager Review') { Review.visible = true; } } |
form.unload
Rules can be used to finalize field values after the users clicks the form's submit button but prior to the Form and Doc Action execution. Rules using form.unload are triggered when the form user clicks the submit button and for workflows when the user clicks continue to go to the next activity or finish to complete the flow.
...
Code Block |
---|
/*member num */ var x; if (form.unload) { eval('x=' + http.get('http://<webhost>/json/getNextOrdernum')); OrderNum.value = x.num; } |
Geo Location
Rules can be used to save a snapshot of location information of any form. For example, an insurance company may want to capture the GPS location of their representatives filling out a form used for audit purposes. The designer can use the Geo Location feature in conjunction with rules like the ones shown in the examples below to accomplish this. When the form loads in the browser, the user will be prompted by to allow/disallow the use of location services. If permission is granted, the position will be calculated and server will be updated via an Ajax which causes the rule to fire. If the user denies permission or there is a timeout, the server will get an Ajax with an error.
...
Code Block |
---|
if (form.positionUpdated) { Latitude.value = _data.getParameter ("position.latitude"); Longitude.value = _data.getParameter ("position.longitude"); Accuracy.value = _data.getParameter ("position.accuracy"); PositionError.value = _data.getParameter ("position.error.message"); } |
...
Code Block |
---|
if (form.positionUpdated) { Latitude.value = _data.getParameter ("position.latitude"); Longitude.value = _data.getParameter ("position.longitude"); Accuracy.value = _data.getParameter ("position.accuracy"); PositionError.value = _data.getParameter ("position.error.message"); StreetNumber.value = _data.getParameter ("position.street_number"); StreetLine1.value = _data.getParameter ("position.route"); City.value = _data.getParameter ("position.locality"); State.value = _data.getParameter ("position.administrative_area_level_1"); County.value = _data.getParameter ("position.administrative_area_level_2"); Country.value = _data.getParameter ("position.country"); ZipCode.value = _data.getParameter ("position.postal_code"); CompleteStreetAddress.value = _data.getParameter ("position.street_address"); } |
When you run the form (unless you get a timeout or error) the address will automatically get filled in and the Google map will display.
Sequential Numbers
The Google Spreadsheet connector can also be used to generate unique sequential numbers. This technique should not be used if you have multiple people using the form at the same time as it's possible that two people simultaneously opening the same form could get the same sequential number.
...
Code Block |
---|
/*member nextid results */ var x; if (form.load) { var readmethod = 'http://www.frevvo.com/google/spreadsheets/query/u/<google username>/p/<google password>/s/SequentialNumberGenerator/w/Sheet1?media=json&query=formname="Checkbook"'; var updatemethod = 'http://www.frevvo.com/google/spreadsheets/update/u/<google username>/p/<google password>/s/SequentialNumberGenerator/w/Sheet1?media=json&query=formname="Checkbook"'; eval('x=' + http.get(readmethod)); var id = 'unknown'; if (x.results === null) { CheckNum.value = 'error'; } else if (x.results.length === 0) { CheckNum.value = 'No Match'; } else { id = x.results[0].nextid; CheckNum.value = id; id = id*1 + 1; } if (id !== 'unknown') { eval('x=' + http.get(updatemethod + '&updates=nextid=' + id + '&_method=put')); } } |
Unique ID
Forms such as invoices, bills of lading, etc often need to be stamped with a unique ID. The Sequential Number example is one approach, however it has some limitations. One is that you must guarantee that only one person at a time is filling out your form. This is because there is no mutex around the read and update of the Google spreadsheet cell.
Here is a simpler method if your unique IDs do not need to be sequential. The data named form.id is guaranteed to be unique for every new form instance. You can just use it as follows:
Code Block |
---|
if (form.load) { InvoiceNum.value = _data.getParameter('form.id'); } |
Repeat Item Initialization
The rule above was one example of initializing a newly added repeating control with a default list of options. This same concept is useful if you want to initialize a repeating control's value. When you add a repeat to your form in the Form Designer you can set a default value in any of those repeating controls visible in the designer. However when the user clicks "+" while using the form to add an additional item the default entered in the Form Designer is not automatically used in this new item. In order to accomplish this you can add a simple rule as follows:
...
Code Block |
---|
if (RepeatTrack.itemAdded) { var index = RepeatTrack.itemIndex; albumOnly[index].value = 0; } |
Again, to initialize repeat items already on your form in the Form Designer by default, either enter your initial default values directly into the in the Form Designer or add this rule.
...
This rule takes into account a repeat where min > 1. If min is 0 or 1, you can simplify this further by removing the for loop and simply have albumOnly[0].value = 0 inside the if (form.load).
Repeat ItemAdded by Init Doc
ItemAdded also executes when adds items found in an init doc. You may want to only initialize items added when the user clicks "+" on the form. And not those added from an initial document. This form contains a Mailing Label that repeats. Each label needs a unique ID assigned. However once the form is submitted the assigned IDs are saved in the database via the form's Doc URI. When the form loads it adds items automatically from rows in the database. They already have assigned Ids. We only need to assign new Ids in the sequence when the user manually adds a new Mailing Label by clicking the "+" button on the form. MLrepeat is the name of the Mailing Label repeat. MLmid is the name of the ID field in the repeat.
Code Block |
---|
if (MLrepeat.itemAdded) { var index = MLrepeat.itemIndex; // This rule is fired both when the user clicks "+" // and when frevvo adds items found in the init doc. // Need to assign new mid only when user clicks "+" // New items added via "+" will have a zero length value. if (MLmid[index].value.length === 0) { // Assign unique ID to label so it can be referenced // in RI Mailing Labels field // // Count the number of existing mailing labels on the form var maxId = 0; for (var i=0; i < MLmid.value.length; i++) { if (MLmid[i].value > maxId) { maxId = MLmid[i].value; } } var next = parseInt(maxId, 10) + 1; MLmid[index].value = next.toFixed(0); } } |
Display Uploaded Image
A rule can dynamically display an image uploaded to your form via the upload control. In this example the upload control is named 'u'. The form also must contain a message control as a place holder for displaying the uploaded image. The rule dynamically creates a URL to the uploaded image in the temporary attachment repository. The upload control's value 'u.value' is a GUID that uniquely identifies the attachment.
Code Block | ||
---|---|---|
| ||
if (u.value.length > 0) { var baseUrl = _data.getParameter('_frevvo_base_url') + "frevvo/web/tn/" + _data.getParameter('tn.id') + "/user/"+_data.getParameter('user.id') + "/app/"+_data.getParameter('app.id') + "/form/"+_data.getParameter('form.id'); im.value = "<img src='" + baseUrl + "/attachment/"+u.value+"/does_not_matter'/>"; } |
Here is the example form before and after the user has upload the orangegrove.png image:
...
Code Block | ||
---|---|---|
| ||
var x = ""; for(var i=0; i< u.value.length; i++) { x = x + u[i].value + ","; } var y = x.split(','); var baseUrl = _data.getParameter('_frevvo_base_url') + "frevvo/web/tn/" + _data.getParameter('tn.id') + "/user/"+_data.getParameter('user.id') + "/app/"+_data.getParameter('app.id') + "/form/"+_data.getParameter('form.id'); if(y[0].length === 1) { im.value = "<img src='" + baseUrl + "/attachment/"+u.value+"/does_not_matter'/>"; } else { var z = ""; for(var i=0; i<= y.length; i++) { z = z + "<img src='" + baseUrl + "/attachment/"+ y[i] +"/does_not_matter'/>" + "<br/>"; } im.value = z; } |
Search Popup
forms can initialize dynamically from backend systems. A common form design pattern is the master/detail. An example of master/detail is a form that contains an order number or employee Id. When the user enters an existing Id into the form field you want all other form fields to populate dynamically from values in a database. This is done using a form created from XSD data source generated by the database connector. This part of the master/detail form design pattern needs no business rule. It happens automatically using the Doc Action manually set document URIs. See the DB Connector Tutorial for complete detail on how to initialize your form using a Doc Action document URI to your database.
...
Code Block |
---|
//Show Employee Search Criteria if (FindEmployee.clicked) { EmployeeSearch_s1.visible = true; EmployeeSearch_s1.expanded = true; } |
The section control contains controls that are the search critiera for finding a RACF ID based on other pieces of information you may know about an employee such as name, type, email address. The values entered into the search criteria can be partial values. For instance entering a name "Smith" will find all employees whose name contains the letters "Smith". If you also select email, it will find all employees whose name contains "Smith" and have an email address containing the string "frevvo".
...
Code Block |
---|
/*member clicked racfid resultSet type */ var x; if (Search.clicked) { eval( eval('x=' + http.get('database/myqset/findEmployee?name={name_s1}&type={type_s1}&racfid={id_s1}&email={email_s1}')); var opts= []; for (var i=0; i < x.resultSet.length; i++) { if (x.resultSet[i]) { opts[i] = x.resultSet[i].racfid + "=" + x.resultSet[i].name + ", " + x.resultSet[i].type; } } SearchMsg.value = x.resultSet.length + ' Matches Found. Change your criteria and click "search" to try again.'; SearchMsg.visible = true; SearchResults.options = opts; SearchResults.visible = true; } |
The Search returns one or more matches and dynamically populates a dropdown control named SearchResults. You can change the search criteria to narrow or expand you search. When you select one of the matches from the SearchResults dropdown this 3rd rule executes to copy the selection into the RACF ID control.
Code Block |
---|
//Select from Search results if (SearchResults.value.length > 0) { racfid.value = SearchResults.value; // Hide Employee Search and clear values EmployeeSearch.visible = false; SearchMsg.visible = false; SearchResults.visible = false; SearchResults.options = []; } |
Once the value is copied into RACF ID the form automatically loads all other values in the form from the database. This is due to the form action document Uri setting.
...
The values in the employee loaded into the form can now be edited. When the users clicks the form's submit button the values will automatically be updated in the database record. This is due to the form action document Uri setting.
Database Connector REST Services
The database connector REST services can be invoked via a rule. The http get returns a JSON object that contains a resultSet array containing each row returned from the SQL query. For details on configuring the database connector see the documentation for Connecting to your Database. For an overview of using JSON to dynamically populate dropdown controls click here.
...
Code Block |
---|
<query name="lastnames"> <statement> <statement> <retrieve>SELECT lastname FROM Customer</retrieve> </statement> </query> <query name="firstnames"> <statement> <retrieve>SELECT firstname FROM Customer</retrieve> </statement> </query> <query name="names"> <statement> <retrieve>SELECT firstname,lastname FROM Customer</retrieve> <statement> </query> |
The form contains a radio control with Yes/No options and a control named getCustomerList. When the Yes option is clicked this rule executes the http get to the database connector URI http://<host>/database/lastnames. The returned JSON resultSet object is an array of all the lastnames returned by the select specified above in configuration.xml. The resultSet is then used to set the options in a dropdown control named customerLastNames.
Code Block |
---|
/*member lastname resultSet */ var x; if (getCustomerList.value === 'Yes') { eval('x=' + http.get('http://<webhost>/database/lastnames')); var opts= []; for (var i=0; i < x.resultSet.length; i++) { if (x.resultSet[i]) { opts[i] = x.resultSet[i].lastname; } } customerLastNames.options = opts; } |
Warning |
---|
The case used in the select statements must be the exact case used in your rule. For example: if query is: select lastname from Customer ,then rule must use x.resultSet[i].lastname if query is: select LastName from Customer,then rule must use x.resultSet[i].LastName |
...
Code Block |
---|
/*member firstname lastname resultSet */ var x; if (getCustomerList.value === 'Yes') { eval('x=' + http.get('http://<webhost>/database/names')); var opts= []; for (var i=0; i < x.resultSet.length; i++) { if (x.resultSet[i]) { opts[i] = x.resultSet[i].firstname + " " + x.resultSet[i].lastname; } } customerFullNames.options = opts; } |
This example is similar except the event that triggers the execution of the rule is an initial instance document loaded into the form when it opens. See the section on Document URIs for details on configuring your form to load an instance document. This rule executes before the form becomes visible in the browser. Thus the dynamic options are populated prior to the user seeing the form.
...
Code Block |
---|
<query name="DocMgmt" autocreate="true"> <create> <!-- Maps to the HTTP POST method --> INSERT into documents SET docId='{docId}' </create> <retrieve> <!-- Maps to the HTTP GET method --> SELECT docname,docId FROM documents WHERE docId='{docId}' </retrieve> <update> <!-- Maps to the HTTP PUT method --> UPDATE documents SET docId='{docId}',docname='{docname}' WHERE docId='{docId}' </update> <delete> <!-- Maps to the HTTP DELETE method --> DELETE from documents WHERE docId='{docId}' </delete> </query> |
First, the URL http://<host>/database/DocMgmt/schema was opened in the browser to automatically generate an XSD describing the resultset returned from the DocMgmt <retrieve> method. This XSD was then uploaded into the application and the resultSet type added to the form. The docname control from that schema was edited and renamed 'docname'.
...
Code Block |
---|
/*member firstname resultSet */ var x; if (docname.value.length > 0) { eval('x=' + http.get('http://<webhost>/database/firstnames')); var opts= []; for (var i=0; i < x.resultSet.length; i++) { if (x.resultSet[i]) { opts[i] = x.resultSet[i].firstname; } } customerFirstNames.options = opts; } |