View logic
ViewLogic controls how a form behaves in the browser. It handles field visibility, requiredness, read-only state, and lifecycle hooks - everything that shapes the user experience after the page loads.
The code lives in views/logic/index.ts inside your app's package structure. It runs entirely client-side. Server-side behavior belongs in actions; view logic is for the UI layer.
Export pattern
View logic files export a default object typed with satisfies ViewLogic<typeof entity>. Each property is a method that the form engine calls at the appropriate time:
import type { EntityFieldName } from '#typings';
import { entity } from '#typings';
import type { ViewLogic } from '@comind/api';
export default {
onReady,
getReadonlyFields,
getInvisibleFields,
onBeforeSave,
} satisfies ViewLogic<typeof entity>;
function onReady() {
const user = ComindView.currentUser;
const mode = ComindView.actionMode;
}
function getReadonlyFields() {
const fields: EntityFieldName[] = [];
if (!ComindView.currentUser.isWorkspaceAdmin) {
fields.push('c_admin_notes');
}
return fields;
}
function getInvisibleFields() {
if (entity.c_budget <= 10000) {
return ['c_approver'] as EntityFieldName[];
}
return [] as EntityFieldName[];
}
function onBeforeSave() {
if (entity.c_budget > 10000 && !entity.c_approver) {
return 'An approver is required for budgets over 10,000';
}
}
Do not use as const satisfies ViewLogic - omit as const. The build system will reject it.
Lifecycle hooks
These methods run at specific points in the form's lifecycle. Include them in your export object to register them.
| Method | Purpose |
|---|---|
onReady() | Form loaded and ready |
onRefresh() | Form data refreshed (entity not yet updated) |
onDestroy() | Form being destroyed |
onReset() | Form reset |
onBeforeSave() | Pre-save validation - return false or an error string to block save |
onGridLoaded() | Embedded grid finished loading |
onInputChange() | Field value changed |
onBeforeSave is the most common hook. Use it to enforce cross-field validation that goes beyond single-field requiredness:
function onBeforeSave() {
if (entity.c_budget > 10000 && !entity.c_approver) {
return 'An approver is required for budgets over 10,000';
}
}
Returning false or a string stops the save and shows the message to the user. Returning nothing (or true) allows the save to proceed.
Dynamic field control
These methods let you change field behavior at runtime based on the current record state, the user's role, or the form mode. Each returns an array of field db names.
| Method | Purpose |
|---|---|
getInvisibleFields() | Return field names to hide |
getRequiredFields() | Return field names to make required |
getReadonlyFields() | Return field names to make read-only |
getFilterKeys() | Filter keys for lookups |
getFieldOverrides() | Override field schema properties dynamically |
helperFunction() | Register helper functions |
The engine re-evaluates these methods whenever the record changes, so the UI stays in sync with the data. A field hidden by getInvisibleFields disappears from the form; a field marked by getRequiredFields shows a validation marker and blocks save if left empty.
function getInvisibleFields() {
// Hide the "approver" field unless the budget exceeds 10,000
if (entity.c_budget <= 10000) {
return ['c_approver'] as EntityFieldName[];
}
return [] as EntityFieldName[];
}
function getRequiredFields() {
// Require "reason" when the state is "rejected"
if (entity.state === 'rejected') {
return ['c_reason'] as EntityFieldName[];
}
return [] as EntityFieldName[];
}
Always use field db names (e.g. c_priority, not the caption). See Fields for naming conventions.
Accessing context
View logic often needs information beyond the current record. Access field values directly on the entity global. The ComindView global exposes context, navigation, and utility methods.
Form context:
ComindView.action- the current action nameComindView.actionMode- form mode:'mass-edit','editable','editing','full-form','plain', etc.ComindView.schema- the loaded app schema with fields, actions, lookups, andpublishing_alias
Workspace context:
ComindView.workspace- the current workspace with.alias,.current_user_id,.group_participants,.tabs
User context:
ComindView.currentUser- object withid,email,isWorkspaceAdmin,isGlobalAdmin, andisInGroup()ComindView.getVars()- get app variablesComindView.getSetting(name)- get an app setting value
Record navigation:
ComindView.parentRecord(fieldName?)- access the parent record's dataComindView.childRecords(appAlias?, fieldName?)- access child recordsComindView.siblingRecords()- access sibling recordsComindView.getLinkedRecordAsync(fieldName)- async fetch of a linked recordComindView.getRecordById(id)- async fetch any record by ID
Use actionMode to tailor behavior by context. For example, you might skip expensive validation in 'plain' mode since the user is only viewing the record.
For the full property and method reference, see Client-side API.
Working with grids and requests
A few additional ComindView methods handle less common scenarios:
ComindView.request(url, config?)- make an HTTP request from the browserComindView.addGridRow(values, appAlias?, fieldName?)- programmatically add a row to an embedded grid
These are useful when your app embeds child grids or needs to call external APIs from the client. For the full method reference, see Client-side API.
Plugin combination
When multiple plugins define ViewLogic, the engine combines their methods. Each plugin's hooks run in sequence, and your app's view logic runs last - after all plugin view logic has executed. This means you can override or extend behavior inherited from plugins.
If two plugins both define onBeforeSave, both handlers run. If either returns false, the save is blocked. Plan your validation accordingly to avoid conflicting rules.