NexJ Logo

Developing Process Management forms

You can develop NexJ Process Management forms using Scheme or JavaScript. For external systems, you can use REST APIs to retrieve forms data from NexJ CRM and update forms data in NexJ CRM.

For information about using REST APIs to develop forms for external systems, see Developing forms using REST APIs.

Forms are stored as libraries in the Resources layer of your model. Within the library, each form is defined as a flow. Each form (or flow) should be defined in its own library, with each flow library using the following naming convention:

Scheme
bp:register-flow <formname> #t
   (bp:flow ...)
)
JavaScript
bp.registerFlow(<formname>, true,
   (function() {
      return bp.flow(...);
   })()
);

Where <formname> is a unique identifier for the form, and composed of all lowercase letters. For example, simple.

Simple forms

A simple form collects information in a single page and single section. A simple form can bind to a single class in the underlying business model. This means that you can use the attributes of the underlying model to provide form values, calculations and validation. 

Depending on the process using the form, you can save form information and use it to update underlying model information.

Creating a simple form

When creating a simple form, assume that you are creating a flow to capture a contact's basic information in your system.

The following example builds a simple form. It creates a page with an ID of Client_Details, captioned with the title Client Details. This page contains a single section labeled Profile. The fields on the page capture the following client information:

  • First name
  • Initials
  • Last Name
  • Full Name
  • Image of the client
  • Birthdate
  • Gender
  • Service tier
  • Notes

When you create a form, define a question for each of these client attributes. These questions become fields on the form. Each question has the following mandatory parts:

  • Type of question - Affects the control used to obtain the answer.
  • ID of the question (optional) - If this ID matches an attribute in the class that the form section binds to, the value of that attribute becomes the initial answer to that question on the form. If you want to refer to the question's answer from elsewhere in the form, you must give it an ID.

Questions of type "label" do not have IDs.

For example:

(bp:flow                          ; Defines the beginning of the flow. The element closes on the last line of the code (after the bind declaration).
   (bp:page Client_Details          ; Declares the start of the page and provides the ID Client_Details, which can be used as a reference in more advanced forms.
                                  ; Additionally provides a title (caption) for the page, and declares the page number.
      (: caption "Client Details")
      (: number "1")
      (bp:section profile          ; Declares a single "section" to contain the entry fields (questions) for the form, and then defines each question.
         (: caption "Profile")
         (bp:text firstName)
         (bp:text initials)
         (bp:text lastName)
         (bp:image image)
         (bp:date birthTime)
         (bp:combo genderCode)
         (bp:combo entityStatus)
         (bp:combo tier)
         (bp:textarea notes)
      )
   )
   (: bind Person)              ; Binds the results to the Person class.
) 
bp.flow(                        //Defines the beginning of the flow. The element closes on the last line of the code (after the bind declaration).
    bp.page(Client_Details,     //Declares the start of the page and provides the ID Client_Details, which can be used as a reference in more advanced forms.
                                //Additionally provides a title (caption) for the page, and declares the page number.
        {
            caption: "Client Details",
            number: "1"
        },
        //Declares a single "section" to contain the entry fields (questions) for the form, and then defines each question.
        bp.section(profile,
            {caption: "Profile"},
            bp.text(firstName),
            bp.text(initials),
            bp.text(lastName),
            bp.image(image),
            bp.combo(genderCode),
            bp.combo(entityStatus),
            bp.combo(tier),
            bp.textarea(notes)
        )
    ),
    {bind: Person}        //Binds the results to the Person class.
)

The following image shows an example of a simple form. The labels on the form come from the caption values of the Person class that the form binds to. Indication that a field is mandatory also comes from the model and reflects whether the attribute's Required property is set.

Simple form

Adding field captions and ghost text

If the attribute caption taken from the model is not sufficient or unclear in the context of your form, you can change what is displayed by adding a caption statement:

Scheme
(: caption "caption_text")
JavaScript
caption: "Caption text",

You can also use this to provide labels for questions that do not bind to a model attribute. Caption text appears on the line of the entry field.

Additionally, for text fields, you can provide instructional text by adding a hint statement:

Scheme
(: hint "hint_text") 
JavaScript
hint: "hint text"

Text in this statement appears underneath the entry field.

For example, the following code creates a form that gets first and last name information from the Person class, but overrides the captions with Given name and Family name. It also includes a date field that has no mapping to the underlying model and the caption for this question comes directly from the form definition.

(bp:flow
    (bp:page example_page
      (: caption "Personal Details")
      (: number "1")
      (bp:section profile
        (: caption "Profile")
        (bp:text firstName
          (: caption "Given name")
          (: hint "This will store as the first name")
         )
        (bp:text lastName
          (: caption "Family name")
          (: hint "This will store as the last name")
         )
        (bp:date dueDate
          (: caption "Due date")
          )
        )
        
   )
   (: bind Person)
  ) 
bp.flow(
    bp.page(example_page,
        {
            caption: "Personal Details",
            number: "1"
            },
        bp.section(profile, {caption: "Profile"},
            bp.text(firstName,
                {
                    caption: "Given Name",
                    hint: "This will store as the first name"
                }
            ),
            bp.text(lastName,
                {
                    caption: "Family name",
                    hint: "This will store as the last name"
                }
            ),
            bp.date(dueDate, {caption: "Due Date"})
        )       
    ),
    {bind: Person}
);

Hint text

Mandatory and optional fields

By default, answers to questions are optional. The form's end-user does not have to provide a value for every question field on the form. In a bound field, if the underlying attribute is marked as required, an answer to that question is also mandatory. If you want to mark other questions as mandatory, use the following attribute statement:

