Dynamic form creation in Perspective

Hi, I seem to be running into a road block with picking up form data. The issue is finding the sibling or child, when the form is dynamic (if this is possible at run time?)


This form and it drop down data is completely dynamic using flex repeaters and templates.

The issue is getting the form data for the submit button as seen below:

def runAction(self, event):

	array = self.view.params.queryData
	dictArray = dict(array)
	
	if not isinstance(dictArray, dict):
		system.perspective.print("Error: queryData is not a dictionary")
		return
  
	params = {}
	for name, field_type in dictArray.items():
		try:
			if field_type == "dropdown":
			    value = None if self.getSibling('FlexContainer').getChild('dropdownRepeaters').getChild('root/'+name).props.value == "" else self.getSibling('FlexContainer').getChild('dropdownRepeaters').getChild('root/'+name).props.value
			elif field_type == "text":
			    value = self.getSibling('FlexContainer').getChild('root/'+name).props.text
			params[name] = value
		except Exception as e:
			system.perspective.print("Could not go through eaceh loop item: {}".format(e))

I get his response:

Could not go through eaceh loop item: 'NoneType' object has no attribute 'getChild'

All of the getSibling, getChild, getNephew, etc., make the application very brittle. If you move anything it breaks. Consider an alternative:

2 Likes

Thank you. Makes sense.
What about the text field. Does it have a similar onAction? Obviously I want the full text of what is input..

The line that says "value = None if..." seems wrong. You probably just want the if statement.

You can definitely dynamically generate forms and even bind them to tags.

You might want to create a tag structure that your form dynamically binds tags to. Then you can see the real time selected value very easily. Tags are easy to work with.

Here's some code from a screen I wrote that reads tags related to the I/O modules in a PLC and dynamically generates an instance of an I/O status template within a template repeater. This code lives inside a template to display the I/O points for a module in the PLC. Another view dynamically populates its template repeater with instances of this template to display I/O for each card in the PLC. You can really go crazy with dynamic binding.

You don't need to do it quite this complicated but the general idea is to use JSON to bind the newly created template instance to a tag binding. In this case it's creating instances for each tag within a folder in the tagdb and resizing the UI when it's done because this particular screen is sometimes used on mobile. The actual tag binding is done with an indirect binding on the template.

def transform(self, value, quality, timestamp):
	 descList = system.tag.browse(value + '/IO_Description', {})
	 returnList = []
	 i = -1
	 
	 for x in descList:
	 	i += 1
	 	returnList.append(
  		{
	 	    "instanceStyle": {
	 	      "classes": ""
	 	    },
	 	    "instancePosition": {},
	 	    "Label": x["fullPath"],
	 	    "Index": i,
	 	    "TagPath": value
	 	  })
	 	  
	 # Resize the screen based on the dynamic content that just got generated
	 #	This is to kill white space when there are varrying numbers of IO points in a block
	 # IO points displayed are determined by the number of description tags in the tagdb
	 self.view.props.defaultSize.height = ((i + 1) * 30) + 40
	 
	 return returnList
1 Like

Have a look at Perspective Event Types Reference | Ignition User Manual. I think you want onCompositionEnd which should happen on pressing Enter or focus moving to another component.

1 Like

The line that says "value = None if..." seems wrong. You probably just want the if statement.

That has to be that way for the SQL insert, as that field can be Null, but a "" which is what the form sends when nothing is selected, is not null, and errors the sql insert

Instead of trying to scan the "value" of each child, is there a reason you're not populating some custom dictionary with the value from each component as part of an onChange script? This would make it so that you can just pull expected values from a dictionary (and make None-type defaults) instead of trying to determine child presence or structure.

Make a custom object at the View level, and then each component should just write to a unique key of the dictionary in an onChange script.

View.custom.formDict = {}

onChange script for each "input":

UNIQUE_KEY_FOR_THIS_COMPONENT = "MyKey"
self.view.custom.formDict[UNIQUE_KEY_FOR_THIS_COMPONENT] = currentValue.value

Then the button can just pull from the dictionary as needed:

value_x = self.view.get("MyKey", "default/fallback value here")
2 Likes

Mostly because I hadn't thought of that, and all the examples in the manual show that way I was doing it with pulling directly that way. :slight_smile:

This is on the parent.

This is on the flex repeat child, that isn't attached to the page until run time. How would I reference the formDict from that template child?

Instead of directly writing to a prop, you'll want to use a message handler:

UNIQUE_KEY_FOR_THIS_COMPONENT = "MyKey"
system.perspective.sendMessage("PROP_UPDATE", payload={UNIQUE_KEY_FOR_THIS_COMPONENT: currentValue.value}, scope="page")

Configure a message listener at either the View or Repeater level (right-click Flex Repeater, then select Configure Scripts):

# PROP_UPDATE Message Handler
# assuming repeater level
self.custom.formDict.update(payload)
3 Likes

Thank you!

1 Like