Using Browser Technologies in Visualforce - Part 2
Part 1 | Part 2 | Part 3 | Part 4
Contents |
Introduction
In this article I will walk you through creating a simple Flex tree control that is used as a UI component on a Visualforce (VF) page. This simple Visualforce page illustrates some key concepts in using browser technologies with Visualforce pages - how to pass data to a Flex component, how to pass data from a Flex component to a Visualforce page and how to interact with a Visualforce page Controller from Javascript.
Overview
The key to using browser technologies on VF pages is, of course, Javascript. Javascript provides the framework for communication between other Javascript objects, HTML elements (including in this example an object tag used to render a Flex application) and the VF controller.
As mentioned in part 1 of this article, VF is on-demand MVC. This means that our UI elements need to be able to communicate, pass data to, and receive data from the controller. In a pure VF page (one that only uses VF components), this is accomplished by the platform providing the data to the page when the page is rendered, and by posting data back to methods on the controller to fire off actions like saving a record.
What We'll Build
The page that we will create will be a simple drag and drop interface to the standard objects Document and Folder. This will be implemented as a simple tree control with each folder a root node of the tree and each document a child of a folder node, much like a file system explorer or finder.
In the case of this example we will need to retrieve all the folders and the documents that belong to them and pass this data to our Flex application for rendering. Once the Flex application has rendered the folders and documents, the user can expand and collapse any folder and drag and drop a document from one folder to another. After the user has moved a document the Flex application will need to communicate this change to the page and provide enough information for the controller to re-parent the document that moved.
Understanding the Moving Parts
To better understand how this browser to controller communication works we'll start with a simple VF form. This form has a text input component, a hidden input component and a command button component to submit the input to the controller.
And the corresponding VF page code for this page.
<apex:page id="pageForm" controller="formController">
<apex:form id="form">
<apex:inputText id="textFld" value="{!myObject.textData}" />
<apex:inputhidden id="otherName" value="{!myObject.hiddenField}" />
<apex:commandButton value="Submit Data" id="btnCopy" rerender="newData" />
</apex:form>
<apex:outputPanel id="newData">
Posted value: {!myObject.textData}<br/>
Action Result: {!myObject.website}
</apex:outputPanel>
</apex:page>
When a value is typed into the inputText field and the button is clicked, we will refresh only the outputPanel "newData" showing the value entered and the new value of the website field. This is accomplished by setting the "rerender" attribute of the commandButton. We are basically telling the page to re-render the outputPanel newData when the request completes. This illustrates a partial page refresh and is a key concept in using other browser technologies in VF pages.
The controller for this page is pretty straight forward. I am not using any force.com objects at this point, just a class that is embedded in the controller. The class has two fields and two pairs of setters and getters, one for each field and a getter for a "calculated" field. You can see these fields referenced in the page code above in the inputText, inputHidden and in the outputPanel for the action result.
public class formController {
public class MyObject {
private String hiddenField = 'Lorem ipsum dolor';
private String textdata = null;
public String getTextData() { return textdata; }
public void setTextData(String data) { textdata = data; }
public String getWebsite() {
if (textdata != null) {
return '<ul><li>' + hiddenField + '</li><li> ' + textdata + '</li></ul>';
} else { return ''; }
}
public String getHiddenField() { return hiddenField; }
public void setHiddenField(String data) { hiddenField = data; }
}
private myObject my_object;
public MyObject getMyObject() {
if (my_object == null) { my_object = new MyObject(); }
return my_object;
}
}
Because I defined an inputText field and bound it to myObject.textdata, that value will be included in the post back to the controller effectively passing the value to the controller. Likewise the inputHidden is passed in the same way.
Controller Communication with Javascript
Now, when the page is rendered an HTML button element of type submit is created on the page. If it is on the page, then I can use it from Javascript. This is another key concept, accessing the generated HTML from Javascript. In theory, I should be able to write some Javascript that can either cause the form to be submitted - form.submit(), or cause the button to be clicked - button.click(). Either one of these methods allows me to submit data to the controller without user interaction.
To illustrate, we can place some Javascript on the page to do this in response to some other event. In this case we will hook the onload event of the body. The new page code is shown below.
<apex:page id="pageForm" controller="formController">
<apex:form id="form">
<apex:inputText id="textFld" value="{!myObject.textData}" />
<apex:inputhidden id="otherName" value="{!myObject.hiddenField}" />
<apex:commandButton value="Submit Data" id="btnCopy" rerender="newData" />
<script> var btnCopy = document.getElementById("{!$Component.btnCopy}"); </script>
</apex:form>
<apex:outputPanel id="newData">
Posted value: {!myObject.textData}<br/>
Action Result: {!myObject.website}
</apex:outputPanel>
<script>
window.onload = new function() { btnCopy.click(); };
</script>
</apex:page>
Correction:
Although the following paragraph is true, the use of the DOM in this way is not supported. Salesforce.com reserves the right to modify the naming convention explained below without notice. The best practice for accessing Visualforce components from within Javascript is by using $Component. This practice is now shown in the preceding code. Using $Component will guarantee that if the underlying DOM naming scheme is changed, your code will continue to work properly.
Two important concepts are shown in the body tag. First, there is a thoughtful and logical way to reference the VF components from Javascript. Notice that we are using a standard way of referencing our submit button using document.getElementById. Also notice that the <apex:page> tag, <apex:form> tag and <apex:commandButton> tag each have an id attribute specified. When you create your page you can choose the ids for any and every component on your page by using the id field. To reference those component's generated HTML counterparts you simply use a "path" of ids concatenated together with colons starting with the page component. Best Practice Once you have put ids on your components you can use $Component to reference the rendered HTML element directly using getElementById as in document.getElementById("{!$Component.btnCopy}").click();. This naming convention is what enables direct access to the generated HTML on your page.
One other important thing to note about using $Component - whenever you have a tag that refers to $Component, the id it's referring to has to be either in your direct hierarchy, or it has to be a sibling of your tag (which can be considered to be a part of your hierarchy).
The other concept is to leverage what is already available in the generated HTML document. For instance, when you place a button on a page, you have access to all the functions defined on that button. In this case I simulating a user clicking a button simply by calling the click() function for that element.
If you are following along and want to test this, you should note that you will need to add a default value to the textData field defined in the MyObject class within the controller.
For instance:
...
...
public class MyObject {
private String hiddenField = 'Lorem ipsum dolor';
private String textdata = 'Body on load fired';
public String getTextData() { return textdata; }
...
...
The results of a page refresh would then look like this with the above changes implemented.
Driving the Data
Now that we know how to get our data to the controller, the next challenge is setting the data that is sent to the controller. Taking the concepts above, referencing the generated HTML elements and leveraging the page capabilities, we can set the data that is sent to the controller.
If you recall the VF page we have been using for this article, there was a hidden field that was defined as part of the form with an id of "otherName". What we want to do is set the value of that element before the form is submitted to the controller action. If you are familiar with Javascript you might be able to see where I am going next.
We know we can access the generated HTML element that represents the "otherName" hidden input based on the discussion above. We would construct the id so that we end up with document.getElementById("{!$Component.otherName}"). Setting the value property of that element will cause that value to be posted by the form.
There are several different ways, or times when you might set the hidden field value, for this article we will hook the onsubmit event of the form. The modified form component tag is shown below.
<apex:form id="form"
onsubmit="javascript:document.getElementById('{!$Component.otherName}').value='NO MORE LOREM IPSUM';" >
While this inline technique works, a more readable and maintainable technique is to create Javascript functions and calling them from the event handler attribute. Below is the refactored version of the page.
<apex:page id="pageForm" controller="formController">
<apex:form id="form" onsubmit="changeValue()" >
<apex:inputText id="textFld" value="{!myObject.textData}" />
<apex:inputhidden id="otherName" value="{!myObject.hiddenField}" />
<apex:commandButton value="Submit Data" id="btnCopy" rerender="newData" />
<script>
var btnCopy = document.getElementById("{!$Component.btnCopy}");
var textFld = document.getElementById("{!$Component.textFld}");
var otherName = document.getElementById("{!$Component.otherName}");
</script>
</apex:form>
<apex:outputPanel id="newData">
Posted value: {!myObject.textData}<br/>
Action Result: {!myObject.website}
</apex:outputPanel>
<script>
window.onload = new function() { submitForm(); };
function submitForm() {
textFld.value = "Set from onLoad event";
btnCopy.click();
}
function changeValue() {
otherName.value = "NO MORE LOREM IPSUM";
return true;
}
</script>
</apex:page>
We are now able to set data to be sent to the controller using Javascript and cause that data to be posted to the controller using Javascript.
Getting the Data to Javascript
The previous section laid the foundation for how to submit data to the controller. In this section I'll layout some techniques for obtaining data from the controller.
While binding input components and hidden components is pretty straight forward and easy to use, it's not long before we need something else to handle arrays of data. As stated at the beginning of this article, we want to create a page that allows us to drag and drop documents between folders. This will require that we send arrays of data to our Flex application.
The technique that I prefer is to create analogs to controller objects. If my controller is going to return an array of objects, then I will set up an array object in Javascript to contain that controller array. Visualforce has several components that allow you to display sets and arrays. For this article I'm going to use the most basic of these, a repeat component.
The repeat component allows you to iterate over a set and does not make any assumptions about how the set is to be rendered. For example, lets modify our controller so that it returns an array. We will add a new List type variable to our MyObject and a new getter for that List type variable.
...
public class MyObject {
private String hiddenField = 'Lorem ipsum dolor';
private String textdata;
private List<String> arrayData = new List<String>{'value1', 'value2', 'value3', 'value4' };
...
public void setHiddenField(String data) { hiddenField = data; }
public List<String> getArrayData() { return arrayData; }
}
...
Now we can add our repeat component to the page itself. Inside the repeat tags, during the iteration, we will add a script tag that pushes the value to an array defined in Javascript. We will simply fill a standard user list (ul) with the contents from the Javascript array when the page loads.
First, adding the Javascript array variable.
<apex:outputPanel id="newData">
Posted value: {!myObject.textData}<br/>
Action Result: {!myObject.website}
</apex:outputPanel>
<!-- Defining Javascript array variable -->
<script> var arrData = []; </script>
Now, to add the repeat tag and fill the array.
<!-- Adding repeat component and grabbing arrayData (use ad to reference item) -->
<apex:repeat value="{!myObject.arrayData}" var="ad">
<!-- Pushing item into Javascript array -->
<script>arrData.push("{!ad}");</script>
</apex:repeat>
Next we can place our array container on the page.
<!-- Put a container in the page to show the array --> <ul id="arrayContainer" />
This is the function that will loop over the array and create list items for our array container.
<script>
window.onload = new function() { submitForm(); };
//Function to show the array elements from Javascript
function showArray() {
for (var i in arrData) {
var li = document.createElement("li");
li.appendChild(document.createTextNode(arrData[i]));
document.getElementById("arrayContainer").appendChild(li);
}
}
Finally, we can add the showArray function call to the onload function.
function submitForm() {
//Iterate our JS array when the page loads.
showArray();
textFld.value = "Set from onLoad event";
btnCopy.click();
}
The entire re-assembled page is shown below.
<apex:page id="pageForm" controller="formController">
<apex:form id="form" onsubmit="changeValue()" >
<apex:inputText id="textFld" value="{!myObject.textData}" />
<apex:inputhidden id="otherName" value="{!myObject.hiddenField}" />
<apex:commandButton value="Submit Data" id="btnCopy" rerender="newData" />
<script>
var btnCopy = document.getElementById("{!$Component.btnCopy}");
var textFld = document.getElementById("{!$Component.textFld}");
var otherName = document.getElementById("{!$Component.otherName}");
</script>
</apex:form>
<apex:outputPanel id="newData">
Posted value: {!myObject.textData}<br/>
Action Result: {!myObject.website}
</apex:outputPanel>
<!-- Defining Javascript array variable -->
<script> var arrData = []; </script>
<!-- Adding repeat component and grabbing arrayData (use ad to reference item) -->
<apex:repeat value="{!myObject.arrayData}" var="ad">
<!-- Pushing item into Javascript array -->
<script>arrData.push("{!ad}");</script>
</apex:repeat>
<!-- Put a container in the page to show the array -->
<ul id="arrayContainer" />
<script>
window.onload = new function() { submitForm(); };
//Function to show the array elements from Javascript
function showArray() {
for (var i in arrData) {
var li = document.createElement("li");
li.appendChild(document.createTextNode(arrData[i]));
document.getElementById("arrayContainer").appendChild(li);
}
}
function submitForm() {
//Iterate our JS array when the page loads.
showArray();
textFld.value = "Set from onLoad event";
btnCopy.click();
}
function changeValue() {
otherName.value = "NO MORE LOREM IPSUM";
return true;
}
</script>
</apex:page>
The complete controller is shown below.
public class formController {
public class MyObject {
private String hiddenField = 'Lorem ipsum dolor';
private String textdata;
private List<String> arrayData = new List<String>{'value1', 'value2', 'value3', 'value4' };
public String getTextData() { return textdata; }
public void setTextData(String data) { textdata = data; }
public String getWebsite() {
if (textdata != null) {
return '<ul><li>' + hiddenField +
'</li><li> ' + textdata + '</li></ul>';
} else {
return '';
}
}
public String getHiddenField() { return hiddenField; }
public void setHiddenField(String data) { hiddenField = data; }
public List<String> getArrayData() { return arrayData; }
}
private myObject my_object;
public MyObject getMyObject() {
if (my_object == null) { my_object = new MyObject(); }
return my_object;
}
public String handleSubmit() {
return null;
}
}
If all is well with your page and controller, then the page should now render this way.
We can pass arrays of complex objects as well as simple types. You are not limited to the most basic types. If you are using complex types in your repeat tag, then the item has all the fields defined in the complex type and are accessible via dot notation.
We now have all the required information and techniques to leverage standard browser technologies from within Visualforce pages. The next article will concentrate on implementing our Flex sample.