Scheme
(: required #t)
JavaScript
required: true

Using the previous example, if you want to make the Due date field mandatory, code the following:

(bp:flow
    (bp:page example_page
      (: caption "Personal Details")
      (: number "1")
      (bp:section profile
        (: caption "Profile")
        (bp:text firstName
          (: caption "Given name")                       ; Required by the underlying model
          (: hint "This will store as the first name")
         )
        (bp:text lastName                                ; Required by the underlying model
          (: caption "Family name")
          (: hint "This will store as the last name")
         )
        (bp:date dueDate                                 ; Not a bound attribute
          (: caption "Due date")                         ;
          (: required #t)                                ; Marked as required explicitly
          )
        )
        
   )
   (: bind Person)
  ) 

      
bp.flow(
    bp.page(example_page,
        {
            caption: "Personal Details",
            number: "1"        
        },
        bp.section(profile,
            {
                caption: "Profile"
            },
            bp.text(firstName,
                {
                    caption: "Given Name",                      // Required by the underlying model
                    hint: "This will store as the first name"
                }   
            ),
            bp.text(lastName,
                {
                    caption: "Family name",                     // Required by the underlying model
                    hint: "This will store as the last name"
                }   
            ),
            bp.date(dueDate,
                {
                    caption: "Due date",                        // Not a bound attribute
                    required: true                              // Marked as required explicitly
                }
            )
        )
    ),
    {bind: Person}
);


Mandatory fields

Mandatory fields are validated immediately on edit. There is no need to validate again when saving. To validate all fields without any edits, click Submit.

Setting initial values

You can set an initial value for a field using the following option:

Scheme
(: initialize "value")
JavaScript
{initialize: "value"}

For example, to set the default value of tier to A, specify the following:

      
(bp:combo tier
  (: initialize "A")
)
      
bp.combo(tier, {initialize: "A"});

Remember to validate that the field allows the initialization value.

If a value for a given question already exists in the underlying model, that value becomes the initial value regardless of what is set by Initialize.

Setting calculated values

In some cases, you may want to display a calculated field to either summarize information from elsewhere in the form, or retrieve information to reference from the system data. Calculated fields are defined using the following option:

Scheme
(: value (string-append <value1> <value2>))
JavaScript
value: <value1> + <value2>

For example, the following code snippet takes the values of the firstName, middleName, and lastName fields, concatenates them, and displays them as a fullName value.

      
(bp:text firstName)
(bp:text lastName)
(bp:text fName  
  (: caption "Full Name")
  (: value (string-append (@ question firstName) " " (@ question initials) " " (@ question lastName)))
 )
      
bp.text(firstName);
 bp.text(lastName);
 bp.text(fname,
    {
        caption: "Full Name",
        value: $.question.firstName + " " + $.question.initials + " " + $.question.lastName
    }
);

To concatenate two data types into a string when creating a caption using Scheme, add a check for values as shown in the following code in order to prevent problems caused by undefined values. In the following example, assetValue is a number and asset is a string.

(: captions
 (if (null? (@ question assetValue))
     (@ question asset)
     ; else
     (string-affix (@ question asset) " " (number->string (@ question assetvalue)))
   )
)

Multi-step and multi-section forms

Defining a multi-step form

All forms appear on a single screen. Multi-step forms organize related questions together into expandable steps on a single screen. You are always able to navigate to steps within a form to review and change answers.

You can also use multi-step forms to control the flow of information gathering by making the appearance of later steps on a form dependent on answers provided earlier.

The following code defines a simple two-step form. The resulting form is also included.

Note the following:

  • Each step is defined in the code as a "page".
  • You can provide a caption for every step to help users navigate longer forms. Step (page) captions are always visible.
  • You can provide section captions, which provide guidance within a given step. Section captions are only visible when a step is open.
  • You can provide a number for each step, which can be up to two characters, and which appears to the side of the form navigation.
  • Step and section captions are not required, but are recommended to help users navigate the form.
(bp:flow
    (bp:page Personal_Details
      (: caption "Personal Details")
      (: number "1")
      (bp:section profile
        (: caption "Profile")
        (bp:text firstName)
        (bp:text initials)
        (bp:text lastName)
        (bp:text fullName)
        (bp:date birthTime)
        (bp:combo genderCode)
        (bp:combo entityStatus)
        (bp:combo tier)
        (bp:textarea notes)
        (bp:object company)
      )
    ) 
    (bp:page Home_Address
      (: caption "Home Address")
      (: number "2")
        (bp:section homeAddress
          (bp:combo type)
          (bp:text address1)
          (bp:text address2)
          (bp:combo countryValue)
          (bp:text zip)
          (bp:text attention)
          (bp:textarea deliveryInstructions)
         )
   )
   (: bind Person)
  ) 
bp.flow(
    {
        bind: Person
    },
    bp.page(Personal_Details,
        {
            caption: "Personal Details",
            number: "1"
        },
        bp.section(profile, {caption: "Profile"},
            bp.text(firstName),
            bp.text(initials),
            bp.text(lastName),
            bp.text(fullName),
            bp.date(birthTime),
            bp.combo(genderCode),
            bp.combo(entityStatus),
            bp.combo(tier),
            bp.textarea(notes),
            bp.object(company)
        )
    ),
    bp.page(Home_Address,
        {
            caption: "Home Address",
            number: "2"
        },
        bp.section(homeAddress,
            bp.combo(type),
            bp.text(address1),
            bp.text(address2),
            bp.text(zip),
            bp.text(attention),
            bp.textarea(deliveryInstructions)
        )
    )
);

Page 1

Multi-page form page 1


Page 2

Multi-page form page 2

Defining a multi-section form

You can additionally divide each step of the form into multiple sections. Like steps, sections help you to group related questions together visually to help with navigating and filling out the form. Unlike steps, however, sections do not expand and collapse as you move between them. The following example code displays the same information as for the multi-step example, but groups it into two sections instead of two steps.

Note the following:

  • Each section within a step displays as a separate card.
  • All of the sections within the step, display at the same time. You cannot expand and collapse sections.
(bp:flow
    (bp:page Personal_Details
      (: caption "Personal Details")
      (: number "1")
      (bp:section profile
        (: caption "Profile")
        (bp:text firstName)
        (bp:text initials)
        (bp:text lastName)
        (bp:text fullName)
        (bp:date birthTime)
        (bp:combo genderCode)
        (bp:combo entityStatus)
        (bp:combo tier)
        (bp:textarea notes)
        (bp:object company)
      )
        (bp:section homeAddress
          (bp:combo type)
          (bp:text address1)
          (bp:text address2)
          (bp:combo countryValue)
          (bp:text zip)
          (bp:text attention)
          (bp:textarea deliveryInstructions)
         )
   )
   (: bind Person)
  ) 
      
bp.flow(
    bp.page(Personal_Details,
        {
            caption: "Personal Details",
            number: "1"
        },
        bp.section(profile,{caption: "Profile"},
            bp.text(firstName),
            bp.text(initials),
            bp.text(lastName),
            bp.text(fullName),
            bp.date(birthTime),
            bp.combo(genderCode),
            bp.combo(entityStatus),
            bp.combo(tier),
            bp.textarea(notes),
            bp.object(company)
        ),
		bp.section(homeAddress,
	    	bp.combo(type),
    		bp.text(address1),
    		bp.text(address2),
   			bp.combo(countryValue),
			bp.text(zip),
 		    bp.text(attention),
 		    bp.textarea(deliveryInstructions)
		)
    ),
    {bind: Person}
);

Multi-section form

You may want to allow a section or step to repeat multiple times in order to collect data sets of varying sizes. To accomplish this, add the following attribute to the form element you want to repeat.

Scheme
(: collection #t)
JavaScript
collection: true

This is usually done on a section-by-section basis, but entire steps can also be defined as collections.

Collections of sections

For example, assume you are creating a form in which part of the information you want to gather is a list of major assets belonging to a client.

For each entry, include a text field with a description of the asset, and another with the estimated value. For example:

      
(bp:flow
    (bp:page example_page
      (: caption "Assets")
      (: number "1")
      (bp:section profile
        (: caption "Assets listing")
        (: captions (@ question asset)  ; Provides caption for each entry in the collection
          )
        (: collection #t)
        (bp:text asset
          (: caption "Asset")
          (: hint "Brief description of asset. e.g. Car, house")
         )
        (bp:currency assetvalue
          (: caption "Asset value")
        
         )
        )
        
   )
   (: bind Person)
  ) 
bp.flow(
    bp.page(example_page,
        {
            caption: "Assets",
            number: "1"
        },
        bp.section(profile,
            {
                caption: "Assets listing",
                captions: $.question.asset, //Provides caption for each entry in the collection
                collection: true
            },
            bp.text(asset,
                {
                    caption: "Asset",
                    hint: "Brief description of asset. e.g. Car, house"
                }
            ),
            bp.currency(assetValue, {caption: "Asset value"})
        )
    ),
    {bind: Person}
);

When this section first opens, it looks like:

Collections screen 1

When you click the Add icon Add button, the first entry opens:

Collections screen 2

After filling out a couple of entries, the form looks like this:

Collections screen 3

You can collapse each section without losing data:

Collections screen 4

Collection of steps

In most cases, sections are used to create collections. For comparison, the following code defines the collection at the step level:

      
(bp:register-flow "docsamplecollection" #f
(bp:flow
    (bp:page example_page
      (: caption "Assets")
      (: collection #t)                      ; Note that the collection is now on the page
        (: captions (@ question asset)       ; The captions still take their values from the answers in each collection entry
          )
      (: number "1")
      (bp:section profile
        (: caption "Assets listing")
        (bp:text asset
          (: caption "Asset")
          (: hint "Brief description of asset. e.g. Car, house")
         )
        (bp:currency assetvalue
          (: caption "Asset value")
        
         )
        )
        
   )
   (: bind Person)
  ) 

)
      
bp.registerFlow("docsamplecollection", jsToScheme("(bp:util:is-finance-mixin)"),
    (function () {
        return bp.flow(
            {
                bind: Person
            },
            bp.page(example_page,
                {
                    caption: "Assets",
                    collection: true,               // Note that the collection is now on the page
                    captions: $.question.asset,     // The captions still take their values from the answers in each collection entry
                    number: "1"
                },
                bp.section(profile, {caption: "Assets listing"},
                    bp.text(asset,
                        {
                            caption: "Asset",
                            hint: "Brief description of asset. e.g. Car, house"
                        }
                    ),
                    bp.currency(assetValue, {caption: "Asset value"})
                )
            )
        );
    })()
);

In this example, the initial state seen by the user is:

Collections screen 5

The user filling out the form can click Save and Add Another to create each entry in the collection. When completed as in the previous example, it looks like this:

Collections screen 6

Only one step in the collection can be open at any time. This is consistent with the general step behavior.

Forms with conditions and validation

You can add conditions to forms to control the behavior of fields, sections, and pages. The two primary uses of conditions are to control visibility and requirement, that is, whether questions, sections, or steps are visible to users, and whether questions are mandatory.

In determining these conditions, you can make use of any number of the following:

  • Answers given to preceding questions in the form.
  • Values from the underlying data that could be included as questions in the form.
  • Operators such as exists, equals, starts with, or contains.

Referencing other questions

When you add a condition that is based on the value provided by another question, you must explicitly reference that question in your condition. The referenced question, therefore, must have an ID in its definition. A question's conditions can reference any other question in the same section, or any question in an earlier section or step.

The syntax for making these references:

  • For referencing a question within the same section:

    Scheme
    @ question <questionID>
    JavaScript
    $.question.questionID
  • For referencing a question in a previous section of the same step:

    Scheme
    @ section <sectionID questionID>
    JavaScript
    $.section.sectionID.questionID
  • For referencing a question on a previous step:

    Scheme
    @ page <pageID sectionID questionID>
    JavaScript
    $.page.pageID.sectionID.questionID

Where:
<questionID> is the ID of the question you want to reference.
<sectionID> is the ID of the section containing the question you want to reference.
<pageID> is the ID of the step containing the question you want to reference.

Referencing other values in the underlying model

Other values, values that you might not want to display, can be accessed by a question and used in another question's condition. The basic method for accessing this information is to create a question earlier in the form to access the data you want, and then use the syntax for referencing other questions to access the resulting value.

The best practice for making the data available is:

  1. Create a question earlier in the model that accesses the data you want to use in a later question.
  2. If you do not want to show the question in that earlier section, set the visibility of either the question or the entire section to #f.
  3. Access the question value through an @ question, @ section, or @ page reference as appropriate, later in the form.

Constructing conditional statements

Form definition is based on the Scheme programming language, which is a functional language related to Lisp. The basic syntax structure when creating your validation expressions, follows this general pattern:

operator first_operand second_operand

If any of the operands themselves are expressions, those expressions are embedded in parentheses that contain the same three-part structure.

For example, the following expression:

Scheme
(= "Y" (@ question hasRealEstateEquity))
JavaScript
$.question.hasRealEstateEquity === "Y"

tests whether the value of the answer to the question hasRealEstateEquity evaluates to Y. If it does, the overall expression gives a value of #t (true). Otherwise, it evaluates to #f (false).

Any logical, comparison, or other formula allowed by Scheme can be used in your conditions.

Details about the these and other functions, and about their syntax, can be found in the NexJ Studio help system under the NexJ Scheme Library Reference information.

Controlling whether a question is mandatory

If you want to make a question mandatory only under certain conditions, specify an expression that evaluates to a Boolean value for the required attribute on the field.

For a simple illustration, suppose you have created a form that asks the following questions:

  1. Does the person have equity in a house?
  2. If so, what is the estimated value of the house and what is the total outstanding in debts against the house (for example, mortgages)?

Assume that you want to make the answer to the second question mandatory only if the answer to the first question is Yes.

To accomplish this, code the following:

(bp:section s2
  (bp:radio hasRealEsstateEquity
    (: caption "Does the client have any real estate equity?")
    (: options YesNoEnum)
    (: required #t)
    (: initialize "Y")
  )
  (bp:number realEstateValue
    (: caption "Estimated value of real estate")
    (: required (= "Y" (@ question hasRealEstateEquity))) ; Question is required if the person has real estate equity.
  )
  (bp:number realEstateDebt
    (: caption "Estimated debts against the real estate")
    (: required (= "Y" (@ question hasRealEstateEquity))) ; Question is required if the person has real estate equity.
  )
)
bp.section(s2,
    bp.radio(hasRealEstateEquity,
        {
            caption: "Does the client have any real estate equity?",
            options: YesNoEnum,
            required: true,
            initialize: "Y"
        }
    ),
    bp.number(realEstateValue,
        {
            caption: "Estimated value of real estate",
            required: $.question.hasRealEstateEquity === "Y" //Question is required if the person has real estate equity.
        }
    ),
    bp.number(realEstateDebt,
        {
            caption: "Estimated debts against the real estate",
            required: $.question.hasRealEstateEquity === "Y" //Question is required if the person has real estate equity.
        }
    )
);

Controlling whether a question is enabled

You can conditionally enable questions in the same way that you conditionally set whether they are required. The only difference is that rather than using the following statement:

Scheme
(: required (expr))
JavaScript
required: $.expr    


use

Scheme
(: enabled (expr))
JavaScript
enabled: $expr

Using the following statement:

Scheme
(: enabled #f)
JavaScript
enabled: false,

allows you to create an effectively read-only field within the form. This is useful for calculated fields, or for showing information that you want the person filling the form to see, but not change.

Controlling the visibility of questions, section, and pages

Control the visibility of a step, section, or control using the following expression:

Scheme
(: visible (expr))
JavaScript
visible ($.expr)

For example, in the following code, the answer to the first question controls the visibility of the final three. See the accompanying pictures for an illustration:

(bp:section employment (: caption "Employment Details")
   (bp:radio status (: caption "Employment Status") (: required #t)
     (: options (collection "Unemployed" "Employed" "Self-Employed" "Retired"))
     (: initialize "Employed")
   )
   (bp:text type (: caption "Type of Business") (: required #t)
     (: visible (in? (@ question status) "Employed" "Self-Employed")) ; Visible only if employed or self-employed
     (: initialize "Retail")
     )
   (bp:text job (: caption "Job Title") (: required #t)
     (: visible (in? (@ question status) "Employed" "Self-Employed")) ; Visible only if employed or self-employed
     (: initialize "Director, Product Research")
   )
   (bp:text employer (: caption "Employer") (: required #t)  ; Visible only if employed
      (: visible (= (@ question status) "Employed"))
      (: initialize "NexJ Systems")
   )
)
bp.section(employment, {caption: "Employment Details"},
    bp.radio(status,
        {
            caption: "Employment Status",
            required: true,
            options: ["Unemployed", "Employed", "Self-Employed", "Retired"],
            initialize: "Employed"
        }
    ),
    bp.text(type,
        {
            caption: "Type of Business",
            required: true,
            // Visible only if employed or self-employed
            visible: ($.question.status === "Employed") || ($.question.status === "Self-Employed"),
            initialize: "Retail"
        }   
    ),
    bp.text(job,
        {
            caption: "Job Title",
            required: true,
            // Visible only if employed or self-employed
            visible: ($.question.status === "Employed") || ($.question.status === "Self-Employed"),
            initialize: "Director, Product Research"
        }   
    ),
    bp.text(employer,
        {
            caption: "Employer",
            required: true,
            // Visible only if employed
            visible: $.question.status === "Employed",
            initialize: "NexJ Systems"
        }   
    )
);

In the first instance, the Employment Status is Employed, and all three follow-on questions appear.

Condition - Employed



In the second instance, the answer to the first question is Self-Employed and no information about the employer is shown. Because the field was initialized to NexJ Systems, this is the value that is stored in the form, though likely ignored by future processing down the line.

Condition - Self-Employed



In this case, the person has indicated they are retired, and no additional employment questions are asked. As with the second instance, the initialization values mean that the form, in fact, stores that the type of business is Retail, the position held was Director, Product Research, and that the employer was NexJ Systems. Again, a well-designed workflow making use of this form later ignores these values.

Condition - Retired

When using conditional statement to control the visibility or enabling of fields, be careful to ensure that all required fields either initialize with an answer, as in the example above, or ensure that users can edit them. If an empty required field becomes hidden or disabled, your users are unable to complete and submit the form.

Forms with dynamic approvals

As of NexJ CRM 9.5, administrators can define dynamic approvals for forms by specifying enty criteria in the Customize workspace in the classic UI (released with 8.X) for business processes. When users submit forms that have been configured for dynamic approvals, the forms are automatically directed to the required approvers.

By default, any supported element types are available for selection in the Entry Criteria superpicker for business processes in the classic UI. The following element types automatically work without any configurations:

Primitives:

  • bp:text
  • bp:textarea
  • bp:integer
  • bp:number
  • bp:checkbox
  • bp:date
  • bp:datetime
  • bp:currency

Enums:

  • bp:combo
  • bp:radio
Any where clauses set on enumerations are not respected in the Entry Criteria superpicker, as where clauses can depend on runtime values that are not available to the superpicker. Only the first 1000 entries of an enumeration are selectable in the superpicker.

In the case of bp:object, the class specified as the "valueType" must have the "UI_COMPONENT" aspect and "UICOMPONENT_NAME" attribute set. This is required to show the correct object picker in the Entry Criteria superpicker.

Unsupported element types are not shown in the superpicker (for example, bp:attachment, bp:image, and bp:label).

National language considerations

Forms support the UTF-8 character set. When composing forms in libraries, you cannot enter non-ASCII characters directly, as they do not display properly in the form. To display unicode characters, you must use their escaped unicode values. For example, if composing a form in French and you want to include a section label that reads "connaître votre client ", enter conna\u00EEtre votre client in your form definition.

If composing a form in a bi-directional language, enter the character symbols in a left-to-right manner. The rendering engine corrects the display to right-to-left as appropriate. For example, to write "أَلِف بَاء", (the names of the first two letters of the Arabic alphabet) enter \u0623\u064E\u0644\u0650\u0641 \u0628\u064E\u0627\u0621.

To aid in conversion of strings to escaped unicode, use an online tool such as the one available at https://r12a.github.io/app-conversion (use the JavaScript output if using this converter).

Form localization

Process Management forms let you display forms in different languages based on user preference. You can change the localization of form controls by using strings defined on the client (.js file). Form contents can be translated using strings defined on the server (.strings file) and client (.js file).

Forms with proper translations for the strings are translated based on your browser preference.

To change the localization of your forms, you must:

  1. Add the translation for the form controls, such as buttons and other elements of the form.
    To do this, go to NexJ Studio, and:
    1. Navigate to the client_project/mod/nexj/ui.bp/i18n folder. You may have to create this folder if it does not already exist.

    2. Make a copy of the en.js (default) file.
      The en.js file can be found in NexJ Studio by navigating to Window > Show View > Project Explorer.
    3. Name the <xx>.js file according to the language you want to use.
      This file defines the behavior of the currency, date, and system in the language and its variant codes. You can make changes to this file through Resources > Locales.
  2. To add the translations for the form contents, you must first enable your project to display your preferred locale. In NexJ Studio:
    1. Navigate to the client_project/mod/nexj/i18n folder. If the folder does not already exist, add it as a new folder.
    2. Add a locale file. Make a copy of the en.js file found in client_project /mod/nexj/i18n/, and rename the file.
    3. Add another translation file to client_project/mod/nexj/ui.chart/i18n/. You can use the en.js file found in client_project/mod/nexj/ui.chart/i18n/ as a sample.
  3. To translate the form content, provide translations for every string used by the form in the .strings file located in Resources > Strings. For example, translate the strings inside en.strings with ids (IDS_BUSINESS_PHONE, idsc.ReportLayout.caption, and so on), but do not end in .en or .fr.
  4. Confirm that your translations are in your client_project.

For example, to create a form in Italian:

  1. Translate the form-specific buttons that show in the Process Management forms Admin Portal by:
    1. Going to NexJ Studio, navigate to the client_project/mod/nexj/ui.bp/i18n folder, and make a copy of the en.js file which you would rename it.js.
    2. Translate the file contents into Italian.
  2. Add the two files containing translations for the portal application by:
    1. Making a copy of the en.js file found in client_project/mod/nexj/i18n/, renaming the file it.js, and placing it in the client_project/mod/nexj/i18n/ folder.
    2. Translate the file into Italian.
    3. Add a second translation file, this time to client_project/mod/nexj/ui.chart/i18n/ by making a copy of the en.js file found in this folder, and renaming it it.js.
    4. Provide the Italian translation for the steps in your form.

When creating localized text inside your forms, use a string identifier, such as idsc.Person.firstName in the caption, captions or valueCaption field of the flow elements in the domain specific language. As an example, a first name text box with a localized caption of First Name would be:

(bp:text firstName (: caption "idsc.Person.firstName"))

To enable localization, you must modify the form template. Any change on the template will trigger a seed of the localization. All newly created templates will work with localization by default.

You cannot change language in the forms portal. This must be done by configuring client locales. See Resources.

If the user interface gets the same locale supported at a later time, the file in the Process Management project will override the user interface version. You will need to remove/modify the Process Management locale as necessary.

Registering page elements on global registry

You can reference pre-registered form libraries inside a form without having to redefine them. If you want to reference a library through bp:util:library-ref, register elements using bp:register-flow-library.

The following example registers a new library containing a question called registered-question, and then references it in a form called myForm.

(bp:register-flow-library "registered-question" #t ; registers a new library
   (bp:text q2
      (: caption "My registered question")
   )
)
 
(bp:register-flow "myForm" #t
   (bp:flow
      (bp:page p1
         (: caption "Page Caption")
         (bp:section s1
            (: caption "Section Caption")
            (bp:util:library-ref "registered-question") ; References the new registered library
         )
      )
   )
)
bp.registerFlowLibrary("registered-question", true,
    (function () {
        return bp.text("q1",{caption: "My registered question"})
    })()
);
 
bp.registerFlow("libraryref", true,
    (function () {
        return bp.flow(
            bp.page("p1",
            {caption: "Page Caption"},
            bp.section("s1",
                {caption: "Section Caption"},
                jsToScheme("(bp:util:library-ref \"registered-question\")") /* References the new registered library */
            ))
        );
    })()
);

Using additional buttons in a page control

You can use a tool property in a page control to add a button to execute an action. The following three dynamic properties are supported:

  • caption
  • visible
  • function

Each property can contain either a static value or multiple question references. The following examples show how to use tool property in a page or a page repeater.

; This template has two pages
; The first page has one tool button
; -- Button isHideSIN: When the checkbox is selected, this button is visible and its caption is "TRUE".
;                      When the checkbox is cleared, this button is hidden. 
; The second page has three buttons
; -- Button isHideSIN: When the checkbox is selected, this button is visible and its caption is "TRUE".
;                      When the checkbox is cleared, this button is visible and its caption is "FALSE". 
;					   Clicking on this button prints "Clicking on isHideSIN tool" in the client.
; -- Button visibleTool: This button is always visible and its caption is "VISIBLE TOOL".
;						 Clicking on this button prints "Clicking on visible tool" in the client.
; -- Button hiddenTool: This button is always hidden. 

(bp:register-flow
   "additionalBtnInPage"
   #t
   (bp:flow
      (bp:page
         p1
         (bp:section
            s1
            (bp:checkbox chkHideSIN (: caption "Hide SIN"))
         )
         (: tools
           (bp:tool isHideSIN (: caption (@ question chkHideSIN)) (: visible (@ question chkHideSIN)))
         )
         (: caption "First Page")
      )
      (bp:page
         p2
         (bp:section s1)
         (: tools
            (bp:tool
               isHideSIN
               (: caption (@ question chkHideSIN))
               (: function (logger'info "Clicking on isHideSIN tool"))
            )
            (bp:tool visibleTool (: caption "Visible Tool") (: function (logger'info "Clicking on visible tool")))
            (bp:tool hiddenTool (: caption "Hidden Tool") (: visible #f) (: function (logger'info "Clicking on hidden tool")))
         )
         (: caption "Second Page")
      )
   )
)
; This page repeater template has three tool buttons
; -- Button typeTool:  This button shows the selected value of custom field type combo in its own page.
;                      When there is no selection in the type combo, this button becomes hidden. 
;					   Clicking on this button prints "Type is {typeName}" in the client.
; -- Button visibleTool: This button is always visible and its caption is "VISIBLE TOOL".
;						 Clicking on this button prints "Clicking on visible tool" in the client.
; -- Button hiddenTool: This button is always hidden. 

(bp:register-flow "additionalBtnInPageRepeater" #t
   (bp:flow
      (bp:page customFieldEntities
         (: caption "Custom Field Type Page")
         (: number "2")
         (bp:section s1
            (bp:combo type)
         )
         (: tools
            (bp:tool
               typeTool
               (: caption ((@ question type)'caption))
               (: visible (not (null? ((@ question type)'caption))))
               (: function (logger'info "Type is: "((@ question type)'caption)))
            )
            (bp:tool visibleTool (: caption "Visible Tool") (: function (logger'info "Clicking on visible tool")))
            (bp:tool hiddenTool (: caption "Hidden Tool") (: visible #f) (: function (logger'info "Clicking on hidden tool")))
         )
      )
      (: bind Person)
   )
)

Binding forms and navigating attributes

Each form is bound to a single class in the business model. By default, the attributes that questions collect data from, are the attributes of that class.

Instead of binding a Process Management form with a NexJ metaclass and associated attributes, you can bind a form with an external schema model using REST API and a JSON structure. For more information about binding a form with an external schema model, see Binding forms with an external schema model using REST APIs.

Complications can arise if the values you want to access are actually within an associated class. In this case, you must create a section specifically for accessing those attributes. Additionally, the ID of that section must match the name of the non-primary attribute whose information you want to access.

For example, consider gathering the home information address for an instance of the Person class:

Person - Class

As shown in this reduced class diagram, the homeAddress attribute is a non-primitive attribute.

Expanded, you can see more clearly that the homeAddress attribute is an instance of the associated Address class.

In order to access and update the homeAddress data for the person, create a section in your form that explicitly references the homeAddress attribute. That section then gives you access to the attributes of the Address class. The following code illustrates this example:

 (bp:flow
    (bp:page Personal_Details
      (: caption "Personal Details")
      (: number "1")
      (bp:section profile
        (: caption "Profile")
        (bp:text firstName)
        (bp:text initials)
        (bp:text lastName)
        (bp:text fullName)
        (bp:date birthTime)
        (bp:textarea notes)
        (bp:object company)
      )
        (bp:section homeAddress   ; The section ID matches the non-primitive attribute name in the Person class.
          (bp:combo type)
          (bp:text address1)
          (bp:text address2)
          (bp:combo countryValue)
		  (bp:text state)
          (bp:text city)
          (bp:text zip)
          (bp:text attention)
          (bp:textarea deliveryInstructions)
         )
   )
   (: bind Person)
  ) 
bp.flow(
    bp.page(Personal_Details,
        {
            caption: "Personal Details",
            number: "1"           
        },
        bp.section(profile, {caption: "Profile"},
            bp.text(firstName),
            bp.text(initials),
            bp.text(lastName),
            bp.text(fullName),
            bp.date(birthTime),
            bp.textarea(notes),
            bp.object(company)
        ),
        bp.section(homeAddress,
            bp.combo(type),
            bp.text(address1),
            bp.text(address2),
            bp.combo(countryValue),
            bp.text(state),
            bp.text(city),
            bp.text(zip),
            bp.text(attention),
            bp.textarea(deliveryInstructions)
        )
    ),
    {bind: Person}
);

Line 27 binds the form as a whole to the Person class. However, as illustrated in the class diagram, the homeAddress attribute is an associated class. To access the attributes within homeAddress, the section ID is specified to match the complex attribute.

The level to which you can traverse the class model is currently limited to one level out. Given the class that you bind your form to, you can name any of its associated classes in a section and access their primitive attributes. You cannot, however, access the associated classes of an associated class.

There is one exception to this limitation, and it is illustrated in the preceding example: the homeAddress attribute type is, strictly speaking, a second-level associated class to the Person class. However, because of its use of bp:OBJECT_AS_ENUM aspect, it behaves as if it is an enumeration, and is treated as such for the purposes of form definition.

Debugging your forms

If you want to add debugging to the server-side of your Process Management forms, add (logger’info …) to the code, and observe the results in the Scheme console. To debug client-side code, use the Chrome debugger.

Using the progress list

When creating a multi-page form, a progress list is displayed in the form's Completion Status by default. This enables users to track their completion of required questions using a progress indicator for each page of the form, as well as a visualization of the entire form's status. This is shown in the following example:

Completion status for Process Management forms

You can disable the progress list by adding the following code, under the bp:flow element, in your form.

Scheme
(: showProgress #f)
JavaScript
showProgress: false

The following code is for an example form that disables the progress list and contains the following sections:

  • Client Details
  • Addresses
  • Related Parties
  • Related Party - Spouse
  • Related Party - Accountant
  • Related Party - Referred By
  • Dummy page
(bp:register-flow "formName" #t
   (bp:flow
      (: showProgress #f)
         (bp:page Client_Details
         (: caption "Client Details")
         (: number "1")
         (bp:section profile
            (: caption "Profile")
            (bp:text firstName)
            (bp:text initials)
            (bp:text lastName)
            (bp:text fullName)
            (bp:date birthTime)
            (bp:combo genderCode)
            (bp:combo entityStatus)
            (bp:combo tier)
            (bp:object company)
            (bp:image image)
         )
         (bp:section homeAddress
            (bp:combo type)
            (bp:text address1)
            (bp:text address2)
            (bp:combo countryValue)
            (bp:text zip)
            (bp:text attention)
            (bp:textarea deliveryInstructions)
         )
      )
      (bp:page Addresses
         (: caption "Addresses")
         (: number "2")
         (bp:section addrs
            (: captions
               (string-append
                  (unless (null? (@ question name)) (string-append "(" (@ question name) ") "))
                  (@ question address1)
               )
            )
            (bp:text name)
            (bp:combo type)
            (bp:text address1)
            (bp:text address2)
            (bp:combo countryValue)
            (bp:text zip)
         )
      )
      (bp:page customFieldEntities
         (: caption "Related Parties")
         (: valueCaption "Related Party")
         (: captions
            (string-affix ((@ question type)'caption) " " ((@ question entityValue)'fullNameFirstLast))
         )
         (: number "3")
         (bp:section s1
            (bp:combo type)
            (bp:object entityValue
               (: caption "Entity")
               (: filterPath "fullTextSearch")
               (: order ())
               (: valueCaption "fullName")
            )
         )
      )
      (bp:page dummyPage (: caption "Dummy page"))
      (: bind Person)
   )
(function() {
function addressPages() {
	return bp.page("Addresses",
		{
			caption: "Addresses",
			number: "2"
		},
		addressSection());
}
function addressSection() {
	return addressSectionHelper();
}
function addressSectionHelper() {
	return bp.section("addrs",
		{
			captions: function() {
				if ($.question.name !== null)
					return "(" + $.question.name + ") " + $.question.address1;
				return $.question.address1;
			}
		},
		bp.text("name"),
		bp.combo("type"),
		bp.text("address1"),
		bp.text("address2"),
		bp.combo("countryValue"),
		bp.text("zip")
	);
}
return bp.flow(
			{showProgress: false}
			bp.page("Client_Details",
				{
					caption: "Client Details",
					number: "1"
				},
				bp.section("profile",
					{caption: "Profile"},
					bp.text("firstName"),
					bp.text("initials"),
					bp.text("lastName"),
					bp.text("fullName"),
					bp.date("birthTime"),
					bp.combo("genderCode"),
					bp.combo("entityStatus"),
					bp.combo("tier"),
					bp.object("company"),
					bp.image("image")
				),
				bp.section("homeAddress",
					bp.combo("type"),
					bp.text("address1"),
					bp.text("address2"),
					bp.combo("countryValue"),
					bp.text("zip"),
					bp.text("attention"),
					bp.textarea("deliveryInstructions")
				)
			),
			addressPages(),
			bp.page("customFieldEntities",
				{
					caption: "Related Parties",
					valueCaption: "Related Party",
					captions: $.question.type.caption + " " +
						$.question.entityValue.fullNameFirstLast,
					number: "3"
				},
				bp.section("s1",
					bp.combo("type"),
					bp.object("entityValue",
						{
							caption: "Entity",
							filterPath: "fullTextSearch",
							order: null,
							valueCaption: "fullName"
						}
					)
				)
			),
			bp.page("dummyPage", {caption: "Dummy Page"}),
			{bind: Person}
		);
})();

If you want to implement the progress list for a single page form, add the following code under the bp:flow element in your form.

Scheme
(: showProgress #t)
JavaScript
showProgress: true

Managing your forms

Process Management forms in use, templates, template attachments, and documents are managed from the Process Management Admin Portal. You can also develop forms using the Admin Portal. Access the Admin Portal from <Application Root URL>/nexj/ui/bp-admin. When working with a form's code on the Templates workspace in the Admin Portal, clicking the Reset button Reset buttonresets all values to the default values. For information about managing your forms using the Process Management Admin portal, see "Managing Process Management forms" in the Application Administration documentation.

Bypassing conflict warning messages

By default, upgrade conflicts are visible in the NexJ CRM user interface. You can configure upgrade conflicts so that users are not notified every time there are changes to a form's structure relating to pages, sections, and questions. To disable upgrade conflicts, set the cpm.showUpgradeConflicts property to false in the Development.properties file.

Configuring Process Management forms using JavaScript

This section provides information specific to those configuring Process Management forms in JavaScript. JavaScript forms are configured as libraries, similar to Scheme files on the metadata level, but using a .njs file extension, enabling the NexJ Studio framework to recognize and load them while the application is running.

The following JavaScript expressions are specific to Process Management.

WHERE clauses

Because WHERE clauses in Scheme are usually in a list (a type not normally supported by JavaScript), there is a new JavaScript expression for configuring WHERE clauses:

bp.where(op, operand, value)
bp.where(=, $.value, ($.question.q0) ? "Y" : "N")
translates to: `(= (@ value) ,(if (@ question q0) "Y" "N"))
 
 
bp.where(=, $.account.primaryOwner, jsToScheme("string->oid")($.global.relatedObjectId))
translates to: `(= (@ account primaryOwner) (string->oid ,(@ global relatedObjectId)))

For complex WHERE clauses that are not supported by bp:where, use the supported Scheme literal expressions in order to use Scheme WHERE clauses as JavaScript.

bp.objects(holdings,
    {
        caption: "Available Holdings",
        valueType: "Holding",
		//Use a Scheme where clause in the JavaScript configuration
        where: jsToScheme("`(any (= (@ account primaryOwner) (string->oid ,(@ global relatedObjectId))))")
    }
)

Scheme literals

To implement any logic that cannot be supported with JavaScript, you can embed Scheme expressions into the JavaScript. Use the jsToScheme() function to wrap stringified Scheme expressions so they can be called inline within existing JavaScript.

bp.flow(
    {
        title: "Simple CPM demo",
        $onSubmit: jsToScheme("(logger 'info \"[OnSubmit] flow has been submitted\")")
    },
    bp.page(p1, ...)
)

In order to access any external identifiers or identifiers written in Scheme, use the sequence #("<identifier name>") as shown below.

bp.flow(
    {
        title: "Sample Form with NexJ Model Bind",
        bind: #("Person")
    },
    bp.page(p1,
        bp.section(s1,
            bp.text(firstName),
            bp.text(lastName)
        )
    )
)

Null is true

Because of how JavaScript is compiled to Scheme expressions and evaluated in a Scheme interpretor, null values will return as true. To avoid any undesired behavior, it is recommended that you explicitly check for null values in the configuration.

bp.text(q1, {
        //this will be true, even when q0 is empty
        visible: this.question.q0,
        //do this instead:
        enabled: this.question.q0 != null
    }
)

NexJ JavaScript vs Scheme resources

Both .njs and .scm forms can coexist under the libraries folder in the project metadata. However, the parser cannot currently differentiate a Scheme vs JavaScript resource when both file names and paths are the same.

Avoid sharing the same name between Scheme and JavaScript resources.

JavaScript limitations

The following statements are not supported by JavaScript and will throw errors:

  • Import statement
  • For ... in statement
  • For ... var... in statement
  • With statement
  • Debugger statement
  • Property getter and Property setter
  • Postfix decrement operation (–)
  • Postfix increment operation (++)
  • Typeof operation
  • Delete operation
  • The >>> and >>>= operator
  • Void
  • Statements using any future reserved words
  • Import of JavaScript modules

For more information, see NexJ JavaScript differences .

Working in a sandbox

When you open the Process Management Admin portal, the Templates tab is in the run-time configurability sandbox. Working in this space, you can execute forms for independent evaluation, monitoring or testing. You do not need to stop and start servers to view any changes you make. No redeployment is needed. Build your forms using Scheme or JavaScript. When you are ready to implement the form, copy the code, paste and check it into an SCM or NJS library file in NexJ Studio.

Forms created/modified in the sandbox space run in the same manner as forms deployed from source control. As such, forms can contain actions that modify data in the system. This feature should be limited to administrator-type users who understand the consequences of any actions added/modified on a form and should only be used in a non-production environment as a tool to test form changes at runtime.

Creating forms in your sandbox

To create a new form:

  1. Click the Add icon .
  2. Click the Edit icon  to change the name of the form.
  3. Choose either run-time JavaScript (NJS) or Scheme (SCM) code.

To edit an existing form, click on the code.

When you create a form, you are presented with a split workspace; one half is code (either JavaScript or Scheme, depending on which you have selected), the other half is a preview of what the form will look like. The code view includes code smart formatting such as auto-completion of Process Management functions, auto indentation and alignments, and keywords formatting.

Changing font size is not recommended as the controls are built to align with Material Design guidelines. Color changes can be achieved by changing the theme at the project level.

After you have made changes to the code, click the Preview button to refresh the form. When you are satisfied with the form's functions and appearance, click Save to publish the form.

In the code for forms using references, helper functions must be situated above the flow, and the entire code must be wrapped in another function following JavaScript and Scheme conventions.

Additional examples

The following examples demonstrate some possible configurations.

Forms using references

((lambda ()
(define (bp:test:p2)
   (bp:page p2 (: caption "p2")
      (bp:section s1
         ;defined questions for page 2, section 1
         (bp:attachment attach) ;comments after code
         (bp:radio options
            ;a comment inside a question
            (: options (collection "1" "2" "3"))
         )
      )
   )
)
(define (bp:test:p3)
   (bp:page p3 (: caption "p3")
      (bp:section s1
         ;comment with brackets ())
         (bp:textarea comments (: value (string-append "test" "123")))
         (bp:integer number (: min 0) (: max 10) (: step 1)) ;after code with brackets ((()
         (bp:integer numberCalculated (: value (@ question number)) (: onServer '(value)))
      )
   )
)
(define (bp:test:p4)
   (bp:page p4 (: caption "p4")
      (bp:section s1
         (bp:text q1)
      )
   )
)
(define (bp:test:p4Helper)
   (bp:test:p4)
)
(bp:flow
      ;pages 2 and 3 are defined in the above functions
      (bp:page p1 (: caption "p1")
         (bp:section s1
            (bp:text q1 (: value "some text;"))
            (bp:text q2
               ;add some custom formatting
                   (: value "some text with a \"")

                 (: caption "text with a \";\";")
            )
         )
      )
      (bp:test:p2)
      (bp:test:p3)
      (bp:test:p4Helper)
   )
))
(function() {
function testAddressPage() {
	return bp.page("Addresses",
		{
			caption: "Addresses",
			number: "2"
		},
		testAddressSection("S1"));
}
function testAddressSection(sectionNumber) {
	return bp.section("addrs",
		{
			caption: "Addresses",
			number: sectionNumber,
			captions: function() {
				var a;
				if ($.question.name != null)
					a = "(" + $.question.name + ") " + $.question.address1;
				else a = $.question.address1;
				return a;
			},
			collection: true
		},
		bp.text("name", {caption: "Address Name"}),
		bp.combo("type",
			{
				caption: "Address Type",
				options: ["Home", "Business"]
			}
		),
		bp.text("address1", {caption: "Address Line 1"}),
		bp.text("address2", {caption: "Address Line 2"}),
		bp.combo("country",
			{
				caption: "Country",
				options: jsToScheme("(collection \"Canada\"" +
					"\"USA\"" +
					"\"UK\"" +
					"\"China\")"
				)
			}
		),
		bp.text("zip", {caption: "Zip Code"})
	);
}
return bp.flow(
			bp.page("Client_Details",
				bp.property(caption, "Client Details"),
				bp.property("number", "1"),
				bp.section("profile",
					{caption: "Profile"},
					bp.text("firstName", {caption: "First Name"}),
					bp.text("lastName", {caption: "Last Name"}),
					bp.text("fullName",
						{
							caption: "Full Name",
							value: function() {
								return $.question.firstName + " " + $.question.lastName;
							}
						}
					),
					bp.date("birthTime", {caption: "Date of Birth"}),
					bp.object("user",
						{
							caption: "User 1",
							where: bp.where("=", $.loginName, "nexjsa"),
							valueType: "User",
							valueCaption: "fullName",
							order: "loginName",
							filterPath: "loginName"
						}
					),
					bp.objects("users",
						{
							caption: "User 2",
							where: jsToScheme("'(= (@ loginName) \"nexjsa\")"),
							valueType: "User",
							valueCaption: "fullName",
							order: "loginName",
							filterPath: "loginName"
						}
					)
				)
			),
			testAddressPage(),
			bp.page("misc",
				{
					caption: "Misc. Questions",
					number: "3"
				},
				bp.section("s1",
					{caption: "Calculated fields"},
					//try out both Scheme and javascript for the same formula
					bp.text("addr0Line1js",
						{
							caption: "Address 0, Line 1 (js)",
							value: function () {
								return $.section.addrs[0].address1;
							}
						}
					),
					bp.text("addr0Line1scm",
						{
							caption: "Address 0, Line 1 (scm)",
							value: jsToScheme("(@ section '(addrs . 0) address1)")
						}
					),
					bp.radio("selectOption",
						{
							caption: "Select an option",
							options: ["Yes", "No"],
							required: true
						}
					),
					bp.text("selectedOption",
						{
							caption: "The selected option is:",
							enabled: false,
							value: this.question.selectOption === "Yes" ? "Y" : "N"
						}
					)
				)
			)
		);
})();
(bp:flow
      ;pages 2 and 3 are defined in the above functions
      (bp:page p1 (: caption "p1")
         (bp:section s1
            (bp:text q1 (: value "some text;"))
            (bp:text q2
               ;add some custom formatting
                   (: value "some text with a \"")

                 (: caption "text with a \";\";")
            )
         )
      )
      (bp:test:p2)
      (bp:test:p3)
      (bp:test:p4Helper)
   )
))


bp.flow(
			bp.page("Client_Details",
				bp.property(caption, "Client Details"),
				bp.property("number", "1"),
				bp.section("profile",
					{caption: "Profile"},
					bp.text("firstName", {caption: "First Name"}),
					bp.text("lastName", {caption: "Last Name"}),
					bp.text("fullName",
						{
							caption: "Full Name",
							value: function() {
								return $.question.firstName + " " + $.question.lastName;
							}
						}
					),
					bp.date("birthTime", {caption: "Date of Birth"}),
					bp.object("user",
						{
							caption: "User 1",
							where: bp.where("=", $.loginName, "nexjsa"),
							valueType: "User",
							valueCaption: "fullName",
							order: "loginName",
							filterPath: "loginName"
						}
					),
					bp.objects("users",
						{
							caption: "User 2",
							where: jsToScheme("'(= (@ loginName) \"nexjsa\")"),
							valueType: "User",
							valueCaption: "fullName",
							order: "loginName",
							filterPath: "loginName"
						}
					)
				)
			),
			testAddressPage(),
			bp.page("misc",
				{
					caption: "Misc. Questions",
					number: "3"
				},
				bp.section("s1",
					{caption: "Calculated fields"},
					//try out both Scheme and javascript for the same formula
					bp.text("addr0Line1js",
						{
							caption: "Address 0, Line 1 (js)",
							value: function () {
								return $.section.addrs[0].address1;
							}
						}
					),
					bp.text("addr0Line1scm",
						{
							caption: "Address 0, Line 1 (scm)",
							value: jsToScheme("(@ section '(addrs . 0) address1)")
						}
					),
					bp.radio("selectOption",
						{
							caption: "Select an option",
							options: ["Yes", "No"],
							required: true
						}
					),
					bp.text("selectedOption",
						{
							caption: "The selected option is:",
							enabled: false,
							value: this.question.selectOption === "Yes" ? "Y" : "N"
						}
					)
				)
			)
		);
})();		

JavaScript configuration using helper functions

; Helper function for returning
; the address section
(define (testAddressSection sectionNumber)
   (bp:section addrs
      (: caption "Addresses")
      (: number sectionNumber)
      (: captions
         (define a ())
         (if (not (null? (@ question name)))
            (set! a (string-append "(" (@ question name) ") " (@ question address1)))
            ;else
            (set! a (@ question address1))
         )
         ;return
         a
      )
      (: collection #t)
      (bp:text name (: caption "Address Name"))
      (bp:combo type
         (: caption "Address Type")
         (: options (collection "Home" "Business"))
      )
      (bp:text address1 (: caption "Address Line 1"))
      (bp:text address2 (: caption "Address Line 2"))
      (bp:combo country
         (: caption "Country")
         (: options (collection "Canada" "USA" "UK" "China"))
      )
      (bp:text zip (: caption "Zip Code"))
   )
)

; Helper function for the address page
(define (testAddressPage)
   (bp:page Addresses
      (: caption "Addresses")
      (: number "2")
      (testAddressSection "S1")
   )
)
   
(bp:register-flow "testFormDSL" #t
   (bp:flow
      (bp:page Client_Details
         (: caption "Client Details")
         (: number "1")
         (bp:section profile
            (: caption "Profile")
            (bp:text firstName (: caption "First Name"))
            (bp:text lastName (: caption "Last Name"))
            (bp:text fullName
               (: caption "Full Name")
               (: value (string-append (@ question firstName) " " (@ question lastName)))
            )
            (bp:date birthTime (: caption "Date of Birth"))
            (bp:object user
               (: caption "User 1")
               (: where `(= (@ loginName) "nexjsa"))
               (: valueType "User")
               (: valueCaption "fullName")
               (: order "loginName")
               (: filterPath "loginName")
            )
            (bp:objects users
               (: caption "User 2")
               (: where '(= (@ loginName) "nexjsa"))
               (: valueType "User")
               (: valueCaption "fullName")
               (: order "loginName")
               (: filterPath "loginName")
            )
         )
      )
      (testAddressPage)
      (bp:page misc
         (: caption "Misc. Questions")
         (: number "3")
         (bp:section s1
            (: caption "Calculated fields")
            (bp:text addr0Line1
               (: caption "Address 0, Line 1")
               (: value (@ section '(addrs . 0) address1))
            )
            (bp:radio selectOption
               (: caption "Select an option")
               (: options (collection "Yes" "No"))
               (: required #t)
            )
            (bp:text selectedOption
               (: caption "The selected option is:")
               (: enabled #t)
               (: value (if (= (@ question selectOption) "Yes") "Y" "N"))
            )
         )
      )
   )
)


/* Helper function for returning
the address section */
function testAddressSection(sectionNumber) {
    return bp.section(addrs,
        {
            caption: "Addresses",
            number: sectionNumber,
            captions: function() {
                var a;
                if (this.question.name != null)
                    a = "(" + this.question.name + ") " + this.question.address1;
                else a = this.question.address1;
                return a;
            },
            collection: true
        },
        bp.text(name, {caption: "Address Name"}),
        bp.combo(type,
            {
                caption: "Address Type",
                options: ["Home", "Business"]
            }
        ),
        bp.text(address1, {caption: "Address Line 1"}),
        bp.text(address2, {caption: "Address Line 2"}),
        bp.combo(country,
            {
                caption: "Country",
                options: jsToScheme("(collection \"Canada\"" +
                    "\"USA\"" +
                    "\"UK\"" +
                    "\"China\")"
                )
            }
        ),
        bp.text(zip, {caption: "Zip Code"})
    );
};
  
  
//Helper function for the address page
function testAddressPage() {
    return bp.page(Addresses,
        {
            caption: "Addresses",
            number: "2"
        },
        testAddressSection("S1"));
};
  
bp.registerFlow("testFormJDSL", true,
    (function() {
        return bp.flow(
            bp.page(Client_Details,
                bp.property(caption, "Client Details"),
                bp.property("number", "1"),
                bp.section(profile,
                    {caption: "Profile"},
                    bp.text(firstName, {caption: "First Name"}),
                    bp.text(lastName, {caption: "Last Name"}),
                    bp.text(fullName,
                        {
                            caption: "Full Name",
                            value: function() {
                                return $.question.firstName + " " + $.question.lastName;
                            }
                        }
                    ),
                    bp.date(birthTime, {caption: "Date of Birth"}),
                    bp.object(user,
                        {
                            caption: "User 1",
                            where: bp.where("=", $.loginName, "nexjsa"),
                            valueType: "User",
                            valueCaption: "fullName",
                            order: "loginName",
                            filterPath: "loginName"
                        }
                    ),
                    bp.objects(users,
                        {
                            caption: "User 2",
                            where: jsToScheme("'(= (@ loginName) \"nexjsa\")"),
                            valueType: "User",
                            valueCaption: "fullName",
                            order: "loginName",
                            filterPath: "loginName"
                        }
                    )
                )
            ),
            testAddressPage(),
            bp.page(misc,
                {
                    caption: "Misc. Questions",
                    number: "3"
                },
                bp.section(s1,
                    {caption: "Calculated fields"},
                    //try out both Scheme and javascript for the same formula
                    bp.text(addr0Line1js,
                        {
                            caption: "Address 0, Line 1 (js)",
                            value: function () {
                                return this.section.addrs[0].address1;
                            }
                        }
                    ),
                    bp.text(addr0Line1scm,
                        {
                            caption: "Address 0, Line 1 (scm)",
                            value: jsToScheme("(@ section '(addrs . 0) address1)")
                        }
                    ),
                    bp.radio(selectOption,
                        {
                            caption: "Select an option",
                            options: ["Yes", "No"],
                            required: true
                        }
                    ),
                    bp.text(selectedOption,
                        {
                            caption: "The selected option is:",
                            enabled: false,
                            value: this.question.selectOption === "Yes" ? "Y" : "N"
                        }
                    )
                )
            )
        );
    })()
);

Questions and answers

Each field is validated immediately on edit. There is no need to validate again on Save. To validate all fields without any edits, click the Submit button.

We do not encourage changing font sizes as the controls are built to pixel perfect standards as specified by Google's Material Design guidelines. You can change colors by applying your own theme. For more information about themes, see UI themes and CSS.

For server-side CPM DSL configuration scripts, you can add (logger’info …) to the code and observe results in the Scheme console.

For client-side code, debug using the Chrome debugger.

Make sure the question you want to sync with the class, has the bind property set. For example, on(bp:flow), set the bind property to the class.

On the question, either rename the question or set the bind property to match the model the attribute name.

Example: A form associated with class class1, and with an associated attribute asc1 to class class2, and the collection in the form collection1 has five questions each for the five attributes of class2.  Assuming asc1 is a collection association, try the following:

(bp:flow
	(bp:bind class1)
	(bp:section asc1
	 (bp:text class2_attr1 …)
	 (bp:text class2_attr2 …)
	 …
	)
)

The following test forms are available in the CPM project:

  • bpdatatestbind.scm
  • bpdatatestquestions.scm

The following code defines a function to translate JavaScript to Scheme:

JavaScript to Scheme translator
(define (translateJS jsText)
   (import 'nexj.core.util.TextPositionReader)
   (((((invocation-context)'metadata)'getLanguageType "js")'parser)'parse
      (nexj.core.util.TextPositionReader'new
        (open-input-string jsText)
        "url"
      )
      (make-hashtable)
   )
)

To translate JavaScript expressions to Scheme expressions:

Translate a JavaScript expression to Scheme expression
(define jsText "bp.where(\"in?\", this.value, [\"a\", \"b\", \"c\"]);")
(bp:js:translateJS jsText)
// > (bp 'where "in?" (this 'value) (collection "a" "b" "c"))

Note that escaped characters inside jsToScheme(...) must be escaped a second time.

Translate jsToScheme() expressions
//double quotes within jsToScheme must be escaped twice
(define jsText "jsToScheme(\"(logger'info \\\"Some Text\\\"\")")
(bp:js:translateJS jsText)
// ((jsToScheme "(logger'info \"Some Text\"")

To run JavaScript code on the Scheme console, you must first switch the console to run JavaScript:

Switch from Scheme to JavaSript console
(*repl*'language "js")

To switch back to running Scheme:

Switch from JavaScript to Scheme console
#"(*repl*'language \"scheme\")";
#"*repl*".language = "scheme";