The Bring Your Own feature is intended for developers who have a strong understanding of the Unqork Designer Platform and JavaScript.
Overview
The Bring Your Own (BYO) 7.26 implementation examples page contains example implementations of a component, event, and operation. Refer to these exampless when building your components with the BYO Component SDK guide.
AG Grid Component
This example displays the customization options, manifest.json
, and implementation of a custom AG Grid component.
Configuration
Settings | Object Type | Description |
---|---|---|
| string componentKey | The definitions of the columns from the |
| string componentKey | The value of the grid data. This version accepts a string, but it can be defined as needed. |
Events
Event Name | Event Value | Description |
---|---|---|
Cell Clicked |
| Add a custom event that emits on cell click. Event Payload: Add the details of the cell that was clicked into the Event Payload to use in logic:
|
Example AG Grid Package
Click on each of the tabs below to view the JSON and Javascript required to implement an AG Grid:
Manifest JSON Example
Get an in-depth understanding of
manifest.json
specifications in our Bring Your Own (BYO): Understanding the manifest.json File article.
{
"name": "AgGrid",
"version": "1.0.0",
"main": "agGrid.js",
"type": "custom",
"productType": "BYO",
"description": "AG Grid integration with Unqork",
"components": [
{
"name": "AG Grid",
"type": "agGrid",
"description": "AG Grid component for Unqork",
"model": {
"type": "object",
"properties": {
"columnDefinitions": {
"name": "Column Definitions",
"type": "array",
"description": "Column definitions for this grid (currently only supporting key and label)",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "Unique key for the column"
},
"label": {
"type": "string",
"description": "Display label for the column"
}
}
}
},
"value": {
"name": "Grid Data (JSON)",
"type": "string",
"description": "JSON string of data for the grid"
}
},
"required": ["columnDefinitions"]
},
"events": [
{
"name": "AG Grid Cell Clicked",
"type": "agGridCellClicked",
"description": "This event is triggered when the user clicks on a grid cell.",
"stability": "STABLE"
}
]
}
]
}
agGrid.js
The implementation javascript file name must match the manifest.json
's main
value. In this example, the agGrid.js
file matches line four in the manifest.
class UnqorkAgGridComponent extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
this.gridOptions = {
columnDefs: [],
rowData: [],
}
}
/**
* This is the only required method to integrate with the Unqork system
*
* @param {*} api The interface to the Unqork Runtime, used for both
* monitoring state and emitting events
*/
initialize(api) {
this.config = api.state.currentState()
this.api = api
if (this.config.columnDefinitions?.length) {
const defs = this._normalizeInput(this.config.columnDefinitions)
this.gridOptions.columnDefs = this._formatColumnDefinitions(defs)
}
if (this.config.value?.length) {
this.gridOptions.rowData = this._normalizeInput(this.config.value)
}
this.renderGrid(this.shadowRoot, this.gridOptions)
this.subscribeColumnDefs()
this.subscribeData()
}
/**
* Subscribe to the source for column definitions
*
* This can be made more robust by first using
* api.state.state$('columnDefsReference') to subscribe to
* any changes to that value (i.e. the target key), and then using
* resolveByKey$ to resolve the current value of that key, but
* keeping it simple for example here
*/
subscribeColumnDefs() {
this
.api
.state
.state$('columnDefinitions').subscribe((columnDefs) => {
console.log('AG Grid column definitions updated to', columnDefs)
// This _should_ be an array, but sometimes components try to set it as a
// JSON string instead, so we handle that here
const value = typeof(columnDefs) === 'string' ? JSON.parse(columnDefs) : columnDefs
this.gridOptions = {
rowData: this.gridOptions.rowData,
columnDefs: value ? this._formatColumnDefinitions(value) : [],
}
this.updateGrid(this.gridOptions)
})
}
subscribeData() {
this.api.state.state$('value').subscribe((rowData) => {
console.log('AG Grid value updated to', rowData)
// We support setting this as a JSON string or already parsed array of objects
// so handle that here
const value = typeof(rowData) === 'string' ? JSON.parse(rowData) : rowData
this.gridOptions = {
rowData: value || [],
columnDefs: this.gridOptions.columnDefs,
}
this.updateGrid(this.gridOptions)
})
}
updateGrid(gridOptions) {
console.log('Updating AG Grid', gridOptions)
this.gridApi.updateGridOptions(gridOptions)
}
async renderGrid(document, gridOptions) {
console.log('Rendering AG Grid component', {gridOptions, agGrid, windowAgGrid: window.agGrid})
document.innerHTML = this.view()
this.gridEl = document.querySelector('#myGrid')
this.gridApi = window.agGrid.createGrid(this.gridEl, gridOptions)
// Set this here so that it doesn't overridden when we update gridOptions
this.gridApi.addEventListener('cellClicked', (ev) => {
console.log('Emitting AG Grid Cell Clicked Event', ev)
this.emitCellClickedEvent(ev)
})
console.log('Created AG Grid', {
document,
gridOptions,
gridEl: this.gridEl,
gridApi: this.gridApi,
})
}
// Emit an event when a cell is clicked
// Full docs: https://www.ag-grid.com/javascript-data-grid/grid-events/#reference-selection-cellClicked
emitCellClickedEvent = (ev) => {
this.api.events.emit({
name: 'onAgGridCellClicked',
payload: ev
})
}
view() {
return `
<div id="myGrid" style="height:500px"></div>
`
}
/**
* Input data can be either JSON string or actual data, due to
* Unqork data constraints, so ensure it's proper data before
* using
*/
_normalizeInput(val) {
return typeof(val) === 'string' ? JSON.parse(val) : val
}
/**
* We store column definitions with standard key/label properties
* But AG Grid using different property names, so we map it here
*/
_formatColumnDefinitions(defs) {
return defs.map(({key, label}) => {
return {
field: key,
headerName: label ?? key
}
})
}
}
// This definition defines the state settings (model) for this component
// It mirrors the fuller description in manifest.json and will
// eventually be used to auto-generate the manifest.json JSON schema
class AgGridDefinition {
columnDefsReference
dataReference
}
// Actual export that exposes both the view layer and the model
// and is consumed by the Unqork Runtime for both configuration and
// execution
// All of these are `async` to allow for us to load them on demand
// without impacting initial page load performance
export const agGrid = {
model: async () => AgGridDefinition,
view: async () => UnqorkAgGridComponent,
}
// This definition describes the schema of the event that is emitted
// when the user clicks on an item
class CellClicked {
name = 'agGridCellClicked'
payload = {
index,
text,
}
}
// This export is used to expose the event to the Unqork Runtime
export const agGridCellClicked = {
model: async () => CellClicked,
}
Understand what's required to build a custom component in BYO Component SDK article.
Task List Component
This example displays the customization options, manifest.json
, and implementation of a custom Task List component. This asset also contains an event and an operation.
Discover how to create a custom task list component in our How To Example: Bring Your Own Task List article.
Configuration
Settings | Type | Object Type | Description |
---|---|---|---|
| Input | string componentKey | The title that displays above the task list. Default value is "Tasks". |
| Input | array componentKey | An array of objects that defines the task items in the list. Each object in the array contains the following:
|
Event
Event Name | Event Value | Description |
---|---|---|
Task List Item Toggled |
| An event that emits when a user checks or unchecks a task item. The event includes a payload with the text and completed status of the updated item. Event Payload: Adds the item name of the item that was clicked. |
Operation
Event Name | Event Value | Description |
---|---|---|
Task List Toggle Task List Item |
| Toggles or explicitly sets the completed state of a single item in a target task list list. Includes the following settings:
|
Example Task List Package
Click on each of the tabs below to view the JSON and Javascript required to implement a Task List component, including an event and operation:
Manifest JSON Example
Get an in-depth understanding of
manifest.json
specifications in our Bring Your Own (BYO): Understanding the manifest.json File article.
{
"name": "TaskList",
"version": "1.0.0",
"main": "taskList.js",
"type": "custom",
"productType": "BYO",
"description": "A simple task list component with an event and an operation.",
"components": [
{
"name": "Example Task List",
"type": "taskList",
"description": "Simple task list with checkboxes.",
"model": {
"type": "object",
"properties": {
"label": {
"name": "Label",
"type": "string",
"description": "Label of Task List",
"default": "Tasks"
},
"value": {
"name": "Value",
"type": "array",
"description": "Array of task items",
"items": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Text to display for the task item"
},
"completed": {
"type": "boolean",
"description": "Whether the item is completed",
"default": false
}
},
"required": ["text"]
},
"default": []
}
},
"required": ["value"]
},
"events": [
{
"name": "Example Task List Item Toggled",
"type": "taskListItemToggled",
"description": "Emitted when an end-user checks/unchecks a task item.",
"stability": "STABLE",
"model": {
"type": "object",
"properties": {
"name": {
"const": "taskListItemToggled",
"type": "string",
"description": "Name of the event"
},
"payload": {
"type": "object",
"description": "The event payload.",
"properties": {
"item": {
"type": "object",
"description": "A task list item.",
"properties": {
"text": { "type": "string", "description": "Task name."},
"completed": { "type": "boolean", "description": "Whether this task is completed."}
}
}
},
"required": [
"item"
]
}
}
}
}
],
"operations": [
{
"name": "Example Task List Toggle Task List Item",
"type": "toggleTaskListItem",
"description": "Toggle (or set) the completion state of a specific item on a target task list.",
"stability": "STABLE",
"model": {
"type": "object",
"properties": {
"options": {
"type": "object",
"properties": {
"targetKey": {
"name": "Target Key",
"type": "string",
"description": "Component key of the task list to modify"
},
"item": {
"name": "Item",
"description": "Index (number) or text (string) of the item to change",
"type": ["number", "string"]
},
"completed": {
"name": "Completed",
"type": "boolean",
"description": "Optional explicit completed value; if omitted, the value is toggled"
}
}
},
"type": {
"const": "toggleTaskListItem",
"type": "string"
}
},
"required": ["targetKey", "item"]
}
}
]
}
]
}
taskList.js
The implementation JavaScript file name must match the manifest.json
's main
value. In this example, the operationsOnly.js
file matches line four in the manifest.
class UnqorkTaskListComponent extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
this.items = []
this.label = 'Tasks'
}
/**
* This is the only required method to integrate with the Unqork system
*
* @param {*} api The interface to the Unqork Runtime, used for both
* monitoring state and emitting events
*/
initialize(api) {
this.api = api
this.config = api.state.currentState()
// Initial label and value from config (if provided)
if (this.config.label != null) this.label = String(this.config.label)
if (this.config.value != null) this.items = this._normalizeItems(this.config.value)
this.render(this.shadowRoot, this.label, this.items)
// Subscribe to state changes such as SET_PROPERTY
this.subscribeLabel()
this.subscribeValue()
}
/**
* Subscribes to changes in the `label` property of the component's state.
*
* This method listens to the `state$('label')` observable for updates to the `label` property.
* When a new value is emitted, it updates the `label` property of the component and re-renders the component.
*
* @returns {void}
*/
subscribeLabel() {
this.api.state.state$('label').subscribe((label) => {
this.label = label ?? ''
this.shadowRoot.innerHTML = this.view(this.label, this.items)
})
}
/**
* Subscribes to changes in the `value` property of the component's state.
*
* This method listens to the `state$('value')` observable for updates to the `value` property.
* When a new value is emitted, it normalizes the value into an array of items and updates the `items` property.
* It then re-renders the component with the updated items.
*
* @returns {void}
*/
subscribeValue() {
this.api.state.state$('value').subscribe((value) => {
this.items = this._normalizeItems(value)
this.shadowRoot.innerHTML = this.view(this.label, this.items)
})
}
/**
* Normalizes the input value into an array of objects with a specific structure.
*
* This method ensures that the input `value` is converted into an array of objects,
* where each object contains the following properties:
* - text: The text to actually display in the taskList item (defaults to an empty string if missing).
* - completed: A boolean indicating whether the item is completed (defaults to `false` if missing).
*
* @param value - The input value to normalize. Can be a JSON string or an array.
* @returns The normalized array of items.
*/
_normalizeItems(value) {
const arr = typeof value === 'string' ? JSON.parse(value) : value
if (!Array.isArray(arr)) return []
return arr.map((item) => ({
text: String(item?.text ?? ''),
completed: Boolean(item?.completed),
}))
}
render(root, label, items) {
root.innerHTML = this.view(label, items)
root.addEventListener('change', (ev) => {
const el = ev.target
if (!(el instanceof HTMLInputElement) || el.type !== 'checkbox') return
const idx = Number(el.getAttribute('data-index'))
if (Number.isNaN(idx) || !this.items[idx]) return
const updatedItem = { ...this.items[idx], completed: el.checked }
const next = [...this.items]
next[idx] = updatedItem
// Update state with new item value
this.api.state.set({ value: next })
// Emit taskListItemToggled event
this.emitItemToggledEvent(updatedItem)
})
}
/**
* Emits a `taskListItemToggled` event when a to-do list item is toggled.
*
* This method triggers a domain event with the name `taskListItemToggled` and includes
* the toggled item's details in the event payload. The payload contains the updated item
* with its `text` and `completed` properties.
*
*/
emitItemToggledEvent = (item) => {
this.api.events.emit({
name: 'taskListItemToggled',
payload: { item },
})
}
// The component view
view(label, items) {
const list = items
.map(
(item, i) => `
<li>
<label>
<input type="checkbox" data-index="${i}" ${item.completed ? 'checked' : ''} />
${item.text}
</label>
</li>`,
)
.join('')
return `
<div>${label ?? ''}</div>
<ul data-role="list">
${list}
</ul>
`
}
}
/**
* This definition defines the state settings (model) for this component
* It mirrors the fuller description in manifest.json and will
* eventually be used to auto-generate the manifest.json schema
*/
class TaskListDefinition {
label
value
}
/**
* This export provides the view and model definitions for the component,
* which are utilized by the Unqork Runtime for configuration and execution.
* Both definitions are `async` to enable on-demand loading, improving initial page load performance.
*
* Component export name MUST match manifest `type`
*/
export const taskList = {
model: async () => TaskListDefinition,
view: async () => UnqorkTaskListComponent,
}
/** ========= Example Task List Item Toggled Event Model ========= **/
// Defines the structure of the payload for the taskListItemToggled event
class TaskListItemToggledPayload {
item = { text: '', completed: false }
}
// This definition describes the schema of the event that is emitted
// when the user toggles an item
class TaskListItemToggled {
// The name of the event
name = 'taskListItemToggled'
// The details of the toggled to-do list item
payload = new TaskListItemToggledPayload()
}
// This export is used to expose the event to the Unqork Runtime
// Note: event export name MUST match manifest `type`
export const taskListItemToggled = {
model: async () => TaskListItemToggled,
}
/** ======== Example Task List Toggle Task List Item Operation ======== **/
// Operation options/schema for toggling a task list item
class ToggleTaskListItemOptions {
// key of the taskList component to modify
targetKey
// number (index) OR string (text) of the item to change
item
// Boolean to set completed property of item to. If not provided, just toggle the existing value (e.g. true -> false and false -> true)
completed
}
class ToggleTaskListItemOperation {
// MUST match the manifest "type" and the named export below
type = 'toggleTaskListItem'
options = new ToggleTaskListItemOptions()
}
class ToggleTaskListItemHandler {
/**
* Execute the operation.
* @param {ToggleTaskListItemOperation} operation
* @param {*} api Operation API: { state: { resolveByKey, set, get }, events: { emit } }
*/
async execute(operation, api) {
const { targetKey, item, completed } = operation.options.config || {}
if (!targetKey) throw new Error('toggleTaskListItem Operation: "targetKey" is required.')
if (item === undefined || item === null)
throw new Error('toggleTaskListItem Operation: "item" (index or text) is required.')
// Resolve the target component state
const target = api.state.resolveByKey(targetKey)
// Expecting a list of items like the following:
// [
// { "text": "Buy groceries", "completed": false },
// { "text": "Complete design doc", "completed": true }
// ]
const list = target?.value ?? []
if (!Array.isArray(list)) throw new Error('toggleTaskListItem Operation: target value is not an array.')
// Find the target index
let index = typeof item === 'number' ? item : list.findIndex((it) => String(it?.text ?? '') === String(item))
// Ensure the index is within bounds, otherwise throw an error
if (index < 0 || index >= list.length) {
throw new Error(`toggleTaskListItem Operation: could not find item "${item}".`)
}
// Get the current item or default to an empty object with default properties
const current = list[index] ?? { text: '', completed: false }
// Determine the next completion state based on the provided `completed` boolean or toggle the current state
const nextCompleted = typeof completed === 'boolean' ? completed : !current.completed
// Create a new list with the updated item at the specified index
const newList = [...list]
newList[index] = { ...current, completed: nextCompleted }
// Update the target component state with the new list
api.state.set(targetKey, { value: newList })
}
}
// The following class is omitted since the operation handler does not return a result
// class ToggleTaskListItemContext {
// ...
// }
// Named export — MUST match manifest "type"
export const toggleTaskListItem = {
model: async () => ToggleTaskListItemOperation,
handler: async () => ToggleTaskListItemHandler,
//This is omitted since the operation handler does not return a result
//contextModel: async () => ToggleTaskListItemContext,
}
Understand what's required to build a custom component in BYO Component SDK article.
Root Level Event
This example displays the Manifest JSON and JavaScript implementation of a custom event. After uploading a package containing an event asset, Creators can use the event in the Operations Builder.
Click on each of the tabs below to view the JSON and Javascript required to implement a root level event:
Manifest JSON Example
Get an in-depth understanding of
manifest.json
specifications in our Bring Your Own (BYO): Understanding the manifest.json File article.
{
"name": "eventsOnly",
"version": "1.0.0",
"main": "eventsOnly.js",
"type": "custom",
"productType": "BYO",
"description": "Asset that contains only general events.",
"events": [
{
"name": "Events Only",
"type": "eventsOnly",
"description": "A top-level event.",
"stability": "ALPHA",
"model": {
"type": "object",
"properties": {
"name": {
"const": "eventsOnly",
"type": "string"
},
"payload": {
"type": "object",
"properties": {
"data": {
"description": "Sample property",
"type": "string"
}
}
}
}
}
}
]
}
eventsOnly.js
The implementation JavaScript file name must match the manifest.json
's main
value. In this example, the eventsOnly.js
file matches line four in the manifest.
class EventPayload {
data = 'defaultValue'
}
class Event {
name = 'eventsOnly'
payload = new EventPayload()
}
// Export for the event
export const eventsOnly = {
model: async () => Event,
}
Understand what's required to build a custom component in BYO Component SDK article.
Average and Round Operation
This example displays an example of the Manifest JSON and JavaScript implementation of a custom averaging and rounding operation. After uploading a package containing an operation asset, Creators can use the operation in the Operations Builder.
Configuration Options
Settings | Type | Object Type | Description |
---|---|---|---|
| Input | Array componentKey | The array of numbers to average. By default this value is |
| Input | Integer | An integer representing the number of decimal places for rounding.
|
Payload
Settings | Description |
---|---|
| The averaged value after rounding to the requested decimals. |
Average and Round Operation Package
Click on each of the tabs below to view the JSON and Javascript required to implement a root level operation:
Manifest JSON Example
Get an in-depth understanding of
manifest.json
specifications in our Bring Your Own (BYO): Understanding the manifest.json File article.
{
"name": "exAvgAndRound",
"version": "1.0.0",
"main": "exAvgAndRoundOperation.js",
"type": "custom",
"productType": "BYO",
"description": "A simple operation that averages an array of numbers and rounds result to specified decimals.",
"operations": [
{
"name": "Example Average and Round",
"type": "exAvgAndRound",
"description": "Averages an array of numbers and rounds result to specified decimals.",
"stability": "STABLE",
"model": {
"type": "object",
"properties": {
"options": {
"type": "object",
"properties": {
"numbers": {
"name": "Numbers",
"type": "array",
"description": "Flat array of numbers to average.",
"items": { "type": "number" },
"default": []
},
"roundNumDec": {
"name": "Round Number Decimals",
"type": "integer",
"description": "Number of decimals to round result.",
"minimum": 0,
"maximum": 10,
"default": 2,
"placeholder": "specify an integer between 0 and 10"
}
}
},
"type": {
"const": "exAvgAndRound",
"type": "string"
}
}
},
"contextModel": {
"type": "object",
"properties": {
"result": {
"type": "object",
"properties": {
"result": {
"name": "Result",
"type": "number",
"description": "Result of the average operation."
}
}
}
}
}
}
]
}
exAvgAndRoundOptions.js
The implementation JavaScript file name must match the manifest.json
's main
value. In this example, the operationsOnly.js
file matches line four in the manifest.
// Operation options
class ExAvgAndRoundOptions {
// Flat array of numbers to average. Defaults to an empty array
numbers = []
// Integer decimals (0..10) to round the result. Defaults to 2
roundNumDec = 2
}
// Operation model
class ExAvgAndRoundOperation {
// MUST match the manifest "type" and the named export below
type = 'exAvgAndRound'
options = new ExAvgAndRoundOptions()
}
// Operation handler
class ExAvgAndRoundHandler {
/**
* Execute the operation.
* @param {ExAvgAndRoundOperation} operation
* @param {*} api Operation API: { state: { resolveByKey, set, get }, events: { emit } }
* @returns {{ result: number }}
*/
async execute(operation, api) {
// Get the operation options or default to an empty object
const opts = operation.options.config ?? {}
// Extract the numbers array from the options
const nums = opts.numbers
// Calculate the mean (average).
// The result is either 0 (if the array is empty) or the calculated mean of the numbers.
const mean = nums.length === 0 ? 0 : nums.reduce((acc, n) => acc + n, 0) / nums.length
// Round the mean to the specified number of decimals
// Javascript method toFixed returns a string so it needs to be coerced back to number
const decimals = opts.roundNumDec
const result = Number(mean.toFixed(decimals))
return { result }
}
}
// Context result (what the op returns into context)
// If the operation handler does not return a result. This can be omitted.
class ExAvgAndRoundContextResult {
result = 0
}
// If the operation handler does not return a result. This can be omitted.
class ExAvgAndRoundContext {
result = new ExAvgAndRoundContextResult()
}
// Export for the operation
// Named export MUST match manifest "type"
export const exAvgAndRound = {
model: async () => ExAvgAndRoundOperation,
handler: async () => ExAvgAndRoundHandler,
contextModel: async () => ExAvgAndRoundContext, // If the operation handler does not return a result. This can be omitted.
}
Understand what's required to build a custom component in BYO Component SDK article.
Resources
Bring Your Own (BYO) Framework Landing Page
Find all the resources needed to build your own components here.
Introduction to the Bring Your Own (BYO) Framework
Get started building your own custom assets, including components, events, and operations.
Create a BYO Package
Discover the structure and content of a BYO Package
Understanding the manifest.json File
Learn about the file's specification, component and event definitions.
Understanding the BYO Runtime Engine API
Discover the interface between your custom BYO components and the Unqork platform.
Custom Component SDK
Extend component functionality in the Unqork Designer Platform.
Custom Component
Understand the state, interface, and events of a custom component.
Custom Events
Learn more about creating custom events for use in the Operations Builder.
Event Payloads
Learn more about viewing and using event payloads with the Operations Builder.
Custom Operations
Learn more about creating custom operations for use in the Operations Builder.
BYO Best Practices
Learn about best practices for implementing and using BYO assets.
BYO General FAQ
Learn about the Bring Your Own Framework and discover frequently asked questions.