Skip to content

Form Service

StephenCote edited this page Sep 30, 2013 · 2 revisions

Stop Reading!

The Easiest Way To Use The Form Service Is Don't Do Anything!

The easiest way to use the form service is don't worry about it and don't do anything. Any input field you put inside an Application Component Template or Fragment will will be handled for you.

Otherwise, continue reading for all of the gory details and another service designed for you to not touch (very much).

Introduction

The Form Service virtualizes HTML Form fields, persisting values within the virtual form whether or not the form field exists, and synchronizing the node value to a new field with the same name.

Overview

The Form Service stores virtual constructs of form and field values, whose synchronization of values with fields is orchestrated by the Application Space Service in conjunction with XHTML Components.

Consider a multi-page wizard to collect data. In order to aggregate all data across all pages, either every form element must exist or the form values must persist. If the form elements are destroyed, they must be recreated with any previously specified value in order to support backwards navigation. If a particular combination of input fields is replicated across multiple widgets or forms, the fields either must be assigned distinct identifiers, or be encapsulated in separate FORM elements. This encapsulation requires any script to recognize the various context.

The Form Service, in tandem with an Application Space, addresses these complexities.

The principal difference between regular forms and forms virtualized by the Form Service is that the virtual forms do not require the FORM or INPUT elements to exist.

API

Creating Virtual Forms

Given some form field:

<form>
    <input type = "text" value = "test text" name = "MyInput" />
</form>

The Application Space service defines the field as an XHTMLComponent, and passes the object to the Form Service for registration. The manual steps, which are automated via Application Spaces, are:

var oInput = document.forms[0].elements["txtText"];
var oXHTMLComponent = Hemi.object.xhtml.newInstance(oInput, 1, oInput.name, "MyForm");
Hemi.data.form.service.addComponent(oXHTMLComponent, "MyForm");

In the previous example, MyForm is created automatically when the XHTMLComponent is added. Also note the duplication of MyForm. The first instance, when the XHTMLComponent is created, stipulates that txtText is a component id and MyForm is a reference id (with XHTMLComponents, the component id correlates to an Application Space reference id). The second instance, when the XHTMLComponent is added to the form service, stipulates that the component is being added to the specified form. This allows the form value to be discoverable with only the XHTMLComponent, or with the form and field names.

The XHTMLComponent class includes a constructor argument for a component collection. In the Application Space Definitions, the definition for XHTMLComponents specifies the Form Service as that component collection. When the Application Space Service creates an XHTMLComponent for a matching definition, supported nodes are automatically registered with the form service.

Therefore, the previous script is not necessary when using Application Spaces. The virtual form name is the Application Space identifier.

Value Synchronization

Virtual Form and Field meta data tracks field type, name, original value, current value, and render status. When used with JSON binding, the property of the specified JSON object is used for the default value, and is also synchronized.

The heart of the Form Service is the synchronizeComponent method. This method is automatically invoked when a form value is queried, when an XHTMLComponent is destroyed, or when an XHTMLComponent is added. For a given Virtual Form Field and its XHTMLComponent binding, values are synchronized in or out, based on whether the object is created, destroyed, or queried.

In the previous example, script was defined to add an existing text field to a virtual form. The following example destroys the framework object and the input field.

oXHTMLComponent.destroy();
oInput.parentNode.removeChild(oInput);

Although both the XHTMLComponent instance and original HTML input field were destroyed, the context of the input field - the named field as belonging to the named form - still exists so long as the Application Space in which is was created exists.

var sValue = Hemi.data.form.service.getValue("txtText","MyForm");

And, if the input field and XHTMLComponent are recreated, and added to the virtual form, the value is automatically synchronized.

var oInput = document.createElement("input");
oInput.setAttribute("type","text");
document.body.appendChild(oInput);
var oXHTMLComponent = Hemi.object.xhtml.newInstance(oInput, 1, "txtText", "MyForm");
Hemi.data.form.service.addComponent(oXHTMLComponent, "MyForm");

In the previous example, after the newly created field is added to the virtual form, the field value is restored from the virtual version.

Clearing Virtual Form Values

For size, security, or synchronicity issues, it may be desirable to clear one or all virtualized forms. By default, Application Spaces clear related virtual forms when the space is destroyed. Otherwise, forms may be cleared using one of several methods.

