Dynamic Apps in Lumira 2.1 – the Component API
Lumira 2.1 contains a very powerful feature. While in former Design Studio and Lumira version an app had constant number of components, in 2.1 you can also create and delete components via scripting. This give you – the app developer – much more freedom to react on external factors, e.g. specific data from your data sources or preferences of the user. We have developed it in 2.0 for Discovery: The Discovery tools is mainly a Designer app and required such a feature, e.g. to create data sources, charts and crosstabs on user interaction or to load and unload stories. In version 2.1 you are free to use it, but be warned: while it is a very powerful feature it is also dangerous: if you don’t exactly know what, you are doing, you could make your app slow or create an unsupported state.
Most of the dynamic features are exposed with the new technical component called “COMPONENTS”. Its methods are also called the Components-API.
Creating a Component
Try it out: simply create an App or a Composite and add a “COMPONENTS” technical component. Then start scripting, e.g. adding a button’s ON_CLICK event.
var myNewText = COMPONENTS.createComponent(ComponentType.Text);
myNewText.setText("Hello world");
myNewText.setLeftMargin(100);
myNewText.setTopMargin(100);
What is special with this script expect that you have one component more in your app after it is executed? Maybe the fact that type of the component returned by COMPONENTS.createComponent depends on the argument.
The parameter “type” is an entry from a special enumeration “ComponentType” – which automatically contains all available component types, including SDK components and Composites. The return type is – according to the documentation “GenericComponentBase” – which is only a placeholder for the concrete type specified by the “type” argument. If you hover, you will see that the function really returned a “Text” component – so you can afterward use all script APIs that are available on a Text component.
If you want to create the new component inside of some container component, e.g. in a Panel, pass the container as second argument.
Deleting a Component
If you later want to delete the component again, you should store it in a Global Script variable: Create a global script variable of type “Text” and modify your creation script like this:
gMyNewText = COMPONENTS.createComponent(ComponentType.Text);
gMyNewText.setText("Hello world");
gMyNewText.setLeftMargin(100);
gMyNewText.setTopMargin(100);
Later in another script you can simply delete the new component:
COMPONENTS.deleteComponent(gMyNewText);
Creating and Deleting Multiple Components
In most cases, you will create multiple components in a loop, e.g. using some data as input:
var dims = DS_1.getDimensions();
dims.forEach(function(dim, index) {
var text = COMPONENTS.createComponent(ComponentType.Text);
text.setLeftMargin(20);
text.setTopMargin(20 + index*30);
text.setWidth(200);
text.setText(dim.name + " (" + dim.text + ")");
});
This is fine as long as you don’t need to delete the created components later.
If you need to delete the components, e.g. to create new ones with nested data, there are two typical strategies:
- Keep all created components in a global array
- Create a container, keep it and later delete the container.
Strategy 1
Create a global variable array – best using the base type “Component”, as it allows you to mix components of multiple types:
// Delete all old dynamic components
gaCreatedComponents.forEach(function(oldComp) {
COMPONENTS.deleteComponent(oldComp);
});
// Trick: make gaCreatedComponents empty
gaCreatedComponents = [me]; // add a dummy component
gaCreatedComponents.pop(); // remove the dummy
// Now create new components
var dims = DS_1.getDimensions();
dims.forEach(function(dim, index) {
var text = COMPONENTS.createComponent(ComponentType.Text);
text.setLeftMargin(20);
text.setTopMargin(20 + index*30);
text.setWidth(200);
text.setText(dim.name + " (" + dim.text + ")");
gaCreatedComponents.push(text);
});
Strategy 2
Create a global script variable with a container component, e.g. a Panel.
Now the first thing you do it to create the panel. All other components will go into it. Later you can completely delete the Panel to implicitly delete all other components as well:
if (gPanel != undefined) {
// This will delete the old panel and all old componnents contained in it
COMPONENTS.deleteComponent(gPanel);
}
// Create a new Panel
gPanel = COMPONENTS.createComponent(ComponentType.Panel);
// Now create new components
var dims = DS_1.getDimensions();
dims.forEach(function(dim, index) {
var text = COMPONENTS.createComponent(ComponentType.Text, gPanel);
text.setLeftMargin(20);
text.setTopMargin(20 + index*30);
text.setWidth(200);
text.setText(dim.name + " (" + dim.text + ")");
});
gPanel.setWidth(200);
gPanel.setHeight(dims.length * 30 + 20);
Creating and Deleting Composites
You can also create instances of Composites. This is the way how Discovery loads and unloads Stories. If you create a composite called “COMP_1” in you document (LUMX file), your will find an entry like “LUM_CC762C8C979A2EEEAFE788A0760955D9_COMP1” in your ComponentType enumeration. The long “number” is internal ID for the document in which the composite is contained. For local documents, it is derived from the LUMX file name. For BIP documents it uses the document’s CUID. Composites from the same document as your current APP can be always created. If you want to create instances of a Composite from a different document, you must have at least one Composite from that document statically contained in your app – else we would not know that there is a reference to the document.
What’s next?
In my next blog I will show you another interesting new API for dynamic app: DS.getDataSelections() – which allows you to iterate over result sets. The third blog explains the APIs COMPONENTS.createBinding and COMPONENTS.getBinding, which I skipped here.
I’m looking forward for you feedback, questions, proposals etc.
Sorry, I forgot the pictures in the first version 🙁
Hi Reiner,
This is indeed a great new feature which I think will open up interesting new possibilities. I have the following questions:
Thanks,
Mustafa.
Hello Mustafa,
great that you like it.
Reiner.
Hi Reiner,
Thanks for the feedback. Regarding point 2, you have understood my intent exactly. I'll look forward to the possibility of "design mode" in future then.
One more question: given that Technical components can be included in Bookmarks, if we include a COMPONENTS technical component, can we assume that dynamically instantiated components will be preserved when bookmarks are saved and subsequently re-loaded?
Mustafa.
Hello Mustafa,
the COMPONENTS technical components itself has no state. Saving it into a bookmark would have no effect.
Persisting dynamically created components into a bookmark is tricky: The basic idea with the new 2.x "delta bookmarks is to store only the modified properties into the bookmark and use the rest from the app. As the dynamically created component does not exist in the app, we can't calculate a delta - and end up with a bookmark containing the whole component definition. And this is what we wanted to avoid - because it makes the stored bookmarks fragile.
I think a much better strategy is to keep some additional information in global script variables. Those variables would go into the bookmark. The script should then consider the script variable when it re-creates the dynamic components.
Hi Reiner,
I suspected persisting dynamic components via bookmarked global variables would be the recommended approach, thanks. Since you have mentioned that Discovery is essentially a standalone Designer app, I would be curious to know the technique used when saving stories to persist the story metadata as a Lumira document?
Mustafa.
Hello Mustafa, thanks for the question.
Discovery does not store Bookmarks, but real Composites: It copies a template composite to a new one and loads an instance of the newly created composite. The the composite is modified, either via Design Mode or via COMPONENTS API etc.
Finally the current state is saved directly into the Composite's content.bicomp. The APIs for self-modifying apps or Composite-creators are not yet public.
Regards,
Reiner.
Thanks for the clarification, Reiner. That makes perfect sense now. Are there any plans to make the APIs for self-modifying apps or Composite-creators public in a future release of Lumira?
Regards,
Mustafa.
I'm only an architect and not a product manager 🙂
I must say that is a very impressive new feature
Hi Reiner,
One suggestion for consideration. As you know, since dynamically generated components have no properties, they must be set via scripting. Therefore, for the createComponent() API to be practical and truly effective, very granular scripting should now be provided for setting component properties. So far, only a very basic subset of the standard component properties have been scriptable via setters. To fully support dynamic component creation and configuration, ideally ALL available properties should be scriptable. Some examples are:
Thanks,
Mustafa.
Hello Mustafa,
thanks to remind me that I have not explained the COMPONENT.copyProperties(). This will copy all properties from one component to another one of the same type. You can use e.g. to have an invisible "template chart" where you have configured all common properties. After you have created your new chart, copy the template and set the ones that are specific.
Regards,
Reiner.
Hi Reiner,
Thanks for pointing out the copyProperties() API. However, I think that creating invisible template components is not a very clean approach and could be impractical in certain scenarios such as the following:
So extending the scope of property scriptability would be very beneficial for allowing the development of truly dynamic apps.
Regards,
Mustafa.
Hello Mustafa,
you are right. We also have a generic property modification API in the Component-API. It is used in such cases by Discovery as you describe.
But the syntax is not really nice, therefore we decided to keep it out of the product.
Best regards,
Reiner.
Reiner,
Really excellent feature we now have here... Question, I've tried to see if I can set event handlers for dynamically generated components, such as the Button component having 'On Click' logic. It doesn't appear possible unless I'm missing something.
Is this planned functionality? Workaround thoughts include creating a "wrapper" composite containing just a Button with some generic script maybe.... I tried using the copyProperties technique but it doesn't appear to pass over Event Handler script.
Hello Mike,
hm, copyProperties should also copy scripts - but I didn't test it so far, maybe there is a reason for not copying scripts.
Else indeed the current API wouldn't allow to set scripts dynamically. The discovery app uses the mentioned private API that allow generic property changes to all properties. But this would end up having script that create scripts dynamically - which would be like the evil eval in JavaScript. Such a feature is really a little over complicated for a tool like Designer 🙂 .
Reiner.
Works well and I think it would be most useful together with the item ADAPTIVE_LAYOUT. When inserting generated components into an adaptive layout, the blocks are generated automatically. But I see no option to set the parameters of a block. 3 default columns is just not always right. There are no scriptable parameters for the block. Has anyone tried this?
Hello Beat,
sounds like a valid feature request.
Thanks,
Reiner.
When I saw this functionality and thought a bit about the possibilities, I fell backwards off the chair. This feature is so so great, thank you very much for this. 😀
😉
Hi Rainer,
awesome Feature!
Just wondered about which technical Name the generated components will get? Is it the same as in Lumira Designer PANEL_1, PANEL_2 etc? Or some generated GUIDs?
Regards
Thilo
Hi Reiner,
Thank you for this informative post. I developed custom composites. I have a situation, you may have some opinion about this one.
Firstly my Lumira document is on BI Platform, with Offline datasources. (To be able to schedule)
As I mentioned, created custom composites as a template. Then I used COMPONENTS.createComponent /Copy properties etc. etc. scripts for that custom composites.
Document works perfectly, no script problems or logic problems exists.
Now, I'm scheduling my lumira designer document on BI Platform. Schedule succeed, but when I check on BI Platform scripts doesn't work properly. I see default values.(Feels like internal ID has changed,but actually it doesn't)
So this situation brings a question. Does scheduling affect internal ID/document’s CUID? (Since its uploaded to BI Platform)
Best regards
Burcu
No idea. Schedule in an own phase, but it should not differ too much from e.g. scripts executed during onStartup.
Hi Reiner,
Very good summary! I'm running into some problems when trying to do this "Component" dynamic creation when working with Composites. Specifically, if I have an Event defined within the composite, for which I add code in the main Application. When I copy that Composite in the script, I tried to run:
COMPONENTS.copyProperties(SOURCE_VIZ_CMP, newComp, [CopyPropertiesOptions.KEEP_SCRIPTING, CopyPropertiesOptions.KEEP_CURRENT_VALUES])
The scripts that are inside the Composite (i.e. on buttons within the composite) work fine, but code in the Event script definition is not copied over.
More step by step example: I have a Composite that has a single button and an Interface Event defined called "sendMessage", and the button's script is simply:
COMPOSITE.fireEvent("sendMessage");
Then, in the main Application, I put the Composite onto the Layout, and put a script in the sendMessage Event like APPLICATION.createWarningMessage("Hello World"); When I run this and click the button, it creates the warning message. However, if I then have something in the main Application that creates a new instance of the Composite and copies the properties (including with "KEEP_SCRIPTING"), it does not work.
var comp = COMPONENTS.createComponent( ComponentType.LUM_4DB4E93A0C5743A4EBB807BC522740BB_NEW_COMPOSITE);
COMPONENTS.copyProperties(SINGLE_BTN_COMP, comp, [CopyPropertiesOptions.KEEP_SCRIPTING, CopyPropertiesOptions.KEEP_CURRENT_VALUES]);
After the new button is created, the first button will still through the warning message, but the second button seems to have no script attached to it.
Is this a bug? Is there any kind of workaround you can think of to copy that script?
Hi,
Is there a Chens question ?
I would also be interested to dynamically create Buttons and assign BIAL script to it. As of right now it is not working ??
Or does anybody has a tip for me ?
BR Manfred
This works with template button (having the desired script) and COMPONENTS.copyProperties(). It has several options to configure what you want to copy.