publication-search
Note You can view the source files for this project in the intermine/intermine-apps-c repo.
This document will guide you through the process of writing a JavaScript client side app (running completely in a browser) using Bower and Grunt tools. This app will connect to an InterMine instance to run a query. The objective will be to fetch a list of publications for each bio entity found that is like our query.
The libraries we will be using:
- Bower to fetch vendor dependencies such as JavaScript, CSS or Fonts.
- canJS is a framework for client-side development handling routing, events etc.
- CoffeeScript a language that compiles down to JavaScript and makes writing an app easier.
- Foundation is a CSS framework of reusable UI components.
- Grunt to build/transpile our source files.
- jQuery is a DOM manipulation library (and more).
- Mustache is a multi-platform templating language allowing us to embed dynamic objects in HTML.
- Node JavaScript desktop software platform.
- Stylus allows us to be more expressive and dynamic with CSS.
- Lodash is a utility toolbelt making actions such as iterating over items easier.
- imjs used to query InterMines from browser or Node. Saves you having to write raw HTTP requests.
#
Initialize ProjectThe first step will be to setup our directory structure.
build/
Will be the directory where our final app package will live. We will develop in languages like Stylus or CoffeeScript and need a way to package all these resources into one whole directory. This is where all these files will live.
bower_components/
This directory will be automatically created and will contain libraries we have requested through the Bower system.
example/
Contains an example of our app in use.
src/
Source files that our code will consist of.
bower.json
Will contain a listing of libraries we want to download using Bower.
package.json
Lists libraries we will need to compile and build our app with.
#
Node.js platformSince our application is targeting JavaScript in the browser, it is pretty useful if we use JavaScript on our computer (desktop) too. Enter Node which allows us to execute JavaScript on our computers instead of just our browsers.
You can fetch binaries from the homepage or use your (hopefully Linux) packman.
Once Node is installed, edit the package.json
file like so:
This file tells Node which libraries will be used to build our app. These are not client-side libraries, but server-side if you will.
The top bit of the devDependencies
lists a bunch of Grunt and Bower related libraries.
In order to install all of these, execute the following:
#
Bower vendor dependenciesNow we want to fetch libraries that our app, when running, will depend on.
Edit the bower.json
file like so:
The file has a bunch of key-value pairs.
name
Name of our application in the Bower ecosystem, required.
version
Version number in the Bower ecosystem, required.
dependencies
Lists the actual libraries and their versions to fetch. You can populate this list by executing $ bower install jquery --save
for example. This will download the latest version of the jquery
component into the bower_components/
directory. You can search for available components using $ bower search jquery
. To actually trigger a search, execute $ bower install
. The different libraries will be introduced as we code along.
#
Grunt buildingGrunt is used to munge files together and execute commands on them. Create a file called Gruntfile.coffee
:
This file is written in CoffeeScript and lists the tasks to run when we want to build our app. From the top:
apps_c
This directive says that we want to take any CoffeeScript and Mustache files we find in src/
and combine them into one JavaScript package.
stylus
Take a Stylus file and turn it into CSS.
concat
Take our vendor files (installed using Bower) and, together with our app, make them into a bundle. If someone else wants to use our app they need our app and its deps too, so this one file will do it for them. Do the same to CSS too.
uglify
Minify our built JavaScript files. This makes them small, but unreadable, so not great for debugging.
cssmin
The same as uglify
but for CSS
Then we have two calls to grunt.registerTask
which bundle a bunch of tasks together. For example, running $ grunt minify
will run the uglify
and cssmin
tasks.
While developing, it is quite useful to watch the source files and re-run the build task like so:
This will run the default Grunt task every 2s.
#
Source files#
Example pageOne needs an access point where our app will get loaded with particular configuration. This is where the example/index.html
comes in:
This file does not do anything else other then load our built CSS and JS files and starts our app once the page loads. In our example, we are pointing to a build
directory relative to the example
directory. So let's make a symbolic link to the actual build
:
Such links get preserved when version controlling using Git. We are linking to our bundled builds that contain vendor dependencies too.
Then we are waiting for the page to load and call our (future) app with some config.
The name ps
is being configured in the Gruntfile.coffee
file in the apps-c
task.
As for the config:
el
Selector where our app should be displayed.
mine
Points to an InterMine.
The require
call relates to CommonJS. It is one way of loading JavaScript modules. It avoids having to expose all of our functions and objects on the global (window
) object and implements a way of relating between different files. For example, to load a module on the same directory level as me:
#
App indexWe have asked to load an app in our example/index.html
page, now we are going to write the backing code.
The apps-c
task (in Gruntfile.coffee
) contains the following two options:
name
How do we call our app for CommonJS require
call?
main
Contains a path (an index) that will be called when we actually call the require
function.
We have specified that our app index lives in src/app.coffee
, so let's create this file:
Each module (file) in our app needs to export some functionality. When we call require
, we will be getting this functionality.
#
ObservableWe are going to be using canJS which gives us objects that can be observed. What this means is that when their values change, others listening to these changes will be notified. When we want to change their value we call attr
function on them. One such example is where we setup the client. We are passing an object which is set on imjs
,a canMap. Or the line below where we set a symbol on a query
which is a canCompute. The advantage here is that whenever we set a new symbol on query
, anyone else will be told it has changed and do something. This something means to trigger a search.
#
ComponentsBut first, we are require some components in the memory. These are canComponent instances. They wrap some user interface functionality (think widget) and are tied to a DOM tag. Whenever this tag appears on the page, a component gets automatically created with the appropriate template and data. For now, let's just say these need to be loaded before we inject our first template into the page. An example of a tag:
We inject the said template, layout, on the line below. Layout will represent the HTML that is true for our app/page. It will have custom tags in it that automatically get rendered as components (as above).
#
LayoutLet us take a look at the layout template then; in /src/templates/layout.mustache
:
Our app will consist of 3 components:
app-search
A component that will represent our input search field.
app-alert
An alert message showing what state the app is in.
app-table
A table with results of our search.
#
Search componentThe search component will bind the query
to our input field; in /src/components/search.coffee
:
To do so, we require the query
module. It is the same module we have seen in our app index. And then, we are off using the standard canComponent notation. There is:
tag
Which is the custom DOM tag/element for this component. Again, if this tag appears on the page, this component will spring to life.
template
This is the template that will get injected into the tag
.
scope
Ah, the magic. You can either pass in an object of key-value pairs that will be accessible within our template
. A more interesting approach is to return a function that returns said object. Doing so will make this component listen in on any changes in the object. In our example, we are (using slightly convoluted notation) listening to changes to query
, which is a canCompute.
events
Makes this component listen to events in the template and then do something. The syntax is <event>
. In our example, whenever the user has pressed (and raised their finger) from a key on a keyboard, we call a function. This function checks that the key was Enter
and updates the query
.
#
Search templateThe search template just outputs the current value of the query:
We are also giving this field the focus on the page so a user can just start typing.
#
Query moduleWe have been talking about this query
for a while, it is time to write its code; in /src/modules/query.coffee
:
First we require some other modules:
pubs
Will represent our results collection/list.
imjs
A module doing the actual search.
state
Will be told what the state of the app is (for alerts).
We initialize the query to be empty using ''
. If a developer wants to pass an initial query, we have seen the relevant code in app index.
Then we have a function that listens in on our changes. Whenever query changes, this function is triggered. We use it to first say that we are starting a search. Then we actually call the imjs
module to do the search. If all went fine, we inject the new results into the pubs
module.
There are two things that could go wrong:
- The search might not be successfull (mine down, malformed query etc.)
- The results may arrive too late, such as when the user sees the first set of results after asking for another set of results.
Both cases are handled.
#
State moduleIs a canMap that keeps track of the app state; it lives in /src/modules/state.coffee
:
The map has two attributes, one for a type of state we are in [ info|success|warning ]
and the other for the actual message.
#
IMJS moduleThis module will do the actual search on the mine. It is called imjs since it is going to be using the imjs library behind the scenes. We will find it in /src/modules/imjs.coffee
:
At the top, we are defining the query that will be used to run the query. The format is that of an InterMine PathQuery. You can see imjs for syntax and more information. One can generate this syntax by visiting the mine in question, running a query in QueryBuilder and then choosing to export to JavaScript in the Results Table.
Our query will be looking for publications, fetching their bio entities (genes, alleles, proteins etc.) and authors. Authors is a separate collection mapped to a publication.
Then we are using the canMap syntax to define a client
attribute and a search
function. An object can have both attributes and functions defined.
We took care of initializing the client
in app index. In that step, we were intializing the imjs library to use a specific mine, MouseMine in our case.
The search function takes two parameters, a symbol and a callback. The first is the search symbol coming from query
module, the second, a function that will be called when we have errors or results. Hopefully the latter.
We are then using imjs syntax to extend our query
with a constraint on a bio entity symbol, matching our symbol and returning tableRows
.
The remap
function is just formatting the results into a format that is useful to us. In our case, we want to have the following data structure which is conducive to being traversed in a Mustache template:
We are extracting the type of the bio entity matched and creating a nested authors
field.
Once we have the new data we are calling back using the cb
function, it is customary to specify an error as the first argument into said function. Since all is well, we are passing a null
value.
#
Publications listWe still have one module to cover. This is the pubs
we have referred to elsewhere; in /src/modules/pubs.coffee
:
We are using the canList object to store an observable array of values. To be honest, we don't need to use an observable object here, but you may want to, if you are going to be changing values in the array rather than replacing the whole thing outright.
#
Alert componentWhen doing our searches we have decided to keep track of the state of the application. Are we searching? Do we have errors? That sort of thing.
We already wrote a module, a canMap, to represent the data structure. Now we just need to write the canComponent for it.
What it does is that it shows up when app-alert
appears and then displays a template and observes when state
changes.
#
Alert templateEach component needs a template. The alert one will look like this:
What we are saying here is to display a Foundation alert box with a custom type and a text. We use {{{ }}}
to display the text which allows us to use HTML in the text
string and have it unescaped.
#
Results table componentNow that we are searching for and updating pubs
with new data, we have to observe them in a canComponent and render them. It lives in /src/components/table.coffee
:
This will make an array of publications available to us in a template under the pubs
key.
#
Results table templateAs for the template that displays the results; in /src/templates/table.mustache
:
Firstly, we are checking if we actually have any results to speak of. If so, we render a table <tr/> element for each publication.
We can see that {{ #pubs }}
and {{ #authors }}
both represent a for loop.
#
StyleWe are going to wrap up by writing a stylesheet. For this, we are going to use Stylus; in /src/styles/app.styl
:
Stylus allows us to write nested rules, such as when we want to select a table cell, <td/>
in a <table/>
.
At the top, we can see a reference to nib. This will make any of our rules to be generated with browser vendor prefixed, where appropriate and allows us to use shorthand notation for various oft repeated rules.
#
FinThis concludes our application. Running a static web server to view the /example
folder, we are presented with a page that displays our app. Typing a symbol into the input box and pressing Enter
launches a request against MouseMine and if successful, shows us results.
#
Appendix#
pomme.jsWhat we have not covered is the case when we want to embed our app beside other apps on a page. If that were the case, all our CSS rules would start conflicting with other rules on the page. Not to speak of canComponents that may pop up in all kinds of places if we are using the same tags across different apps.
One way to deal with this issue is to make use of the pommejs library. What it does is create a sandbox (using an <iframe/>
) which is isolated from anything else on the page. One would load an app inside one such sandbox and not have to worry about library collusion.
For example, we would create a pure pommejs build in Grunt. In Gruntfile.coffee
, add the following task:
This requires you to have the following task installed:
In order to download the library itself using Bower:
Now, we are copying a bundled version of pommejs into our build directory.
To create this sandbox, we are going to require pommejs instead; in /example/index.html
:
In the section above, we can see a placeholder for a template. In that place we need to return a string which will correspond to the html that needs to be executed within the sandbox. It should look something like this (but as a string!):
So our example index.html
has moved into a string and is being executed inside an iframe.
Refer to the pommejs documentation if you'd like to know how to open a two way communication channel between the parent page and the iframe window.