By changing or clearing values through the form service, any rendered field nodes will be updated.

Clear Form Value

For sensitive fields that need to be cleared, such as passwords, without resetting the entire form:

Hemi.data.form.service.setValue(sName,vValue, sForm);

Clear Form Values

Clear or reset virtual form values. The resetDataForm method invokes clearDataForm with bClear specified as true.

Hemi.data.form.service.resetDataForm(sForm, bReset);
/// Or
Hemi.data.form.service.clearDataForm(sForm, bClear, bReset);

Remove Forms

The virtual form may be removed entirely with one of the following methods:

/// Toss everything
Hemi.data.form.service.clearForms();

/// Toss a specified form
Hemi.data.form.service.removeForm(sForm);

/// Toss a form, and nullify pointers
Hemi.data.form.service.removeDataForm(sForm);

For dynamic pages with many forms being created and destroyed, the removeDataForm method is the preferred way to clear a form and remove object references. This method is invoked when an Application Space is destroyed.

Validation

The Data Validator Service supports chained validation definitions. Validation definitions are boolean, replacement, or pass-through types that combine regular expressions and definition dependencies to create a validation rule.

Note: The Rocket Application includes a persistence layer for forms, form elements, form values, and validation rules.

Example Validation Rule Definition

The not-empty validation rule is simple and examplifies dependency chaining. This built-in rule is specified in the Data Validator Definitions file as:

/// Id, Type, Comparator, Match, Replace, AllowNull, ErrorText, aDepends
addNewPattern("not-empty", "bool", "true", "\\S", 0, 0,"Value cannot be an empty string.",["trim-ends"]);

The not-empty rule is:

  • A boolean rule.
  • A valid condition is that rule execution returns true, including all dependencies.
  • A match for any non-whitespace character.
  • Includes the trim_ends rule.

The trim ends rule is only an aggregate of two other rules:

addNewPattern("trim-ends", "none", 0, 0, "", 0,0,["trim-begin","trim-end"]);

And the trim-begin and trim-end rules are replacement rules:

addNewPattern("trim-begin", "replace", 0, "^\\s*", "", 0,0);
addNewPattern("trim-end", "replace", 0, "\\s*$", "", 0,0);

Therefore, validating the not-empty rule will first trim whitespace, and then assert whether or not the remaining value contains any non-whitespace characters. This is redundant save the desired out come was to merge the trim and validation.

Asserting Validation

Given a field that was added to a virtual form, such as txtText in MyForm, the value may be validated with the following statement. Note that the Form Service encapsulates the validation call to the Data Validator Service.

var bValid = Hemi.data.form.service.validate("txtText","not-empty","MyForm");

Form field nodes may also define pattern identifiers as attributes, which are discovered by the Form Service and preserved as part of the virtual field meta data.

<input type = "text" pattern-id = "not-empty" name = "txtText" />

If the previous INPUT element is added to a virtual form, the validation rule is then associated with the virtual field. Subsequent calls may then be made to validate the field without naming the rule.

Binding Fields to JSON Beans

Form fields managed by the Form Service may be bound to a JSON bean (previously registered as a framework object) using the bind attribute, or specifying a binding identifier when adding the component.

JSON Beans

A valid JSON bean is a Hemi Framework Object. JSON Beans can be manually assembled outside of a template, or dynamically created using the Application Component setBean method.

The binding is based on the global object identifier. Application Components create a custom object identifier based on its own id, allowing a friendly name and EL statement to be used to create the binding. Alternately, a globally unique id can be assigned to the bean.

For example, given some JSON object:

var oObject = {
   theCustomText : "Test Text";
};

Standard Hemi Framework APIs may be used to prepare and register this object with a friendly identifier, such as DemoBean.

Hemi.prepareObject("bean", "1.0", false, o, true);
/// Override the GUID
///
o.setObjectId("DemoBean");
Hemi.registry.service.addObject(o);

Then, the field may be bound to this object.

<input type = "text" bind = "DemoBean" name = "theCustomText" />

When the virtual form is synchronized, the backing bean value is used if the object and property exist. In this way, oObject.theCustomText is mapped to the field.

Within templates, setting up the bean is easier, and only needs to be done once.

