-
Notifications
You must be signed in to change notification settings - Fork 4
cdo.form_and_model_generators
org.nasdanika.cdo
bundle provides several classes for generating Bootstrap/AngularJS/KnockoutJS forms and models from EClass and EOperation metadata
and annotations:
-
FormGeneratorBase - Abstract base class. It has a number of protected methods which can be overriden by subclasses to fine-tune form
generation. Default method implementations use metadata such as feature/parameter name and type and annotations to generate forms.
- EClassFormGenerator - Generates HTML/Bootstrap form from EClass metadata and annotations.
- EOperationFormGenerator - Generates HTML/Bootstrap form from EOperation metadata and annotations.
-
AngularJsFormGeneratorBase - Abstract base class for generating forms and models to work with AngularJS.
- AngularJsEClassFormGenerator - Generates HTML/Bootstrap AngularJS form and model from EClass metadata and annotations.
- AngularJsEOperationFormGenerator - Generates HTML/Bootstrap AngularJS form and model from EOperation metadata and annotations.
-
KnockoutJsFormGeneratorBase - Abstract base class for generating forms and models to work with KnockoutJS.
- KnockoutJsEClassFormGenerator - Generates HTML/Bootstrap KnockoutJS form and model from EClass metadata and annotations.
- KnockoutJsEOperationFormGenerator - Generates HTML/Bootstrap KnockoutJS form and model from EOperation metadata and annotations.
- KnockoutJsOverlaidFormGenerator - This class generates a container DIV with an overlay div, a form, and a form handling script.
Annotations used by the generators are:
-
org.nasdanika.cdo.web.html.form-control
for EClass features - attributes and references (by default controls are not generated for references) and for EOperation parameters. This annotation supports the following details keys: -
attribute:<attribute name>
- Allows to specify control attribute. -
group-attribute:<attribute name>
- Allows to specify group attribute. -
control-id
- Control ID, defaults to<feature|parameter name>_control
. -
default
- Default value. If not set then attribute default value is used for attributes. -
help-text
- Help text. No default. In AngularJS generators help text is used to display control-level validation messages. -
inline
- Inline checkbox control if set to true. -
input-type
- One of InputType values. Default value is computed from feature/parameter type bygetInputType()
methods which can be overriden. -
label
- Control label. Defaults to feature/parameter name split by camel case with the first word capitalized and the rest uncapitalized. E.g. a label foruserName
parameter or feature would be "User name". -
placeholder
- Control placeholder for controls which support placeholders. Default is the same as for label. -
private
- Iftrue
then feature/parameter is not included into the generated form. -
required
- Marks the generated control as required if set totrue
. -
style:<style name>
- Allows to customize control style. E.g.style:background
->yellow
-
group-style:<style name>
- Allows to customize group style. -
validator
- Control validator used by AngularJS and KnockoutJS generators. The value of thevalidator
details key shall be a JavaScript function body returning validation message or a promise for a validation message. If the return value is falsey, e.g. undefined or an empty string, then validation is successful, otherwise the return value is displayed as a control-level error message. The function body has access to the control value throughvalue
parameter and to the whole model throughthis
. -
org.nasdanika.cdo.web.html.form
for EClasses and EOperations. This annotation supports the following details keys: -
model
- Object declarations to add to the model definition, e.g. helper functions. -
validator
is used by AngularJS and KnockoutJS generators in generation ofvalidate()
model function. The value of thevalidator
details key shall be a JavaScript function body returning validation message or a promise for a validation message. If the return value is falsey, e.g. undefined or an empty string, then validation is successful, otherwise the return value is displayed as a form-level error message. The function body has access to the model data throughvalue
parameter and to the whole model throughthis
. -
org.nasdanika.cdo:context-parameter
andorg.nasdanika.cdo:service-parameter
- parameters with these annotations are ignored because their values are computed on the server side.
The form control annotation can be populated with a table editor:
AngularJS generators have generateModel()
method which returns JavaScript object definition with the following entries:
-
data
- an object which is either empty or contains default values for features/parameters. Form controls are bound to this object's entries. For existing object replace the generateddata
entry with the JavaScript API object. -
createData()
- a function returning a fresh data object either empty or with default values. -
clear()
- this function sets model data to a fresh object and empties validation results. -
validationResults
- an empty object to hold validation messages for controls. -
validate()
- a function which invokes validators for controls and the form and returns a promise of boolean value. If the promise is resolved withtrue
then the form is valid. -
apply(target)
- this function is generated byAngularJsEOperationFormGenerator
. It invokes the target passing model data as arguments in the order in which they are defined in the ECore model. Iftarget
is an object with<operation name>
function, then that function is invoked, otherwisetarget
assumed to be a function to be invoked. -
validateAndApply(target)
- this function is generated byAngularJsEOperationFormGenerator
. It invokesvalidate()
. If validation is successful, then it invokesapply(target)
.validateAndApply
returns a promise resolved with return value ofapply()
or rejected with{ validationFailed: true[, validationResults: ...] }
if client side or server side validation failed, or{ targetInvocationError: <target rejection reason> }
. This function expectstarget
function to return a promise, which is the case for JavaScript API functions generated for model objects' EOperations.
KnockoutJS generators have generateModel()
method which returns JavaScript constructor function definition with the following entries:
-
data
- a property with getter and setter. The setter copies entries from the value to observables, the getter returns an object with its properties having getters and setters bound to observabled. -
observableData
- an object with observables linked todata
entries. Form controls are bound to this object's entries. -
clear()
- this function sets model data undefined/default values and empties validation results. -
validationResults
- a property similar todata
setting and returning validation results. -
observableValidationResults
- an object with observables to which form error messages are bound. -
validate()
- a function which invokes validators for controls and the form and returns a promise of boolean value. If the promise is resolved withtrue
then the form is valid. -
apply(applyTarget)
- For EOperations it invokes
applyTarget
passing model data as arguments in the order in which they are defined in the ECore model. IfapplyTarget
is an object with<operation name>
function, then that function is invoked, otherwiseapplyTarget
assumed to be a function to be invoked. - For EClasses it sets
applyTarget
properties to data entries, then invokesapplyTarget.$store
function, and returns its return value.
- For EOperations it invokes
-
validateAndApply(applyTarget)
- Invokesvalidate()
. If validation is successful, then it invokesapply()
.validateAndApply
returns a promise resolved with return value ofapply()
or rejected with{ validationFailed: true[, validationResults: ...] }
if client side or server side validation failed, or{ targetInvocationError: <target rejection reason> }
. This function expectsapplyTarget
function to return a promise, which is the case for JavaScript API functions generated for model objects' EOperations and$store
function. -
loadModel(source)
is generated for EClasses only. It loads data fromsource
to observables and can be invoked to refresh views.
Nasdanika WebTest Hub user management application consists of a table listing existing users, Add button to create users, and a dialog for creating/updating users.
The create/update dialog has a different title depending on whether it shown to create or update a user. In update mode login text input is disabled. If authentication is "Password" then "Password" and "Password confirmation" fields are displayed and are required in create mode.
The dialog model validates that a login for a new user does not already exists and that password and password confirmation match in the authentication mode is "Password".
In this application the dialog and the model are generated from a createOrUpdateUser()
EOperation metadata and annotations:
.
The application template is generated by the usersApp() method, which uses AngularJsEOperationFormGenerator (line 725) to generate a form (line 732) and a model, which is passed to the controller generator (line 745).
When the controller is loaded data into the table is retrieved from userList
hub
property generated from userList
operation with getter
annotation:
After that the table is updated with data returned by deleteUser()
and createOrUpdateUser()
functions.
In the controller template
the form model is injected into $scope
at line 41.
The controller's createOrUpdateUser()
function (line 57) is invoked on form submission.
$scope.createOrUpdateUser = function() {
showFormOverlay();
$scope.userModel.validateAndApply(hub).then(
function(userList) {
$scope.$apply(function($scope) {
$scope.userList = userList;
hideFormOverlay();
jQuery("#createUpdateUserFormModal").modal('hide');
});
},
function(reason) {
if (reason.validationFailed) {
$scope.$apply();
} else {
console.trace(reason);
alert(reason.targetInvocationError ? reason.targetInvocationError : reason);
}
hideFormOverlay();
}).done();
}
The function displays an overlay div over the form to provide a visual cue that there is a server interaction in progress and to prevent
user interaction with the dialog. Then it invokes generated validateAndApply()
function with hub
argument. validateAndApply
returns
a promise. If the promise is resolved, $scope.userList
is set to the promise fulfillment value. If the promise is rejected due to
validation problems, then $scope.$apply()
is invoked to display error messages. Otherwise, an alert is displayed.
validateAndApply()
invokes (also generated) hub.createOrUpdateUser()
, which in turn invokes Hub.createOrUpdateUser()
operation
on the server.
In WebTest Hub user registration can be implemented with KnockoutJS form generator or with KnockoutJsOverlaidFormGenerator. User registration story uses two operations:
-
registrationForm
is a route operation which renders a registration form. -
register
processes user registration and is invoked through the JavaScrpt API.
org.nasdanika.cdo.security:permissions
annotation on Guest class specifies that story/registration
permission implies the following permissions
-
GET/registrationForm
- allows to perform HTTP GET on theregistrationForm
route -
invoke/register
- allows to invokeregister
function through JavaScrpt API -
extension/js
- allows to use JavaScript API
registrationForm
operation is annotated as a route and its parameter is annotated as a context parameter. This operation uses Knockout operation form
generator to generate a form and a model:
HTMLFactory htmlFactory = context.adapt(HTMLFactory.class);
KnockoutJsOverlaidFormGenerator generator = new KnockoutJsOverlaidFormGenerator(
HubPackage.eINSTANCE.getGuest__Register__WebSocketContext_String_String_String_String_String(),
htmlFactory,
context.getObjectPath(GuestImpl.this),
"window.location.href = value;",
"window.history.back();");
args[0].contentPanel(generator.generateSpinnerOverlaidFormContainer(Spinner.spinner));
The generator generates a form, an overlay DIV, and a script to handle the form. When the form is submitted, the overlay is displayed over the form until a response comes from the server side. If the server responds with an error, it is displayed in the form header. Otherwise the response value is used to navigate the browser to the location returned by the server.
If the user clicks the "Cancel" button, the browser navigates to the previous page.
This approach is more involved and shall be used in situations when the simpler approach described above is not applicable, e.g. if the viewModel contains not only the form, but also other elements.
KnockoutJsEOperationFormGenerator formGenerator = new KnockoutJsEOperationFormGenerator(
HubPackage.eINSTANCE.getGuest__Register__WebSocketContext_String_String_String_String_String(),
"model",
"submitHandler",
"cancelHandler") {
};
HTMLFactory htmlFactory = context.adapt(HTMLFactory.class);
Form form = formGenerator.generateForm(htmlFactory);
GuestRegistrationGenerator<Context, String> viewModelGenerator = new GuestRegistrationGenerator<Context, String>();
String script = viewModelGenerator.execute(
context,
context.getObjectPath(GuestImpl.this),
formGenerator.generateModel(),
"registrationContainer");
args[0].contentPanel(
htmlFactory.div(
htmlFactory.spinnerOverlay(Spinner.spinner).id("registrationFormOverlay").style("display", "none"),
form).id("registrationContainer"),
htmlFactory.tag(Tag.TagName.script, script));
View model template from which GuestRegistrationGenerator is generated contains form processing logic which delegates to the KnockoutJS form model generated by the form generator:
<%@ jet package="org.nasdanika.webtest.hub.impl" class="GuestRegistrationGenerator" skeleton="Command.skeleton"%>
<%
T moduleName = args[0];
T formHandler = args[1];
T applyId = args[2];
%>
require(["<%=moduleName%>.js", 'q', 'jquery', 'knockout', 'domReady!'], function(guest, Q, jQuery, ko, doc) {
ko.applyBindings({
model: new <%=formHandler%>(undefined, guest),
submitHandler: function(form) {
var overlay = jQuery('#registrationFormOverlay')
overlay.width(form.offsetWidth);
overlay.height(form.offsetHeight);
overlay.css("display", "block");
this.model.validateAndApply().then(function(value) {
if (value.validationResults) {
this.validationResults = value.validationResults;
} else {
window.location.href = value;
}
overlay.css("display", "none");
}.bind(this.model),
function(reason) {
if (reason.targetInvocationError) {
this.validationResults['$this'] = reason.targetInvocationError;
}
overlay.css("display", "none");
}.bind(this.model));
},
cancelHandler: function() {
window.history.back();
}
}, doc.getElementById('<%=applyId%>'));
});
registration()
form control annotations were populated with a UI form table editor:
registration()
implementation performs server-side validations, which could also be defined through server
key of org.nasdanika.cdo.validator
annotation, as
exemplified in passwordConfirmation
parameter. If validations pass, the operation creates a new user and returns path to the Hub home page, which
redirects to the principal home page:
public Object register(
WebSocketContext<LoginPasswordCredentials> context,
final String login,
String name,
String eMail,
final String password,
String passwordConfirmation) throws Exception {
Map<String, Object> ret = new HashMap<>();
Map<String, Object> validationResults = new HashMap<>();
// Server-side explicit validation.
if (login==null || login.trim().length()==0) {
validationResults.put("login", "Login is blank");
}
if (password==null || password.trim().length()==0) {
validationResults.put("password", "Password is blank");
}
if (passwordConfirmation==null || passwordConfirmation.trim().length()==0) {
validationResults.put("passwordConfirm", "Password confirm is blank");
} else if (password!=null && !password.equals(passwordConfirmation)) {
validationResults.put("passwordConfirm", "Passwords don't match");
}
// TODO - min length, strength checks.
if (!validationResults.isEmpty()) {
ret.put("validationResults", validationResults);
return ret;
}
Hub hub = (Hub) eContainer();
CDOLock writeLock = hub.cdoWriteLock();
if (writeLock.tryLock(5, TimeUnit.SECONDS)) {
try {
for (User u: hub.getAllUsers()) {
if (u instanceof LoginPasswordHashUser) {
LoginPasswordHashUser lphUser = (LoginPasswordHashUser) u;
if (lphUser.getLogin()!=null && lphUser.getLogin().equalsIgnoreCase(login)) {
validationResults.put("login", "Login already exists");
ret.put("validationResults", validationResults);
return ret;
}
}
}
org.nasdanika.webtest.hub.User newUser = HubFactory.eINSTANCE.createUser();
newUser.setLogin(login);
//newUser.setName(name); - later
hub.setPasswordHash(newUser, password);
hub.getUsers().add(newUser);
// Permission
Permission permission = SecurityFactory.eINSTANCE.createPermission();
permission.setTarget(newUser); // self-target
permission.setAllow(true);
permission.setName("GET");
permission.setQualifier("/home");
newUser.getPermissions().add(permission);
//((UserImpl) newUser).init();
Principal authenticatedUser = context.authenticate(new LoginPasswordCredentials() {
@Override
public String getPassword() {
return password;
}
@Override
public String getLogin() {
return login;
}
});
if (newUser!=authenticatedUser) {
throw new ServerException("Registration failed - server error");
}
return context.getObjectPath(hub)+".html";
} finally {
writeLock.unlock();
}
}
throw new org.nasdanika.web.ServerException("Cannot acquire write lock");
}
- Create a route method to generate the application template (
usersApp
). - Create data retrieval operation(s), if required. If parameterless, annotate them as getters. In the hub user management example the data
retrieval operation is used because
isAdmin
andauthentication
are computed properties. Otherwisehub.users
reference could have been used. - Create data modification operations. These operations may return the same data as data retrieval operation(s).
- Create a controller template. Controller methods shall delegate validation and data submission to the generated functions.
- Add forms generated from the operations into the application template, and models into the controller.