I spent some time this morning looking for a link or a document about the unique environment Client Scripts which are part of a Scoped Application execute in. I wasn't able to find an official document outlining the details, not in the Wiki, nor in the Developer API Reference, nor in the KB on Hi. Since I implemented most of this functionality, I thought I'd write up a blog post explaining it. We'll also work on official documentation in the correct places, of course.
What are the differences between a scoped Client Script and a global one?
Scoped Client Scripts- Client Scripts which are created as part of a Scoped Application- are executed slightly differently than their non-scoped counterparts. They are wrapped in a closure, and certain APIs that would normally be accessible are either modified, or unavailable altogether. We'll get into the reasons later on, but here is a quick rundown of the differences.
Most of the inaccessible APIs can be re-enabled on a per-application basis. To do so, you need to ship a True/False system property in your application named glide.script.block.client.globals with the value false. As with all system properties in a Scoped Application, the name of the property will automatically have your application's scope name prefixed to it- you don't need to do that yourself. I see access to these global objects as a failure on our part to provide appropriate APIs. Direct DOM manipulation is sometimes the only way to achieve a certain goal, but it's also prone to breaking between releases. Something that worked fine in UI11 may not work in UI15, for example. By tracking which scoped applications needs these globals, we can better target our development efforts on creating or updating APIs to address those needs without resorting to the DOM.
I said most of the inaccessible APIs can be turned back on. There is only one that is not available even with that system property set to false- client-side GlideRecord. This should be considered deprecated. I would like to have removed it completely, but there is a lot of legacy code out there in the wild using it. Client-side GlideRecord has some convenience, but it comes at the cost of large payloads, and the fact that almost everybody using it does so synchronously. Everything client-side GlideRecord does can be done with GlideAjax and an appropriately-written Script Include or processor.
So what about the modified APIs?
The version of GlideAjax that is available to scoped Client Scripts can only make asynchronous requests. Requests made from scoped GlideAjax requests will also take place within the application's scope. You don't need to prefix your scope name on calls to your Script Includes, and other scoped Client Scripts have to respect your access policies.
The scoped version of g_form is slightly more involved. Any calls to getReference must provide a callback function. This makes it an asynchronous action, and doesn't hang the interface while the client is communicating with the server. Scoped applications cannot change the Mandatory, ReadOnly, Display, or Disabled status of fields outside of their scope- with one interesting exception. If your Client Script runs on a table in your own application, and that table extends a table from another scope, the fields you inherit from that base table are considered in-scope when they are displayed on your form.
Consider the difference between adding a field to the Incident table and extending Incident with a new table that has only one field. In the first case, your field is being displayed on Incident. Client Scripts you add to the Incident table can do all the regular things with your field but are limited in what they can do to other fields. You cannot make the Caller mandatory and disable the Category field, because that could negatively affect the functionality of other Client Scripts and UI Policies on the Incident table.
In the second case, where you extend Incident, you are inheriting the fields that are Incident (and in its parent chain), but nothing you do on this table can actually break the functionality of Incident itself. You can set fields to be Mandatory or Disabled or ReadOnly as your requirements dictate, without worrying that you might be breaking some other application. Similarly, fields added onto your table by other scopes (if your Design-time Table Access is set up to allow that) cannot break your application's functionality. If they add Client Scripts to your application, they can't break your carefully-crafted rules about which fields must be filled in by the user before submission, and they can't hold up your user's interaction because they want to make a lot of Ajax calls as part of their functionality.
|setReadOnly/setReadonly||Can only apply to fields in the same scope as the calling script.||Extend the table instead of adding a field to it|
|setMandatory||Can only apply to fields in the same scope as the calling script.|
Extend the table instead of adding a field to it
|setDisabled||Can only apply to fields in the same scope as the calling script.||Extend the table instead of adding a field to it|
|setDisplay||Can only apply to fields in the same scope as the calling script.||Extend the table instead of adding a field to it|
|getReference||Must use a callback||g_form.getReference("ref_field",callback);|
The scoped version of GlideDialogWindow is only marginally interesting. When making calls to GlideDialogWindow, HTML is generated server-side, and then passed down to the client to be displayed. The change to this API is invisible to your application- it merely ensures that the HTML that gets generated is appropriate for your scope. For instance, if you are rendering a form that contains Client Scripts, those Client Scripts are appropriately scoped as well.
You mentioned a closure?
Technically it's an IIFE- an immediately-invoked function expression. We wrap your scoped Client Scripts in a strict-mode IIFE to provide the appropriate environment, and to prevent leaking variables into the global scope. This prevents two Client Scripts from accidentally overwriting one another's variables and functions. This doesn't remove access to everything in the global lexical scope, it just makes sure that variables and functions you declare don't change underneath you because someone else also likes those names. The downside to this approach is with sharing functions between Client Scripts. In the old model, you could close your onChange or onLoad script, define a function, and it would be available to your other Client Scripts because it was part of the global window object. With the new approach, shared functions need to be defined in UI Scripts. The new model for Scoped UI Scripts creates an object in the global window with the same name as your scope, and allows you to add properties to this object which are then available to all your other UI Scripts and Client Scripts, just by accessing the object.
Ok, I realize I'm suddenly talking about UI Scripts, and we seem to have taken a left turn. Trust me, you want to know this.
Scoped UI Scripts have a specific format that we'd prefer applications to adhere to. It's not a requirement because we recognize that UI Scripts serve many purposes, and that "acting as a library of shared functions" is only one of them. But if you adhere to the format that is pre-filled for you when you create your UI Script, you'll be able to add public methods to an object that you control, and can access those methods just by calling scope_name.methodName() in your Client Scripts (assuming the UI Script has been included on the form your script runs on). You can even define new properties on this object from within your Client Scripts, and therefore make them available to your other Client Scripts similar to how you did in the past- without worrying that you are clobbering some other global variable or function, and without worrying someone else is clobbering yours.
That should probably be covered in some Best Practices documents, right?
I agree (not surprising- I wrote that question). In addition to the documentation on Client Scripts, we'll work on some examples that show how we envision all of this being put into practice.
OK, but why (the short version)?
There were 3 things we wanted to address:
- Keep the interface responsive at all times.
- Reduce the likelihood for conflicts between two Scoped Applications running at the same time.
By pushing developers away from direct DOM manipulation and into supported APIs, we are trying to ensure that the application you build for Fuji keeps working on Geneva. If you build an application in UI14, it should work when the user switches to UI15. And when a new interface comes along, the app shouldn't need to worry about it- if it's using the supported APIs.
Most importantly (to me, anyway) is that users aren't negatively affected by applications. Something that they used yesterday still works today- even if an enhancement was added to the Incident form by way of a sweet new application that makes the entire form turn chartreuse when the Incident caller is a VIP, or which automatically searches previous incidents by the same Company for relevant issues so they can get up-to-speed quicker and deliver an answer that is more on-point than ever before. Enhancements and applications should make the experience better, they shouldn't be able to make it worse.
That's a lot of information- what's the short, short version?
Yes, it is. And that's not everything that I could say on this topic- I could probably talk for a couple of hours about it. But that's annoying because I tend to wander and get stuck in esoteric side-channels and bore the heck out of anyone unlucky enough to be within hearing range. So here is the short, short version:
- ASYNC ALL THE THINGS
- DON'T MESS WITH STUFF THAT ISN'T YOURS
- DON'T ACCESS THE DOM DIRECTLY IF YOU CAN HELP IT
- BE EXPLICIT WITH SHARED VARIABLES BY PUTTING THEM IN A UI SCRIPT
- glide.script.block.client.globals IS A WORKAROUND (SOMETIMES)
I hope that was informative.
I have strange and mystical powers.