Examples
The LDK includes several example projects that may be useful to look at to understand how to use various aspects of the SDK.
Below you can find both code snippets and sample Loop code.
You can find all of our examples here. Alternatively, they can also be found in the LDK itself under the ldk/javascript/examples folder.

Code Snippets

To speed up your Loop development, here are code snippets of functionality that our internal development team uses frequently in our own Loops.

1. Reading and validating data from Excel

The following code snippets are examples from a Loop that reads data from each Excel file as its backend, and displays the content in easily navigable Whispers for its frontend.
Code Snippet Purpose: This Loop must first be configured to read data from an Excel sheet at a user-designated filepath. This snippet reads data from the Excel filepath and returns a workable object for subsequent data validation.
1
/**
2
* Read the given Excel file and return a valid Workbook
3
* @param fileData - object containing file path of the excel file in users computer
4
*/
5
static async readInternal(fileData: KnowledgeAreaConfig): Promise<Result<Workbook>> {
6
   try {
7
     /*
8
     * Uses the filesystem aptitude in @oliveai/ldk to read the file path and convert
9
     * the excel contents into an object 'workbook' (e.g. { worksheets: Worksheet[] })
10
     *
11
     * export interface Worksheet {
12
     *  hidden: boolean;
13
     *  hiddenColumns: number[];
14
     *  hiddenRows: number[];
15
     *  name: string;
16
     *  rows: Row[];
17
     * }
18
     */
19
     const bytes = await filesystem.readFile(fileData.path);
20
     const workbook = await document.xlsxDecode(bytes);
21
     return { isOk: true, data: workbook };
22
   } catch (err) {
23
     console.error(err);
24
   }
25
   return { isOk: false, error: 'ReadFileError' };
26
 }
Copied!
Code Snippet Purpose: To validate the Excel data object and ensure it contains the worksheet of interest that will serve as the Loop’s backend.
1
export enum ErrorType {
2
 ExcelSheetValidation = 'Excel Sheet Validation',
3
 ExcelSheetDataValidation = 'Excel Sheet Data Validation',
4
}
5
6
export type DataError = Partial<Record<ErrorType, string>>;
7
8
/**
9
* Reads an Excel workbook and looks for a specific name of worksheet, if it doesn't
10
* contain a matching name it adds to an array of type DataError, otherwise it reads and
11
* parses the worksheet with the custom made Sheet class
12
* @param workbook - Entire Excel file
13
*/
14
export const readWorkbook = (workbook: Workbook): [Array<Record<string, string>>, DataError[]] => {
15
 const rows: Array<Record<string, string>> = [];
16
 const errors: DataError[] = [];
17
 /*
18
  * Looks for any worksheets in the excel file with the tab name 'Content' (CONTENT is
19
  * a constant)
20
  * workbook.worksheets found in the @oliveai/ldk
21
  */
22
 const contentSheets = workbook.worksheets.filter((sheet) => sheet.name === CONTENT);
23
 if (!contentSheets.length) {
24
   errors.push({
25
     'Excel Sheet Validation':
26
       'Cannot find Content tab. Please ensure the tab is named according to the template file and still exists.',
27
   });
28
 } else {
29
   contentSheets.forEach((sheet) => {
30
     // Sheet class seen in the following gist, takes the worksheet name and all the filled rows
31
     const sheetData = new Sheet(sheet.name, sheet.rows);
32
     rows.push(...sheetData.data);
33
     errors.push(...sheetData.errors);
34
   });
35
}
36
};
Copied!
Code Snippet Purpose: To validate the contents of the worksheet to ensure they match this Loop’s data model and can be translated into the Whisper format.
1
export class Sheet {
2
 readonly data: Record<string, string>[] = [];
3
4
 readonly errors: DataError[] = [];
5
6
 /**
7
  * Reads and parses the content worksheet in the excel file and validates the format
8
  *
9
  * @param name - readonly
10
  * @param rows - Excel content tab Rows
11
  */
12
 constructor(readonly name: string, rows: Row[]) {
13
   const headerRow = rows[0].cells;
14
   const dataRows = rows.slice(2);
15
16
   /*
17
    * Runs a utility method verifyHeaders that checks the first row of cells in the
18
    * worksheet
19
    */
20
   const headerError = verifyHeaders(headerRow);
21
   if (headerError) {
22
     this.errors.push(headerError);
23
   }
24
25
   // dataRows = all the rows in worksheet from row 3 down
26
   if (dataRows.length) {
27
     // creates an array of formatted rowObjects, each one to be validated by verifyRequiredData
28
     You are all set. Is there anything else I can do for you?\""}]}