if(!this.getBean("DemoBean")) this.setBean(oObject, "DemoBean");

Bean values may be referenced using two EL statements. ${bean.BeanName} evaluates to the object id, useful for defining bind attribute values in templates, and ${bean.BeanName.FieldValue} evaluates to the field value.

For example, within a template:

<input type = "text" bind = "${bean.DemoBean}" name = "theCustomText" />

Submitting Values

The form service leaves value submission to the implementation. Examples of submitting values as an XML document or JSON structure is provided in the TemplateTools.xml utilities import for Templates.

Adding support for a preferred submission method is straightforward, whether it is XML, JSON, or traditional.

The following example demonstrates how to retrieve all the values from a specified virtual form.

var oForm = Hemi.data.form.service.getFormByName(sForm);
var aElements = oForm.getElements();
for(var i = 0; i < aElements.length; i++){
   var sName = aElements[i].getName();
   var sFieldType = aElements[i].getType();
   /// Use the service's getValue to ensure a synchronized value is obtained
   var sValue = Hemi.data.form.service.getValue(sName,sForm);
   /// From this point, generate the form submission as desired
}

In the provided TemplateTools.xml SerializeForm method, either all fields or a named subset of fields are composed into an XML document for an AJAX submission. The separate CoreWeb project includes a class that deconstructs this XML document into a SerialForm object to retrieve the results.

From the context of a Template, using the AJAX and XML combination with SerializeForm results in a straightforward dynamic post:

var oXml = this.SerializeForm(); var oResponseXML = Hemi.xml.postXml("/path/to/app/", oXml);

Alternately, the JSON object may be submitted directly to a JSON-RPC method.

Virtual Forms and Templates

Application Components may load Templates. When using the Quick Fix attribute for an XHTMLComponent, or when specified via the API, a Template may be loaded into its own Application Space. Application Spaces and XHTMLComponents orchestrate the association with Virtual Forms. For reusability without colliding variable or field names, it is recommended that templates be loaded into their own application space.

Using Virtual Forms within Templates is straightforward.

Given some HTML:

<div template = "/path/Template1.xml"></div>

The previous HTML node loads the specified template contents and defines those contents as a separate Application Space. Any form elements are automatically registered to a Virtual Form associated with that space. If a reference id is not specified, the id or name attribute values are used.

The following example shows a simple form with one text field and one button. Note that the button onclick handler uses the ${this} token to refer to the Application Component created to load the template.

<Template>
    <p>
        <input type = "text" name = "txtText1" />
        <input type = "button" value = "Next" onclick = "${this}.loadTemplate('/path/Template2.xml');" />
    </p>
</Template>

The second template includes a similar structure, except the input value is disabled and a previous navigation button was added to return to the first template.

<Template>
    <p>
        Previously Entered:
        <input type = "text" name = "txtText1" disabled = "true" />
    </p>
    <p>
        <input type = "text" name = "txtText2" />
        <input type = "button" value = "Prev" onclick = "${this}.loadTemplate('/path/Template1.xml');" />
        <input type = "button" value = "Next" onclick = "${this}.loadTemplate('/path/Template3.xml');" />
    </p>
</Template>

EL Statements in Templates and Fragments

Application Components, which host templates and fragments, include token processing for client-side bean and form expression language (EL) statements. Any virtual form field value can be referenced via an EL statement, using the format: ${form.fieldName}. The third template includes three EL statements to display the values:

   <p>
      First value: ${form.txtText1}
   </p>
   <p>
      Second value: ${form.txtText2}
   </p>

Scripting Field Values

Alternately, the third template may include two labels, import the TemplateTools.xml utilities, and include embedded-script. This variation extracts the form values preserved in the virtual form from the first two templates (scoped to the Application Space created for the Application Component to load the templates into), and copies the contents into labels.

