Skip to main content

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.

MethodPurpose
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.

MethodPurpose
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 name
  • ComindView.actionMode - form mode: 'mass-edit', 'editable', 'editing', 'full-form', 'plain', etc.
  • ComindView.schema - the loaded app schema with fields, actions, lookups, and publishing_alias

Workspace context:

  • ComindView.workspace - the current workspace with .alias, .current_user_id, .group_participants, .tabs

User context:

  • ComindView.currentUser - object with id, email, isWorkspaceAdmin, isGlobalAdmin, and isInGroup()
  • ComindView.getVars() - get app variables
  • ComindView.getSetting(name) - get an app setting value

Record navigation:

  • ComindView.parentRecord(fieldName?) - access the parent record's data
  • ComindView.childRecords(appAlias?, fieldName?) - access child records
  • ComindView.siblingRecords() - access sibling records
  • ComindView.getLinkedRecordAsync(fieldName) - async fetch of a linked record
  • ComindView.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 browser
  • ComindView.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.