This part of the technical documentation will cover all of the core technologies used within the Framework.
1. The Domain Entity
Having a quality understanding of how to configure domain entities is essential to build other types of configuration offered by the Framework. Furthermore, it is a great place to be considered as an entry point in using the Framework. This chapter will describe the primary concepts and usage behind the domain entity.
1.1. Introduction to the Domain Entity
At it’s core, the domain entity is nothing more than a simple encapsulated Java object. It is the blueprint for the objects that the Framework will create and use, similarly to the way a class declaration is used to create an instance of an object in Java.
Aside from being a straightforward Java bean, the domain entity may have a specific Framework metadeta decorating it’s fields (the Framework refers to these as Config Annotations) which will be used to instruct the Framework on how to handle the domain entities created from this domain entity during certain events.
See the following sample domain entity:
@Domain("person")
@Getter @Setter @ToString
public class Person {
private String firstName;
private String lastName;
}
In this example Person
has been defined as a simple domain entity with getters and setters defined for each of it’s fields.
It may be intuitive that this entity is a data-centric model created solely for the purpose of collecting data; it is. While this example is a simple one, it will be used in the examples to follow to demonstrate the domain modeling feature.
Domain Entity Config Annotations
In the previous example, @Domain
was used to decorate a class declaration so that the Framework used to identify Person
as a domain entity. Domain defines the root or topmost level of a domain entity. The Framework supports other types of domain entity markers as well.
The config annotations that can be used to register domain entities are:
Defining Behavior
There are many different variations of how the Framework can utilize domain entities. One such manner is to define a ListenerType
on the domain entity that provides the Framework with extra instruction on how to handle an instance of it during specific scenarios. Consider the following example:
@Domain(value = "person", includeListeners = { ListenerType.persistence })
@Repo(value = Database.rep_mongodb, cache = Cache.rep_device)
@Getter @Setter @ToString
public class Person {
@Id
private String id;
private String firstName;
private String lastName;
}
This example is very similar to the first, except the decorative annotation @Domain
contains an additional attribute with a value of ListenerType.persistence
and a @Repo
config annotation. This configuration instructs the framework to have the underlying persistence layer perform certain actions on instances of this domain entity during the Framework lifecycle. Consider a use case where there is a need to update the values of a particular entity within the application’s database according to what is stored in an instance of this Person
object. Specifying ListenerType.persistence
as a listener for this domain entity will handle such a scenario.
1.2. Core versus View
A key concept to the Framework in terms on the domain entity is understanding the difference between a core domain entity and a view domain entity.
The Framework is reponsible for handling two primary sets of data in terms of configuration data:
-
The "view" data which tells the framework how to render the graphical user interface.
-
The "core" data that the application creates, reads, updates, and deletes.
Therefore it is essential that it knows how to differentiate between what is core and what is view. At the same time to take full advtange of abstraction capabilities within the Framework, the domain entities are essentially created in the same fasion. How the Framework will interpret between the domain entities is entirely up to how they are configured given the configuration metadata, which will be explained in later sections (see Mapping params and Configuration).
A core domain entity is a domain entity who’s primary responsibility is maintaining the integrity of the data contained within the application.
A view domain entity is a domain entity who’s primary responsibility is maintaining instructions on how the GUI should be rendered.
1.3. Defintion Structure
1.3.1. Simple Domain Entities
The Person
domain entity previously defined is a representation of what is understood to be a simple domain entity.
A simple domain entity is a domain entity who’s field data does not contain other domain entity within it.
In other words, it’s defined fields consist only of simple types.
While not always the case, simple domain entities are typically associated with core domain entities.
1.3.2. Complex Domain Entities
Aligning with the theme of object oriented programming offered by Java, the Framework supports the notion of "nesting" domain entity definition within each other.
A complex entity, or complex object, is a domain entity* that contains another _domain entity declared within it.
Consider the following example of a view domain entity representing a form for the aforementioned Person
:
@Model
@Getter @Setter @ToString
public class VFPersonForm {
@TextBox
private String firstName;
@TextBox
private String lastName;
private Address address;
}
@Model
@Getter @Setter @ToString
public class Address {
@TextBox
private String line1;
...
}
In this example, both VFPersonForm
and Address
are decorated with @Model
. Therefore, VFPersonForm
is a complex domain entity because it contains an instance of Address
.
This scenario contains only one level of nesting between VFPersonForm
and Address
. It is not uncommon to have multiple layers of "nesting" within a domain entity, hence giving rise to the term complex. Consider a common scenario in HTML where a page contains a section, which contains a tab, which contains a component, which contains a… the context of what could be rendered in the view could be boundless. The Framework is able to support this via "nesting" and using these complex domain entities.
While not always the case, complex domain entities are typically associated with view domain entities.
1.3.3. Defining a View
The UI Framework expects a specific pattern in terms of where components are placed. Each of the View Config Annotations has explicitely defined which components are to be placed where.
The same is true for setting up the standard page view. The following configuration is a typical view configuration to get started:
@Domain(value="sample_view1", includeListeners = { ListenerType.websocket })
@Repo(Database.rep_none)
public class VRSampleView1 {
@Page
private VPMain vpMain;
@Model @Getter @Setter
public static class VPMain {
@Tile
private VTMain vtMain;
}
@Model @Getter @Setter
public static class VTMain {
@Section
private VSMain vsMain;
}
@Model @Getter @Setter
public static class VSMain {
// Components here...
}
}
As mentioned, this is the standard view config which can be expanded upon for an application’s specific needs. The primary point to note is the structure of Domain Entity > Page > Tile > Section. Only once this structure has been defined will. For more specific information on what components can go where, please review the View Config Annotations section.
1.4. Loading the Domain Entities
The com.antheminc.oss.nimbus.domain.config.builder
package is reponsible for consuming the domain entity information during the initial loading process. On application startup, the Framework consumes the defined domain entity configurations and stores that information included within them in memory.
The registration process for domain entities starts with a registered Spring bean of type DomainConfigBuilder
. This instance of DomainConfigBuilder
uses the Spring Framework’s scanning capabilities to retrieve Domain
configuration defined within specific packages.
Configuring Base Packages
The packages that are scanned in this manner can and should be defined via the application properties using the standard Spring PropertyPlaceholderConfigurer
mechanism (using application.properties, application.yml, etc.).
The property used to identify packages for registration is: domain.model.basePackages
.
domain:
model:
basePackages:
- com.antheminc.oss.nimbus.entity
- com.acme.app.model.feature_a.core
- com.acme.app.model.feature_a.view
- com.acme.app.model.feature_b.core
- com.acme.app.model.feature_b.view
Note
|
The order of which the entities are loaded may play an impact on certain framework features. It is recommended to always load core domain config prior to view domain config. |
1.5. The Param Interface
Consider that the Framework is acting much like an object oriented programming language in that a domain entity definition is playing the part of a class declaration. The next step in the Framework lifecycle is to use that definition to create an instance, much like the way an object is instantiated in an Objected Oriented Programming language.
This section focuses on the creation and use of the resulting object created by using the configuration consumed from the domain entity. The framework refers to this as the param, created using the Param interface.
This chapter will focus on explaining the Param interface in depth.
1.5.1. Introduction
After the framework registers domain entity definitions during application startup, it is creating what the Framework understands as a Param
. The Param
interface defines a number of properties on it that both the framework and UI framework will use to perform a given operation. Some examples might be, setting the value of the object, setting the object’s visibility, or setting whether or not the object should have validation applied to it (if it is a form element). Whatever the case, the important thing to note is that the Param
interface is collectively the "model" data that is used everywhere throughout the Framework.
While understanding the core concepts of the Param
is not essential for building out the UI of the application, it is important to understand it when dealing with certain functional scenarios. For example, an application can be created using the Framework by simply using the View Config Annotations and Conditional Config Annotations; however, when needing to manipulate the model directly (perhaps in a rule or SpEL expression), having an understanding of Param
will be extremely useful.
1.5.2. Param Path
As previously explained, every Java variable created under a domain entity definition will eventually result in the creation of a param. Each param that is created in this way has a unique path by which it can be identified through using the framework’s Command Query DSL.
Introduction
The path is represented as a URI address that uniquely identifies a param’s location within the Framework. It is built by traversing through a Java class’s members, with a /
denoting a child member (child members are also often referred to as "nested" members).
An example of some param paths can be found in the sample code below:
@Domain("petshop")
@Repo
@Domain(value="petshop", includeListeners = { ListenerType.persistence })
@Repo(alias = "petshop", Database.rep_mongodb)
@Getter @Setter
public class PetShop {
// Param Path: /petshop:1/name
private String name;
// Param Path: /petshop:1/address
private Address address;
// Param Path: /petshop:1/boardedPets
private List<Pet> boardedPets;
// Collection elements are also retrievable by supplying an index
// immediately following member name.
// Collection Element Param Path with index 0:
// /petshop:1/boardedPets/0
// Collection Element Param Path with index 1:
// /petshop:1/boardedPets/1
// Collection Element Param Path with index n (where n is an
// integer >= 0):
// /petshop:1/boardedPets/n
@Model
@Getter @Setter
public static class Address {
// /petshop:1/address/street
private String street;
...
}
@Model
@Getter @Setter
public static class Pet {
// /petshop:1/boardedPets/0/name
// /petshop:1/boardedPets/1/name
// /petshop:1/boardedPets/.../name
private String name;
...
}
}
Please note that in the example above all of the provided param paths are of the form: /petshop:1/…
. The index 1
is known as a RefId, which will be explained in the following section.
RefId
A refId is a unique identifier used to identify a particular instance of domain entity that has been created.
When a domain entity is configured with @Repo
, the refId would be equivalent to the persistence ID used to store the object in the configured database.
Using the refId
The refId is typically created and maintained by the Nimbus framework. It is often used in pathing or Nimbus DSL statements to retrieve data specific to a persisted entity.
refId is configurable in the param path string with the syntax: "{DOMAIN_ALIAS}:{REF_ID}"
. (e.g. /petshop:1
or /petshop:2
)
Relative param paths
While params may be resolved from the root param, it is not uncommon for params to be resolved relative to another param. The relative param is commonly referred to as a context param. This simply means that the path to the resolving param is simply relative from the path of the context param. This is achieved in the framework by using the familiar unix-style pathing syntax. For example, consider the following configuration:
@Model
@Getter @Setter
public static class SectionA {
private String p1; (1)
private Subsection1 subsection1;
@Model
@Getter @Setter
public static class Subsection1 {
private String p2; (2)
}
}
-
Assume the full path for this param is:
"/some_domain/page/view/tile/sectionA/p1"
-
Assume the full path for this param is:
"/some_domain/page/view/tile/sectionA/subsection1/p2"
Using relative pathing, the following examples demonstrate relativity from one param to another:
-
p2
is relative top1
by the path:"/../subsection1/p2"
-
p1
is relative top2
by the path:"/../../p1"
Cross Domain Pathing
Sometimes it may be necessary to change the domain entirely to retrieve the data needed when working with param paths. This is achievable through the use of the param path marker /p
. Simply start a param path with /p
and the root domain context will be changed.
See Reserved Keywords for more details on /p
.
Path Variable Resolver
The Command Query DSL offers certain reserved variables or keywords that can be used in a path. The Framework uses a default implementation of CommandPathVariableResolver
to resolve a param path from a provided from the context of another param.
Property Placeholders
The default implementation of CommandPathVariableResolver
will attempt to first resolve ${…}
placeholders in the given path, replacing them with corresponding property values. Unresolvable placeholders without default value having been set is not supported.
Reserved Keywords
After resolving property placeholders reserved framework keywords will be resolved. The framework provides support for the following reserved keywords:
Keyword | Description |
---|---|
|
The param path marker that denotes the start of a param path. This is only needed to be given when using a full URL or switching param contexts and is only recognized when used as the start of the param path. |
|
Resolves to the path of the param that is mapped to the param identified by the preceeding path. (e.g. |
|
Resolves to the path of the param that is the root domain of the param identified by the preceeding path. (e.g. |
|
Resolves to the elemId of the command param, provided it is a collection element param. Works for mapped params as well. |
|
Uses the Spring |
|
Resolves to the RefId of the |
|
When preceeded with |
|
Resolves to the path of the param in the current context. |
|
Resolves to a string constructed from |
|
Resolves to a query parameter statement of the form: |
1.5.3. Param Subclasses
There are many different types of Param
implementations that are available. Each describes the model as created in the domain entity in such a way that the Framework is able to understand. For specific details about the implementations, see the com.antheminc.oss.nimbus.domain.model.state
package.
@Domain("sample_entity")
@MapsTo.Type(SampleCoreEntity.class)
public class SampleEntity {
// Creates a leaf param
private String leaf;
// Creates a nested param
private NestedEntity nestedEntity;
@Model @Getter @Setter
public static class NestedEntity {
// Creates a leaf param inside of a nested param
private String nestedLeafEntity;
}
// Creates a collection param, and potentially collection element params
private List<NestedEntity> collectionEntity;
// Creates a leaf param and a mapped param
@MapsTo.Path
private String mappedEntity;
}
Refer to SampleEntity.java
above when reviewing the different classifications of Param
below.
Leaf Param
A leaf param is a Param
instance who has no children or nested params. A leaf param is represented by the interface LeafParam
.
The variable leaf
in the SampleEntity.java
domain entity would be constructed by the Framework as a LeafParam
.
Nested Param
A nested param is a Param
instance who has children params. A nested param is represented by the interface Model
.
The variable nestedEntity
in the SampleEntity.java
domain entity would be constructed by the Framework as a Model
.
Collection Param
A collection param is a Param
instance who is defined as an array or collection type. A collection param is represented by the interface ListParam
.
The variable collectionEntity
in the SampleEntity.java
domain entity would be constructed by the Framework as a ListParam
.
Collection Element Param
A collection element param is a Param
instance contained as a child of a ListParam
. It represents a child of a collection in the same sense that Java considers a collection element to belong to a java.util.List
. A collection param element is represented by the interface ListElemParam
.
The variable collectionEntity
in the SampleEntity.java
domain entity would contain 0 or more ListElemParam
instances.
Mapped Param
A mapped param is a Param
instance who’s entity definition has been annotated with @MapsTo.Type
. A mapped param is represented by the interface MappedParam
.
The variable mappedEntity
in the SampleEntity.java
domain entity would be constructed by the Framework as a MappedParam
.
For more information about mapped params and their behaviors, see Mapping params.
Transient Param
TODO
1.5.4. State
The methods getState()
and setState(…)
of Param
are available for retrieving and setting the stored value of a param located at a particular path.
For example, consider working with a param at the domain level using the following Domain Entity definition:
@Domain("sample_object")
public class SampleObject {
private String a;
private NestedObject1 b;
@Model
public static class NestedObject1 {
private String c;
private NestedObject2 d;
}
@Model
public static class NestedObject2 {
private String e;
}
}
Consider also that one domain entity of SampleObject
has been created with RefId = 1, and has been set with the following data:
{
"a": "1",
"b": {
"c": "2",
"d": {
"e": "3"
}
}
}
This means that the framework will have created params for the sample_object
domain entity with RefId = 1, and the user will be able to retrieve that data using the framework’s DSL. The framework DSL to retrieve this item might look like the following:
/p/sample_object:1/_get
The framework retrieves state by recursively iterating over a param’s child params (also commonly referred to as nested params). If thinking of /p/sample_object:1
as the root entity, then using the domain entity definition for SampleObject
, the children entities would look like the following:
-
/p/sample_object:1
-
/p/sample_object:1/a
-
/p/sample_object:1/b
-
/p/sample_object:1/b/c
-
/p/sample_object:1/b/d
-
/p/sample_object:1/b/d/e
Efficiency
For the sake of effiency, it is important to be aware at a high level of how the framework is handling operations regarding state. When working with simple domain entities, such as an individual String or Integer the operation is straightforward. When working at a layer which contains nested paths, caution should be exercised to understand if the get/set state is truly needed at that layer to avoid invoking unnecessary operations.
For the previous example, when setState(…)
is invoked on root domain entity using RefId=1, the framework is actually invoking more than a single setState(…)
. See the diagram below:

This diagram attempts to render a visual representation of how each of the setState(…)
invocations would occur when setting the state of a particular object in JSON. It illustrates that if a user is attempting to execute a setState(…)
call on the object located at /p/sample_object:1
, 6 setState(…)
invocations will occur (See 1-6 in the diagram above).
Consider a scenario where only a particular value within a domain entity is needed to be set. In that case, we can target the unique param for that value by using it’s path. (e.g. update the param at /p/sample_object:1/b/d/e
instead of the entire param at /p/sample_object:1
. See #6 in the diagram above.) This would mean that the framework has avoided the processing of 5 extra setState(…)
calls, when it only needed to execute 1.
1.5.5. Param messages
Messages are available to be set into a param object that can be used in a variety of ways to display additional data to the user. While the rendering of param messages decided by specific attributes provided with a Message
object, the use of the different types of available messages are entirely up to application to decide the manner in which they are used.
Displaying messages using @MessageConditional
The easiest way to display a message is to conditionally apply a message using @MessageConditional
.
Displaying messages manually
When a more manual approach is needed, a Message
object can be set directly into a param object.
// Provided param from some source
Param<?> p = ...;
// Create a new message
Message message = new Message("messageId", 'Some message here', Message.Type.INFO, Message.Context.INLINE, "css-class-1 css-class-2");
// Collect any existing messages available on the param object
Set<Message> messageSet = new HashSet<>();
if (!CollectionUtils.isEmpty(param.getMessages())) {
messageSet.addAll(param.getMessages());
}
// Add the newly created message
messageSet.add(message);
// Set the messages into the param
param.setMessages(messageSet);
Tip
|
Clearly, there are some extra management steps when adding a message manually. Prefer to use @MessageConditional .
|
Displaying a global notification message
Global notification messages in other words are TOAST
messages. There are two kinds of messages that can be set on a Param
. One of them is Context.TOAST
and the other being Context.INLINE
.
@TextBox
@NotNull
@MessageConditional(when="state =='set'", isTransient=true, context=Context.TOAST, message="'Message is set for p1'")
@Label(value = "Test p1")
private String p1;
@TextBox
@NotNull
@Label(value = "Transient p2")
@MessageConditional(when="state !=null", context=Context.TOAST, targetPath="'/.d/'", isTransient=true, message="'Global Message Set")
private String p2;
The above code has two parameters and the @MessageConditional
on 'p1' can be read as follows - When p1 has a value of 'set', a toast message would be shown on the UI.
The toast messages are always shown at the top right corner of the screen and can either be closed on click or would go away after a few seconds.
If there is a use case to show message on navigating between pages - it can be achieved using TOAST
messages where the target path for the message to be shown would be the path to the domain. In the example above, parameter 'p2' would have a message set at the domain if the state is not null.
Displaying inline messages
TODO
Setting the message type
TODO
Preserving messages on successive loads or page navigations
The default behavior for messages is to clear the messages on successive retrievals of the param object. This is intentionally done to support displaying a message only once. If there is a need to preserve the message on successive retrievals (such as when navigating away from a page and back to it), use the isTransient
property as false
.
@Label("Do you like apples?")
@Radio(postEventOnChange = true)
@Values(YesNo.class)
@MessageConditional(when = "state =='Yes'", targetPath = "/../q2", message = "'Apple lovers do not like oranges!'", isTransient = false)
private String q1;
Message message = new Message("messageId", 'Some message here', Message.Type.INFO, Message.Context.INLINE, "css-class-1 css-class-2");
message.setTransient(false)
2. Configuration
The core concept of the Nimbus Framework is that one is able to write the different types of configuration quickly to build an application. Configuration is a high-level term used to describe the code that ultimately drives the logic of the Framework. The topmost and most common type of configuration will typically be found within an application’s domain entities.
Several different means of configuration are available to the application through Java based annotations. As seen in several of the previous chapters, much of the emphasis of configuration is on decorating domain entity variables. The most common types of annotation configs used in an application fall under the following categories:
View Config Annotations |
Instruct the UI to render a specific view component (e.g. textbox, calendar, accordion) |
Core Config Annotations |
Execute functional instructions or commands within the Framework |
Conditional Config Annotations |
Execute common functional instructions based on boolean SpEL expressions |
This chapter will explore and explain the numerous configuration features of the Framework.
2.1. Writing view configuration
This section is intended to explain detailed concepts of writing view configuration. At it’s core, view configuration is nothing but a domain entity… just another object that is provided for the framework to create and manage. The only difference is that it has additional configurative decorators that define behaviors (such as how the UI should render the final view component).
Note
|
When reading this chapter, please note that explicit details for how an individual view configuration annotation is used can be found in the API or in the Appendix. This chapter is more dedicated to common scenarios and larger concepts that need to be explained when writing view configuration. |
2.1.1. Creating a simple view domain
Creating a view domain often follows a common design structure containing the following components:
A simple hello world template can be seen below using this structure:
@Domain(value = "homepage", includeListeners = { ListenerType.websocket })
@Repo(value = Repo.Database.rep_none, cache = Repo.Cache.rep_device)
@ViewRoot(layout = "")
@Getter @Setter
public class VRHomepage {
@Page(defaultPage = true)
private VPMain vpMain;
@Model
@Getter @Setter
public static class VPMain {
@Tile
private VTMain vtMain;
}
@Model
@Getter @Setter
public static class VTMain {
@Section
private VSMain vsMain;
}
@Model
@Getter @Setter
public static class VSMain {
@Label("Hello World!")
@Paragraph
private String hello;
}
}
2.1.2. Navigating between pages
2.1.3. Defining layouts
A layout is just another view domain which acts as a reusable view for other view domains through the use of @ViewRoot
.
@Domain(value="home", includeListeners={ListenerType.websocket})
@Repo(value = Database.rep_none, cache = Cache.rep_device)
@Getter @Setter
public class VLHome {
@Page
private VPHome vpHome;
@Model @Getter @Setter
public static class VPHome {
@Section(Type.HEADER)
private VSHomeHeader vsHomeHeader;
@MenuPanel
private VSGlobalNav vsGlobalNav;
}
@Model @Getter @Setter
public static class VSGlobalNav { ... }
}
The configuration is similar to any view domain with a page that has a section and a menu panel. It is important to note that '@MenuPanel' and '@Section(Type.HEADER)' are supported in a '@Page'. Once the global layout(header, menupanel and footer) is defined, every other subflow could have its own embedded layout. For example, once the user lands on the home page, he/she can be navigated to a new screen that has new header, new side navigation panel in additional to the global header, footer.
So for the above example, a new layout domain needs to be configured and that layout domain alias needs to be configured in the view domain.
Configuring a view domain to use a layout
@ViewRoot
is used to tell the UI framework which layout definition to use by it’s domain alias.
@Domain(value="newviewroot", includeListeners={ListenerType.websocket})
@Repo(value = Database.rep_none, cache = Cache.rep_device)
@ViewRoot(layout = "home")
@Getter @Setter
public class VRNewViewRoot {
...
}
The above code illustrates how to bind the "home"
layout to the "newviewroot"
view domain so that when the view domain is rendered the corresponding layout renders along with it.
Supported components
A layout is nothing more than a simple view domain definition with a specific set of components that the UI framework provides rendering support for:
Note
|
*@Section has a number of Type values that can be provided. See the Section.Type javadocs for more information.
|
Defining the "home" layout
The framework expects that there is a default layout defined with a domain alias "home"
. This would always be rendered for any application that has the global header, footer, etc. This is commonly referred to as the "global layout".
Defining embedded layouts
Once the global layout is defined every other page might need to have it’s own embedded layout. For example, once the user lands on the home page he/she can be navigated to a new screen that has a new header and a new side navigation panel in addition to the components rendered via the global layout.
@Domain(value="innerlayout", includeListeners={ListenerType.websocket})
@Repo(value = Database.rep_none, cache = Cache.rep_device)
@Getter @Setter
public class VLInnerLayout {
...
}
Defining a view with no layout
If no layout is needed, simply give @ViewRoot("")
with an empty value.
@Domain(value="newviewroot", includeListeners={ListenerType.websocket})
@Repo(value = Database.rep_none, cache = Cache.rep_device)
@ViewRoot(layout = "")
@Getter @Setter
public class VRNewViewRoot {
...
}
2.1.4. Working with labels
@Label
is used to display descriptive information for the user in coordination with any valid view annotations.
@Label("Enter a value here: ")
@TextBox
private String p1;
Each view annotation component has it’s own definition for how label related information should be rendered, however most UI components will typically keep the display consistent.
SpEL Support
Spring expression language (SpEL) property replacement is allowed here:
@Label("${env.labels.common.p1}")
@TextBox
private String p1;
Command Path Resolver Support
Path Variable Resolver support is available here:
@Label("Hello <!/../p1!>!")
@TextBox
private String p1;
Adding help text
Additional information about how to use the rendred component can be supplied using the helpText
feature.
@Label(value = "Enter a value here: ", helpText = "Please use MM/YYYY format.")
@TextBox
private String expiration;
2.1.5. Styling components with configuration
TODO
2.1.6. Printing content
The @Button
component can be configured with style = Button.Style.PRINT
. This will instruct the UI on click of the rendered button to open a printable view of content that can easily be printed using the browser’s native print dialog window. The printed content is determined by supplying printPath = "[PARAM_PATH_TO_CONTENT]"
.
@Label("Print Demo")
@Button(style = Button.Style.PRINT, printPath = "/domain/page/tile/section")
private String printButton;
The previous example opens a print dialog with the rendered HTML content resolved from the @Button
attribute: printPath
, or the content rendered from "/domain/page/tile/section"
, in a new window/tab.
Customizing printable content
The rendered printable content can be configured by decorating print button definition with @PrintConfig
. This allows the configurer the ability to give detailed instructions on how the content should be rendered or how the printing experience should behave.
@Label("Print Demo")
@Button(style = Button.Style.PRINT, printPath = "/domain/page/tile/section")
@PrintConfig(useAppStyles = true, closeAfterPrint = false)
private String printButton;
Only certain components are allowed to be "printable", such as container-based elements. See the @PrintConfig
javadocs for more information on which components are able to be printed.
Tip
|
@PrintConfig values may need to be different under different scenarios. In that case, define multiple view config fields (e.g. another @Button ) and conditionally hide or show the print button that performs the desired functionality.
|
Controlling the look and feel of printable content
The default behavior of a clicking on a "print button" will open the rendered content in a new browser tab or page with no additional styles added. Styling can be configured by providing additional details for @PrintConfig
. See the @PrintConfig
javadocs for more details.
2.1.7. UI Error Messages
All uncaught exceptions within the framework are handled by a global exception handler and delivered to the UI to be rendered and displayed to the user. This includes exceptions type that occur from within the framework itself or client managed beans. The WebActionControllerAdvice
class is a good starting point for a deep-dive on how the exception handling response is delivered.
Providing custom messages for an exception
The UI offers some generic customization options for unhandled exceptions. When an unhandled exception makes it’s way to the UI, a default error message as well as a unique identifying id will be displayed to the user. By default, the error message is engineer-friendly but is not likely descriptive to the user.
Custom messages can be generically be defined for an exception type by defining the following in the application.yml
configuration:
application:
exceptions:
com.acme.foo.exceptions.MySpecialException: Some custom message here
java.lang.NullPointerException: Something was null
Reserved exception keywords
There are certain reserved keywords that when used in defining custom exception messages in application.yml
that will be replaced.
Value |
Description |
ERR.UNIQUEID |
The unique generated error id. |
Defining a custom exception handler
The @ExceptionHandler
defined within the framework is a catch all for all types of Throwable
exceptions. Custom logic can be applied by defining a @ExceptionHandler
within a nimbus client application to return a desirable response.
@Autowired
CommandTransactionInterceptor interceptor;
@ExceptionHandler(NullPointerException.class)
@ResponseBody
public ResponseEntity<MultiExecuteOutput> exception(NullPointerException exception) {
HttpHeaders headers = new HttpHeaders();
ExecuteOutput<?> executeOutput = new ExecuteOutput<>();
executeOutput.setExecuteException(new ExecuteError(exception.getClass(), "Some custom message"));
MultiExecuteOutput output = interceptor.handleResponse(executeOutput);
return new ResponseEntity<>(output, headers, HttpStatus.INTERNAL_SERVER_ERROR);
}
Note
|
CommandTransactionInterceptor provides some utility methods for building the expected response the UI expects for a server side error.
|
Suppressing UI error messages
The UI will not render popup error messages if the error message that is returned from the server is null
or empty. Simply set the error message to be empty to suppress a UI error message from being displayed.
application:
exceptions:
# Displays "Some cool exception message"
com.foo.MyException: Some cool exception message
# Does not render!
java.lang.NullPointerException:
2.2. Configuring data source connections
TODO
2.2.1. MongoDB
TODO
2.2.2. External web services
Domain entity values can be persisted and retrieved using an external web service as the backing data source. To achieve this, a REST call must be configured to be sent to a web service that will provide or store the data.
@Domain(value = "ext_client")
@Repo(value = Database.rep_ws, cache = Cache.rep_device)
@Getter @Setter @ToString(callSuper = true)
public class ExtClient { ... }
Notice @Repo(value = Database.rep_ws)
in the code above. This indicates that the framework will treat the domain entity for ExtClient
as having an external web service as it’s data store. One additional configuration for ext.repository.targetUrl.[ALIAS]
in application.yml
is needed to define the entry point to the external web service.
ext:
repository:
targetUrl:
ext_client: https://acoolwebservice.com/
With this configuration given, requests that look like: http://localhost:8080/a/b/p/ext_client/_get
now become: https://acoolwebservice.com/a/b/p/ext_client/_get
.
2.2.3. Custom repository implementations
The framework offers the ability to configure custom repository implementations that allows users to configure anything that is achievable from within the ModelRepository
interface.
To configure a domain entity definition to use a custom model repository implementation, use @Repo
with Database.rep_custom
and modelRepositoryBean
:
@Domain(value = "sample_core", includeListeners = { ListenerType.persistence })
@Repo(value = Database.rep_custom, modelRepositoryBean = "customRepositoryBean", cache = Cache.rep_device)
@Getter @Setter
public class SampleCoreEntity {
...
}
and create a ModelRepository
Spring bean using the qualifier as the value provided for modelRepositoryBean
.
public class ReallyCoolModelRepository implements ModelRepository {
...
}
@Bean("default.customRepositoryBean")
public ModelRepository reallyCoolModelRepository() {
return new ReallyCoolModelRepository();
}
Note
|
Consider using rep_custom when needing to configure different data sources or multiple databases for one or more domain entity definitions.
|
2.3. Mapping params
The section Core versus View explains the difference between a core domain entity and a view domain entity. While a core domain entity is the primary holder of the data, the view domain entity is primarily concerned with the display of the data.
Such a distinction is necessary as view domain entities tend to be hierarchical in nature, whereas core domain entites tend to be more flat by comparison. It also allows for us to separate the view configuration when interacting with the UI rendered components and the core configuration our domain entities use when interacting with the underlying database.
To establish a connection between the core and the view (similar to how an MVC application does), the Framework introduces the concept of mapping the state between Param
instances. This provides for a two-way binding between the state of mapped core and view params in that when the state of the core is updated, the state of the view will also be updated. The same is true when updating the state of a mapped view param, in that the state of the core param would be updated.
2.3.1. Configuring a Mapping
The process for defining a mapping between params is relatively straightforward. See the following example:
@Domain("sample_core", includeListeners = { ListenerType.persistence })
@Repo(value = Database.rep_mongodb, cache = Cache.rep_device)
@Getter @Setter @ToString
public class SampleCoreEntity {
private String name;
}
This class represents the definition of our core domain entity. Params created from this defintion will be mapped to from view Params.
@Domain(value="sample_view1", includeListeners = { ListenerType.websocket })
@Repo(Database.rep_none)
@MapsTo.Type(SampleCoreEntity.class) (1)
public class VRSampleView1 {
// Page/Tile/Section/Form declarations omitted for brevity.
@MapsTo.Type(SampleCoreEntity.class) (2)
@Getter @Setter
public static class SampleForm {
@TextBox
@MapsTo.Path (3)
@Label("Enter name: ")
private String name;
}
}
This class represents the definition of a view domain entity.
Mapping a Type
Providing @MapsTo.Type
gives instruction to the framework that the object structure it decorates should map children parameters to the given class. Notice that both VRSampleView1
and SampleForm
map to SampleCoreEntity.class
.
Note
|
When using @MapsTo.Path , the framework requires that @MapsTo.Type be provided as a decorator to the parent model, or else an InvalidConfigException will be thrown.
|
Mapping a Param
Now that name
is decorated with @MapsTo.Path
, the Framework knows that the textbox component defined at SampleForm.name
is mapped to SampleCoreEntity.name
. Hence the value displayed in the textbox would be equal to the state found in SampleCoreEntity.name
.
2.3.2. Mapping an view entity to a core entity with a different name
A common scenario when writing configuration is that your view domain’s variable names may not match the variable names within the core domain. This is allowable. Using a concept similar to overriding, the mapped field be changed by specifying the path (relative to the core domain root). For example:
@MapsTo.Type(SampleCoreEntity.class)
@Getter @Setter
public static class SampleForm {
@FieldValue
@MapsTo.Path("name") (1)
@Label("Hello: ")
private String personName;
}
-
Since
"name"
is provided to@MapsTo.Path
,personName
would map to thename
in the core domain instead ofpersonName
2.3.3. Mapping with hierarchies
Since @MapsTo.Path
accepts a string containing a relative path from the core root domain object, it is possible to map between hierarchies.
@Domain("sample_core", includeListeners = { ListenerType.persistence })
@Repo(value = Database.rep_mongodb, cache = Cache.rep_device)
@Getter @Setter @ToString
public class SampleCoreEntity {
private Person emergencyContact1;
}
public class Person {
private String name;
private Address address;
}
@MapsTo.Type(SampleCoreEntity.class)
@Getter @Setter
public static class SampleForm {
@FieldValue
@MapsTo.Path("/emergencyContact1/name") (1)
@Label("Hello: ")
private String personName;
}
-
Maps to the value at
name
ofemergencyContact1
declared inSampleCoreEntity
.
2.3.4. Mapping at multiple layers
Since @MapsTo.Type
is given at the class definition level, it is possible to define nested layers of mapping by unlinking the child parameter from the parent’s @MapsTo.Type
using @MapsTo.Path(linked = false)
. For example:
@MapsTo.Type(SampleCoreEntity.class) (1)
@Getter @Setter
public static class SampleSection {
@TextBox
@MapsTo.Path
private String name;
@CardDetail
@MapsTo.Path(linked = false) (2)
private VCD1 vcd1;
// Internal classes
@MapsTo.Type(Person.class) (3)
@Getter @Setter
public static class VCD1 { ... }
}
-
The parent
SampleSection
is mapped toSampleCoreEntity
. -
The child
vcd1
is no longer mapped toSampleCoreEntity
. -
The child
vcd1
is now mapped toPerson
.
2.4. Defining executable logic
2.4.1. Executing @Config statements
A majority of configuration is executed using the @Config
decoration to invoke a Nimbus Command DSL statement.
The WebCommandDispatcher
is responsible for handing off all HTTP
requests to the DefaultCommandExecutorGateway
, which is ultimately responsible for executing any @Config
statements. Hence, any HTTP request with a valid param path in the command DSL would have the statements given in any decorating @Config
annotations executed.
@Domain(value = "dashboard", includeListeners = { ListenerType.websocket })
@Repo(value = Repo.Database.rep_none, cache = Repo.Cache.rep_device)
@Getter @Setter
@ViewRoot(layout = "")
public class VRDashboard {
@Config(url = "/p2/_get") (1)
private String p1;
private String p2;
@Config(url = "/p2/_get") (2)
@Config(url = "/p2/_delete") (3)
private String p3;
@Config(url = "/p3/_get") (4)
private String p4;
}
If the application is up and running and the following HTTP GET call is made:
Then statement <1> would be executed.
The execution of multiple (more than one @Config
decorating a field) @Config
is supported. This is as suggested by making an HTTP GET call to:
This would execute both statements <2> and <3>.
The execution of transitive (a @Config
which invokes another @Config
) @Config
is supported. This is as suggested by making an HTTP GET call to:
This would execute statement <4>, which would then execute both statements <2> and <3>.
Use @Initialize to execute a set of @Configs after page load
@Initialize
simply informs the UI framework to invoke a _get
action call to the parameter that @Initialize
is decorating. Specifically, it works for components also annotated with @Section
and only after the page has rendered. So using @Initialize
is a way to trigger any configs, events, or state changes to execute after the page has rendered.
Consider the following example:
// page, tile omitted for brevity (assume path to this point is: "/sampleview/vpMain/vtMain")
@Section
@Initialize (1)
@Config(url = "some-command-query-dsl-here") (2)
@Config(url = "some-other-command-query-dsl-here") (3)
private VSSampleSection vsSampleSection;
@Model
@Getter @Setter
public static class VSSampleSection { ... }
-
Ensures that the configs 2 and 3 would be executed once the page renders.
2.5. Rule configuration
TODO
2.6. Events
The framework offers a set of hooks (called events
) that can be used to define executable logic to be exected after the event takes place. One such example is any of the conditional event handlers used to drive the conditional annotations.
2.6.1. Predefined events
The following list shows the predefined events available out-of-the-box:
OnParamCreate |
TODO |
OnRootCommandExecute |
TODO |
OnRuntimeStart |
TODO |
OnRuntimeStop |
TODO |
OnSelfCommandExecute |
TODO |
OnStateChange |
TODO |
OnStateLoad |
TODO |
OnStateLoadGet |
TODO |
OnStateLoadNew |
TODO |
Script |
TODO |
OnParamCreate
TODO
OnRootCommandExecute
TODO
OnRuntimeStart
TODO
OnRuntimeStop
TODO
OnSelfCommandExecute
TODO
OnStateChange
TODO
OnStateLoad
TODO
OnStateLoadGet
TODO
OnStateLoadNew
TODO
Script
TODO
2.6.2. Creating a custom event handler
TODO
2.7. Business process model and notation (BPMN)
TODO
2.8. Change log
Any command execution now gets captured in a collection called changelog in mongodb.
Attribute | Type | Description |
---|---|---|
on |
Date |
command config execution date & time |
by |
String |
command config execution by userId |
root |
String |
root entity on which command config is executed |
refId |
Long |
root entity id on which command config is executed |
action |
Action |
action performed during command config execution |
url |
String |
command config complete uri |
path |
String |
path of the param path that is being updated(_replace/_update) as part of command config execution |
value |
Object |
value of the param that is being updated (_replace/_update) as part of command config execution |
2.8.1. Usage Examples
Lets take the example domain named Test with an alias "testdsl". it has a form with a button marked with @Configs.
@Getter @Setter
@Domain(value = "testdsl")
@Repo(Database.rep_mongodb)
@ToString
public class Test extends IdString{
@Ignore
private static final long serialVersionUID = 1L;
private String status;
@Form
private Form form;
@Model
@Getter @Setter
public static class Form {
@Button
@Configs ( {
@Config(url="/p/testdsl2/_new"),
@Config(url="/status/_update?rawPayload=\"Test_Status\"")
})
private String button;
}
}
In above example, when the user (logged user 'testUser') triggers an action by clicking submit button (testdsl has a RefId of 1), below command urls would get invoked in sequence:
-
../p/testdsl:1/form/button/_get?b=$execute
-
../p/testdsl2/_new
-
../p/testdsl:1/status/_update?rawPayalod=\"Test_Status\"
changelog collection would now contain entries for all 3 command executions:
on | by | root | refId | action | url | path | value |
---|---|---|---|---|---|---|---|
current datetime |
testUser |
testdsl |
1 |
_get |
/p/testdsl/form/button/_get?b=$execute |
||
current datetime |
testUser |
testdsl2 |
2 |
_new |
/p/testdsl2/_new |
||
current datetime |
testUser |
testdsl |
1 |
_update |
/p/testdsl/status/_update?rawPayload=\"Test_Status\" |
2.9. Validation
Note
|
Only client side validation is supported today. |
2.9.1. Client side form validation
The Nimbus Framework supports client side validation by offering support for JSR Validation annotations, specifically from the javax.validation.constraints
package. This includes annotations such as @NotNull
, @Min
, @Max
, etc. Simply decorate a field underneath a @Form
with a JSR supported validation and include a @Button
with type Button.Type.submit
to enable client side validation. See the following example:
@Form
private VFSample vfSample;
@MapsTo.Type(Person.class)
@Getter @Setter
public static class VFSample {
@Label("First Name")
@TextBox
@NotNull
@Path
private String firstName;
@Label("Submit")
@Button(style = Button.Style.PRIMARY, type = Button.Type.submit)
@Config(url = "<!#this!>/../_update")
private String submit;
}
This sample would result in firstName
being a required field.
Warning
|
Currently, any view parameter that is expected to use form validation must not have the same variable name as any other field under the form hierarchy. Java restricts duplicate variable names for class, but not within nested classes. This is a current limitation of the UI validation framework. |
2.9.2. Validation Messages
Validation messages that will be displayed to the end-user using javax.validation.constraints
annotations are configurable with respect to the following order:
-
Messages explicitly set in the corresponding
javax.validation.constraints
annotation’smessage
attribute. -
Default messages defined within client jar (application.properties and YAML variants).
-
Default messages defined within the Nimbus Framework
Default Message
A set of default values are provided by the framework for supported annotations.
@Label("Enter a value:")
@TextBox
@NotNull
private String textbox;
The previous code example would display the message Field is required.
, as is the default message for @NotNull
.
Overriding a Default Message
The default validation message for an annotation can be overriden by supplying a key/value definition within the client jar’s application.properties or YAML variants.
Each javax.validation.constraints
annotation has a unique message key set into it’s message
attribute by default, which is used by the framework to determine the appropriate default message value. Simply provide the unique key for the javax.validation.constraints
annotation to replace the default value across the application.
# Default Validaton Message Overrides
javax:
validation:
constraints:
NotNull:
message: Very Important Field!
@Label("Enter a value:")
@TextBox
@NotNull
private String textbox;
This would result in Very Important Field!
being displayed on a validation error.
Set an Explicit Validation Message
To explicitely set a validation message for a field, provide a value for the message
attribute of the javax.validation.constraints
annotation.
@Label("Enter a value:")
@TextBox
@NotNull(message = "Not allowed to be empty!")
private String textbox;
SpEL Property Resolver Support
As SpEL Property Resolver support is included in all annotation attributes, it can also be utilized within the message
attribute.
@Label("Enter a value:")
@TextBox
@NotNull(message = "${sample.form.textbox.notnull}")
private String textbox;
sample.form.textbox.notnull=REQUIRED
3. The Command Query DSL
The Command is the instruction that the Framework understands to execute and come back with an output. It is similar to writing the traditional method calls for an action (such as a button click) to perform business logic, but attempts to introduce a standardized process via the use of a domain specific language (DSL) that the Framework can interpret. This is called the Command Query DSL.
The Command Query DSL is represented in URL format (commonly referred to as the Command URL) and is based on Query DSL.
Apart from the host (typical "host", or provider, of the application utilizing the Framework), the DSL’s Command URL can be broken down into four main subsections of:
-
Client-alias - identification path used to segregate data/logic for one or multiple "clients". Supports clientCode, leaf-org-id, and appAlias
-
Domain-alias - identification path within the framework to locate a defined object, or domain entity
-
Action - instruction used to identify the operation to perform on the object identified by domain-root
-
Query Parameters - A standard set of query parameters
The following diagram shows each of the subsections of the Command Query DSL mentioned above in various examples in an "exploded" view:

In the diagram above, several examples of Command URL’s can be seen. By splitting the URL into it’s individual parts, the diagram represents each of the Command URL’s as a vertical section. Requests sent to the Framework using each of these URLs perform different logic as defined by the Framework. The sections that follow will explain the logic defined for each of the individual subections of the Command URL as well as expain the Command Query DSL in-depth.
3.1. Client Alias
TODO
3.2. Domain Alias
The domain alias (also referred to as path) is the essential and active portion of working with the Command Query DSL. It is what is used to identify which object, or domain entity, the framework should perform a defined action on.
Each domain alias starts with a domain root, which is the highest level in the domain path used for identification purposes. This will correspond with values used in @Domain
. Sometimes a RefId will be provided, to denote a persisted domain root object to retrieve.
From there, the Command Query DSL traverses the Java objects that are domain entities and finds a particular field identified by the remaining path. See The Param Interface for more information.
Using this path in the Command Query DSL can then be used to target a specific Action to perform, which will be discussed in the next section.
3.3. Actions
The Action is the third primary subsection of the Command URL. Action identifies which type of logic or instructions which should be applied to a given domain entity. The domain entity on which the action will be applied is identified by the domain-alias path given in the Command URL preceding the action parameter (see Domain Alias).
A Command URL without an action is not understood and hence not supported by the Framework. Hence, action is required.
See the following example:
@Config(url = "/domainobject/page/tile/section/form/_get?b=$execute")
In the example above, the action is _get. This states that we should perform the instructions defined for _get on the param located at domainobject/page/tilesection/form
.
Each of the sections that follow will describe the logic defined within the Framework for that specific action.
3.3.2. _get
_get: Fetches the instance of the model referenced by the Id
3.3.3. _save
_save: Saves the model into the database
3.3.4. _replace
The action replace
sets the state of the param identified by the _domain alias in the Param Path to a given state. The state to set can be provided in the following manners:
-
provided as a JSON object in the HTTP POST Body
-
provided as a JSON query parameter: rawPayload
In each of these scenarios, the provided JSON should conform to the object identified by the Param Path.
When using the _replace
action, not providing explicit values for fields will result in any existing state within the identified param to be set to null
. In a situation where only some values of the state need to be set and the others preserved, consider using _update.
Note
|
See _replace Examples for examples using _replace .
|
3.3.5. _update
The action update
sets individual fields of the state of the param identified by _domain alias in the Param Path to the fields of a given state.
-
provided as a JSON object in the HTTP POST Body
-
provided as a JSON query parameter: rawPayload
In each of these scenarios, the provided JSON should conform to the object identified by the Param Path.
When using the update action, not providing explicit values for fields will result in any existing state within the identified param to be preserved. In a situation where only the state needs to be explicitly set, consider using _replace.
Note
|
See _update Examples for examples using _update .
|
3.3.6. _delete
_delete: Removes the model from the database
3.3.7. _search
_search: Searches the model based on a search criteria
3.3.8. _process
_process: Executes asigned workflow process or custom handlers
3.3.9. _nav
This action can be used to instruct the UI application to navigate to a specified pageId in the config. When @Domain has more than 1 @Page
annotations configured, this action helps in configuring the required page that the user has to land on performing an action.
@Domain(value = "viewdomain")
@Repo(value = Repo.Database.rep_none, cache=Repo.Cache.rep_device)
@Getter @Setter
@MapsTo.Type(CoreDomain.class)
@ViewRoot(layout = "")
public class VRDomain {
@Page(defaultPage=true)
private Page1 page1;
@Page
private Page2 page2;
@Model
@Getter @Setter
public static class Page1 {
@Config(url = "/page1/_nav?pageId=page2")
@Button
private String navToNewPage;
@Label("Nav Externally")
@Link
@Config(url = "/_nav?redirectUrl=https://mywebsite.com")
private String navExternally;
}
}
In the example above there are two pages configured in a domain with 'page1' being the default page. By configuring defaultPage=true the UI is getting instructions that if there is no navigation pageId specified by the server response, render 'page1' as a default.
The config on button can be read as follows - When the user clicks on the button(navToNewPage), redirect the user to page2.
The navigation can also be external to an application and an example can be seen above with the redirection to https://mywebsite.com
3.4. Query Params
TODO
3.5. Function Handlers
Function Handlers are an abstraction within the framework to execute a common set of instructions for a given Action.
Given an action, a particular function handler can be executed by specifying a value for the query parameter fn
(e.g. /_process?fn=_set
). Together, the action and fn
value define a Function Handler ID, or a unique key by which to identify the function handler. Function handlers use the param resolved by the path preceeding the function handler id as the param context from which to execute logic against. For example:
@Config(url = "/p/patient:<!/.m/id!>/_process?fn=_set")
In the example above, /_process?fn=_set
invokes the Set State Function Handler of the param located at /p/patient:<!/.m/id!>
.
3.5.1. Providing Arguments to a Function Handler
Additional query params can be provided as arguments to a function handler to produce different behavior. For example,
@Config(url = "/p/patient:<!/.m/id!>/name/_process?fn=_set&value=Bob")
Please refer to the specific function handler implementation for more details on which and how parameters are supported.
3.5.2. Predefined Function Handlers
There are several default function handlers defined within the core framework to handle common instructions, such as setting the state of a parameter or adding a parameter value into a collection.
Param |
_get?fn=param |
InitEntity |
_new?fn=_initEntity |
Add |
_process?fn=_add |
BPM |
_process?fn=_bpm |
Filter |
_process?fn=_filter |
Set |
_process?fn=_set |
Set by Rule |
_process?fn=_setByRule |
Update |
_process?fn=_update |
Search by Example |
_search?fn=example |
Search by Lookup |
_search?fn=lookup |
Search by Query |
_search?fn=query |
Param
The param function handler allows for retrieval of a given parameter, as well as the ability to execute logic using the given parameter as a context.
Usage
The param function handler is invoked by ending the command query with "/_get?fn=param"
, using the param resolved by the preceeding path as the context param. Specific implementation details can be found within DefaultParamFunctionHandler
.
@Label("Field 1")
@TextBox
private String field1;
@Label("Click to say hello")
@Button
@Config(url = "<!#this!>/../field1/_get?fn=param&expr=setState('Hello World!')")
private String actionA;
@Label("Counter")
@TextBox
private int counter;
@Config(url = "<!#this!>/../counter/_get?fn=param&expr=setState((null != state ? state : 0) + 1)")
private String actionB;
Supported Query Parameters
Name |
Required |
Description |
expr |
false |
A string SpEL expression to be executed, using the context param as the context |
Use Cases
The param function handler is extremely versatile in that the executor can execute nearly anything that one could craft in a SpEL expression. Beyond simply retrieving a param value, a common scenario for using the get param function handler is to manually invoke one of the methods directly on the given param instance. This may be useful when working with a certain type of param (e.g. a Collection Param or a Transient Param).
Caution
|
A word to the wise: Whatever the case, the ability to execute SpEL expressions at lower layers of the Nimbus Framework is inherently powerful and may introduce problematic or difficult-to-debug behavior. |
InitEntity
The initEntity function handler instantiates the context param with provided values.
Usage
The initEntity function handler is invoked by ending the command query with "/_new?fn=_initEntity"
, using the param resolved by the preceeding path as the context param. Specific implementation details can be found within DefaultActionNewInitEntityFunctionHandler
.
@Label("Note Type")
@ComboBox
@Values(NoteTypes.class)
private String noteType;
@Label("Note Description")
@RichText(readOnly = true)
private String noteDescription;
@Config(url = "/p/notes/_new?fn=_initEntity&target=/noteDescription&json=\"<!/../noteDescription!>\"&target=/noteType&json=\"<!/../noteType!>\"")
private String submitAndAddNew;
Supported Query Parameters
Name |
Required |
Description |
target |
true |
A string representing a relative path of a param to instantiate (from the context param). Used exclusively with the |
json |
true |
A JSON representation of the object to set into the param resolved from the path given in the preceeding |
Add
The add function handler attempts to add a Collection Element Param to a Collection Param.
Usage
The add function handler is invoked by ending the command query with "/_process?fn=_add"
, using the param resolved by the preceeding path as the context param. The context parameter should resolve to a Collection Param. Specific implementation details can be found within AddFunctionHandler
.
Note
|
The add function handler is subclassed from URL Based Function Handler, which means that it shares common behavior amongst some of it’s attributes. See the URL Based Function Handler documentation for more details. |
@Model
@Getter @Setter
public static class VSSection {
private int counter;
private SampleLineItem tmp;
@Label("Sample Grid")
@Grid
private List<SampleLineItem> collection;
@Label("Add an item")
@Button
@Config(url = "<!#this!>/../counter/_get?fn=param&expr=setState((null != state ? state : 0) + 1)")
@Config(url = "<!#this!>/../tmp/_replace?rawPayload={\"name\": \"Item <!/../counter!>\"}")
@Config(url = "<!#this!>/../collection/_process?fn=_add&url=/petcareassessmentview/<!#this!>/../tmp")
private String add;
}
@Model
@Getter @Setter
public static class SampleLineItem {
@Label("Item Name")
@GridColumn
private String name;
}
Supported Query Parameters
Name |
Required |
Description |
value |
false |
A string containing the value to add into the context param. |
url |
false |
A param path of a param containing the desired state to set into the context param. The param path may be either internal or external. |
BPM
The BPM function handler executes a BPM flow.
Usage
The BPM function handler is invoked by ending the command query with "/_process?fn=_bpm"
, using the view root param as the context param for the BPM process. Specific implementation details can be found within StatelessBPMFunctionHanlder
.
@Button
@Config(url = "/_process?fn=_bpm?processId=statelessbpmtest") (1)
private String submit;
-
This assumes that the file
statelessbpmtest.bpmn20.xml
exists in the process-defs directory(src/main/resources/process-defs/
).
Supported Query Parameters
Name |
Required |
Description |
processId |
true |
The name of the BPM process (or file name) to execute |
Filter
The filter function handler attempts to set the state of a given Collection Param to the result set obtained by removing collection elements do not meet a truthful evaluation of a given SpEL predicate.
Note
|
|
Usage
@Domain(value = "sample_view", includeListeners={ ListenerType.websocket })
@MapsTo.Type(SampleCoreEntity.class)
@Repo(Database.rep_none)
@Getter @Setter
public class VRSampleViewRootEntity {
private String[] arr;
private List<String> simple_list;
private List<ComplexObject> complex_list;
@Config(url = "/arr/_process?fn=_filter&expr=contains('pickme')")
@Config(url = "/simple_list/_process?fn=_filter&expr=contains('pickme')")
@Config(url = "/complex_list/_process?fn=_filter&expr=state.p1.contains('pickme')")
private String action_demoFilter;
@Model
@Data
public static class ComplexObject {
private String p1;
}
}
The expected SpEL predicate syntax is slightly different when the action parameter’s collection element type is complex versus when it is not. Non-complex elements will simply expose each collection element’s state to the SpEL expression context, while complex elements will expose the entire Param for each collection element.
@Config(url = "/simple_list/_process?fn=_filter&expr=contains('pickme')")
Since simple_list
has type String
, it is non-complex. Notice in this example that expr
is making use of the String.contains
method. This is because the state of the collection element (type String
) is exposed as the context for the SpEL expression.
@Config(url = "/complex_list/_process?fn=_filter&expr=state.p1.contains('pickme')")
Since complex_list
has type ComplexObject
, it is complex. For complex typed collections, the Param
interface for the collection element parameter is exposed as the context for the SpEL expression, which offers the ability to reference state
. In this way, greater filtering support is available for complex elements.
Supported Query Parameters
Name |
Required |
Description |
expr |
true |
A SpEL expression to evaluate to determine whether or not a collection element should be included in the final result set. |
targetParam |
false |
The path to the param to set the final result set into. The type of the |
Set
The set function handler sets the state of the context param to the provided state.
Usage
The set function handler is invoked by ending the command query with "/_process?fn=_set"
, using the param resolved by the preceeding path as the context param. Specific implementation details can be found within SetFunctionHandler
.
Note
|
The set function handler is subclassed from URL Based Function Handler, which means that it shares common behavior amongst some of it’s attributes. See the URL Based Function Handler documentation for more details. |
@Label("Field 1")
@TextBox
private String field1;
@Label("Click to say hello")
@Button
@Config(url = "<!#this!>/../field1/_process?fn=_set&value=Hello World!")
private String actionA;
Supported Query Parameters
Name |
Required |
Description |
value |
false |
A string containing the state to set into the context param. |
url |
false |
A param path of a param containing the desired state to set into the context param. The param path may be either internal or external. |
Set by Rule
The set by rule function handler executes a given rule file from the perspective of a context param.
Usage
The set by rule function handler is invoked by ending the command query with "/_process?fn=_setByRule"
, using the param resolved by the preceeding path as the context param. Specific implementation details can be found within SetByRuleFunctionalHandler
.
@Model
@Getter @Setter
public static class VFForm {
...
@Button
@Config(url = "/../_process?fn=_setByRule?rule=sample_rule") (1)
private String submit;
}
-
This assumes that the file
sample_rule.drl
exists in thesrc/main/resources
directory.
Supported Query Parameters
Name |
Required |
Description |
rule |
true |
The path to the rule file, relative to |
Update
The update function handler adds a Collection Element Param to a Collection Param.
Usage
The update function handler is invoked by ending the command query with "/_process?fn=_update"
, using the param resolved by the preceeding path as the context param. Specific implementation details can be found within UpdateFunctionHandler
.
Note
|
The update function handler is subclassed from URL Based Function Handler, which means that it shares common behavior amongst some of it’s attributes. See the URL Based Function Handler documentation for more details. |
@Model
@Getter @Setter
public static class VSSection {
private int counter;
private SampleLineItem tmp;
@Label("Sample Grid")
@Grid
private List<SampleLineItem> collection;
@Label("Add an item")
@Button
@Config(url = "<!#this!>/../counter/_get?fn=param&expr=setState((null != state ? state : 0) + 1)")
@Config(url = "<!#this!>/../tmp/_replace?rawPayload={\"name\": \"Item <!/../counter!>\"}")
@Config(url = "<!#this!>/../collection/_process?fn=_update&url=/petcareassessmentview/<!#this!>/../tmp")
private String add;
}
@Model
@Getter @Setter
public static class SampleLineItem {
@Label("Item Name")
@GridColumn
private String name;
}
Supported Query Parameters
Name |
Required |
Description |
value |
false |
A string containing the value to add into the context param. |
url |
false |
A param path of a param containing the desired state to set into the context param. The param path may be either internal or external. |
Search by Example
The search by example function handler performs a search over the targeted domain entity by using the provided param state as search criteria.
Usage
The search by example function handler is invoked by ending the command query with "/_search?fn=example"
, using the preceeding path as the domain entity over which to search. Specific implementation details can be found within DefaultSearchFunctionHandlerExample
.
@Model
@Getter @Setter
public static class VFForm {
@Label("Search Term")
@TextBox(postEventOnChange = true)
private String searchTerm;
@Label("Test Search Results")
@Button
@Config(url = "/p/person/_search?fn=example&rawPayload={\"firstName\": \"<!/../searchTerm!>\"}") (1)
private String submit;
}
-
The config will execute a search within the database that returns a result set of all documents having a
firstName
matching the state ofsearchTerm
.
Note
|
The observant will note that we aren’t actually doing anything with the data in this particular example. This is intentional, as it is a simplified example. |
Supported Query Parameters
Name |
Required |
Description |
aggregate |
false |
An aggregate query (if applicable) |
page |
false |
An integer representing the starting page number |
pageSize |
false |
An integer for the number of elements to display per page |
projection.alias |
false |
A string representing the domain entity id used to determine the resulting object type |
rawPayload |
false |
A JSON String object that provides search criteria. |
sortBy |
false |
A |
Setting example search criteria
The search criteria used to perform an example search can be provided by passing an example object. The example object is given as an argument (rawPayload
) that matches the type of the object being searched. In other words, if searching over a core domain entity with an alias of person
and that domain entity is built from a Person
Java object, then rawPayload
should also be of the type Person
.
All of the data contained within the example object is then used by the repository implementation to obtain a list of search results, matching results similarly to how the where
clause of a typical database query would.
Note
|
Wherever possible, the searching features will be delegated to the native repository implementation. For example, the default repository implementation (MongoDB) simply passes the example criteria into MongoDB and lets MongoDB do what it does best. |
Providing rawPayload
One way to provide rawPayload
is to simply pass a JSON string as a query parameter in the command query. This is seen in the example above. Using this method, it is possible to provide dynamic values using the Path Variable Resolver features.
Tip
|
The rawPayload that is specified here as a query parameter is actually much more complex in how it is set. Consequently, there are other ways to provide it to a function handler. For more understanding on how to provide rawPayload to a command query, see Sending a payload with a Command
|
Search by Lookup
The search by lookup function handler performs a search over the targeted domain entity by using the search criteria provided as query parameters.
Usage
The search by lookup function handler is invoked by ending the command query with "/_search?fn=lookup"
, using the preceeding path as the domain entity over which to search. Specific implementation details can be found within DefaultSearchFunctionHandlerLookup
.
@Label("Choose a Person")
@Values(url = "/p/person/_search?fn=lookup&projection.mapsTo=code:id,label:name")
@ComboBox(postEventOnChange = true)
private Long chosenPersonId;
@Label("Choose a Woman")
@Values(url = "/p/person/_search?fn=lookup&where=person.sex.eq('female')&projection.mapsTo=code:id,label:name")
@ComboBox(postEventOnChange = true)
private Long chosenWomanId;
@Label("Choose a Gender")
@Values(url = "/p/staticCodeValue/_search?fn=lookup&where=staticCodeValue.paramCode.eq('/gender')")
@ComboBox
private String chosenGender;
Supported Query Parameters
Name | Required | Description |
---|---|---|
|
false |
A QueryDSL filter statement used to determine whether or not a record of data should be contained in the final result set. |
|
false |
A string containing the name of the field by which to group and direction to sort the result set by. (e.g. |
|
false |
An integer value representing the total number of records that should be returned. |
|
false |
An aggregate query (if applicable) |
|
false |
A key/value comma-separated string with the keys being the fields of |
Sorting the result set is achievable at different layers, using in memory sorting or the native repository sorting provided by a database/repository implementation.
In Memory Sorting
Sorting (ascending or descending) happens in memory using the Collections' comparator
interface on the property configured with orderby
clause (for staticCodeValue
as well as dynamic domain entities), only when the orderBy
clause does not start with domain entity alias.
"/p/person/_search?fn=lookup&where=person.sex.eq('female')&orderBy=name.asc()"
would perform an in memory sort of all the females by their name
field in ascending order, because the orderBy.asc()
value is name.asc()
.
A sortThreshold
is defined for sorting to prevent potential memory leaks. If sorting is attempted on a collection with size greater than the configured integer sortThreshold
, the sorting will simply be skipped.
The sortThreshold
can be configured by configuring the property search.lookup.inMemory.sortThreshold
(default: 500
) with an integer
value in application.yml
.
In some cases, it may be desirable to throw an exception when the sorting threshold has been violated. Nimbus supports this natively by configuring the property search.lookup.inMemory.exceptionIfOverSortThreshold
(default: false
) in application.yml
. Setting this value to true
will result in a FrameworkRuntimeException
when attempting to sort such a collection.
Repository Sorting
When the orderby
clause starts with the domain alias, the sorting will occur inside the configured model repository’s query execution. In other words, since the default configured model repository uses MongoDB, the sorting would occur within the MongoDB query execution layer.
"/p/person/_search?fn=lookup&where=person.sex.eq('female')&orderBy=person.name.asc()"
would perform a sort within MongoDB of all the females by their name
field in ascending order, because the orderBy.asc()
value is person.name.asc()
.
Special Use Case: staticCodeValue
The domain staticCodeValue
is a special use case within the Nimbus Framework.
Search by Query
The search by query function handler performs a search over the targeted domain entity by using the provided query data.
Usage
The search by query function handler is invoked by ending the command query with "/_search?fn=query"
, using the preceeding path as the domain entity over which to search. Specific implementation details can be found within DefaultSearchFunctionHandlerQuery
.
@Label("Pets")
@Grid(onLoad = true)
@Path(linked = false)
@Config(url = "<!#this!>/.m/_process?fn=_set&url=/p/pet/_search?fn=query&where=pet.ownerId.eq(<!/../.m/id!>)") (1)
private List<PetLineItemOwnerLanding> pets;
-
The right hand side of this query performs the search by query, with a where clause that returns a result set of all objects in the
pet
table that have anownerId
matching the resolved value of<!/../.m/id!>
.
Supported Query Parameters
Name |
Required |
Description |
aggregate |
false |
An aggregate query (if applicable) |
|
false |
An integer value representing the total number of records that should be returned. |
|
false |
A string containing the name of the field by which to group and direction to sort the result set by. (e.g. |
page |
false |
An integer representing the starting page number |
pageSize |
false |
An integer for the number of elements to display per page |
projection.alias |
false |
TODO |
projection.mapsTo |
false |
TODO |
sortBy |
false |
A |
where |
false |
A query dsl statement that contains the desired matching conditions. This clause should always begin with the domain alias |
3.5.3. Custom Function Handlers
The Nimbus framework provides the ability for developers to define custom function handlers.
Defining custom function handler logic
The logic for a function handler resides in the abstract execute
method of a FunctionHandler
implementation.
public class CustomFunctionHandler<T, S> implements FunctionHandler<T, S> {
@Override
public S execute(ExecutionContext eCtx, Param<T> actionParameter) {
// Add custom function handler logic here
return null;
}
}
Defining a custom function handler bean
The implementation must be constructed as a Spring bean and injected into the application context to be recognized by the framework.
@Configuration
public class PetClinicExtensionConfig {
@Bean(name="petclinic._process$execute?fn=_custom")
public CustomFunctionHandler<?, ?> customFunctionHandler(BeanResolverStrategy beanResolver) {
return new CustomFunctionHandler<>(beanResolver);
}
}
Bean naming conventions for function handlers
The naming convention for all function handlers must follow the pattern:
<app-id>.<action><behavior>?fn=<function-handler-name>
-
default._process$execute?fn=_setByRule
-
default._get$execute?fn=_custom
The <app-id>
of default
is used for all default implementations within the framework. Custom implementations can and should use their own unique <app-id>
(e.g. petclinic
). All function handler implementations (including default
) are able to be overridden by simply overriding the desired Spring bean.
Invoking custom function handlers
Invoking a custom function handler is no different than invoking any other function handler. Simply provide the Function Handler ID as the final part of the command query.
@Model @Getter @Setter public static class VFForm { @Button @Config(url = "/page/tile/section/form/_process?fn=_custom") (1) private String submit; }
-
The
CustomFunctionHandler
logic would be invoked withactionParameter
resolving to the param located at"/page/tile/section/form"
.
3.5.4. URL Based Function Handler
URL Based Function handlers are function handlers that perform an operation over a single provided value.
Usage
The URL Based Function Handler is an abstract support class, extended by several other function handlers. It is not invoked directly. As it is expected to be fairly common across function handlers, it’s details are documented here.
Supported Query Parameters
Name |
Required |
Description |
value |
false |
A string containing the state to perform an operation over |
url |
false |
A param path of a param containing the desired state to perform an overation over. The param path may be either internal or external. |
Providing the state to add
The hierarchy of how the state is determined and passed to subclasses is decided by URLBasedAssignmentFunctionHandler
and is prioritized as follows:
-
value
attribute -
url
attribute
3.6. Sending a payload with a Command
The rawPayload
attribute of Command
is used in several different client facing implementations to input data into Nimbus for processing. Primarily used as an argument, it is a unversal o
3.6.1. Send rawPayload as a query parameter
Simply provide rawPayload
as a query parameter with a JSON string value representing the desired value to provide within the command query statement.
3.6.2. Send rawPayload as HTTP Payload content
The WebActionController
offers native payload support for some HTTP operations such as POST
or PUT
. One such example can be seen with the following button implementation
@Form
private VFForm vfForm;
@MapsTo.Type(Person.class)
@Getter @Setter
public static class VFForm {
@Label("Search Term")
@TextBox
@Path("/firstName")
private String searchTerm;
@Button(type = Button.Type.submit) (1)
@Config(url = "<!#this!>/../_replace") (2)
private String submit;
}
-
On click of this button on the UI, the browser will send an HTTP
POST
with a JSON payload representing the form data. TheWebActionController
knows to convert this data into therawPayload
, which can then be forwarded on to subsequent commands. -
The command generated by this query will have it’s
rawPayload
set and consequently will use the _replace behavior to set the state ofvfForm
to it.
Note
|
The submit button behavior occurs since the default UI behavior for Button.Type.submit will post it’s parent’s encapsulated form data.
|
See the WebActionController
implementation for more details.
3.6.3. Priority of how rawPayload is set
The priority of how rawPayload
is used in the search by example handler is:
-
Query param
-
HTTP payload
4. Extensions
This section explains instructions regarding popular extensions and hooks that the framework exposes that a developer might be interested in using. The examples in this will typically be quite complex and expect the reader to have a thorough understanding of the Nimbus Framework before attempting to implement these features.
4.1. Importing data files to an entity
The framework provides support for the ability to upload data files and directly map the contained data into an associated a Nimbus configured Java object. The framework exposes a FileImportGateway
object that can be used to process the data file uploaded alongside the traditional HttpServletRequest
.
In this way, clients can configure their application to allow a user to upload a data file and process its contents into a framework domain entity, allowing any rules or process flows to execute over it.
4.1.1. Exposing a controller to capture the uploaded data file
The framework does not expose a native controller to handle file uploads, instead allowing the client to provide it’s own implementation. A sample can be seen below:
@Autowired
private FileImportGateway fileImportGateway;
@RequestMapping(value = "/**/p/event/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = "application/json", method = RequestMethod.POST)
public Object handleUpload(HttpServletRequest req, @RequestParam("pfu") MultipartFile file, @RequestParam("domain") String domain) {
Object obj = fileImportGateway.doImport(req, domain, file);
Holder<Object> output = new Holder<>(obj);
return output;
}
4.1.2. Supported file types
The default implementation DefaultFileImportGateway
provides support for the following data types:
CSV
TabularDataFileImporter
handles all .csv
typed files.
-
Configurable Parser/Conversion properties exposed via query parameters.
Excel
ExcelFileImporter
handles importing .xls
and .xlsx
file types.
-
Configurable Parser/Conversion Properties exposed via query parameters.
-
Configure excel specific configurable properties with
ExcelParserSettings
Importing other types of files
Other types of files can be imported by the configured FileImportGateway
by providing custom Importer
implementations.
public class SpecialFileImporter implements Importer {
@Override
public <T> void doImport(Command command, InputStream stream) {
...
}
@Override
public boolean supports(String extension) {
return ".special".equals(extension);
}
}
@Bean
public SpecialFileImporter specialFileImporter() {
return new SpecialFileImporter();
}
4.1.3. Configurable Parser/Conversion Properties
The framework uses Univocity to parse data files. Univocity offers a number of configurable properties, such as the ability to define a title row. These configurable properties are exposed via the command query dsl, as below:
/**/event/upload?domain=mypojo&PROPERTY_KEY=VALUE
4.1.4. Error Handling
Configure which type of error handling strategy should be used through the command URL:
**/event/upload?domain=mypojo&errors=STRICT
-
Types
-
SILENT
(default): Silently continues processing when an error parsing row data occurs -
STRICT
: Rethrows the thrown data parsing exception when an error parsing row data occurs
-
More specific error handling may be determined by the specific Importer
implementation.
4.1.5. POJO Writing Strategies
Configure which type of writing strategy should be used when converting a record of data into a POJO through the command URL:
**/event/upload?domain=mypojo&writeStrategy=MODEL_REPOSITORY
-
Types
-
COMMAND_DSL
(default): Use Command DSL’s_new
implementation to write each record of row data processed -
MODEL_REPOSITORY
: Use the provided domain’sModelRepository
implementation to write each record of row data processed
-
5. Appendix
5.1. Config Annotations
This section contains instructional reference material for each of the config annotations available within the Framework. Use this section when needing to identify specific features of a config annotation.
For more information about how config annotations are applied within the framework, see Configuration.
5.1.1. Conditional Config Annotations
This section covers the conditional behavior that can be applied to params. In this context, conditional behavior refers to performing a set of actions based on a given condition. The condition is evaluated by using the powerful capabilities of SpEL (Spring Expression Language).
Typically this conditional behavior is evaluated from the context of the conditionally decorated param (the param that is annotated
with a conditional annotation), meaning that the condition will inspect the decorated param object to infer if the condition
should be true
or false
.
The following example shows what a conditional configuration might look like:
@SampleConditional(when = "state == 'YES'")
@Radio(postEventOnChange = true)
@Values(YesNo.class)
private String question;
The when
condition is effectively stating, when question.getState().equals("YES")
. In this case, since question
is the decorated
param, we are evaluating from the context of question
. This is why state
is understood in the SpEL condition state == 'YES'
.
Note
|
In this scenario @SampleConditional is not a real annotation understood by the framework, but it illustrates the idea that when
this particular condition is true , we should perform some sort of behavior. We defer that logic to State Event Handlers.
|
AccessConditional
@AccessConditional
allows to control the access of the specific area/functionality within the application based on User’s assigned role(s) and permission(s).
The logic for @AccessConditional
can be found in AccessConditionalStateEventHandler
. The handler logic will be executed during the following framework events:
-
OnStateLoad
Example:
@AccessConditional(whenAuthorities="?[#this == 'entity_assign'].empty", p=Permission.HIDDEN)
private Section_EntityAssignment vsEntityAssignment;
Above configuration would hide the entity assignment section for user(s) who do not have access to "entity_assign" action.
ActivateConditional
@ActivateConditional
is an extension capability provided by the framework. The annotation is used to conditionally activate/deactivate the param based on a SpEL condition.
To dectivate a param means to mark the param the enabled
and visible
properties as well as nullify the state of the target param.
Tip
|
It is common to misuse @ActivateConditional when @EnableConditional or @EnableConditional would be a better choice. While this annotation is similar to using @EnableConditional and/or @VisibleConditional , it should be noted that @ActivateConditional nullify the state of the target param.
|
The logic for @ActivateConditional
can be found in ActivateConditionalStateEventHandler
. The handler logic will be executed during the following framework events:
-
OnStateLoad
-
OnStateChange
@CheckBox(postEventOnChange=true)
@ActivateConditional(when="state != 'true'", targetPath="/../sectionG_skip")
private String skipSectionG;
private sectionG_Skip sectionG_skip;
@Radio(postEventOnChange=true)
@Model.Param.Values(value=YNType.class)
@ActivateConditional(when="state == 'No'", targetPath="/../q3Level2")
private String answerYesOrNo;
private Q3 q3Level2;
@CheckBoxGroup(postEventOnChange=true)
@Model.Param.Values(value=Days.class)
@ActivateConditional(when="state == new String[]{'Sunday'}",targetPath="/../../../sundayDeliverySection")
private String[] deliveryDays;
/*Check if selection contains one or more specific values*/
@CheckBoxGroup(postEventOnChange=true)
@Model.Param.Values(value=Days.class)
@ActivateConditional(when="state != null && state.?[#this=='Sunday'] != new String[]{}",targetPath="/../../../sundayDeliverySection")
private String[] deliveryDays;
@CheckBoxGroup(postEventOnChange=true)
@Model.Param.Values(value=VisitCount.class)
@ActivateConditional(when="state != null && state.length > 2",targetPath="/../../section")
private String[] visits;
ConfigConditional
@ConfigConditional
is an extension capability provided by the framework. The annotation is used to conditionally execute @Config
calls based on a SpEL based condition.
The logic for @ConfigConditional
can be found in ConfigConditionalStateChangeHandler
. The handler logic will be executed during the following framework events:
-
OnStateChange
@ConfigConditional(
when = "state == 'Completed'", config = {
@Config(url="<!#this!>/../state/_update?rawpayload=\"Closed\""),
@Config(url="/p/dashboard/_get")
})
private String status;
Note
|
In the above example , whenever there is statechange of status and the status is changed to Completed, the Configs will be executed. |
EnableConditional
@EnableConditional
is an extension capability provided by the framework. The annotation is used to conditionally activate/deactivate the param based on a SpEL condition.
The logic for @ConfigConditional
can be found in ConfigConditionalStateChangeHandler
. The handler logic will be executed during the following framework events:
-
OnStateChange
@EnableConditional(when="state == 'hooli'", targetPath="../enable_p2")
private String enable_p1;
private String enable_p2;
ExpressionConditional
@ExpressionConditional
is a very versatile conditional annotation that can be used to apply any logic that is able
to be crafted using SpEL expressions.
The logic for @ExpressionConditional
can be found in ExpressionConditionalStateEventHandler
. The handler logic will be executed during the following framework events:
-
OnStateLoad
-
OnStateChange
@ExpressionConditional(when="null == state && onLoad()", then="setState(T(java.time.LocalDate).now())")
private LocalDate initDateOnLoad;
Caution
|
The observant will notice that @ExpressionConditional is versatile in that it could be used to perform the
same logic achieved by several other framework defined conditional annotations. Ideally it should not be used when
another conditional annotation is available to achieve the same behavor.
|
MessageConditional
@MessageConditional
is a conditional annotation that allows the ability to conditionally assign of one or more Message
objects to a param.
The logic for @MessageConditional
can be found in MessageConditionalHandler
. The handler logic will be executed during the following framework events:
-
OnStateLoad
-
OnStateChange
@Label("Do you like apples?")
@Radio(postEventOnChange = true)
@Values(YesNo.class)
@MessageConditional(when = "state =='Yes'", targetPath = "/../q2", message = "'Apple lovers do not like oranges!'")
private String q1;
@Label("Do you like oranges?")
@TextBox
private String q2;
ValidateConditional
@ValidateConditional
is used to conditionally set validations that should appear for a param based on a SpEL condition. This
annotation can be triggered for multiple conditions, if necessary.
The logic for @ValidateConditional
can be found in ValidateConditionalStateEventHandler
. The handler logic will be executed during the following framework events:
-
OnStateLoad
-
OnStateChange
@ValidateConditional
works by first evaluating the when
attribute by means of a SpEL condition. When the when
condition is true
, the
framework will attempt to identify a subset of params with Validation constraints and apply validation logic to those params. See the
following example:
@ValidateConditional(when = "state == 'dog'", targetGroup = GROUP_1.class)
@TextBox(postEventOnChange = true)
private String petType;
@NotNull
@TextBox
private String petName;
@NotNull(groups = { GROUP_1.class })
@TextBox
private String dogFood;
@NotNull(groups = { GROUP_2.class })
@TextBox
private String catFood;
In the above example, assuming the state of petType
is "dog"
, the validations that will be applied are: petName
and dogFood
.
-
petName
- There are no groups associated withpetName
, hence it is seen as a static validation and always applied. -
dogFood
-GROUP_1.class
is present within the@NotNull’s `groups
attribute, and this is matchingpetType’s `@ValidateConditional’s `targetGroup
attribute(GROUP_1.class
) and is applied.
The validations that will not be applied are: catFood
.
-
catFood - While catFood has an entry in
@NotNull’s `groups
attribute(GROUP_2.class
), it is not matching@ValidateConditional’s `targetGroup
attribute(GROUP_1.class
).
As seen in the previous example, there may be many params decorated with javax.validation.constraints. The framework needs to uniquely
identify which validations should be applied when the when
condition is true. To handle this, the groups
attribute will be used as
it is supplied by of all javax.validation.constraints annotation classes. The final subset of params where validations will be applied
will be composed of only those whos groups
attribute contains the @ValidateConditional
targetGroup
attribute.
@ValidateConditional’s `targetGroup
parameter is simply a marker interface of type ValidationGroup
to be used for identification
purposes by the framework to identify the subset of params.
Note
|
Use
|
In the previous example, we used targetGroup = GROUP_1.class
. GROUP_1.class
is an identity class that implements the
ValidationGroup
interface and is used by the framework to identify javax.validation.constraints annotatons that should be applied.
Any implementation that implements the ValidationGroup interface may be used as a group identity class. For convenience, a set of
identify class implementations have been defined within @ValidateConditional
as GROUP_X.class
, where 0 <= X <= 29.
Note
|
If additional marker classes are needed, simply create a new implementation of ValidationGroup and use that class in the targetGroup
attribute as well as the corresponding param’s javax.validation.constraints annotation.
|
The framework has provides the ability to define a ValidationScope
as part of @ValidateConditional’s `scope
to provide control over which params should be considered for conditional validation. The following scopes are available:
Name | Description |
---|---|
SIBLING |
Applies validations to sibling params relative to the target param. |
CHILDREN |
Applies validations to children params relative to the target param. |
Note
|
Scopes are recursive in that all nested params underneath params located in the scopes identified above would also have conditional validations applied. |
There may be a need to apply conditional validations to one or many different paths based on some when
condition decorating a single param. The attribute targetPath
can be used to target one or more paths to apply conditional validation to when a corresponding when
condition is true
. Consider the scenario below:
@Model @Getter @Setter
public static class Section1 {
@NotNull(groups = { GROUP_1.class })
@Calendar
private String dateOfBirth;
...
}
@Model @Getter @Setter
public static class Section2 {
@NotNull(groups = { GROUP_1.class })
@Calendar
private String neuterDate;
...
}
@ValidateConditional(when = "state == 'dog'", targetGroup = GROUP_1.class, targetPath = { "../section1", "../section2" }, scope = ValidationScope.CHILDREN)
@TextBox(postEventOnChange = true)
private String petType;
private Section1 section1;
private Section2 section2;
In this scenario, the configuration for petType
mandates that both section1
and section2
would be targetted and all children params of these sections (recursive) would have conditional validation applied if GROUP_1.class
is present on the Constraint
annotation.
In this case, both dateOfBirth
and neuterDate
would have conditional validation applied.
Note
|
Similar behavior could have been achieved by omitting the targetPath attribute and using ValidationScope.SIBLING . This was simply an example to portrait the flexibility of targetPath and scope .
|
ValuesConditional
@ValuesConditional
is an extension capability provided by the framework. The annotation is used to conditionally set the @Values
configuration for a dependent field based on a SpEL condition.
The logic for @ValuesConditional
can be found in ValuesConditionalStateEventHandler
. The handler logic will be executed during the following framework events:
-
OnStateLoad
-
OnStateChange
Consider the following sample defined Values:
// Sample Values Implementations
public static class SRC_FOODS_ALL implements Source {
@Override
public List<ParamValue> getValues(String paramCode) {
final List<ParamValue> values = new ArrayList<>();
values.add(new ParamValue("Generic Food 1", "Generic Food 1"));
values.add(new ParamValue("Generic Food 2", "Generic Food 2"));
return values;
}
}
public static class SRC_FOODS_DOG implements Source {
@Override
public List<ParamValue> getValues(String paramCode) {
final List<ParamValue> values = new ArrayList<>();
values.add(new ParamValue("Dog Food 1", "Dog Food 1"));
return values;
}
}
public static class SRC_FOODS_CAT implements Source {
@Override
public List<ParamValue> getValues(String paramCode) {
final List<ParamValue> values = new ArrayList<>();
values.add(new ParamValue("Cat Food 1", "Cat Food 1"));
return values;
}
}
Given a defined set of Values that we can assign using the @Values annotation, we can explicitly define conditions to set a dependent field’s values.
@Model
@Getter @Setter
public static class StatusForm {
@ValuesConditional(target = "../petFoodSelection", condition = {
@Condition(when = "state=='dog'", then = @Values(SRC_FOODS_DOG.class)),
@Condition(when = "state=='cat'", then = @Values(SRC_FOODS_CAT.class)),
}
)
@TextBox(postEventOnChange = true)
private String petType;
@Radio
@Values(SRC_FOODS_ALL.class)
private String petFoodSelection;
}
In this example, petType is the field and petFoodSelection is the dependent field. We set petFoodSelection to contain the defaults ["Generic Food 1", "Generic Food 2"] initially and conditionally define those values when petType's state is "dog" or "cat".
When the state of petType is "dog", then the Values for petFoodSelection will be ["Dog Food 1"]. Similarly when the state of petType is "cat", then the Values for petFoodSelection will be ["Cat Food 1"].
Conceptually speaking, we are pushing the updates of Values to the dependent field whenever the state of the annotated field loads or is changed.
VisibleConditional
@VisibleConditional
is an extension capability provided by the framework. The annotation is used to conditionally set the UI visibility of a param based on a SpEL condition.
The logic for @VisibleConditional
can be found in VisibleConditionalStateEventHandler
. The handler logic will be executed during the following framework events:
-
OnStateLoad
-
OnStateChange
@VisibleConditional(when="state == 'hooli'", targetPath="../p2")
private String p1;
private String p2;
5.1.2. Core Config Annotations
ConceptId
@TextBox(postEventOnChange = true)
@ConceptId(value = "IOT1.1.1")
@Label(value = "If Other, provide reason")
private String otherReason;
Config
@Config
is core annotation that handles the execution of a Nimbus Command DSL statement.
@Config(url = "/pageAddEditGoal/tileEditGoal/sectionEditGoal/goalDetailsForm/_nav?pageId=pageCarePlanSummary")
@Button(type = Button.Type.PLAIN)
private String cancel;
ConfigNature
Ignore
Framework persists the data objects of a class in database using @Repo by serializing the class and associating a version number that is called seriaVersionUID. However, if we do not want the framwework to serialize for time being, we can use @Ignore component of ConfigNature class. The following example shows that: -
@Domain(value="patient", includeListeners={ListenerType.persistence})
@Repo(value=Database.rep_mongodb, cache=Cache.rep_device)
@ToString
public class Patient extends IdString {
@Ignore
private static final long serialVersionUID = 1L;
}
Domain
Core Config configuration @Domain annotation persists data.
Core config @Domain will always be followed by @Repo that will specify the way data is persisted.
includeListeners={ListenerType.persistence, ListenerType.update} of @Domain
specifies that the data will be persisted.
value=Database.rep_mongodb of @Repo specifies that a class with
@Domain annotation will use MongoDb for persistence.
@Domain(value="cmcase", includeListeners={ListenerType.persistence, ListenerType.update})
@Repo(value=Database.rep_mongodb, cache=Cache.rep_device)
public class CMCase extends IdString {
}
Note
|
Please read @Repo for mroe information regarding @Repo annotation. |
DomainMeta
Similar to ViewStyle component, DomainMeta component is used to define an ANNOTATION_TYPE level annotation that is used with few view config annotations,
as follows: -
-
@ConceptId
@Retention(RetentionPolicy.RUNTIME)
@Target(value={ElementType.ANNOTATION_TYPE})
@Inherited
public @interface DomainMeta {
}
Here is an example: -
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@DomainMeta
public @interface ConceptId {
String value() default "";
}
Execution
Execution is inherited annotation for @Config and @KeyValue. It is not currently being directly used, but is there for hieracrchial purposes.
More documentation will be added here if Execution expands or is directly used.
Initialize
@Section
@Initialize
@Config(url="/vpAdvancedMemberSearch/vtAdvancedMemberSearch/vsMemberSearchCriteria/vfAdvMemberSearch/_process?fn=_setByRule&rule=updateadvmbrsearchcriteria")
private VSMemberSearchCriteria vsMemberSearchCriteria;
MapsTo
Path
@Path()
private Long version;
Type
@MapsTo.Type(CMCase.class)
public static class SectionEditGoal {
}
Note
|
If it is not mapped (@Mapped), an exception will be thrown. If no exception is thrown, defaults to silent. |
Model
@Model
is a marker annotation for the framework to acknoledge an class declaration as a model entity. Useful when created "nested entities".
Model is similar to Domain, except that domain is seen as the "root" entity, or the topmost parent in a nested entity. Typically, model is expected to be used on nested class declarations. See the following example:
@Domain("parent")
@Getter @Setter @ToString
public class Parent {
private Child1 child;
}
@Model
@Getter @Setter @ToString
public class Child1 {
private Child2 child;
}
@Model
@Getter @Setter @ToString
public class Child2 {
}
In this example, the object hierarchy is Parent
> Child1
> Child2
, where Child1
and Child2
are decorated with @Modal
.
ParamContext
@ParamContext is used to set the contextual properties of a field during the OnStateLoad event (e.g. visible, enabled).
The intent of @ParamContext is to be able to decorate fields to define default contextual behavior. For example:
public static class SampleView {
@TextBox
@ParamContext(enabled=false, visible=false)
private String myTextBox;
}
In this scenario we have configured the contextual values for enabled and visible to be false for myTextBox. These values will be set during the OnStateLoad event and myTextBox consequently will not be enabled or visible when the corresponding page is rendered.
Tip
|
@ParamContext can also be defined on annotations. In these scenarios when a field is decorated with that annotation, then the handler for @ParamContext will execute. This may be useful when building a framework extension. |
PrintConfig
Defines print configuration that should be applied when invoking the print action on a view component.
@PrintConfig
currently is currently supported for the following components:
@Button(style = Button.Style.PRINT, printPath = "/domain/page/tile/modal")
@PrintConfig(autoPrint = false)
private String print;
SearchNature
StartsWith
This component is ued to validate a field. The wilcard attribute determines the validation criteria for a field. This is a server side component.
@NotNull
@StartsWith
@Label(value = "First Name")
private String firstName;
Note
|
The example will always search the first name that starts with anything, represented with the default value. A specific search criteria can be specified using wildcard attribute of @StartsWith. |
Repo
@Repo is used to determine where the data will be persisted. It is always used along with @Domain.
rep_mongodb
The following example shows how data is persisted/ retrieved using MongoDB as a source.
@Domain(value="cmassessment", includeListeners={ListenerType.persistence})
@Repo(alias="cmassessment",value=Database.rep_mongodb, cache=Cache.rep_device)
@Getter @Setter
public class CMAssessment extends IdString {
}
Rule
@Rule allows its decorated field a mechanism for triggering one or more rule definitions during its OnStateLoad and OnStateChange events.
@Domain(value="sample_rule_entity", includeListeners={ListenerType.persistence})
@Repo(Database.rep_mongodb)
@Getter @Setter
public class SampleRuleEntity {
// Execute the rule at "rules/sample_increment" during the OnStateLoad and
// OnStateChange events of ruleParam.
@Rule("rules/sample_increment")
private String rule_param;
}
By default, the framework provides support for firing all rules for a given domain entity. That is, for the SampleRuleEntity.java above we might have a rule file defined as sample_rule_entity.drl which will be automatically fired by naming convention.
For cases where additional configuration for other rules is needed, @Rule can be used.
Values
Values provides a mechanism for populating a fields values property. This can be used by a number of components to perform such functions as: define a set of selections for radio buttons and checkboxes, or populating a dropdown list.
Source
The Source is a simple abstraction for providing a contract between implementations to provide data to the framework.
Source is exclusively used for @Values.
public static interface Source {
public List<ParamValue> getValues(String paramCode);
}
We can use this to define several different types of values providers. A simple static Source implementation is shown below:
public class SampleStaticSource implements Source {
public List<ParamValue> getValues(String paramCode) {
List<ParamValue> values = new ArrayList();
values.add(new ParamValue("sample.value.1", "Sample Value 1"));
return values;
}
}
EnableAPIMetricCollection
@EnableAPIMetricCollection allows its decorated class, method level logging mechanism with log entries for method - entry, exit, arguments and response. It can be configured with log level of info or debug for arguments and response.
@EnableAPIMetricCollection(args=LogLevel.info,resp=LogLevel.info)
@Getter(value=AccessLevel.PROTECTED)
public class SampleMetricLog {
public Object calculate(String input) {
// some logic
}
}
By Default, the logging level for arguments & response is debug and info for entry and exit. Also, method exit logs the execution time of the method in milliseconds.
Client applications can annotate the custom config beans with @EnableAPIMetricCollection to enable a similar logging mechanism.
Name | Type | Default | Description |
---|---|---|---|
args |
LogLevel |
debug |
LogLevel for logging method arguments |
resp |
LogLevel |
debug |
LogLevel for logging method response |
5.1.3. View Config Annotations
Accordion
Accordion groups a collection of contents in tabs.
@Accordion
private VAQuestionnaire vaQuestionnaire;
@Model
@Getter @Setter
public static class VAQuestionnaire {
@AccordionTab
private VATTab1 vatTab1;
@AccordionTab
private VATTab2 vatTab2;
@Model
@Getter @Setter
public static class VATTab1 { ... }
@Model
@Getter @Setter
public static class VATTab2 { ... }
}
AccordionTab
AccordionTab contains a collection of contents for an individual section within an Accordion.
Accordion Tab supports all of the same children that are supported as children by Form.
@AccordionTab
private VATTab1 vatTab1;
@Model
@Getter @Setter
public static class VATTab1 {
@TextBox
private String txt1;
@ButtonGroup
private VBGMain vbgMain;
}
ActionTray
ActionTray is an extension to the standard button input element with icons and theming.
Note
|
ActionTray can only be contained within a View Layout definition. |
@ActionTray
private VSActionTray vsActionTray;
@Model
@Getter @Setter
public static class VSActionTray {
@Label(value = " ")
@Config(url = "...")
@Button(imgSrc = "fa-id-card", cssClass = "icon btn btn-icon mr-0", title = "Record a Contact")
private String contactRecord;
}
Autocomplete
Autocomplete is a text input component.
None. @Autocomplete
should decorate a field having a simple type.
@Autocomplete(display="label", postEventOnChange = true, minLength = 2)
@Config(url = "/p/owner/_search?fn=lookup&where=owner.firstName.containsIgnoreCase('<!autocompletesearchvalue!>')&projection.mapsTo=code:id,label:firstName")
private String autocompleteField;
Button
Button is an extension to the standard button input element with icons and theming.
None. @Button
should decorate a field having a simple type.
@Config(url = "...")
@Button(type = Button.Type.DESTRUCTIVE)
private String delete;
ButtonGroup
ButtonGroup contains a collection of Button components.
@ButtonGroup(cssClass="text-sm-right")
private VBGGoals vbgGoals;
@Model
@Getter @Setter
public static class VBGGoals {
@Button
private String btn1;
@Button
private String btn2;
}
Calendar
Calendar is an input component to select a date.
None. @Calendar
should decorate a field having a simple type of a supported date type:
-
LocalDate
-
LocalDateTime
-
ZonedDateTime
-
Date
@Path
@Calendar(postEventOnChange=true, showtime=true, hourFormat="24")
private LocalDate startDate;
@Path
@Calendar(postEventOnChange=true, timeOnly=true)
private LocalDateTime startDate;
CardDetail
CardDetail is a flexible container component.
@CardDetail(title="Member Overview", cssClass="contentBox right-gutter bg-alternate mt-0")
private VCDMember vcdMember;
@Model
@Getter @Setter
public static class VCDMember {
@CardDetail.Header
VCDHMain header;
@CardDetail.Body
VCDBMain body;
@Model
@Getter @Setter
public static class VCDHMain { ... }
@Model
@Getter @Setter
public static class VCDBMain { ... }
}
Note
|
contentBox right-gutter bg-alternate mt-0 overrides the default cssClass specified for the @CardDetail component.
|
CardDetail.Body
CardDetail.Body is a container component within CardDetail that contains the main content.
@CardDetail.Body
VCDBMain body;
@Model
@Getter @Setter
public static class VCDBMain {
@FieldValue
private String value1;
}
CardDetail.Header
CardDetail.Header is a container component within CardDetail that contains the header content.
@CardDetail.Header
VCDHMain header;
@Model
@Getter @Setter
public static class VCDHMain {
@FieldValue
private String value1;
}
CardDetailsGrid
CardDetailsGrid contains a collection of CardDetail components.
A field decorated with @CardDetailsGrid
should be a collection/array with a defined type. The defined type is treated as the definition for the row data in the rendered content. This is referred to as the collection element type. The allowed children components of the collection element’s type are:
@CardDetailsGrid(onLoad = true)
@Config(url = "...")
@Path(linked = false)
private List<ConcernsLineItem> vcdgConcerns;
@Model
@Getter @Setter
public static class ConcernsLineItem {
@CardDetail
private VCDMain vcdMain;
@Model
@Getter @Setter
public static class VCDMain { ... }
}
Chart
Chart renders a visually appealing graph/chart based on an open source HTML5 based charting library: Charts.js
None. @Chart
should decorate a field having a collection with type DataGroup
(e.g. List<DataGroup>
.)
-
Line
-
Bar
-
Pie
-
Doughnut
-
xAxisLabel
= ``-
Applicable only for cartesian graphs and denotes the label to be shown for X-axis
-
-
yAxisLabel
= ``-
Applicable only for cartesian graphs and denotes the label to be shown for Y-axis
-
-
stepSize
= ``-
Applicable only for cartesian graphs and denotes the incrments of the Y-axis. If not given, the size would be random based on the Y-axis values supplied to the graph
-
@Label("Bar Graph")
@Chart(value=Type.BAR, xAxisLabel="Domain", yAxisLabel="Count")
private List<DataGroup> barGraph;
@Label("Line Graph")
@Chart(value=Type.LINE, xAxisLabel="Domain", yAxisLabel="Count")
private List<DataGroup> lineGraph;
@Label("Pie Graph")
@Chart(value=Type.PIE)
private List<DataGroup> pieGraph;
@Label("Doughnut Chart")
@Chart(value=Type.DOUGHNUT)
private List<DataGroup> doughnutGraph;
Data Type of Chart
Every chart that is configured with a datatype as List<DataGroup>
. In a chart there could be 1 or more graphs that can be plotted. Each DataGroup
qualifies to be painted as a graph. A DataGroup
has a legend
that indicates what the graph is and has a list of x,y co-ordinates.
If line graph configured above has the following dataset provided to the UI -
[
{
"legend": "Region 1"
"datapoints": [
{1,1},{2,2},{3,3}
]``
}
{
"legend": "Region 2"
"datapoints": [
{10,10},{20,20},{30,30}
]
}
]
There would be 2 line graphs rendered on the page, one for Region 1 and the other for Region 2.
CheckBox
Checkbox is an extension to standard checkbox element.
None. @CheckBox
should decorate a field having a simple type of Boolean
or boolean
.
@CheckBox(postEventOnChange=true)
private boolean admin;
CheckBoxGroup
CheckBoxGroup is used for multi-select CheckBox components.
None. @CheckBoxGroup
should decorate a field having a collection/array of type String
.
@CheckBoxGroup(postEventOnChange=true)
private String[] days;
ComboBox
Combobox is used to select an item from a collection of options.
None. @ComboBox
should decorate a field having a simple type.
@Path
@Values(url = "...")
@ComboBox
private String goalCategory;
When decorated with @Values
, the resolved values as defined by the logic within Values will be used as the options for the rendered @ComboBox
component.
FieldValue
FieldValue is a container for displaying a single value. Has the ability to become an in place edit field with several configurable properties.
None. @FieldValue
should decorate a field having a simple type.
@MapsTo.Type(Goal.class)
@Getter @Setter
public static class VCDHGoals {
@FieldValue(showName = false, cols = "2")
@Path
private String description;
@FieldValue(showName = false, iconField = "date")
@Path
private LocalDate targetDate;
@FieldValue(showName = false, iconField = "planned")
@Path("/status")
private String status;
@FieldValue(showName = false, datePattern = "MM/dd/yyyy")
@Path
private LocalDate startDate;
}
FieldValueGroup
FieldValue is a container for displaying a logical grouping of multiple FieldValue components.
@FieldValueGroup
private AddressGroup addressGroup;
@MapsTo.Type(Address.java)
@Getter @Setter
public static class AddressGroup {
@FieldValue
@Path
private String line1;
@FieldValue
@Path
private String line2;
@FieldValue
@Path
private String city;
@FieldValue
@Path
private String state;
@FieldValue
@Path
private String zip;
}
FileUpload
FileUpload is an advanced uploader with dragdrop support, multi file uploads, auto uploading, progress tracking and validations.
None. @FileUpload
should decorate a field having a simple type.
@FileUpload(type = "${app.config.upload.allowedTypes}")
private String uploader;
Form
A Form is an HTML form container for user input contents.
Accordion, Button, ButtonGroup, Calendar, CheckBox, CheckBoxGroup, ComboBox, FileUpload, FormElementGroup, Grid, Header, InputMask, Paragraph, PickList, Radio, RichText, Signature, TextArea, TextBox TreeGrid
@Form(cssClass = "twoColumn")
@Path(linked = false, state = State.External)
private VFGoals vfGoals;
@Model
@Getter @Setter
public static class VFGoals { ... }
FormElementGroup
FormElementGroup is a grouping container that enables the form elements to be grouped in such a way that they appear to look as a sub section of the form.
For example if there is a Name of person that needs to be gathered with 3 sub fields firstName
, middleName
and lastName
, this can be displayed as a FormElementGroup for better user experience.
@Model @Getter @Setter
public static class VSAddEditForm {
@Form
private VFAddEditForm vfAddEditForm;
}
@Getter @Setter
public static class VFAddEditForm {
@Label("Name")
@FormElementGroup(cssClass="oneColumn")
private SectionName sectionName;
}
@Model
@Getter @Setter
public static class SectionName {
@Label("First Name")
@TextBox(cssClass="inline")
@NotNull
@Path
private String firstName;
@Label("Middle Name")
@TextBox(cssClass="inline")
@NotNull
@Path
private String middleName;
@Label("Last Name")
@TextBox(cssClass="inline")
@NotNull
@Path
private String lastName;
}
The above code illustrates the usecase of showing name field in the form of a sub group of 3 other fields and gives the visual experience of grouping them for better undertanding. The css class "oneColumn" helps in formatting the fields as if they fall under the "name" field. If the existing style classes are not sufficient, a custom css class can be added to the app styles and be used.
TODO
TODO
Grid
Grid is a table container capable of displaying tabular data.
@Grid
should decorate a field having a collection/array with a defined type. The defined type is treated as the definition for the row data in the rendered HTML table. This is referred to as the collection element type. The allowed children components of the collection element’s type are:
@Grid(export=true)
private List<PetLineItem> vgPets;
@Model
@Getter @Setter
public static class PetLineItem {
@GridColumn
private String field1;
@LinkMenu
private VLMMain vlmMain;
@GridRowBody
private ExpandedRowContent expandedRowContent;
@Model
@Getter @Setter
public static class VLMMain { ... }
@Model
@Getter @Setter
public static class ExpandedRowContent { ... }
}
Multiple row selection
@Grid
offers the ability to select multiple rows of data and submit the indexes of the selected rows as JSON data to the server for processing.
To enable this feature, the following attributes must be set:
-
rowSelection
=true
-
renders a checkbox at the beginning of each row in the table for row selection
-
-
postButton
=true
-
renders a button below the rendered
@Grid
that when clicked, submits an HTTP POST request with a payload containing the selected index to the path atpostButtonUri
-
-
postButtonUri
-
a parameter path relative to the decorated
@Grid
parameter to invoke an HTTP POST request on with the selected indexes
-
-
postButtonTargetPath
-
the key to be used for the JSON object sent as a payload
-
-
(Optional)
postButtonLabel
-
controls the label of the button rendered by
postButton
-
-
(Optional)
headerCheckboxToggleAllPages
-
when
true
, selecting the "select all" checkbox in the header of the@Grid
will select all the records in the dataset across all pages. Whenfalse
, only within the current page items will be selected
-
-
(Optional)
export
-
exposes the capability to download the grid data in .csv format
-
@Grid(onLoad = true, rowSelection = true, postButton = true, postButtonUri = "../actionRemove", postButtonTargetPath = "ids", postButtonLabel = "Remove")
@Path
@Config(url = "/p/member/_search?fn=example")
private List<MemberLineItem> membersGrid;
@Config(url = "/page/tile/section/selectedIdsWrapper/_replace") //(1)
@Config(url = "/p/member:<!/../membersGrid/<!col!>/memberId!>/_delete", col = "<!/selectedIdsWrapper/ids!>") //(2)
private String actionRemove;
private SelectedIdsWrapper selectedIdsWrapper;
@Model
@Getter @Setter
public static class SelectedIdsWrapper {
private List<String> ids;
}
@MapsTo.Type(Member.class)
@Getter @Setter
public static class MemberLineItem { ... }
In this example, assume there are ten rows of data and the user selects indexes 1, 3, and 7 (Indexes are zero based). When clicking the "Remove"
button, the an HTTP POST call is sent to the param at actionRemove
with the payload:
{
"ids": ["1", "3", "7"]
}
Next, the @Config
statements decorating actionRemove
are executed. Config #1 uses _replace
to update the sepectedIdsWrapper
ids
field to be ["1", "3", "7"]
. Config #2 is a bit trickier to read, but it iterates over the values just set into "selectedIdsWrapper/ids"
and for each id, retrieves a record from membersGrid
. From that point, the object in context is a MemberLineItem
object, from which memberId
is retrieved and used with _delete
to delete the selected members them from the member
domain.
The attribute export=true
instructs the grid component to show an export
link (clicking of which the grid data can be exported).
There are 2 kinds of scenarios where the export of grid data can be used.
1. When grid is configured to use client side pagination, the export functionality would export all the data to a csv file that gets downloaded on the machine.
2. When the grid is configured with server side pagination, the export functionality would export only the current page data.
Note
|
The data downloaded would be in CSV format. |
Using inline edit for @Grid records
@Grid
supports inline editing, allowing the user to edit the first-level visible fields in a table row.
@Grid(editRow = true) (1)
private List<SampleLineItem> editableGrid;
@Model @Getter @Setter
public static class SampleLineItem {
private String _action_onEdit; (2)
// Line item fields below here
}
-
Toggling the attribute
editRow
will enable/disable an edit icon which will make a_get
request parameter defined byonEdit
. -
Decorate
_action_onEdit
with any@Config
or framework supported annotations.
Configuring server side pagination
By default, if the the @Grid
component is configured to use pagination, it uses client side pagination. The rendered table can be configured to work with the framework by providing Command DSL statements that the framework then interpolates and hands off to Spring to take advantage of it’s rich server side pagination features.
Certain pagination specific query parameters can be used to leverage framework support.
page
|
An integer representing the starting page number |
pageSize
|
An integer for the number of elements to display per page |
sortBy
|
A "key:value" string with the |
@MapsTo.Path(linked = false)
@Grid(onLoad = true, pageSize = "3", lazyLoad = true) (1)
@Config(url = "/vpVisitsServer/vtVisits/vsVisits/visits.m/_process?fn=_set&url=/p/visit/_search?fn=example&<!page=y!>") (2)
private List<VisitLineItem> visits;
Setting lazyLoad = true
as seen in <1> informs the client side to use server side pagination by sending Command DSL statements to the server. The client side will send pagination information as query parameters and payload data depending upon the action taken upon the rendered @Grid
component.
Setting <!page=y!>
as seen in <2>, invokes the Path Variable Resolver to construct/pass along any query parameters/payload data sent from the incoming Command DSL statement for the pagination query parameters. For example, the following statement:
would result in <2> being invoked as:
/vpVisitsServer/vtVisits/vsVisits/visits.m/_process?fn=_set&url=/p/visit/_search?fn=example&page=0&pageSize=5
Note
|
When serverside pagination is enabled and <!page=y!> is given, it is expected that \_get calls invoking the parameter would also send the pagination query parameters, otherwise pagination will not work as expected.
|
Server side sorting can be achieved by setting sortBy
as a query paremeter in the Command DSL statement.
/vpVisitsServer/vtVisits/vsVisits/visits.m/_process?fn=_set&url=/p/visit/_search?fn=example&page=0&pageSize=5&sortBy=firstName:DESC
Server side filtering can be achieved by setting the filters
JSON in the raw payload alongside the Command DSL statement.
Request URL: /vpVisitsServer/vtVisits/vsVisits/visits.m/_process?fn=_set&url=/p/visit/_search?fn=example&page=0&pageSize=5
Payload:
filters: [
{
"code": "firstName",
"value": "bob"
}
]
GridColumn
GridColumn is a container for displaying a single value within a Grid.
None. @GridColumn
should decorate a field having a simple type.
@Model
@Getter @Setter
public static class PetLineItem {
@GridColumn
private String field1;
}
GridRowBody
GridRowBody is used to display additional content about the row data within a Grid.
@GridRowBody
will display children components in the same manner as Section does. See the Allowed Children Components of Section for more details.
@MapsTo.Type(Pet.class)
@Getter @Setter
public static class PetLineItem {
@GridColumn
@Path
private String name;
@GridRowBody
private ExpandedRowContent expandedRowContent;
@Model
@Getter @Setter
public static class ExpandedRowContent {
@CardDetail
private CardDetails cardDetails;
}
@Model
@Getter @Setter
public static class CardDetails {
@CardDetail.Body
private CardBody cardBody;
}
@Model
@Getter @Setter
public static class CardBody {
@FieldValue
@Path
private String id;
}
}
Header
Header is a container with a text header, equivalent to an HTML header.
None. @Header
should decorate a field having a simple type.
@Header(size = Header.Size.H1)
private String header;
Hints
Hints is a UI styling behavior applied for some components. The behavior applied is delegated to each individual component where @Hints
is supported.
@Link(url = "/#/h/cmdashboard/vpDashboard", imgSrc = "anthem-rev.svg")
@Hints(AlignOptions.Left)
@PageHeader(Property.LOGO)
private String linkHomeLogo;
InputMask
InputMask is a text input component used to enter input in a certain format such as numeric, date, currency, email and phone.
None. @InputMask
should decorate a field having a simple type.
@InputMask(mask = "(999) 999-9999")
@Label("Phone Number")
InputSwitch
InputSwitch is used to select a boolean value.
None. @InputSwitch
should decorate a field having a simple type.
@Label("On/Off")
@InputSwitch
private boolean on;
Image
An image component used for displaying images.
None.
None. @Image
should not decorate a field and will typically be used as an annotation attribute in other components.
@MenuPanel(imgType = Image.Type.SVG, imgSrc = "notesIcon")
private VMPVisits vmpVisits;
Label
Label is used to display label text for a view component.
@Label can be used on all View annotations (e.g. Page, Tile, TextBox, etc.), wherever content is necessary. Multiple @Label
annotations can be provided for managing content for different locales.
@Label("First Name")
private String firstName;
@Label(value="Last Name")
private String lastName;
@Label(value = "Select 3" , helpText = "Please select atleast 3 cities")
@CheckBoxGroup
private String[] city;
@Tile
@Label("Dashboard")
private VTDashboard vtDashboard;
@Page
@Label(" ")
private VPDashboard vpDashboardWithEmptyLabel;
@Label(value="Test Label C in English", helpText="some tooltip text here C")
@Label(value="Test Label A in French", localeLanguageTag="fr")
private String staticText;
@Textbox
private String addressline;
Link
Link is a hyperlink component used for navigation or user interaction of displayed text.
None. @Link
should decorate a field having a simple type.
// UI Navigation to page vpHome under domain petclinic
@Link(url = "/h/petclinic/vpHome")
private String linkToHome;
// Executes a request against _subscribe, and sets the image
@Link(url = "/notifications/_subscribe:{id}/_process", b="$executeAnd$configAnd$nav", method = "POST")
private String subscribeToEmailNotifications;
// Creates an external link, as in a link navigating outside of the context of the Nimbus framework.
@Link(url = "https://www.mywebsite.com", value = Link.Type.EXTERNAL, target = "_new", rel = "nofollow")
private String myWebsiteLink;
LinkMenu
LinkMenu is a dropdown component used for displaying links within a Grid row.
@LinkMenu
private VLMMain vlmMain;
@Model
@Getter @Setter
private static class VLMMain {
// Display a link that performs some @Config action
@Label(value = "View Case")
@Link
@Config(url = "...")
private String viewCase;
// Display a link that performs some @Config action and uses an image icon.
@Label(value = "Assign Case Owner")
@Link(imgSrc = "task.svg")
@Config(url = "...")
private String assignCase;
// Display a link that performs an external navigation in a new tab and passing a dynamic value
@Link(value = Type.EXTERNAL, target = "_blank", url = "${app.endpoints.documents}/showDocument.aspx?documentid={documentId}")
@Path(linked = false)
private String viewDoc;
private String documentId;
}
Menu
Menu is a container intended to display other navigation components.
@Menu
private SideMenu sideMenu;
@Model
@Getter @Setter
public static class SideMenu {
@Link(url = "#")
private String home;
@Link(url = "#")
private String signup;
}
MenuLink
MenuLink is nestable menu link component that is exclusively used with MenuPanel.
None. @MenuLink
should decorate a field having a simple type.
@Label("Link 1")
@MenuLink
private String link1;
@Label("Link 2")
@MenuLink
private String link2;
MenuPanel
MenuPanel is a hybrid of accordion-tree components.
Attributes
-
(Optional)
flow
-
If specified, aids the breadcrumb to identify the parent domain reference and renders the links correctly when a domain(other than the parent domain reference) and page is configured as a menulink
-
@MenuPanel
private VMPView vmpView;
@Model
@Getter @Setter
public static class VMPView {
@Label("Show View...")
@MenuLink
private String showView;
@Label("Appearance")
@MenuPanel(flow="domainAalias")
private VMPAppearance appearance;
@Model
@Getter @Setter
public static class VMPAppearance {
@Label("Toggle Full Screen")
@MenuLink(url="domainAalias/page1")
private String toggleFullScreen;
@Label("Toggle Always on Top")
@MenuLink(url="domainBalias/page1")
private String toggleAlwaysOnTop;
@Label("Toggle with Animation")
@MenuLink(url="domainAalias/page2")
private String toggleWithAnimation;
}
}
In the above configuration, the MenuLink
has a url that points to either domainAalias
or domainBalias
. Lets say VMPView
belongs to the domain that is the layout of the domainA. In other words, the view domain A has the layout of MenuPanel
and MenuLink
. So initially, When 'toggle full screen' MenuLink
is clicked the breadcrumb would say - Home→Toggle Full Screen
When 'Toggle Always on Top' MenuLink
is clicked, the breadcrumb would say Home→Toggle Always On Top.
When 'Toggle with Animation' is clicked the breadcrumb would say Home→Toggle With Animation
Its important to note that even though the transition is from domainBalias
to domainAalias
, the flow
attribute tells the MenuPanel
that the parent flow of MenuLink
clicked is domainAalias
. The value domainAalias
is looked up in the breadcrumb list and the value is appended as a suffix.
Modal
Modal is a container to display content in an overlay window.
@Label("Modal Title")
@Modal
private VMMyModal vmMyModal;
@Model
@Getter @Setter
public static class VMMyModal {
@Section
private VSMain vs;
@Model
@Getter @Setter
public static class VSMain { ... }
}
@Modal
makes use of @ParamContext
to render it’s visible property to false
during initialization by default. This and other similiar behaviors can be overridden by supplying the context
attribute of @Modal
with a @ParamContext
.
@Modal(context = @ParamContext(enabled = true, visible = true))
private VMMyModal vmMyModal;
public static class VMMyModal { ... }
See ParamContext for more details.
Page
Page is a container component that groups a collection of contents.
@Page(defaultPage = true)
private VPHome vpHome;
@Page
private VPPets vpPets;
@Model
@Getter @Setter
public static class VPHome {
@Tile
private VTMain vtMain;
@Model
@Getter @Setter
public static class VTMain { ... }
}
@Model
@Getter @Setter
public static class VPPets { ... }
PageHeader
TODO
TODO
TODO
@PageHeader(PageHeader.Property.USERNAME)
private String fullName;
Paragraph
Paragraph is a container for displaying text content.
None. @Paragraph
should decorate a field having a simple type.
@Paragraph("Hello! Welcome to Pet Clinic.")
private String welcome;
PickList
PickList is used to reorder items between different lists.
@PickList(sourceHeader = "Available Category", targetHeader = "Selected Category")
@Values(value = SomeCategory.class)
private PicklistType category;
@MapsTo.Type(SomeClass.class)
@Getter @Setter
public static class PicklistType {
@PickListSelected(postEventOnChange = true)
@Path("category")
@Values(value = SomeCategory.class)
@NotNull
private String[] selected;
}
The @PickList
implementation is works closely with Values. In the previous sample configuration @PickList
and @PickListSelected
are using the same @Values
source. If the values assigned to the @PickList
component need to be conditionally set, meaning that the values on the source list are able to change, ValuesConditional can be used. See the following example:
@ComboBox(postEventOnChange = true)
@Values(value = petType.class)
@ValuesConditional(target = "../category", condition = {
@ValuesConditional.Condition(when = "state == 'Dog'", then = @Values(value = DogCategory.class)),
@ValuesConditional.Condition(when = "state == 'Cat'", then = @Values(value = CatCategory.class))
})
@VisibleConditional(targetPath = { "../category" }, when = "state != 'Horse'")
@EnableConditional(targetPath = { "../category" }, when = "state != 'Parrot'")
private String type;
@PickList(sourceHeader = "Available Category", targetHeader = "Selected Category")
private PicklistType category;
@MapsTo.Type(SomeClass.class)
@Getter @Setter
public static class PicklistType {
@PickListSelected(postEventOnChange = true)
@Path("category")
@Values(value = AllCategory.class)
@NotNull
private String[] selected;
}
One caveat to this implementation is that the @PickListSelected
component should have it’s values set to the comprehensive set of possible values that the parent source list could contain. (e.g. The values in DogCategory.class
and CatCategory.class
should be contained in AllCategory.class
). Not doing so may result in undesirable results.
PickListSelected
PickListSelected is a required supplementary configuration for PickList. The @PickList
works exclusively with @PickListSelected
by providing the UI a server-side location to store the selected values and any other metadata associated with those selected values.
None. @PickListSelected
should decorate an array or collection.
@PickListSelected(postEventOnChange = true)
@Path("category")
@Values(value = AllCategory.class)
@NotNull
private String[] selected;
Radio
Radio is an extension to standard radio button element.
None. @Radio
should decorate a field having a Boolean
or boolean
value.
@Label("Some question:")
@Radio(postEventOnChange = true, controlId = "27")
@Values(url="~/client/orgname/staticCodeValue/_search?fn=lookup&where=staticCodeValue.paramCode.eq('/q14')")
@Path
private String q14;
RichText
RichText is a rich text editor based on <a href="https://www.primefaces.org/primeng/#/editor">PrimeNG’s Editor</a>.
None. @RichText
should decorate a field having a simple type.
@RichText
private String richTextbox;
Displaying Readonly Content
The rich text editor supports readonly mode, which makes the field a true readonly
form field that preserves rich text HTML formatting and removes any extra rich text features (e.g. toolbar and other similar features).
@RichText(readOnly = true)
private String richTextbox;
Tip
|
Use readOnly when needing to simply display the state of a decorated field containing an HTML formatted string.
|
Configuring the Toolbar
The rich text editor toolbar by default has most of the available features enabled by default (see the Javadoc for explicit details). Toolbar features can be enabled/disabled via the following:
@RichText(toolbarFeatures = { ToolbarFeature.BOLD, ToolbarFeature.ITALIC, ToolbarFeature.UNDERLINE })
private String richTextbox;
Configuring Fonts
Assuming the Toolbar feature ToolbarFeature.FONT
is enabled, the fonts that are able to be used are entirely configurable and controlled by providing the following:
-
A unique string value for the font label
-
A CSS definition within the client application styles
@RichText
@Fonts({ "Times New Roman", "CoolFont" })
private String richTextbox;
.ql-editor .ql-font-times-new-roman {
font-family: "Times New Roman";
}
.ql-editor .ql-font-cool-font {
font-family: "Cool Font";
}
Note
|
In order to use custom fonts (such as "Cool Font" ), clients must ensure to load any/all necessary font files (e.g. .woff , .ttf ).
|
Section
Section is a container component that groups a collection of contents.
Accordion, Button, ButtonGroup, CardDetail, CardDetailsGrid, Chart, ComboBox, Form, Grid, Link, Menu, Paragraph, StaticText, TextBox, Tab
@Section
private VSMain vsMain;
@Model
@Getter @Setter
public static class VSMain { ... }
Signature
Signature is an HTML canvas element that can be used to capture signature content. The @Signature
component will render the following on the UI:
-
canvas on which the user can draw
-
Clear button, which will clear the canvas
-
Accept button, which will store the user-drawn content captured in canvas as a data-url representation
-
<img>, which contains the stored image after clicking accept button
None. @Signature
should decorate a field having a simple type.
@Signature(postEventOnChange = true)
private String employeeSignature;
StaticText
StaticText is a container for displaying html content or text "as is" to the UI. @StaticText
may be used to bind unsafe HTML directly onto the page.
With great power comes great responsibility.
None. @StaticText
should decorate a field having a simple type.
@StaticText
private String description;
Tab
Tab is a container component to group content with TabPanels.
@TabPanel. TabPanel can support section or another Tab within it.
@Tab
@Path(linked=false)
private Tab tab;
@Model
@Getter @Setter
public static class Tab {
@Label("Tab Panel")
@TabPanel
@Path(linked=false)
private TabPanel panel;
}
@Model
@Getter @Setter
public static class TabPanel {
@Section
@Path(linked=false)
private TabContent tabContent;
@Tab
@Path(linked=false)
private AnotherTabPanel anotherPanel;
}
TextArea
TextArea is a text input component that allows for a specified number of rows.
None. @TextArea
should decorate a field having a simple type.
@TextArea
private String description;
TextBox
TextBox is a text input component.
None. @TextBox
should decorate a field having a simple type.
@TextBox
private String sampleTextField;
Tile
Tile is a container component that groups a collection of contents.
@Tile(size = Tile.Size.Large)
private VTMain vtMain;
@Model
@Getter @Setter
public static class VTMain { ... }
TreeGrid
TreeGrid is used to display hierarchical data in tabular format.
@TreeGrid
should decorate a field having a collection/array with a defined type. The defined type is treated as the definition for the row data in the rendered HTML table. This is referred to as the collection element type. The allowed children components of the collection element’s type are:
@TreeGrid
@Path(linked = false)
@Config(url = "<!#this!>/.m/_process?fn=_set&url=/p/pethistory/_search?fn=example")
private List<PetHistoryLineItem> treegrid;
@Model
@Getter @Setter
public static class PetHistoryLineItem { ... }
TreeGrid
TreeGridChild is the recursive child of TreeGrid and is used to display hierarchical data in tabular format.
@TreeGridChild
should decorate a field having a collection/array with a defined type. The defined type is treated as the definition for the row data in the rendered HTML table. This is referred to as the collection element type. The type should ALWAYS match the collection element type of the parent field decorated with @TreeGrid
and consequently, the allowed children are the same as the allowed children of TreeGrid.
@TreeGrid
@Path(linked = false)
@Config(url = "<!#this!>/.m/_process?fn=_set&url=/p/pethistory/_search?fn=example")
private List<PetHistoryLineItem> treegrid;
@MapsTo.Type(PetHistory.class)
@Getter @Setter
public static class PetHistoryLineItem {
@GridColumn
@Path
private String name;
@TreeGridChild
@Path
private List<PetHistoryLineItem> children;
}
ViewRoot
ViewRoot is the entry point for a view domain definition.
None.
@Domain(value = "sampleview", includeListeners = { ListenerType.websocket }, lifecycle = "sampleview")
@MapsTo.Type(SampleView.class)
@Repo(value = Database.rep_none, cache = Cache.rep_device)
@ViewRoot(layout = "sampleviewlayout")
@Getter @Setter
public class VRSampleView { ... }
5.2. Examples
5.2.1. Conditional Configuration Examples
The following samples showcase how to apply common conditional scenarios using the framework’s @Conditional annotations.
@TextBox
@ParamContext(enabled = false, visible = true)
private String textbox;
@TextBox(postEventOnChange = true)
@EnableConditional(when = "state == 'Yes'", targetPath = { "/../textbox" })
private String condition;
@TextBox
private String textbox;
@TextBox
@NotNull
private String textbox;
@TextBox(postEventOnChange = true)
@ValidateConditional(when = "state == 'Yes'", targetGroup = ValidationGroup.GROUP_1.class })
private String condition;
@TextBox
@NotNull(groups = { ValidationGroup.GROUP_1.class })
private String textbox;
@TextBox(postEventOnChange = true)
@ValidateConditional(when = "state == 'Yes'", targetGroup = ValidationGroup.GROUP_1.class })
private String condition;
@TextBox
@NotNull(groups = { ValidationGroup.GROUP_1.class })
@Pattern(regexp = "Hello (.*)!", groups = { ValidationGroup.GROUP_1.class })
private String textbox;
@TextBox
private String textbox;
@TextBox(postEventOnChange = true)
@ValidateConditional(when = "state == 'Yes'", targetGroup = ValidationGroup.GROUP_1.class })
private String condition;
@ComboBox(postEventOnChange = true)
@NotNull(groups = { ValidationGroup.GROUP_1.class })
@ValuesConditional(target = "../condition", condition = {
@Condition(when = "state=='Yes'", then = @Values(YES_VALUES.class)),
@Condition(when = "state=='No'", then = @Values(NO_VALUES.class))
})
private String combobox;
// Editable by default
@TextBox
private String textbox1;
// Not Editable by default - Method 1 (Preferred)
@TextBox
@ParamContext(enabled = false, visible = true)
private String textbox2;
// Not Editable by default - Method 2
@TextBox
@EnableConditional(when = "false", targetPath = "../")
private String textbox2;
@TextBox(postEventOnChange = true)
@EnableConditional(when = "state == 'Yes'", targetPath = { "/../textbox" })
@ValidateConditional(when = "state == 'Yes'", targetGroup = ValidationGroup.GROUP_1.class })
private String condition;
@TextBox
@NotNull(groups = { ValidationGroup.GROUP_1.class })
private String textbox;
@TextBox(postEventOnChange = true)
@EnableConditional(when = "state == 'Yes'", targetPath = { "/../textbox" })
@ValidateConditional(when = "state == 'Yes'", targetGroup = ValidationGroup.GROUP_1.class })
private String condition;
@TextBox
@NotNull(groups = { ValidationGroup.GROUP_1.class })
@Min(10)
private String textbox;
@TextBox(postEventOnChange = true)
@EnableConditional(when = "state == 'Yes'", targetPath = { "/../form" })
private String condition;
@Form
private MyForm form;
@Model @Getter @Setter
public static class MyForm {
// Form implementation here...
}
@TextBox(postEventOnChange=true)
@ActivateConditional(when = "state == 'No'", targetPath = { "../textbox" })
@ValidateConditional(when = "state == 'Yes'", targetGroup = GROUP_1.class)
private String condition;
@TextBox
@NotNull(groups = { GROUP_1.class })
private LocalDateTime textbox;
@TextBox(postEventOnChange=true)
@ValidateConditional(when = "state != null && state != 'Yes'", targetGroup = GROUP_1.class)
private String condition;
@TextBox
@NotNull(groups = { GROUP_1.class })
private String textbox;
@TextBox(postEventOnChange=true)
@EnableConditional(when = "state != null && state != 'Yes'", targetGroup = GROUP_1.class)
private String condition;
@TextBox
@NotNull(groups = { GROUP_1.class })
private String textbox;
@TextBox(postEventOnChange=true)
@ActivateConditional(when = "state!= null && state != 'Yes'", targetGroup = GROUP_1.class)
private String condition;
@TextBox
@NotNull(groups = { GROUP_1.class })
private String textbox;
5.2.2. _replace Examples
The following samples showcase how to apply common scenarios using the framework’s _replace action.
The following class will be used in the _replace
scenarios to follow.
@Domain("sample_entity")
@Getter @Setter
public class SampleEntity {
private String single_element;
private List<String> collection;
}
The following scenarios will also assume that the param at sample_entity:1 has a pre-existing state of:
{
"single_element": "empty_value",
"collection": ["empty_value_1"]
}
Replacing a single element state
@Config(url = "sample_entity:1/single_element/_replace?rawPayload='hello'")
{
"single_element": "hello",
"collection": ["empty_value_1"]
}
As the path in this scenario is pointing to sample_entity:1/single_element
, only the field single_element
is subject to be replaced with the rawPayload
. The value for the field of collection
is unaffected and retains it’s previous value.
Replacing a collection element state
@Config(url = "sample_entity:1/collection/0/_replace?rawPayload='hello'")
{
"single_element": "empty_value",
"collection": [ "hello" ]
}
As the path in this scenario is pointing to sample_entity:1/collection/0
, only the collection element having index 0
for the field collection
is subject to be replaced with the rawPayload
. The value for the field of single_element
is unaffected and retains it’s previous value.
Replacing a collection state
@Config(url = "sample_entity:1/collection/_replace?rawPayload='[\"1\", \"2\"]'")
{
"single_element": "empty_value",
"collection": [ "1", "2" ]
}
As the path in this scenario is pointing to sample_entity:1/collection
, only the field collection
is subject to be replaced with the rawPayload
. The value for the field of single_element
is unaffected and retains it’s previous value.
Replacing a complex object
@Config(url = "sample_entity:1/_replace?rawPayload='{\"single_element\": \"hello\"}'")
{
"single_element": "hello"
}
As the path in this scenario is pointing to sample_entity:1
, both fields are subject to be replaced with the rawPayload
. In this scenario, only single_element
has been provided which results in collection
having a null
value.
Note
|
The important thing to note in this scenario, is that like all other scenarios the existing state is replaced. This means that if collection of the entity located at sample_entity:1 had an existing value, it will be set to null since the rawPayload parameter did not explicitely set it.
|
Replacing an entity state with the value of another entity state
@Config(url = "sample_entity:1/single_element/_replace?rawPayload=<!json(PATH_TO_PARAM)!>)
where PATH_TO_PARAM
is path which points to another param, relative to the location of single_element
.
As the path in this scenario is pointing to sample_entity:1/single_element
, only the field single_element
is subject to be replaced with the rawPayload
. The value to be set will be the same as the state of the object identified by PATH_TO_PARAM
.
Tip
|
This scenario can be used to copy a param’s state from one param to another. |
Replacing an entity state with post data
@Config(url = "sample_entity:1/single_element/_replace")
"hello"
{
"single_element": "hello",
"collection": ["empty_value_1"]
}
As the path in this scenario is pointing to sample_entity:1/single_element
, only the field single_element
is subject to be replaced with the HTTP post data. The value for the field of collection
is unaffected and retains it’s previous value.
Replacing an entity state to null
@Config(url = "sample_entity:1/single_element/_replace)
-
Assumes post data is not present
{
"collection": ["empty_value_1"]
}
As the path in this scenario is pointing to sample_entity:1/single_element
, only the field single_element
is subject to be replaced with the HTTP post data. In this case the post data is null
so the resulting value for single_element
will then be null
. The value for the field of collection
is unaffected and retains it’s previous value.
5.2.3. _update Examples
The following samples showcase how to apply common scenarios using the framework’s _update action.
The following class will be used in the _update scenarios to follow.
@Domain("sample_entity")
@Getter @Setter
public class SampleEntity {
private String single_element;
private List<String> collection;
}
The following scenarios will also assume that the param at sample_entity:1 has a pre-existing state of:
{
"single_element": "empty_value",
"collection": ["empty_value_1"]
}
Updating a single element state
@Config(url = "sample_entity:1/single_element/_update?rawPayload='hello'")
{
"single_element": "hello",
"collection": ["empty_value_1"]
}
As the path in this scenario is pointing to sample_entity:1/single_element
, only the field single_element
is subject to be updated with the value of rawPayload
. The value for the field of collection
is unaffected and retains it’s previous value.
Updating a collection element state
@Config(url = "sample_entity:1/collection/0/_update?rawPayload='hello'")
{
"single_element": "empty_value",
"collection": ["hello"]
}
As the path in this scenario is pointing to sample_entity:1/collection/0
, only the collection element at index 0
for the field collection
is subject to be updated with the value of rawPayload
. The value for the field of single_element
is unaffected and retains it’s previous value.
Updating a collection state
@Config(url = "sample_entity:1/collection/_update?rawPayload='\"new_value\"'")
{
"single_element": "empty_value",
"collection": ["empty_value_1", "new_value"]
}
As the path in this scenario is pointing to sample_entity:1/collection
, only the field collection
is subject to be updated with the value of rawPayload
. Performing an _update call on a collection essentially performs an add operation. The value for the field of single_element
is unaffected and retains it’s previous value.
Tip
|
If needing to replace elements within the collection as a whole, consider using _replace. |
It is also permissible to pass an array as a payload.
@Config(url = "sample_entity:1/collection/_update?rawPayload='[\"new_value_1\", \"new_value_2\"]'")
{
"single_element": "empty_value",
"collection": ["empty_value_1", "new_value_1", "new_value_2"]
}
In this scenario, all of the array elements within rawPayload
would be added to the collection.
Updating a complex object
@Config(url = "sample_entity:1/_update?rawPayload='{\"single_element\": \"hello\"}'")
{
"single_element": "hello",
"collection": ["empty_value_1"]
}
As the path in this scenario is pointing to sample_entity:1
, both fields are subject to be updated with the value of rawPayload
. Performing an _update call in this call preserves any previously existing state for fields not specified in the rawPayload
. Consequently, the value for the field of single_element
is updated, while collection
is unaffected and retains it’s previous value.
Tip
|
Nested domain models can become quite large! Use _update when needing to target specific values within a domain entity. |
Updating an entity state with the value of another entity state
@Config(url = "sample_entity:1/single_element/_update?rawPayload=<!json(PATH_TO_PARAM)!>)
where PATH_TO_PARAM
is path which points to another param, relative to the location of single_element
.
As the path in this scenario is pointing to sample_entity:1/single_element
, only the field single_element
is subject to be updated with the value of rawPayload
. The value to be set will be the same as the state of the object identified by PATH_TO_PARAM
.
Tip
|
This scenario can be used to copy a param’s state from one param to another. |
Updating an entity state with post data
@Config(url = "sample_entity:1/single_element/_update")
"hello"
{
"single_element": "hello",
"collection": ["empty_value_1"]
}
As the path in this scenario is pointing to sample_entity:1/single_element
, only the field single_element
is subject to be updated with the HTTP post data. The value for the field of collection
is unaffected and retains it’s previous value.
5.2.4. Values Configuration Examples
The following samples showcase how to apply common scenarios using the framework’s @Values annotation.
@Values(SampleStaticSource.class)
@CheckBoxGroup
private String petTypes;
In this example, all of the values retrieved from SampleStaticSource.getValues will be displayed as a collection of checkboxes.
@Values(url="CLIENT_ID/ORG/p/staticCodeValue/_search?fn=lookup&where=staticCodeValue.paramCode.eq('/petType')")
@CheckBoxGroup
private String petTypes;
In this example, all of the values retrieved from the url defined in @Values will be displayed as a collection of checkboxes.