<Template>
   <import-xml src = "Templates/TemplateTools.xml" id = "TemplateTools" />
   <p>
      First value: <span rid = "lblFirstValue">...</span>
   </p>
   <p>
      Second value: <span rid = "lblSecondValue">...</span>
   </p>

   <p>
      <input type = "button" value = "Prev" onclick = "${this}.loadTemplate('/path/Template2.xml');" />
   </p>
   <embedded-script><![CDATA[
      template_init : function(){
         /// The GetFormValue method is supplied by TemplateTools
         var sVal1 = this.GetFormValue("txtText1");
         var sVal2 = this.GetFormValue("txtText2");
         /// The GetElementByRID method is supplied by TemplateTools
         Hemi.xml.setInnerXHTML(this.GetElementByRID("lblFirstValue"), sVal1);
         Hemi.xml.setInnerXHTML(this.GetElementByRID("lblSecondValue"), sVal2);
      }
   ]]></embedded-script>
</Template>

Custom EL Statements in Templates

In a previous example, virtual form values were displayed using the integrated ${form.fieldName} EL statement. These values could also be accessed using custom EL tokens by creating a custom EL statement with the Application Component token processor virtual method, local_handle_xhtml_token.

For example, the form values could be represented as a token, such as ${custom.txtText1} and ${custom.txtText2}.

Then, a custom handler could be defined to process these tokens.

<Template>
   <import-xml src = "Templates/TemplateTools.xml" id = "TemplateTools" />
   <p>
      First value: ${custom.txtText1}
   </p>
   <p>
      Second value: ${custom.txtText2}
   </p>
   <p>
      <input type = "button" value = "Prev" onclick = "${this}.loadTemplate('/path/Template2.xml');" />
   </p>
   <embedded-script><![CDATA[
   local_handle_xhtml_token : function(iType, sTokenValue){
      var sExp = /\$\{custom\.(\S+)\}/,aM;
      if((aM = sTokenValue.match(sExp)) && aM.length > 1){
         var sName = aM[1];
         /// Template tools aren't loaded yet
         var sValue = Hemi.data.form.service.getValue(sName,this.getTemplateSpace().space_id);
         sTokenValue = sTokenValue.replace(sExp,sValue);
      }
      return sTokenValue;
   }
   ]]></embedded-script>
</Template>

Making a Reusable Custom EL Processor

In the previous example, a custom EL implementation was used to map form values to custom tokens. While the implementation was inline with the template it can be encapsulated into its own reusable library. This could either be a global function with a direct mapping specifier per template, or, via an external fragment with embedded-script.

The following example demontrates binding the local handler to an external function.

<embedded-script><![CDATA[
local_handle_xhtml_token : function(iType, sTokenValue){
   return SomeGlobalHandler(iType, sTokenValue);
}
]]></embedded-script>

To reuse the script as an imported statement, the processing order of the Application Component must be considered. Because the template processing order is embedded-script, token processing, application space processing, the tokens would be handled by imported fragments, but not in the top-level template. Therefore, the fields must be separately encapsulated into their own fragment.

First, create a separate fragment to include the reusable script. Note that fragments customarily use the html-fragment root. In this example, assume the fragment is saved to /SomePath/SomeFragment1.xml.

<html-fragment>
<embedded-script><![CDATA[
   fragment_init : function(){
      /// ...
   },
   local_handle_xhtml_token : function(iType, sTokenValue){
      var sExp = /\$\{custom\.(\S+)\}/,aM;
      if((aM = sTokenValue.match(sExp)) && aM.length > 1){
         var sName = aM[1];
         /// Template tools aren't loaded yet
         var sValue = Hemi.data.form.service.getValue(sName,this.getTemplateSpace().space_id);
         sTokenValue = sTokenValue.replace(sExp,sValue);
      }
      return sTokenValue;
   }
]]></embedded-script>
</html-fragment>

Second, move the fields into their own separate fragment. In this example, assume the fragment is saved to /SomePath/SomeFragment2.xml.

<html-fragment>
   <p>
      First value: ${custom.txtText1}
   </p>
   <p>
      Second value: ${custom.txtText2}
   </p>
</html-fragment>

Third, remove both the fields and script from the template, and add import statements for both.

<Template>
   <import-xml src = "Templates/TemplateTools.xml" id = "TemplateTools" />
   <import-xml src = "/SomePath/SomeFragment1.xml" />
   <import-xml src = "/SomePath/SomeFragment2.xml" />
   <p>
      <input type = "button" value = "Prev" onclick = "${this}.loadTemplate('/path/Template2.xml');" />
   </p>
</Template>

Clone this wiki locally