Custom Options : Hierarchical Lists

Following on from the previous post, where we looked at how you might use an Entity and it’s instances to provide the dynamic input for a List of Values at run-time, this post will investigate how we might go one step further and implement a hierarchical lists with two dynamic lists based on instances.

Going back to something I mentioned in the previous post, I’ve generally been mystified why Hierarchical Lists have always been a struggle in Oracle Policy Modeling. I don’t think that exporting a List, modifying the XML file and then re-importing it is exactly business friendly. I know and appreciate that Stephen Estes created a cool Excel tool to make it more friendly by hiding all the XML manipulation from the naked eye, but the overall process just seems, well, lame to me.

In the previous post, we looked at building a Custom Options extension to pull values from an Entity. That at least means that instances become the run-time source of the values. But what about having two value lists in a hierarchy, and having both of them based on Entity Instances? Basically, create an Excel spreadsheet for your dynamic values using inferred instances, and read that into the hierarchy dynamically?

As soon as you sit down and look at this, some thorny problems jump out. Most obviously the following:

  1. The Custom Options extension does not provide any keys for our code like the ones you see in Inputs and elsewhere : updatevalidatemount or unmount. So the concept of “update the child because the parent changed” becomes harder.
  2. The Entities (assuming for the moment that there are two) will be arranged in a hierarchy, which means a double iteration – find the parents, then find the children for each parent. This would be time consuming.

