Context, events, and binding
Learning objectives
In this lesson, you will learn about communication between portlets, including:
- Portal bus events: events that can be broadcast between portlets.
- Context events: shared context between portlets.
- Binding of controls to the business model.
- How to do all this from AFL portlets
Key concepts
- The bus is where inter-portal action takes place. Typically, an event is raised, potentially with parameters, and we handle the event in another portlet.
- Handlers within AFL portlets are typically "on<something>" events on UI controls (for example, onClick). Standard JavaScript stuff.
- Sharing context or events between portlets where we communicate over the portal bus, which is a communication bus that enables us to send and receive events with parameters. An important event is sysContext, which provides a context variable name and value as parameters. One portlet can say "I've changed the EntityId context and it is now Tim Lamont," and other portlets that are listening can respond.
Context is shared between portlets. For instance, in the Contacts workspace, the main context variable is
EntityId
(case is important). There are two types of participants - context setters and context receivers.- The bus capabilities are supplied by the
nexj/ui/bus
plugin module tonexj/ui
. It provides enhancements to the Portlet control for onContext events, the.context()
property, theui.ContextValue()
object for binding, and more. - Binding is provided by the
nexj/ui/obj
plugin module tonexj/ui
and also thenexj/rpc/obj
module. These libraries provide query capabilities with theui.ObjectCollection(...)
and different types of bound values for controls such asui.AttributeValue
.
In preparation for the learning activities in this lesson, make sure that your files from the previous lesson contain the following source code. If not, copy the code into your files before continuing.
training:Contact.portal
<Portal caption="idsa.training.portal.Contact.caption">
<Tool name="toolNotifications" caption="ids.notifications" icon="icon:notifications" event="setSidebar" parameter="NOTIFICATIONS"/>
<WorkspaceRef name="refSandbox" workspace="training:Sandbox"/>
<WorkspaceRef name="refHome" workspace="mda:Home"/>
<WorkspaceRef name="refContacts" workspace="mda:Contacts"/>
<Drawer event="setSidebar" name="refNotifications" portlet="mda:NotificationDrawer"/>
</Portal>
training:Sandbox.workspace
<Workspace caption="idsa.training.workspace.Sandbox.caption" icon="icon:attach_money">
<Page name="page">
<Tab layout="cols:3 fluid:true" name="tabDetail" type="grid">
<PortletRef name="mda_EntityNavigator_portlet" portlet="mda:EntityNavigator"/>
<PortletRef name="app_myportlet" portlet="app/myportlet"/>
</Tab>
</Page>
</Workspace>
idsa.training.en.strings
idsa.training.portal.Contact.caption=My Training Application
idsa.training.workspace.Sandbox.caption=Sandbox
training/mod/app/myportlet.js
/** Example AFL Portlet */
define("app/myportlet", ["nexj/sys", "nexj/ui"],
function(sys, ui) {"use strict";
return function() {
return ui("Portlet", {children: [
ui("Card", {caption: "AFL Card", layout: {cols: 4}, children: [
ui("Label", {caption: new ui.Value("Label"), layout: {span: 3}}),
ui("Text", {value: new ui.Value(), caption: "First Name", layout: {span: 2}}),
ui("Text", {value: new ui.Value(), caption: "Last Name", layout: {span: 2}}),
ui("Button", {
caption: "Display It",
layout: {span: 2},
onClick: function () {
ui("Dialog", {
caption: "The Dialog Caption",
size: "x",
children: [
ui("Card", {children: [
ui("Text", {caption: "First Name"}),
ui("Text", {caption: "Last Name"})
]})
]
}).open();
}}),
]})
]});
}}
)
Working with Context
First you are going to change the Label control to bind to a ui.ContextValue()
model rather than a plain old ui.Value()
model.
Change the Label definition to the following code:
ui("Label", {caption: new ui.ContextValue("EntityId"), layout: {span: 3}}),
This should result in something like:
Notice as you click around in the list that the Context label is updating to the unique identifier of the selected item. This is because you included the nexj/ui/bus
plugin module and set the value of the context label based on the ui.ContextValue("EntityId")
model. This kind of object binds to the context variable passed in i.e. "EntityId"
.
Another way to detect context changes is by inspecting your portlet's context
property. Here you will add a button that will raise an alert with the value of the EntityId
context.
Update your portlet as follows:
/** Example AFL Portlet */
define("app/myportlet", ["nexj/sys", "nexj/ui", "nexj/rpc/obj", "nexj/ui/obj", "nexj/ui/bus"],
function(sys, ui, obj) {"use strict";
var portlet;
return function() {
return portlet = ui("Portlet", {children: [
ui("Card", {caption: "AFL Card", layout: {cols: 4}, children: [
ui("Label", {caption: new ui.ContextValue("EntityId"), layout: {span: 3}}),
ui("Text", {value: new ui.Value(), caption: "First Name", layout: {span: 2}}),
ui("Text", {value: new ui.Value(), caption: "Last Name", layout: {span: 2}}),
ui("Button", {caption: "Show Context in Flash", layout: {span: 2}, onClick: function () {ui.Flash.open(portlet.context("EntityId").value())}}),
]})
]});
}}
)
First, you added a few more module dependencies to support binding and the context bus.
Although some of these modules would have been available just because our portlet is running in the Portal Application, which would load the bus for instance, we explicitly added them here as a best practice so the portlet can run outside of the Portal Application, if required.
Then we declared a variable called portlet
and set it to the ui("Portlet"
control. You then show the portlet.context("EntityId")
context in a flash when you click the new button. This will result in something like:
As you move towards binding, let's look at the onContext
event of the portlet. Update your portlet definition as follows.
training/mod/app/myportlet
/** Example AFL Portlet */
define("app/myportlet", ["nexj/sys", "nexj/ui", "nexj/rpc/obj", "nexj/ui/obj", "nexj/ui/bus"],
function(sys, ui, obj) {"use strict";
var portlet;
return function() {
return portlet = ui("Portlet", {children: [
ui("Card", {caption: "AFL Card", layout: {cols: 4}, children: [
ui("Label", {caption: new ui.ContextValue("EntityId"), layout: {span: 3}}),
ui("Text", {value: new ui.Value(), caption: "First Name", layout: {span: 2}}),
ui("Text", {value: new ui.Value(), caption: "Last Name", layout: {span: 2}}),
ui("Button", {caption: "Show Context in Flash", layout: {span: 2}, onClick: function () {ui.Flash.open(portlet.context("EntityId").value())}}),
]})],
onContext: {EntityId: function(name, value) {ui.Flash.open("name: '" + name + "', value: '" + value + "'")}}
});
}}
)
As you click around in the list, you should see a flash something like the following.
What's going on here? The portlet's onContext()
event has a set of properties with context name and handler function pairs. The handler function for a particular context receives the context name and the current value of the context when it changes.
We will use this now to illustrate binding to the business model.
Binding
Most controls have the ability to bind to models. Some models are simply JSON structures. Some others are still JSON structures, but are created from intelligent and efficient rpc with the server.
Collection controls have properties like items
or rows
. Other controls, like Labels or Text controls, simply have a value
property.
Again, a great reference is the demo apps at http://localhost:7080/training/ui/demo along with the source code found in `training/mod/app`. For an example of a collection binding, see the masterdetail-obj example in `training/mod/app/masterdetail-obj.js`
In this example, you will create a model that gets an instance of an Entity
from the business model with an ObjectCollection()
model. You will then bind some UI controls to that model with the ui.AttributeValue()
model wrapper. It is really pretty simple.
First create the model and make a request to the server. Here you will:
- add a
model:
property to our card that will select an instance where thefirstName = "Tim"
. bind the two text controls to the
firstName
andlastName
of the current instance using the AttributeValue model.
Update your portlet definition as follows:
training/mod/app/myportlet.js
/** Example AFL Portlet */
define("app/myportlet", ["nexj/sys", "nexj/ui", "nexj/rpc/obj", "nexj/ui/obj", "nexj/ui/bus"],
function(sys, ui, obj) {"use strict";
var portlet;
return function() {
return portlet = ui("Portlet", {children: [
ui("Card", {
caption: "AFL Card",
layout: {cols: 4},
model: ui("ObjectCollection", {name: "contacts", type: "Entity", where: obj.op("=", obj.attr("firstName"), "Tim")}),
children: [
ui("Label", {caption: new ui.ContextValue("EntityId"), layout: {span: 3}}),
ui("Text", {value: new ui.AttributeValue("contacts", "firstName", "string"), caption: "First Name", layout: {span: 2}}),
ui("Text", {value: new ui.AttributeValue("contacts", "lastName", "string"), caption: "Last Name", layout: {span: 2}}),
ui("Button", {caption: "Show Context in Flash", layout: {span: 2}, onClick: function () {ui.Flash.open(portlet.context("EntityId").value())}}),
]})],
onContext: {EntityId: function(name, value) {ui.Flash.open("name: '" + name + "', value: '" + value + "'")}}
});
}}
)
Notice the changes on line 10, 13 and 14. Refresh your application and you should see Tim Lamont in your AFL Card.
On line 10 you are using the nexj/rpc/obj
module to construct the where clause `(= (@ firstName) "Tim")
. This is what the obj.op()
function does.
This, however is not dynamic. We are always selecting firstName = "Tim"
without listening to EntityId
context changes. To make this happen:
- add a variable called
contacts
- assign that variable to our model
- change our model's initial
where
clause tofalse
so we don't make an initial query - set the
where
clause on our model dynamically whenever the context changes.
Update your portlet definition as follows:
training/mod/app/myportlet.js
/** Example AFL Portlet */
define("app/myportlet", ["nexj/sys", "nexj/ui", "nexj/rpc/obj", "nexj/ui/obj", "nexj/ui/bus"],
function(sys, ui, obj) {"use strict";
var portlet, contacts;
return function() {
return portlet = ui("Portlet", {children: [
ui("Card", {
caption: "AFL Card",
layout: {cols: 4},
model: contacts = ui("ObjectCollection", {name: "contacts", type: "Entity", where: false}),
children: [
ui("Label", {caption: new ui.ContextValue("EntityId"), layout: {span: 3}}),
ui("Text", {value: new ui.AttributeValue("contacts", "firstName", "string"), caption: "First Name", layout: {span: 2}}),
ui("Text", {value: new ui.AttributeValue("contacts", "lastName", "string"), caption: "Last Name", layout: {span: 2}}),
ui("Button", {caption: "Show Context in Flash", layout: {span: 2}, onClick: function () {ui.Flash.open(portlet.context("EntityId").value())}}),
]})],
onContext: {EntityId: function(name, value) {contacts.where(obj.op("=", obj.attr(""), obj.oid(value))).reload();}}
});
}}
)
Now as you click around in the list, you should see the first name and last name updating correctly. We made changes here on line 4, 10, and 17.
Before you were selecting `(= (@ lastName) "Tim") with obj.op("=", obj.attr("lastName"), "Tim")
. Now, the onContext
handler sets the model's where
clause to be `(= (@) ,(oid value))
. Where obj.attr("")
means the oid
of the object (in other words, (@)
) and obj.oid(value)
converts the string context EntityId
into an actual object identifier to pass to the server. Once the contacts model's where
clause has been set, you chain on a .reload()
to requery.
Events
To trigger events, simply use portlet.fire(<eventName>, <parameters>, <system>)
. For example:
portlet.fire("sysContext", {"EntityId": previous, "ObjectId": previous}, true);
To handle events, you may use the onEvent: method of the portlet, similar to the onContext method. For example:
onEvent: function(event, params, system) {alert(event, params, system);
Summary
This lesson presented fundamental information and examples of using context, binding, and events in AFL portlets.