29
     dataRows.reduce((acc, row, counter) => {
30
      // one row in dataRows is formatted like so {"cells":[{"value":"cell1 data"},{"value":"cell2 data"},{"value":"cell3 data"}]}
31
       const rowObject = row.cells.reduce((topic: Record<string, string>, { value }, i) => {
32
         return { ...topic, [headerRow[i].value]: value };
33
       }, {});
34
35
       // +3 to offset zero index and skip first two rows
36
       // You can do some validation here on the contents of the objects in the cell
37
       this.errors.push(...verifyRequiredData(rowObject, counter + 3));
38
39
       acc.push(rowObject);
40
       return acc;
41
     }, this.data);
42
   } else {
43
     this.errors.push({
44
       'Excel Sheet Validation':
45
         'The [Content] tab of your file is empty. Please return to the file and ensure it is populated with content.'
46
     });
47
   }
48
 }
49
}
Copied!

2. Utilizing the Vault Aptitude

The following code snippets are examples from the Healthcare Newsfeed Olive Original Loop that pulls relevant news from an API and surfaces it in Daily Digest Whispers, categorized by healthcare news topic.
Code Snippet Purpose: All Loops need a starting point for you to interact with them. This snippet uses the ui.loopOpenHandler method so that when end users click on this Loop in the dropdown menu, either a DigestionWhisper or SettingsWhisper will appear depending on existing settings in the vault.
1
import { ui, vault } from '@oliveai/ldk';
2
3
import vaultKeys from '../vault/keys';
4
import { DigestWhisper, SettingsWhisper } from '../../whispers';
5
6
/**
7
* openHandler found in the UI aptitude of @oliveai/ldk, Registers a handler function
8
* for the Olive Helps Loop Open Button
9
* In this case depending on settings found in the vault aptitude, will conditionally
10
* open one of these whisper classes
11
*/
12
export const handler = async () => {
13
 if (await vault.exists(vaultKeys.settings)) {
14
   new DigestWhisper().show();
15
 } else {
16
   new SettingsWhisper().show();
17
 }
18
};
19
20
export default {
21
 start: () => ui.loopOpenHandler(handler),
22
};
Copied!
Code Snippet Purpose: End users can configure the Healthcare Newsfeed Loop to deliver the Daily Digest at a preferred time (ie. 12pm for lunch break), this code snippet shows a CRON job to determine if it’s time for the Loop to trigger the Daily Digest Whisper.
1
/**
2
* Calls the above openHandler() function, is used to conditionally determine what
3
* whisper to show after
4
* refreshing and checking the vault every 5 minutes
5
* An example of how to make a make-shift CRON job
6
*/
7
const run = async () => {
8
 openHandler.start();
9
10
 const checkDeliveryTime = async () => {
11
   const now = new Date();
12
   const dailyDigestLastTriggeredBeforeToday =
13
     !(await vault.exists(vaultKeys.dailyDigestLastTriggered)) ||
14
     differenceInCalendarDays(
15
       new Date(await vault.read(vaultKeys.dailyDigestLastTriggered)),
16
       now
17
     ) < 0;
18
   if ((await vault.exists(vaultKeys.settings)) && dailyDigestLastTriggeredBeforeToday) {
19
     const deliveryTimeHasPassed = isBefore(
20
       parse((await getStoredSettings()).deliveryTime, 'h:mm a', now),
21
       now
22
     );
23
     if (deliveryTimeHasPassed) {
24
       new DigestWhisper().show();
25
       await vault.write(vaultKeys.dailyDigestLastTriggered, now.toString());
26
     }
27
   }
28
 };
29
 // Check on launch then check every 5 minutes.
30
 // This timer should run regardless of what happened on launch in case the user left
31
 // OH running overnight
32
 await checkDeliveryTime();
33
 setInterval(checkDeliveryTime, 5 * 60 * 1000);
34
};
Copied!
Code Snippet Purpose: To help end users configure the Healthcare Newsfeed Loop to subscribe to particular news topics of interest (ie. Public Health, Digital Health, etc.). This method stores preferences in the vault.
1
vaultKeys = {
2
 settings: 'settings',
3
 dailyDigestLastTriggered: 'dailyDigestLastTriggered',
4
};
5
6
/**
7
* The Vault aptitude allows Loops to retrieve and store strings in the system's secure
8
* storage
9
* This example shows loop settings configurations being set retrieved and set, so
10
* users do not
11
* have to re-fill out the options.
12
*
13
* Saves preferences for different new topics, allows users to have a personalized newsfeed
14
*/
15
export const getStoredSettings = async () => {
16
 const storedSettingsExist = await vault.exists(vaultKeys.settings);
17
 if (!storedSettingsExist) {
18
   console.log('Stored settings do not exist. Using defaults.');
19
   return getDefaultSettings();
20
 }
21
22
 const vaultValue = await vault.read(vaultKeys.settings);
23
 const storedSettings = JSON.parse(vaultValue);
24
 if (!isSettings(storedSettings)) {
25
   console.error(
26
     'Stored settings are invalid. Using defaults. Settings read from Vault: %s',
27
     vaultValue
28
   );
29
   return getDefaultSettings();
30
 }
31
32
 console.log('Stored settings loaded from Vault.');
33
 return storedSettings as Settings;
34
};
35
36
export const saveSettings = async (settings: Settings) => {
37
 await vault.write(vaultKeys.settings, JSON.stringify(settings));
38
 console.log('Settings saved successfully.');
39
};
Copied!

3. Creating Simple Whispers

The following code snippets are examples of how we create simple Whispers in all our Olive Original Loops.
Code Snippet Purpose: Creating simple default Whispers that don’t need to be updatable.
1
import { whisper } from '@oliveai/ldk';
2
import { stripIndent } from 'common-tags';
3
4
export const onClose = (err?: Error) => {
5
 if (err) {
6
   console.error('There was an error closing Export Default whisper', err);
7
 }
8
 console.log('Export Default whisper closed');
9
};
10
11
/**
12
* + More desired Typescript style
13
* + Allows for a consistent WhisperName.show() format
14
* + Default export can be named however the developer wants
15
* + Default export does not include test-only exports which can be explicitly imported
16
* separately
17
* - Does not support updatable in a clean and easy-to-test way
18
*/
19
export default {
20
 show: async () => {
21
   const markdown: whisper.Markdown = {
22
     type: whisper.WhisperComponentType.Markdown,
23
     body: stripIndent`
24
       This is a simple whisper
25
     `,
26
   };
27
   return whisper.create({
28
     components: [markdown],
29
     label: 'Export Default Whisper',
30
     onClose,
31
   });
32
 },
33
};
Copied!

4. Updatable Whispers

The following code snippets are examples of how we use the updatable Whisper method to display different Loop content without re-triggering new Whispers in our Olive Originals.
Code Snippet Purpose: Update Whispers with different content stored as created Whisper objects
1
import { whisper } from '@oliveai/ldk';
2
3
interface Props {
4
label: string;
5
}
6
7
/**
8
* + Class instances allow more control over updatable props
9
* + Updating whispers requires storing the created whisper object
10
* + Separation of concern for different methods, allows for fine tune control of whisper
11
* + Testing makes more sense without explicit exports solely for testing
12
* - Considered overkill for any whispers that don't need updating
13
*/
14
export default class UpdatableClassWhisper {
15
whisper: whisper.Whisper | undefined;
16
17
label = 'Updatable Class Whisper';
18
19
props: Props = {
20
label: '',
21
};
22
23
// Runs when .close() method is called at the end of this class
24
static onClose(err?: Error) {
25
if (err) {
26
console.error('There was an error closing Updatable Class whisper', err);
27
}
28
console.log('Updatable Class whisper closed');
29
}
30
31
// Creates the UI of what is to be shown on the whisper
32
createComponents = () => {
33
const updatableLabelInput: whisper.TextInput = {
34
type: whisper.WhisperComponentType.TextInput,
35
label: 'Change Whisper Label',
36
onChange: (_error: Error | undefined, val: string) => {
37
console.log('Updating whisper label: ', val);
38
this.update({ label: val });
39
},
40
};
41
42
return [updatableLabelInput];
43
};
44
45
// Method is called to show this whisper UI from other whispers/part of the code
46
async show() {
47
this.whisper = await whisper.create({
48
components: this.createComponents(),
49
label: this.label,
50
onClose: UpdatableClassWhisper.onClose,
51
});
52
}
53
54
// Can pass in additional props, in case this whisper requires data from other whispers
55
update(props: Partial<Props>) {
56
this.props = { ...this.props, ...props };
57
this.whisper?.update({
58
label: this.props.label || this.label,
59
components: this.createComponents(),
60
});
61
}
62
63
close() {
64
this.whisper?.close(UpdatableClassWhisper.onClose);
65
}
66
}
Copied!

5. Testing Component Handlers w/ Jest

The following code snippets are examples of how we test our component handlers with Jest.

settings.test.ts

Code Snippet Purpose: Test Button component handling.
1
/**
2
* When testing component handlers, Breadcrumbs, Link, Button testing can be a bit
3
* tricky thanks to static analysis,
4
* below is an example of testing for the Dropzone Component and TextInput(nameInput).
5
*
6
*/
7
describe('Component Handlers', () => {
8
// Different component types
9
type SettingsComponents = [
10
whisper.Breadcrumbs,
11
whisper.Divider,
12
whisper.Markdown,
13
whisper.Divider,
14
whisper.DropZone,
15
whisper.TextInput,
16
whisper.TextInput,
17
whisper.Box
18
];
19
let breadcrumb: SettingsComponents[0];
20
let div1: SettingsComponents[1];
21
let markdown: SettingsComponents[2];
22
let div2: SettingsComponents[3];
23
let dropzone: SettingsComponents[4];
24
let nameInput: SettingsComponents[5];
25
let descriptionInput: SettingsComponents[6];
26
let pairButtonBox: SettingsComponents[7];
27
let pairButton: whisper.Button;
28
29
beforeEach(() => {
30
/* settingsWhisper.createComponents() returns an array of components we can
31
*destructure and re typecast as SettingsComponents, which allow us to have
32
* access to the .onDrop() method
33
*/
34
[breadcrumb, div1, markdown, div2, dropzone, nameInput, descriptionInput, pairButtonBox] =
35
settingsWhisper.createComponents() as SettingsComponents;
36
[pairButton] = (pairButtonBox as whisper.Box).children as whisper.Button[];
37
});
38
39
describe('Dropzone', () => {
40
it('updates the whisper when dropzone is given a file', () => {
41
const mockFile = {} as whisper.File;
42
43
// You can
44
dropzone.onDrop(undefined, [mockFile], MOCK_WHISPER);
45
46
expect(settingsWhisper.file.value).toEqual(mockFile);
47
expect(MOCK_WHISPER.update).toBeCalled();
48
});
49
50
it('logs an error', () => {
51
dropzone.onDrop(new Error(), [], MOCK_WHISPER);
52
53
expect(console.error).toBeCalled();
54
});
55
});
56
57
describe('Name input', () => {
58
it('updates the whisper when name input is changed', () => {
59
const mockName = 'name';
60
61
nameInput.onChange(undefined, mockName, MOCK_WHISPER);
62
63
expect(settingsWhisper.name.value).toBe(mockName);
64
expect(MOCK_WHISPER.update).toBeCalled();
65
});
66
67
it('clears an error when updated', () => {
68
settingsWhisper.name.error = 'test';
69
70
nameInput.onChange(undefined, '', MOCK_WHISPER);
71
72
expect(settingsWhisper.name.error).toBeUndefined();
73
});
74
75
it('logs an error', () => {
76
nameInput.onChange(new Error(), '', MOCK_WHISPER);
77
78
expect(console.error).toBeCalled();
79
});
80
});
81
});
Copied!

settings.test.ts

Code Snippet Purpose: To test Dropzone and TextInput component handling.
1
/*
2
* Another smaller example of Button component handling following the same structure as
3
* above
4
*/
5
describe('Confirmation Component Handlers', () => {
6
     let cancelButton: whisper.Button;
7
     let confirmButton: whisper.Button;
8
9
     beforeEach(() => {
10
       const [_a, _b, _c, box] = settingsWhisper.createConfirmationComponents();
11
       [cancelButton, confirmButton] = (box as whisper.Box).children as whisper.Button[];
12
13
       settingsWhisper.refresh = jest.fn();
14
       settingsWhisper.validate = jest.fn();
15
     });
16
17
     it('should go back to the settings whisper if "Cancel" is clicked', () => {
18
       cancelButton.onClick(undefined, MOCK_WHISPER);
19
20
       expect(settingsWhisper.refresh).toBeCalled();
21
       expect(settingsWhisper.validate).not.toBeCalled();
22
     });
23
24
     it('should go through validation if "Yes, Update" is clicked', () => {
25
       confirmButton.onClick(undefined, MOCK_WHISPER);
26
27
       expect(settingsWhisper.refresh).not.toBeCalled();
28
       expect(settingsWhisper.validate).toBeCalled();
29
     });
30
   });
Copied!

Loop Examples

Form Validation Loop

The Form Validation Loop provides an example of how one might structure a Loop to present a fillable form from an end user and validate it.

JavaScript Loop

This JavaScript Loop provides a very simple example that creates a Whisper when an end user copies something to their clipboard.

Self Test Loop

The Self Test Loop is an incredibly detailed Loop that covers every single Aptitude and Whisper component. We use this internally for testing and validation.

TypeScript Loop

The TypeScript Loop provides a simple example of how to use typescript files. It behaves exactly the same as the JavaScript Loop above.

RxNav Example Loop

This is an Olive Helps Loop used to look up drug information through the National Library of Medicine's publicly available RxNav API.

Universal Example Loop

The Universal Example Loop provides a simple example of creating a form using React.