Creating a Custom Page
1. Make a New Page
For our custom app, we would like to make a new page to show our results in a graph.
The new page consists of 2 elements: an HTML page controlling the displays, and a TypeScript page controlling the logics. Like with the plugin used to create the data loader, we use a decorator to initialize the new page:
import { route } from '@scifeon/plugins';
@route({
route: 'app/demo-page',
title: 'Demo Page',
})
The above code defines a new page. The route
is the URL extension for the page - the full URL would be something like "http://localhost:5000/#/app/demo-page ", and the title
property is the title of the browser tab.
The route decorator also has other properties such as the chunk
property which changes how the page is loaded and is useful if our page is very data-heavy.
2. Introduction to Aurelia
Aurelia is a front-end framework, designed to easily connect objects in your TypeScript page with graphical elements in your HTML. Here, we will give a brief introduction to how Aurelia works. If you want to learn more, visit their documentation.
In Aurelia, the graphics-bearing HTML page is usually called a "view" while the logics-bearing TypeScript page is called a "view model". To connect the two, the files usually have the same name (but with different extensions) such as "demo-page.html" and "demo-page.ts". The same is true for the class defined in the view model. In our case the class would be defined as:
export class DemoPage {
}
This also makes it easier for other developers to quickly identify which objects belong to which files. Note that the same logic is applied to the route in the decorator.
To make an object in the view model available to the view, it simply has to be directly owned by the class (being accessible with the this
keyword). Any data type is applicable.
export class DemoPage {
displayMe: any;
bindMe: string;
constructor() {
this.displayMe = {text: "Hello World"}
this.bindMe = ""
}
displayMe
and this.bindMe
of the DemoPage class are now available in the view:
<template>
<div class="page-panels">
<div class="panel">
<div class="panel-body">
<p>${displayMe.text}</p>
<p><input class="form-control" value.bind="bindMe"/></p>
</div>
</div>
</div>
</template>
Notice that Aurelia pages are wrapped by the <template>
element.
The paragraph elements show how to access data from the view model and how to send it back. The first paragraph uses the ${}
notation. This gives access to objects from the view model. You can also write Typescript expressions between the brackets.
The input field has an additional parameter: the value.bind
binds whatever is typed in the form to the variable name.
In this wway we are able to send data back and forth. Note that the value binding goes both ways: If we had initialized the bindMe
variable to a non-empty string, this would be displayed in the form whenever the page is loaded.
This produces the following on the website:
3. Accessing the New Page - Menu Item
You can access the new page by going to the URL specified in the route decorator - "http://localhost:5000/#/app/demo-page " in the above example.
This can, however, be a bit cumbersome. Instead, we would like to implement a menu point for easy accessibility.
This is done through the contributions.json file. This file should be located in your Scifeon instance folder and contains meta information about custom apps. It can be used for creating new pages without using the decorators, new data types, menu items, and much more.
The structure of the json object can be seen below. The "text"
property is displayed on a mouseover while the "routeId"
is where clicking the button will send you. This should be the same as the route you picked for your custom page.
{
"menu": {
"main": [
{
"text": "Scifeon Demo Page",
"routeId": "app/demo-page"
}
]
}
}
Other properties you can use to customize your menu point:
"iconFA"
lets you pick a custom icon from the Font Awesome Gallery. without this, the icon will simply be the first letter of the"text"
property."rank"
lets you decide where on the sidebar your button is placed with 0 being at the top and 1000 being at the bottom."url"
replaces the routeId and lets you link to other websites."html"
lets you insert a custom html element instead of the default button.
4. Accessing the New Page - Dashboard Panel
Another access point could be a custom element on the dashboard panel such as the one below:
<img src="dashboard_panel.png" class="zoom"
This demo panel is very simple and can be expanded on to, for instance, show information from the database or create new entities.
To make a dashboard panel we once again use the scifeon plugin decorator, this time with the DASHBOARD_PANEL
type and, since we want the panel shown at all times, a match
expression returning true.
Instead of dedicating an entire page for the html, we use the view
function to mark the html piece used to render the dashboard panel:
view() {
return `
<a href="/#/app/demo-page">Click here to enter.</a>
`
}
The function simpy returns the html to be rendered. You can still use all the functionalities of Aurelia notations such as ${}
to use variables from the TypeScript class. In this demo we went for simplicity and the dashboard is just a link to the custom page. You are, however, free to customize however you like.
To change meta data of the panel, or add a header, import the DashboardPanelConfig
class and add a getConfig
function to your class such as shown in the codeblock below:
import { DashboardPanelConfig } from '@scifeon/plugins';
static getConfig(): DashboardPanelConfig {
return {
header: 'Demo App',
rank: 500
};
}
This static method is used to provide meta data such as size and rank of the panel.
5. Custom Page Elements
With multiple access points to our page and the Aurelia framework fresh in our minds, we can begin figuring out what we want on our custom page and how we will build it.
Remember the samples we loaded in the beginning of the tutorial? They represent the pH-value of a solution. The samples are taken in one hour intervals meaning the first is taken at the beginning of the experiment, and the last is taken 19 hours later.
We would like to be able to select an experiment, for instance from a drop down menu, and then visualize its result values in a graphing element. To do this we need a combination of Scifeon functions querying the database, Aurelia elements for rendering and databinding, and a custom Scifeon html elements utilizing the C3 library for visualizing the graph.
6. Database Query
We start out by looking at how to get data back from the database. This is typically done using a database querying function. The basic query looks like this:
import { ServerAPI } from '@scifeon/core';
export class DemoPage {
constructor(private server: ServerAPI) { }
const dataset = await this.server.datasetQuery([
{ eClass: 'class', entity: 'variableName' }
])
...
The ServerAPI
contains functions for altering and viewing the database. We import it into our custom page and use the datasetQuery
function.
The input of the function is a list of objects. Each object is a list of details for a single database query. In our example we are just doing a single query, but the function allows for multiple queries at the same time. The required properties of a query are an eClass
, deciding what kind of data to return, and either an entity
or a collections
property, deciding the output format with the value being the name of the returned object. entity
returns just a single object while collections
returns a list of all objects fitting the query (similar to the JavaScript functions find() and filter()).
There are several other properties that can be useful to narrow down the query. For instance the filter
property, which filters the data, accepting only those who fulfill a certain criteria.
Here's an example of a database query we use in our demo app:
const dataset = await this.server.datasetQuery([
{ eClass: 'Experiment', collection: "experiments", filters: [{ field: 'Type', value: 'DemoExperiment' }]},
])
In the above example we extract all of the entities of the "Experiment" class. These are then filtered so only experiments of type "DemoExperiment" are accepted. These are then saved to the dataset.experiments
list.
You can add multiple filters by adding more objects to the filters
list.
Sometimes when doing a database query, we would also like to base a second query on the results of the first. This can be done using the a $ notation such as in the following example from our demo app:
...
{ eClass: "ResultSet", entity: "rs", filters: [{ field: "OriginID", value: this.step.id}]},
{ eClass: "ResultValue", collection: "values", filters: [{ field: "ResultSetID", value: '$rs.id'}]},
...
First, we find the ResultSet class defined by the ID of the step from which it belongs. We then use the '$rs.id'
to refer to the search just completed to get all the ResultValues belonging to the set.
There are many more ways to build your database query. You can read the full function documentation here.
7. Dropdown Menu
For the demo we have just made a single dataset tied to a single experiment, but in case we had multiple experiments with different results we would like to be able to pick one out for further analysis. This can be done by, for instance, making a drop down menu.
<select value.bind="selectedExperiment" change.delegate="fetchRS()">
<option repeat.for="experiment of experiments" model.bind="experiment">${experiment.name}</option>
</select>
The codeblock above shows the html for a dropdown menu utilizing Aurelia keywords and bindings. Let's walk through each element:
The <select>
element is a drop down menu with each <option>
element being a selectable option.
the repeat.for
is an Aurelia keyword similar to a for
loop in TypeScript: It allows us to iterate through a list element. In our case, we iterate through the "experiments" list. Each option then represents an experiment from the experiments list.
What is actually shown in the drop down menu is found through the Aurelia ${} notation as this allows us to access properties, such as the name, of the experiment object.
When the desired experiment is selected by the user, it is then bound by the model.bind
to the variable written in the value.bind
property of the <select>
element.
The change.delegate
element observes the drop down and, whenever the value is changed, triggers the bound TypeScript function (in this case the fetchRS()
function). This triggers our app to get the result set entities related to the selected experiment and to render the graph with the given data values.
8. Chart Element
Once we have found the data we would like to visualize, all that is left is formatting the points correctly and then sending it on to the chart element in the html.
The chart element in the html looks like this:
<chart type="xy" legends="none" series-group.bind="series" ></chart>
There are several chart types and options for configuring the chart, but for our purposes, this should do the job. The series-group
is bound to the series
variable which has to be an object of a specific structure:
this.series = [{
name: "Demo Series",
content: {
points: this.values.map(v => {
return { valueX: i++, valueY: Number(v.valueText)}
})
}
}]
The points property ends up being a list of {valueX: x, valueY: y}
objects where the x is an incrementing integer matching the number of hours since the experiment begun, and y is the pH-value.
Note that the series object is actually a list capable of taking, and charting, multiple datasets at once.
Once it's working your custom page should look something like this:
9. Finished!
You finished the tutorial! You are on the verge of becoming a true Scifeon developer!
If you would like to compare your app to the one we made you can download it here.