From a component perspective, we will have two Controls, both Custom Options. Two JavaScript files. They will need to have a few Custom Properties on the Controls, to support the concept (such as the Property “EntityName” which we will pass into the JavaScript. But the child list will need to be able to access the Parent value.

In the end, for the purposes of demonstration, we are going to do this with three files. Two Custom Options and a Custom Input. The Custom Input does not do much, except provide us with a hook to hang a line of code, so that when the parent value is updated we can refresh the child values.

Our basic spreadsheet looks like this:

OPA 12 - Custom Options Hierarchy Data Source

Regular readers will recognize this as a data set we have used previously in posts about RuleScript. This time however we will just use it as source data for our Custom Options hierarchy. There are two Entities, the station and the child station. Each parent has multiple children. You get the idea. Pick a station, then view and pick a child station. The entities and attributes have been set up with names to be able to reference them in our JavaScript.

  1. These attributes have nothing to do with the example, they are just to fill out the page
  2. The parent and child entities are added with Entity Containers. There is a Boolean attribute to ensure that they are hidden, but they need to be present for the data to be available to JavaScript
  3. These are the attributes involved in the Hierarchy
  4. This Control has a Custom Option extension. It is the parent list.
  5. This Control has a Custom Input extension. It is never entered manually, and simply shows the user what they selected. A decent CSS would make it either invisible or at least a bit nicer to look at.
  6. This Control has a Custom Option extension. It is the child list.

First the parent list, which is identical to the example shown in the previous post. It simply reads the instances into the Options list.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
OraclePolicyAutomation.AddExtension({
	customOptions: function (control, interview) {
		if (control.getProperty("name") == "xOptionsParent") {
			console.log("Custom Options Extension found " + control.getProperty("name"));
				var entityId = control.getProperty("entityname");
				var entities = interview._session.config.data;
				var entityinstance;
				var entity;
				var myStations = []
				for (i = 0; i < entities.length; i++) {
					entity = entities[i];
					if (entity.entityId === entityId) {
						break;
					}
				}
				var Stations = entity.instances;
				for (i = 0; i < Stations.length; i++) {
 
					myObject = new Object();
					for (j = 0; j < Stations[i].attributes.length; j++) {
						entityinstance = Stations[i].attributes[j];
 
						if (entityinstance.attributeId === "station") {
							textofentry = entityinstance.value
						}
 
					}
 
					myObject.text = textofentry.toString();
					myObject.value = textofentry.toString();
					myStations.push(myObject);
				}
 
				console.log("List of stations now ready" + JSON.stringify(myStations));
 
				return {
					options: myStations,
					controlType: "Dropdown"
				}
 
		}
	}
});

The child list Custom Options file is very similar:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
 * Richard Napier The OPA Hub Website January 2019
 * Educational Example of Custom Options List as Hierarchical LOVs
 * I will remember this is for demonstration purposes only
 */
OraclePolicyAutomation.AddExtension({
	customOptions: function (control, interview) {
		if (control.getProperty("name") == "xOptionsChild") {
			//console.log("Custom Options Extension found " + control.getProperty("name"));
			var myStation = interview.getValue("parentstation");
				var entityId = control.getProperty("childentityname");
				var entities = interview._session.config.data;
				var entityinstance;
				var entity;
				var myChildStations = []
				for (i = 0; i < entities.length; i++) {
					entity = entities[i];
					if (entity.entityId === entityId) {
						break;
					}
				}
				var Stations = entity.instances;
				var shortList = Stations.filter(function (el) {
						return el.parentId.attrVal == myStation;
					});
				for (i = 0; i < shortList.length; i++) {
 
					myObject = new Object();
					for (j = 0; j < shortList[i].attributes.length; j++) {
						entityinstance = shortList[i].attributes[j];
 
						if (entityinstance.attributeId === "child_station") {
							textofentry = entityinstance.value
						}
 
					}
 
					myObject.text = textofentry.toString();
					myObject.value = textofentry.toString();
					myChildStations.push(myObject);
				}
 
			//	console.log("List of child stations now ready" + JSON.stringify(myChildStations));
				return {
					options: myChildStations,
					controlType: "Dropdown"
				}
		}
	}
});

Lines 22 -25 try and optimize the process of grabbing the child stations by using a direct access to the child stations in JavaScript and then filtering the array to only get those whose parent id is the correct one. This seemed faster than iterating over all the children first.

So far so good. The two options will work fine, if you place them on different screens, with the Parent before the child. But we need them to be on the same Screen. The third JavaScript file, the Custom Input, looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/*  Generated by the OPA Hub Website 23/01/2019 15:22
Educational Example of Custom Input Extension for Oracle Policy Automation
 I will remember this is for demonstration purposes only. 
*/
OraclePolicyAutomation.AddExtension({
    customInput: function(control, interview) {
        if (control.getProperty("name") == "xInput") {
            return {
                mount: function(el) {
                    //console.log("Starting customInput Mount " + control.getProperty("name"));
					var div1 = document.createElement("divinput")
                    var div2 = document.createElement("input");
                    div1.id = "myContainer";
					div2.id = "myInput";
                    div2.value = control.getValue();
					div1.appendChild(div2);
                    el.appendChild(div1);
                    //console.log("Ending customInput Mount");
                },
                update: function(el) {
					var now = new Date();
					//console.log("Starting customInput Update " + control.getProperty("name") + " " + now);
                    //control.setValue(interview.getValue("parentstation"));
                    //console.log("Ending customInput Update " + now + " " + interview.getValue("parentstation"));
					//console.log("Control " + control.getValue());
					if(interview.getValue("parentstation") != null && interview.getValue("parentstation") != "" && interview.getValue("parentstation") != control.getValue()){
						control.setValue(interview.getValue("parentstation"));
						//console.log("Updating Now");
					interview.saveData();
					}
                },
                validate: function(el) {
                    console.log("Starting customInput Validate");
                        control.setValue(document.getElementById("myInput").value);
						//interview.setInputValue("parentstation", document.getElementById("myContainer").value)
						//console.log(document.getElementById("myInput").value);
						//interview.update();
		                return true;
                    console.log("Ending customInput Validate");
                },
                unmount: function(el) {
                    if (control.getProperty("name") == "xInput") {
                       // console.log("Starting customInput UnMount");
                        var myContainer = document.getElementById("myInput");
                        myContainer.parentNode.removeChild(myContainer);
                       // console.log("Ending customInput UnMount");
                    }
                }
            }
        }
    }
})

The only lines of code that matter are the update. Basically whenever the parent station is not null, and has a different value to this custom input, then save the interview. This triggers a refresh, and the parent and child lists are redrawn, and the new values are available to the user.

It’s not pretty, but short of hacking some real DOM events we couldn’t find a way to update the list since (as we mentioned earlier) it does not have any keys like update or validate that we might use.

In the end the user experience is reasonably cool, with the lists being refreshed:

Hierarchical Lists Animation

Hierarchical Lists Animation
If you are interested in the finished Project just drop a comment and I will happily share it, with absolutely no warranty.