Breadcrumb
- Sabre Red 360 Software Development Kit Help
- Web Red Apps
- Web Modules
- Examples
- com.sabre.redapp.example3.desktop.form Sample
com.sabre.redapp.example3.desktop.form Sample
To create a web module by using TypeScript, refer to Web Modules - Development Cycle chapter.
To see how to contribute a command helper button, a form popover and template rendering with your Web Module, check out the web-src/formplugin-sabre-sdk-sample-form (TypeScript) and com.sabre.dynamo.example.desktop.form (Eclipse) samples.
All the UI implementation code is under web-src/formplugin-sabre-sdk-sample-form/src/code.
export class Main extends Module { (1)
protected autoExposeClasses = false;
init(): void {
super.init(); (2)
const dto = getService(DtoService);
getService(ExtensionPointService).addConfig('novice-buttons', new WidgetXPConfig(StaticButton, -1000)); (3)
dto.registerDataModel('[.Structure][.ExtensionPoint_Summary][com_sabre_redapp_example3_web_form.SearchResultStruct][0]', SearchResultsModel);
dto.registerDataView(SearchResultsModel, SearchResultsNovice); (4)
const drawer = getService(DrawerService);
drawer.addConfig('search-result', { (5)
details: [ (6)
{
caption: t('Trip Information'),
print: '{{#with drawer-context-model}}' +
'<span class="drawer-detail-caption">' + t('TRIP_ID') + ': </span>' +
'<span class="drawer-detail-value">' + '{{tripId}}' + '</span>' +
'{{/with}}'
},
{
print: '{{#with drawer-context-model}}' +
'<span class="drawer-detail-caption">' + t('CUSTOMER_NAME') + ': </span>' +
'<span class="drawer-detail-value">' + '{{customerName}}' + '</span>' +
'{{/with}}'
}
]
});
}
}
-
Main class has to extend Module class.
-
In init function call init() function from super class.
-
Add button configuration to add a command helper or noivce button. In this sample StaticButon is the button class that extends AbstractBootstrapPopoverButton class.
-
Register any data models and view using DtoService.
-
Add configs to DrawerService for rendering the template when submitting the form.
-
Add details like captions and values that needs to be rendered when panel widget is expanded on template rendering.
StaticButton.ts
Next, create button class StaticButton.ts in case of this sample.
Before creating the class add @CssClass and button as shown below.
@CssClass('com_sabre_redapp_example3_web_form btn btn-default') (1)
@Initial<any>({ (2)
caption: '<i class="fa fa-edit"></i> <span class="hidden-xs dn-x-hidden-0-8">Example</span>',
type: 'default'
})
-
Using @CssClass mixin, add css class name that resembles the eclipse plugin name as your styles all wrapped inside this class name like .com_sabre_redapp_example3_web_form\{…..}
-
Using @Initial mixin add the caption you want to show on button. 'Example' is the caption in this case.
This class extends AbstractBootstrapPopoverButton and initialize StatefulComponentWithSaga by setting the following component options.
export default class StaticButton extends AbstractBootstrapPopoverButton {
private content = new StatefulComponentWithSaga(
{
componentName: 'SamplePopover',
rootReducer,
rootSaga,
rootReactComponent: Form,
parentEventBus: this.eventBus
}
);
-
componentName: string; A name used in Redux dev tools to identify the store.
-
rootReducer: Reducer<State>; See redux docs.
-
rootSaga: any; See redux-saga docs.
-
rootReactComponent: React.ComponentClass<Componentprops>; Base React class to be mounted as root in ReactDOM.render().
-
componentWrapperType: string; Default 'div', but you may need to change to 'li' or 'span' or so.
-
parentEventBus: EventBus; Will be passed to Saga, use a way of bottom =⇒ top communication.
-
middlewares: Middleware[]; Optional, if you ever wanted to use a middleware to plug into Redux flow.
-
Initialize button as shown below and also register any content events.
/**
* Initialize Button
*/
initialize(options: any): void {
super.initialize(options);
this.registerContentEvents();
}
-
In this sample we are registering events for cancel action. Register events with action name and the handler method that should be invoked.
/**
* An example event handler to demonstrate proper side effect handling in React layer.
*
* We implement here child(React) => parent(app) communication pattern
* with Redux-Saga middleware triggering an event on parent's event bus
* when given Redux Action gets dispatched.
*/
private registerContentEvents() {
this.eventBus.on('close-form', this.handleCloseEvent.bind(this));
}
-
When cancel action happens, the action is handled in sagas.ts and when the 'cancel' event is triggered handleCancelEvent is triggerd and in here we are unmounting the React component and doing some cleanup like closing and disposing the popover.
private handleCloseEvent(): void {
this.content.unmount();
this.togglePopover();
this.content.dispose();
}
-
Next step is to create React Component, Action, State, Reducers and Sagas etc that needs to be set in StatefulComponentWithSaga class.
Note: In this sample we are going to create one React component with simple form that has 2 input type elements and 2 action buttons. This is just an example demonstrating the feature, but injecting React+Redux can be done in your own approach and you can simply use exposed class StatefulComponentWithSaga to pass your designed components.
type StateProps = State;
type DispatchProps = {
handleSubmit(event: React.FormEvent<HTMLFormElement>): Action;
handleChange(event: React.ChangeEvent<HTMLInputElement>): Action;
handleCancel(event: React.FormEvent<HTMLButtonElement>): Action;
}
type ComponentProps = StateProps & DispatchProps;
export class Component extends React.Component<ComponentProps> {
render(): JSX.Element { (1)
return (
<div className='com-sabre-redapp-example3-desktop-form-web-module'>
<div className='sample-form-container'>
<form onSubmit={this.props.handleSubmit} ref='form'>
<div className='fields-container'>
<div className='row'>
<div className='form-group col-xs-4 col-sm-4 col-md-4'>
<label className='control-label'>{t('TRIP_ID')}</label>
<input type='text' id='tripId' name='TripId'
title='TripId input'
className='input-field form-control'
placeholder={this.props.tripId}
onChange={this.props.handleChange}/>
</div>
<div className='form-group col-xs-4 col-sm-4 col-md-4'>
<label className='control-label'>{t('CUSTOMER_NAME')}</label>
<input type='text' id='customerName' name='Customer Name'
title='Name input'
className="input-field form-control"
placeholder={this.props.customerName}
onChange={this.props.handleChange}/>
</div>
</div>
</div>
<div className='buttons-container'>
<div className='row'>
<div className='right-buttons'>
<button
className='cancel-button js_form-cancel btn btn-outline btn-success'
onClick={this.props.handleCancel}>Cancel
</button>
<button type='submit'
className='search-button js_form-submit btn btn-success'>
Submit
</button>
</div>
</div>
</div>
</form>
</div>
</div>
);
}
}
const mapStateToProps = (state: State) => state; (2)
const mapDispatchToProps = (dispatch: Dispatch<unknown>): DispatchProps => {
return {
handleSubmit: (event: React.FormEvent<HTMLFormElement>): Action => { (3)
event.preventDefault();
return dispatch({type: 'submit', id: ''});
},
handleChange: (event: React.ChangeEvent<HTMLInputElement>): Action => { (4)
return dispatch({type: 'update', id: event.target.id, value: event.target.value});
},
handleCancel: (event: React.FormEvent<HTMLButtonElement>): Action => {
event.preventDefault();
return dispatch({type: 'cancel', id: ''});
}
};
};
/**
* connect component's state with Redux
*/
export const Form = simpleConnect(
() => mapStateToProps,
() => mapDispatchToProps
)(Component);
-
Simple form as react component
-
Simple scenario, no fancy mapping here, our state is 1:1 with the form props
-
Submit sends an Action which will trigger the saga. Saga will then fire a side effect: an event on the parent event bus
-
Read react d.ts for all available event types, there’re many of them
const INITIAL_STATE: State = {
customerName: 'Provide Customer Name...',
tripId: 'Provide Trip Id...'
};
export const rootReducer: Reducer<State> = (1)
(state: State = INITIAL_STATE, reduxAction: ReduxAction): State => {
const action = reduxAction as Action;
switch (action.type) {
case UPDATE:
let newState = {...state};
newState[action.id] = action.value;
return newState;
case CANCEL:
return INITIAL_STATE;
default: (2)
return state;
}
};
-
Returns a configured root reducer. See Redux docs for a detailed description of the reducers' composition. Reducer is a function that takes current application state, incoming acton and return new state. Mind the immutability, we have to return new state object each and every time.
-
Note that we do not handle 'submit' action at all here, it’ll be passed further by Redux and handled by Saga middleware
export default function* rootSaga(eventBus: EventBus): any {
yield [
submitFormSideEffectSaga(eventBus),
cancelFormSideEffectSaga(eventBus)
];
}
function* submitFormSideEffectSaga(eventBus: EventBus): any { (1)
yield takeEvery(isSubmitAction, submitActionHandler, eventBus);
}
function* cancelFormSideEffectSaga(eventBus: EventBus): any {
yield takeEvery(isCancelAction, cancelActionHandler, eventBus);
}
function isSubmitAction(action: Action): boolean { (2)
return action.type === 'submit';
}
function isCancelAction(action: Action): boolean {
return action.type === 'cancel';
}
export function* submitActionHandler(): any { (3)
const state = yield select();
let commandFlow = cf('NGV://REDAPP/SERVICE/com.sabre.redapp.example3.desktop.form.service.SearchFormExtensionPointService:execute')
.addRequestDataObject(new SearchFormRequestData(state))
.send();
yield put({type: 'cancel'});
}
export function* cancelActionHandler(eventBus: EventBus): any {
yield eventBus.trigger('close-form');
}
-
When a 'submit' action happens, do a side effect: trigger an event on the passed EventBus
-
Predicate used to filter the actions
-
Submit action handler. See below
Refer to https://redux-saga.js.org for more information on handling side effects and middleware.
On Submit action, we are making a CommandFlow call to call back service registered in plugin.xml as extension point.
/**
* Submit action handler
*/
export function* submitActionHandler(): any {
const state = yield select();
let commandFlow = cf(' NGV://REDAPP/SERVICE/com.sabre.redapp.example3.desktop.form.service.SearchFormExtensionPointService:execute')
.addRequestDataObject(new SearchFormRequestData(state))
.send();
// clean and close form after submit
yield put({type: 'cancel'});
}
-
The call back service name registered as flowExtensionPoint in Redapp plugin.xml file
-
Pass the submit form data model as request data object. Create SearchFormRequest.ts interface and SearchFormRequestData.ts class to define the form request structure and .send() to send the command to back end.
-
Dispatch 'cancel' action using 'yield' Saga helper API to close the form and do clean up the store after submitting.
Note: Find more information about Saga Helper at https://redux-saga.js.org/docs/api/#saga-helpers.
Rendering Template:
Create a data model and view classes to handle the response that is returned from the call back service SearchFormExtensionPointService. The data model and view must be registered in Main.ts file as explained in the beginning.
Response data is enveloped and handled by following model classes:
-
src/code/models/SearchResults.ts
-
src/code/ models /Result.ts
Search Results response is drawn by two views:
-
src/code/views/SearchResultsNovice.ts - for whole response
-
src/code/views/SearchResultsNoviceRow.ts - for each response row
SearchResults.ts (model class)
Call back service json response:
"d.Structure": {
"o.ExtensionPoint_Summary": {
"com_sabre_redapp_example3_desktop_form.SearchResultStruct": [{
"searchResult": [{
"tripId": "1234",
"customerName": "Adam"
},
{
"tripId": "5678",
"customerName": "Tom"
},
{
"tripId": "9123",
"customerName": "Sam"
}]
}]
}
}
@Initial<DataOptions>({ (1)
dataRoot: '[d.Structure][o.ExtensionPoint_Summary][com_sabre_redapp_example3_desktop_form.SearchResultStruct][0]'
})
@Initial<AbstractModelOptions>({
autoPropagateData: true,
nonLazyMembers: [
'searchResults' (2)
]
})
export class SearchResults extends EnhancedResponseData { (3)
getSearchResults() { (4)
return this.fromRoot().get<Array<JSON>>('[searchResult]').value().map(function (item: Object) {
return new Result(item);
});
}
}
-
Register the data root, it is the path to call back service json response. once set as data root, you can extract the inner values by using this.fromRoot() in getter methods.
-
Pass on the members so that it can be accessed in handlebar templates. If your method name is getTripId() the member name will be 'tripId' and this can be access in template like {{tripId}}.
-
Class should extend EnhancesResponseData.
-
Extract [searchResult] object and the return the Result object.
-
Create Result.ts model to create getters for tripId and customerName fields.
Backend implementation is in com.sabre.dynamo.example.desktop.form plugin.
-
Create call back service interface and implementation like SearchFormExtensionPointService.java and SearchFormExtensionPointCallbackService.java.
-
Refer to chapter Data Types and Serialization on creating POJO classes with Jaxb mappings.
-
SearchFormRqStruct.java is the POJO for request structure
"o.ExtensionPoint_Summary": {
"com_sabre_redapp_example3_web_form.SearchFormRqStruct": [{
'com_sabre_redapp_example3_web_form.tripId': "1234",
'com_sabre_redapp_example3_web_form.customerName': "TestName"
}]
}
-
SearchFormResultStruct.java and SearchResult.java are the two classes for response structure.
"d.Structure": { "o.ExtensionPoint_Summary": {
"com_sabre_redapp_example3_desktop_form.SearchResultStruct": [{
"searchResult": [{
"tripId": "1234",
"customerName": "Adam"
},
{
"tripId": "5678",
"customerName": "Tom"
},
{
"tripId": "9123",
"customerName": "Sam"
}]
}]
} }
-
Provide JAXB Mapping to the Transformer which is responsible for converting Java object to JSON. Create transformer.properties file in src directory with package and namespace details as shown below.
pkg.com_sabre_redapp_example3_desktop_form=com.sabre.redapp.example3.desktop.form.data
ns.com_sabre_redapp_example3_desktop_form=http://redapp.sabre.com/example3/desktop/form
-
Create service definition under OSGI-INF like SearchFormExtensionPointService.xml file and register this to Service-Component in Manifest.mf.
<?xml version="1.0" encoding="UTF-8"?> <scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
name="com.sabre.redapp.example3.desktop.form.service.SearchFormExtensionPointService">
<implementation
class="com.sabre.redapp.example3.desktop.form.service.impl.SearchFormExtensionPointCallbackService"/>
<service>
<provide interface="com.sabre.redapp.example3.desktop.form.service.SearchFormExtensionPointService"/>
</service>
</scr:component>
-
Update MANIFEST.MF file with dependencies and other configurations.
-
Register call back service in plugin.xml as
<extension point="com.sabre.edge.dynamo.flow.flowextpoint.registry"> <flowExtensionPoint
callbackService="com.sabre.redapp.example3.desktop.form.service.SearchFormExtensionPointService:execute"
extensionPointId="execute" flowId="dynamo.api.executor">
</flowExtensionPoint>
</extension>
-
Register WebModule
<extension point="com.sabre.edge.dynamo.web.module"> <modules>
<module id="formplugin-sabre-sdk-sample-form"/>
</modules>
</extension>
Execution Steps:
-
Include sample plugin in Eclipse run configurations.
-
Run the application.
-
Login to application using EPR, password and PCC.
-
Click on Command Helper button.
-
Click Example button to see the form popover.
-
Populate Search Fields and click Submit to render template.
Example Button and Form Popover:
Results Template: