Web Developer training - Day two
This day covers:
- Complex forms and data entry components through Orders screen
- Introduction to Genesis Foundation Comms lib
Orders screen
Let's continue the development of our web app creating an order screen. We're going to work on these files:
- order.template.ts
- order.ts
- order.styles.ts
You should have created these files in the last exercise of the previous day of the training with the navigation bar pointing to them as well.
Now, let's replace the dummy content of these files with the actual implementation we want.
Requirements
The goal of our app is to list all the orders with some filters and actions to insert a new order, edit an existing order and cancel an order.
Fields
Field | Type | Editable | Notes |
---|---|---|---|
Instrument | Select or Search (autocomplete field) | Yes | Load data from ALL_INSTRUMENTS Data Server |
Market data | Display price of the selected symbol | No | Load data from INSTRUMENT_MARKET_DATA ReqRep |
Quantity | Integer | Yes | Must be positive |
Price | Double | Yes | Must be positive |
Total | Double | No | Display Quantity * Price |
Direction | Dropdown | Yes | Display types from ENUM DIRECTION |
Notes | String | Yes | Free text up to 50 chars |
Actions
Insert, edit and cancel.
Adding the new Order modal
Let's start with the simplest way to create a form, using the foundation-form
component:
import {html} from '@genesislcap/web-core';
import type {Order} from './order';
export const OrderTemplate = html<Order>`
<foundation-form
class="order-entry-form"
resourceName="EVENT_ORDER_INSERT">
</foundation-form>
`;
This component is able to retrieve the meta-data from the EVENT_ORDER_INSERT
backend resource (an Event Handler) and automatically builds a simple form for you. In simple scenarios, it can be good enough.
Try to run it now and you'll notice that, even though the form is displayed, nothing happens when you click on Submit. We have to bind the submit button to a function, like this:
<foundation-form
class="order-entry-form"
resourceName="EVENT_ORDER_INSERT"
@submit=${(x, c) => x.insertOrder(c.event as CustomEvent)}>
</foundation-form>
This is related to binding as we briefly explained in the previous day. If it's still unclear, make sure to check Understanding bindings and Events
We define insertOrder
function in order.ts
...
import {Connect} from '@genesislcap/foundation-comms';
import {logger} from '../../utils';
...
export class Order extends GenesisElement {
@Connect connect: Connect;
constructor() {
super();
}
public async insertOrder(event) {
const formData = event.detail.payload;
const insertOrderEvent = await this.connect.commitEvent('EVENT_ORDER_INSERT', {
DETAILS: {
INSTRUMENT_ID: formData.INSTRUMENT_ID,
QUANTITY: formData.QUANTITY,
PRICE: formData.PRICE,
DIRECTION: formData.DIRECTION,
NOTES: formData.NOTES,
},
});
logger.debug('EVENT_ORDER_INSERT result -> ', insertOrderEvent);
}
}
Introducing Genesis Foundation Comms lib
As you can see in the insertOrder
code, we are importing Connect
from @genesislcap/foundation-comms
, which is Genesis core communication system with the server.
You can use the foundation-comms in any modern web app, based on FAST or not. This gives you full flexibility on how to interact with the server without, necessarily, relying on the UI components provided.
Alternatively, you could use any HTTP client to access the server resources as they are exposed as HTTP endpoints as well. However, we strongly encourage the use of Foundation Comms as it handles things like the web socket connection, authentication and authorization, data subscription and so on.
One of the key objects provided by the Foundation Comms is the Connect
object whose main methods are:
-
connect
: connects to the server through a web socket (when WS is available or http as fallback). You must pass the server host URL. In most apps, such as the one we're building in this training, the connection is already handled by the MainApplication component on initialisation relying on the config provided by the app. -
commitEvent
: use it to call event handlers on the server. You must pass the name of the event and an object with the input data required by the event. This data must be in JSON format with key DETAILS. See the example above of theinsertOrder
function. -
getMetadata
: it retrieves the metadata of a resource, that can be an event handler, data server query or a request server. When we used the foundation-form component previously, for example, it used internally getMetadata passing the event handler name to get all the input fields of the event. -
request
: use it to call a request server resource. You must pass the request server resource name. -
snapshot
andstream
: use them to get a snapshot of data or to stream data in real time from a resource (usually, a data server query).
Those are the most common features from Foundation Comms you will use. We're going to use most of them and give more practical examples throughout the training. However, please note that there are more components provided by Foundation Comms such as Auth, Session, User, Analytics. Feel free to import these components and explore their methods to get a sense of what's provided.
Creating a custom form
Using foundation-form
is good for simple forms or prototyping, but it is probably not enough for our use case - we need much more customisation.
To achieve this, you must create each form element manually, and take care of storing user data that the user inputs.
Start by adding elements to the template:
export const OrderTemplate = html<Order>`
<div class="row-split-layout">
<div class="column-split-layout">
<span>Instrument</span>
<zero-select>Instrument</zero-select>
<label>Last price</label>
<zero-text-field type="number">Quantity</zero-text-field>
<zero-text-field type="number">Price</zero-text-field>
<label>Total</label>
<zero-select>Direction</zero-select>
<zero-text-area>Notes</zero-text-area>
</div>
</div>
`;
Add to your order.styles.ts
the following, so you get a nice look on your forms
import {css} from "@genesislcap/web-core";
import { mixinScreen } from '../../styles';
export const OrderStyles = css`
:host {
${mixinScreen('flex')}
align-items: center;
justify-content: center;
flex-direction: column;
}
.column-split-layout {
text-align: center;
flex-direction: column;
flex: 1;
width: 100%;
}
.row-split-layout {
justify-content: center;
display: block;
flex-direction: row;
flex: 1;
width: 100%;
height: 50%;
}
zero-select, zero-text-area, span{
display: block;
}
`
We're just showing the relevant code for the functionality we're building, with an example of customisation. Feel free to surround the elements with div
, or to use any other resource to make your form look better. For that, you will only need some css styling knowledge.
Then, define the variables that will hold the values that are entered.
In the file order.ts, add the following properties to the class: Order
:
import {customElement, GenesisElement, observable} from '@genesislcap/web-core';
...
@observable public instrument: string;
@observable public lastPrice: number;
@observable public quantity: number;
@observable public price: number;
@observable public direction: string;
@observable public notes: string;
...
Now we need to add event handlers that respond to user changes and store the data that the user has input.
We can do it in the traditional way by adding @change
event handler or we can use the sync
directive from Genesis Foundation Utls that would do that for us.
Let's add it to each form element:
import {html} from '@genesislcap/web-core';
import type {Order} from './order';
import { sync } from '@genesislcap/foundation-utils';
export const OrderTemplate = html<Order>`
<span>Instrument</span>
<zero-select :value=${sync((x)=> x.instrument)}></zero-select>
<span>Last price: ${(x) => x.lastPrice}</span>
<zero-text-field :value=${sync((x)=> x.quantity)}>Quantity</zero-text-field>
<zero-text-field :value=${sync((x)=> x.price)}>Price</zero-text-field>
<span>Total: ${(x) => x.quantity * x.price}</span>
<span>Direction</span>
<zero-select :value=${sync((x)=> x.direction)}>Direction</zero-select>
<zero-text-area :value=${sync((x)=> x.notes)}>Notes</zero-text-area>
`;
Note that we have also added the calculation of the total field that doesn't require a property in the Order class. It's just an information on the screen that should not be sent to the server.
You probably realized we don't have any options in our select components, so let's fix that now.
Loading data from the server into the select fields
Let's start with instrument field. We want to load the data once Order the component is initialized so, then, the select field can just iterate through the list of instruments loaded from the server.
Order is a Web Component and, as such, it supports a series of lifecycle events that you can tap into to execute custom code at specific points in time. To make the Order component load data on initialisation, we can override one of the lifecycle events called connectedCallback
that runs when the element is inserted into the DOM.
...
export class Order extends GenesisElement {
@Connect connect: Connect;
...
@observable public allInstruments: Array<{value: string, label: string}>; //add this property
constructor() {
super();
}
public async connectedCallback() { //add this method to Order class
super.connectedCallback(); //GenesisElement implementation
const msg = await this.connect.snapshot('ALL_INSTRUMENTS'); //get a snapshot of data from ALL_INSTRUMENTS data server
console.log(msg); //add this to look into the data returned and understand its structure
this.allInstruments = msg.ROW?.map(instrument => ({
value: instrument.INSTRUMENT_ID, label: instrument.INSTRUMENT_NAME}));
}
...
}
If you're not entirely familiar with async function, it is a modern JavaScript function to enable asynchronous behaviour and the await keyword is permitted within it. They enable asynchronous, promise-based behaviour to be written in a cleaner style, avoiding the need to explicitly configure promise chains.
Also, check this practical resource on Async Await.
As you can see, we used connect.snapshot
to retrieve the data from a data server resource called ALL_INSTRUMENTS
. If you wanted to stream data in real time, you could use the connect.stream
method instead. Remember to always use these methods to get data from data server resources.
Once we have the list of instruments from the server we can make use of it in the template file.
To dynamically include list of options we use repeat directive and iterate through the items.
import {html, repeat} from '@genesislcap/web-core';
...
export const OrderTemplate = html<Order>`
<span>Instrument</span>
<zero-select :value=${sync((x)=> x.instrument)}>
${repeat((x) => x.allInstruments, html`
<zero-option value=${(x) => x.value}>${(x) => x.label}</zero-option>
`)}
</zero-select>
...
<zero-text-area :value=${sync((x)=> x.notes)}>Notes</zero-text-area>
`;
You should see the instrument field populated now with the instruments from the server.
Now let's get the direction field sorted. We could just add two static options BUY and SELL like this:
...
export const OrderTemplate = html<Order>`
...
<span>Direction</span>
<zero-select :value=${sync((x)=> x.direction)}>
<zero-option>BUY</zero-option>
<zero-option>SELL</zero-option>
</zero-select>
<zero-text-area :value=${sync((x)=> x.notes)}>Notes</zero-text-area>
`;
However, any changes on the backend would require a change in the options. Wouldn't it be much better if we could just retrieve all direction options from the server? We already know how to get data from a data server resource, now let's use the getMetadata
method from Connect to get some metadata of a field, direction field in our case.
...
@observable public allInstruments: Array<{value: string, label: string}>; //add this property
@observable public directionOptions: Array<{value: string, label: string}>; //add this property
...
public async connectedCallback() {
super.connectedCallback(); //GenesisElement implementation
const msg = await this.connect.snapshot('ALL_INSTRUMENTS');
console.log(msg);
this.allInstruments = msg.ROW?.map(instrument => ({
value: instrument.INSTRUMENT_ID, label: instrument.NAME}));
console.log(this.allInstruments);
const metadata = await this.connect.getMetadata('ALL_ORDERS');
console.log(metadata);
const directionField = metadata.FIELD?.find(field => field.NAME == 'DIRECTION');
this.directionOptions = Array.from(directionField.VALID_VALUES).map(v => ({value: v, label: v}));
}
...
Next, let's just use the repeat directive again to iterate through the directionOptions:
...
export const OrderTemplate = html<Order>`
...
<span>Direction</span>
<zero-select :value=${sync((x)=> x.direction)}>
${repeat((x) => x.directionOptions, html`
<zero-option value=${(x) => x.value}>${(x) => x.label}</zero-option>
`)}
</zero-select>
<zero-text-area :value=${sync((x)=> x.notes)}>Notes</zero-text-area>
`;
Reload your screen and should see the select fields being populated now!
For learning purposes, we are not doing proper error handling in our code.
Things like checking null or empty data from the server, arrays out of bounds etc.
When working on production code, make sure to add those validations.
Loading Market Data
We're still missing the lastPrice field that, based on the instrument selected, must display the corresponding lastPrice.
We have a request server resource (a.k.a reqRep) available on the server called INSTRUMENT_MARKET_DATA
. It takes the INSTRUMENT_ID as input and returns the last price of the given instrument.
We already know how to get data from data servers, now let's see how to get data from a reqRep.
Add this method to the Order class:
...
export class Order extends GenesisElement {
...
public async getMarketData() {
const msg = await this.connect.request('INSTRUMENT_MARKET_DATA', {
REQUEST: {
INSTRUMENT_ID: this.instrument,
}});
console.log(msg);
this.lastPrice = msg.REPLY[0].LAST_PRICE;
}
}
And change the template to make the instrument field like this:
...
export const OrderTemplate = html<Order>`
<span>Instrument</span>
<zero-select :value=${sync((x)=> x.instrument)} @change=${(x) => x.getMarketData()}>
<zero-option :selected=${sync((x) => x.instrument==undefined)}>-- Select --</zero-option>
${repeat((x) => x.allInstruments, html`
<zero-option value=${(x) => x.value}>${(x) => x.label}</zero-option>
`)}
</zero-select>
...
`;
Note that we used the @change
binding to call getMarketData()
when the value selected changed.
We've used console.log to display the data returned from the server so we can get a better understanding of the data structure returned by each kind of resource (data servers, request replies, metadata etc).
Remember that you can also use POSTMAN or any HTTP client to retrieve and analyse the data as we saw in the Developer Training.
Exercise 2.1: using Foundation Comms
30min
Let's revisit our Marketdata component. Make it retrieve all instruments from the server and display all instrument names and their corresponding last prices.
Server resources to be used: ALL_INSTRUMENTS and INSTRUMENT_MARKET_DATA.
Sending the data
Now when we gathered all the data we're ready to send it over the wire:
Let's add a simple button with click event handler:
...
export const OrderTemplate = html<Order>`
...
...
<zero-text-area :value=${sync((x)=> x.notes)}>Notes</zero-text-area>
<zero-button @click=${(x)=> x.insertOrder()}>Add Order</zero-button>
</div>
`;
Then let's amend our insertOrder function to work with the custom form now:
...
export class Order extends GenesisElement {
...
public async insertOrder() {
const insertOrderEvent = await this.connect.commitEvent('EVENT_ORDER_INSERT', {
DETAILS: {
INSTRUMENT_ID: this.instrument,
QUANTITY: this.quantity,
PRICE: this.price,
DIRECTION: this.direction,
NOTES: this.notes,
},
});
console.log(insertOrderEvent);
}
...
}
Reload your screen and try to insert a new order. For now, just check your browser console and see if you find the result of the insertOrder()
call.
Let's improve our screen a little bit and add a simple success or error message based on the result from the EVENT_ORDER_INSERT
event to showcase how to handle the response from the server.
...
export class Order extends GenesisElement {
...
public async insertOrder() {
const insertOrderEvent = await this.connect.commitEvent('EVENT_ORDER_INSERT', {
DETAILS: {
INSTRUMENT_ID: this.instrument,
QUANTITY: this.quantity,
PRICE: this.price,
DIRECTION: this.direction,
NOTES: this.notes,
},
});
console.log(insertOrderEvent);
if (insertOrderEvent.MESSAGE_TYPE == 'EVENT_NACK') {
const errorMsg = insertOrderEvent.ERROR[0].TEXT;
alert(errorMsg);
} else {
alert("Order inserted successfully.")
}
}
...
}
Adding a simple Orders data grid
In the template file, let's add the Genesis data source pointing to the ALL_ORDERS
resource and wrap it in grid-pro.
Add this code to the end of html template code:
...
export const OrderTemplate = html<Order>`
...
<div class="row-split-layout">
<zero-grid-pro>
<grid-pro-genesis-datasource
resource-name="ALL_ORDERS"
order-by="ORDER_ID">
</grid-pro-genesis-datasource>
</zero-grid-pro>
</div>
...
`;
This will result in a grid displaying all the columns available in the for the ALL_ORDERS
resource.
Take a moment to play around, insert new orders and see the orders in the grid.
Exercise 2.2: customizing order entry further
30min
Implement these changes in the order entry form:
- There's a field ORDER_ID in the ORDER table which is generated automatically by the server. However, if a value is given, it will use the given value instead. Generate a random value on the frontend and pass the value to the EVENT_ORDER_INSERT event.
- Fields instrument, quantity and price are mandatory on the server. Whenever a null or empty value is passed, make sure to capture the error response from the server and paint the missing field label in red.
To generate the ORDER_ID value you can use Date.now()
Exercise 2.3: revamp the Trade screen
60min
Remember the Trade screen from the Developer Training? Rebuilt it now using a custom form like we did with the Order screen instead of using the entity-management micro frontend. Make sure to populate the dropdown fields and handle the server response as well.