-
Notifications
You must be signed in to change notification settings - Fork 6
Chaise Dev Guide
This is a guide for people who develop Chaise. Because this is not an exhaustive guide, consider looking through @johnpapa's Angular 1 style guide to understand the spirit and conventions of Angular development.
- Use
controllerAs
syntax instead of$scope
whenever possible and refrain from using$rootscope
- Angular allows for users to define a module and later extend that module with new services, controllers, factories, providers, and so on. These other components should be defined in separate files to avoid having one single
*.app.js
file.
- Use one-time binding for improved performance.
- If you know an Angular expression won't change its value after the first digest (e.g. displaying an ERMrest table name), prepend the binding with
::
to benefit from one-time binding. For more details about this, see the One-Time Binding section of the doc on Angular expressions.
The general guidelines for handling errors in promises are that:
- do not let your handlers "eat" any unhandled errors, always throw an unhandled error so that later catch blocks may handle it;
- always conclude any promise chain with
catch(catch_all)
our if everything else fails error handler.
One style when working with a single promise is to use success, reject, and "catch" handlers. The catch
function is just a syntactic sugar for then(null, function)
where the success handler is not given.
promise.then(
function(result) {
// this handler will get called if the promise was resolved
...
},
function(err){
// this handler will get called if the promise was rejected
...
// as a general practice, conclude by throwing any unhandled error
throw err;
}
).catch(
function(err) {
// this handler will get called if:
// - the success handler threw an exception, or...
// - the previous reject handler threw an exception
// this can be useful for adding some common error handling logic
// for errors that could be raised by either of the previous handlers
...
// again as a general practice, conclude by throwing unhandled error
throw err;
}
).catch(
// always conclude a promise chain with our common catch_all
catch_all
);
The above convention should run logically like the following:
try:
resp = http.method()
if resp.status = 200
handle_success();
else
handle_reject();
except my_errors:
handle_my_errors();
except *
catch_all();
An alternate style for working with a single promise is to use only success and catch handlers. One advantage of this style is that a single reject handler defined in the catch
block will handle both a rejected promise or any exceptions thrown by the success block.
promise.then(
function(result) {
// this handler will get called if the promise was resolved
...
}
).catch(
function(err) {
// this handler will get called if:
// - the original promise was rejected, OR...
// - the success handler threw an exception
...
// as a general practice, conclude by throwing any unhandled error
throw err;
}
).catch(
// always conclude a promise chain with our common catch_all
catch_all
);
One style for handling errors in a promise chain is to first chain all of your success blocks and then conclude with a catch for your particular errors and finally a catch_all if all else has failed. The advantage of this style is that it is relatively easy to read and is logically similar to a try/catch block that has (depending on the language, one or more) catch block(s) at the end.
promise.then(
function(result) {
// handle the first response
...
}
).then(
function(result) {
// handle the next response
...
}
).then(
function(result) {
// handle the next response
...
}
).catch(
function(err) {
// handle any upstream errors from any of the promises being rejected or
// any exceptions thrown within any of the success handlers.
// you might want conditional checks for different categories of error
// conditions. This is actually true of any error handling blocks of course,
// but in this example it is particularly worth noting.
if (...) {
// handle error case 1
...
return; // if you resolved the error, return
}
if (...) {
// handle error case 2
...
return; // if you resolved the error, return
}
// if none of the above error handling blocks resolved the error.
// then make sure to throw the error again, as usual.
throw err;
}
).catch(
// always conclude a promise chain with our common catch_all
catch_all
);
When it is possible to recover from an error, you may want to interleave catch blocks so that subsequent success blocks may execute.
promise.then(
function(result) {
// handle the first response
...
}
).catch(
function(err) {
// handle a recoverable error
...
// if the error could not be recovered from, then you will need to
// throw the error again so that the next reject/catch handler will
// process it, and so that it will skip any success handlers.
throw err;
// if the error was recovered from, you may need to return something
// based on what the next success handler expects. Otherwise you can
// return without a return parameter or technically the function could
// terminate without any return statement (again, so long as the next
// success handler is expecting no required parameters). In this example,
// the next handler expects a result.
return recovered_result;
}
).then(
function(result) {
// handle the next response
...
}
).then(
function(result) {
// handle the next response
...
}
).catch(
function(err) {
// handle any upstream errors that were not previously recovered from.
...
// as usual, if the error could not be handled in this block, throw it again.
throw err;
}
).catch(
// always conclude a promise chain with our common catch_all
catch_all
);
- For event-handling cases (e.g. onClick(), setTimer(), etc.) : call a wrapper function that will wrap your function with catch_all
catcher(call_back_function);
/* Jessie: add the rest of the code snippet here */
function catcher(fn) {
try {
fn();
} catch (e) {
catch_all();
}
}
-
try/catch : always be very specific about what to catch, so the errors will not be eaten up.
-
window.onerror() : by default the error messages are already logged into the console. We decided not to do anything further related to this.
- This old article discussed issues with window.onerror. https://danlimerick.wordpress.com/2014/01/18/how-to-catch-javascript-errors-with-window-onerror-even-on-chrome-and-firefox/
- This article summarizes window.onerror support for each browser and provide an example to wrap a function so the exception can be logged. Maybe we can use the same method? https://blog.sentry.io/2016/01/04/client-javascript-reporting-window-onerror.html
- When writing functionality that can already be found in another part of Chaise, that's usually a good candidate for pulling this functionality out and into the
chaise/common
folder. Instead of refactoring the common functionality out of an app, a good practice is to simply copy the existing functionality into thecommon
folder and then have the existing functionality call the code inchaise/common
. Actual refactoring will occur in a later cycle. This minimizes disruptions to existing apps and encourages code reuse. - An example: I'm working on a Feature X for the RecordEdit app, and this function has been already been written into the Search app. Instead of duplicating the function in both apps, I copy the code for Feature X from Search into
chaise/common
and genericize the function as necessary. In Data Entry, I simply call the code inchaise/common
to get Feature X. In Search, the function's body is replaced with a call to the Feature X function inchaise/common
.
The Navbar fetches the session object for information to display in the Navbar. Each app needs to also individually fetch the session so that we can make sure the session is available before trying to do anything with the reference. NOTE: This was causing a race condition before when we were relying on the session being fetched in the navbar and attaching it to $rootScope.
The purpose of using variables or enumeration is to avoid rewriting (or copy-and-pasting) the same string in multiple places.
- if you need to make a lookup based on a tag name only in one call site, simply use a string (e.g., "tag:isrd...")
- if you have more than one use for the tag name string, but only in one script comprising ERMrest, then define it toward the top of the closure, and keep it local to that closure (e.g., var _tag_default = "tag:...default")
- if you have more than one call site in more than one script, then define the variable in the utilities.js script and add it to the module (e.g., module._tag_default = "tag:...default") and it may then be used by code in different ermrestjs scripts (e.g., referencing module._tag_default somewhere). Note that this is not being added to the public interface. Code outside of the various ermrestjs scripts are not intended to use these variables. Hence we follow the underscore prefix convention _variableName, which by convention indicates that the variable should be considered private to the module and clients are at least warned not to use it.
- Example: The
messageMap
constant can be used to store and display user-facing messages in Chaise.
Ermrestjs#68 contains detail discussion related to this topic.
There are a few naming conventions that are being used across the apps. This pertains to variables, module names, and file names.
- File names should be written in camel case (camelCase) with identifying information separated by
.
(*.controller.js
,*.app.js
,*.html
). - Angular modules need to be defined like the following
chaise.*
. Chaise identifies the set of apps it applies to and the*
is that modules purpose in chaise. - Service, Factory, Provider, Controller, and other angular classes should be defined with camel case text leading with a capital letter. For example:
ErrorDialogController
is the convention for naming controllers. Don't shorten the text toctrl
because we should be using controller as syntax and want to have a more readable structure to our code. - Variables should follow a similar naming convention using camel case text. Variables and functions that are prefixed with an underscore
_
, should be treated as private variables and used with caution. - Folder names should be different from file names. Of course folders don't have an extension so it's more apparent that they are folders, but developers should use
-
separated names for folders, i.e.common\templates\data-link
.
When using <a></a>
tags to trigger in-page functionality instead of linking to an external website, do not use the href
attribute. In the past it may have been common to use <a href="#" onclick="myJsFunc();">Link</a>
or <a href="javascript:void(0)" onclick="myJsFunc();">Link</a>
in order to satisfy validation requirements and other reasons; however, the HTML5 spec now allows anchor tags without href
and instruct browsers to treat these as placeholder hyperlinks.
We want to open the RecordEdit in a new tab from different apps to add new records or edit existing records of some table. We may want to pass the prefill data to the RecordEdit app. Upon submission of the changes, we want to see our originating app/tab updated with the new or modified records without refreshing the whole page. We want to avoid using cookies (sent from/to the server with each http response/request) or local storage (no expiration date and requires clean up).
Parent tab defines function calls on its window object, which are used by the child tab to communicate with. The child tab first calls window.opener
to get a handle of the parent's window object.
- parent defines entry points which consult local hashmaps etc.
-
window.getPrefill(url)
function -
window.childChanged(url, ...)
function
-
- parent sets up hashmap contents using child url as key
- parent launches child (and captures window object to variable)
- child starts to run
- child does conditional prefill startup
- have
window.opener
, or skip prefill - have
window.opener.getPrefill
, or skip prefill prefill = window.opener.getPrefill(child_location)
-
prefill != null
, or skip prefill
- have
- child does remaining app logic
- child does submit/data change
- child does conditional signalling after server update success
- have
window.opener
, or skip signal - have
window.opener.childChanged
, or skip signal window.opener.childChanged(child_location, ...)
- have
- parent can poll (or listen?) for child window close status if needed
- ACLs In ERMrestJS and Chaise
- Facet Examples
- Facets JSON Structure
- Logging
- Model Annotation
- Model-based Logic and Heuristics
- Preformat Annotation Guide
- Export Annotation Guide
- Pseudo-Column Logic & Heuristics
- Table Alternatives
- Intro to Docker
- Chaise Dev Guide
- Dev Onboarding
- ERMrest 101
- ERMrest Howto
- ERMrestJS Dev Guide
- Extend Javascript Array
- Custom CSS guide
- Towards a style guide