A while back I was working on a form for an application that had to have a dynamic number of inputs. Let's simplify the use case for this article and say we collect a person's income. A person can have multiple incomes and there's no limit to how many. So, no problem, we create a list and inside that list we put an input widget, set its value to be list.current.income and we are good to go, so I thought. Unfortunately, there is a problem, the client wants client-side validations, and that nice red box with the validation message reminding the user that "; DROP Users" is not a valid number .
You may think "No problem! In the OnSubmit action I'll just check if the ...." uh oh, in OutSystems, you don't have access to the widget inside the list. So, what should you do now? Well, there are multiple ways to tackle this issue, but the one I am about to propose to you as a solution is using just a bit of JavaScript.
The publish/subscribe pattern
In general, when people talk about pub/sub, it is more of an application architecture discussion where applications are small and composable, an application can publish messages and other applications can choose to subscribe to said messages or not, without ever knowing the internals of the application they are subscribed to. It's an excellent way to create loose coupling between applications.
If a server publishes a message and it has no subscribers, did it really publish a message?
, Isaiah Paget 2024
We can take this concept to the front-end in OutSystems 11 (OS11), and even write all our front-end logic in screen actions using just a tiny bit of JavaScript. In OS11 we have this concept of input parameters and events, both ways for a block to either talk to its direct parent or child. This script below allows us to go beyond that and send “events” to grandparents, siblings, and even cousins.
You will notice that the class in this script has only one property which is an object, we are going to use this as a key-value store where our keys will be the name of the event and the value is going to be an array of function references.
In the subscribe method we will be checking if a key exists, and if not we will create one, then we are pushing our passed-in function reference to the end of the array
In the trigger method, we check if the key is valid. If it is, we invoke every function in that array
In the unsubscribe method, we are checking if that key is valid and if it is then we remove the function reference from that array
This script is to allow blocks to offer up their screen actions to be invoked by the eventManager class instance. As an example, the “Foo” block can give its ReColor action to the eventManager and call it the ReColorEvent, and then the “Bar” block can tell the eventManager to trigger the ReColorEvent and it will invoke the ReColor action on the “Foo” block. The possibilities are truly limitless, but let’s get back to how this can help us solve our original problem.
Note: The comments in the script are in JSDoc notation more documentation is found here: Typescript JSDoc supported types
How to implement
Upload the script to the module where you will be defining your block.
Note: We don't need to require this script in every module that references the class instance "eventManager". We only need to require it in the end-user module, this is because when the application starts, the browser loads this class instance into memory, and from there, any JS executed in any action from any module, will have access to the instance.
Solving client-side validation inside lists of blocks
Let’s start by creating a web block where we are going to create the structure needed for each line of the different incomes. For this use case, let's call it IncomeType. In this block, we are going to set each input to mandatory and link them to their respective label.
All the actions are empty, and we will fill them out as we go. Next, we are going to fill out the widget tree for each of our blocks.
To host all the different incomes, created another web block, called IncomeDetails where I added a List widget and in the content of the list item, the IncomeType web block I created before.
It is not important to define what built-in form validation is set to on the button since it will be the custom JavaScript doing the needed validations. Hopefully it will click later down below :)
Now are going to hook everything up
To make our lives easier, we are going to create a static entity to put a name to all the events we are going to use. We are going to set the Id as Text, with its value being the name of the record, in this case, “Validate Income“. Recall the Trigger method in the EventManager class mentioned above. This method uses the Events.ValidateIncome.Id as the key to access the correct value in the subscriber's property of the event manager class instance.
It is now time to go through the different actions that compose our web blocks.
Inside CheckIsValid_Subcribed, it’s where we are going to be validating our inputs and setting the so much needed validation messages, just like we normally would by assigning the values to the widgets. Keep in mind, that this is the action that is being given to the eventManager so that any other block can tell the eventManager to execute it. This also highlights the reason why the values in the subscriber's object are always arrays. Since the block IncomeType is going to be in a list there will be multiple instances of the block and they will all have their instances of this screen action that needs to be executed to validate their internal inputs.
You will notice that this action also invokes an event – IsValid. This is happening so that IncomeType can tell IncomeDetails if it is valid or not. Since there are going to be multiple IncomeTypes, IncomeDetails will have to store these validity results inside a list. Conveniently IsValid_List should do the trick.
In the OnInitialize action of the IncomeType web block, we need to create a JavaScritp node and invoke the subscribe method on our eventManager class instance. We are going to pass in the EventId which will be Entities.ValidateIncome, and the reference to our CheckIsValid_Subscribed action, meaning we will not include the () to not invoke it.
Just in case it’s unclear, let’s go over what is happening when you invoke this method.
The method checks if there is not a key by the name of eventName: “ValidateIncome” already on the subscriber’s property of this class instance, if that is the case then we create that key and assign it to an empty array.
We access either the newly created or already existing array by the key: “ValidateIncome” and push the function reference that we passed in via callback: “CheckIsValid_Subscribed” onto the end of said array.
When IncomeType is initializing it is offering up its validation action to be invoked by the eventManager class instance.
Next in the IncomeTypeIsValid_Handler in IncomeDetails, we are just going to append the value that comes with the triggering of the event in Valid to the IsValid_List. Later in the article, you will understand in detail why this is done.
In the IncomeValidate_Trigger client action, we are going to create another JavaScript node and add the following code.
The parameter EventId in this case will be entities.ValidateIncome. This is going to be the action that tells the eventManager to execute all the CheckIsValid_Subscribed actions on each instance of the income type block.
Note: We want to have this in action so that we can "find usages" on it. Because there may be various actions within a single block or screen that are invoking an event.
Doing a deeper dive back into the JavaScript, this action is checking if the eventName is a valid key, and if it is then it is getting the right array given its key and invoking all the function references. You might have noticed another parameter – “data” - which is not used in this example. Thanks to JavaScript‘s dynamic types, we don’t have to pass in a value for "data“, however, this value can be used to pass anything you want into the action you are invoking. For example, CheckIsValid_Subscribed could have an input parameter and in that case ”data” would be the value passed into that input parameter. This could be a simple string, a whole stringified structure, or even another action reference.
When you trigger the event, all the blocks in the list that have offered up their CheckIsValid_Subscribed action will execute, and then the IncomeTypeIsValid_Handler will add the boolean they send up to the IsValid_List. I told you that at some point this would click ;)
To wrap up this development, we are going to create the final logic for submitting the form. We are going to check if any of the values in IsValid_List are false and if they are then we will not continue submitting the form, because that would mean that one of our IncomeTypes is not valid.
It is now time to publish and open the browser to enjoy the custom validation messages in a list containing blocks containing inputs all inside one form!
Note: The image above has some extra styling that was not covered in the article.
Drawbacks
This could become extremely unwieldy if you were to overuse this. You could have actions calling other sets of actions all behind the scenes. This is why I would encourage you to use this sparingly, name the actions consistently, and be diligent in wrapping the trigger JavaScript nodes in action so you can use the OutSystems feature "find usages" on them. I would suggest that you only use this when you just can’t deliver on that extra special UX/UI with default OS11 functionality since this custom development will need to be extra-supported during future platform upgrades.
Conclusion
As you can validate, this script is not only useful for forms. The idea for this script originally came from finding myself frequently in situations where I have multiple blocks deep in a wizard and realizing that I needed to send an event up through all those blocks to get the wizard to progress. Started to wonder why can't I just send an event right to the great-grandparent, or even a cousin for that matter?!?
You can imagine that you could have a shopping cart that you could add to a cart and in the header where a little counter above the cart icon would go up and down as you add or remove items. This script helps you extend OS11 and provide better UX/UI for your customers.
Comentarios