LFForm API Recipes

Practical patterns you can copy and adapt for Laserfiche Forms.

Recipe Index

API References

Dive deeper into specific areas of the LFForm API:

  • Read API

    Learn how to retrieve field values and find fields.

  • Write API

    Discover how to modify fields and handle operations.

  • Events API

    Subscribe to and handle form, field, and lookup events.

  • Properties API

    Understand runtime form properties and state.

Additional Resources

  • Recipes

    Learn more about common patterns and solutions.

  • Custom HTML guide

    Check out our custom HTML and sandbox integrations.


fields

Reset a Group of Fields

const formFields = {
  line1: { fieldId: 60 },
  line2: { fieldId: 61 },
  postalCode: { fieldId: 62 },
};

const fieldsToReset = [formFields.line1, formFields.line2, formFields.postalCode];

const resetAddress = async () => {
  for (const f of fieldsToReset) {
    await LFForm.setFieldValues(f, '');
  }
};

resetAddress().catch(console.warn);

localization

Localize Labels by Language

const submitLabelByLanguage = {
  en: 'Submit Request',
  fr: 'Soumettre la demande',
  es: 'Enviar solicitud',
};

const localizeButtons = async () => {
  const lang = LFForm.language ?? 'en';
  const label = submitLabelByLanguage[lang] ?? submitLabelByLanguage.en;
  await LFForm.changeActionButton('Submit', { label });
};

localizeButtons().catch(console.warn);

lookups

Cancel Lookup Calls Conditionally

const formFields = {
  lookupMode: { fieldId: 50 },
};

LFForm.onLookupTrigger(() => {
  const mode = LFForm.getFieldValues(formFields.lookupMode);
  if (mode === 'Manual') {
    return { cancelLookup: true };
  }
}, { lookupRuleId: 3 });

settings

Dynamic Labels by Runtime Context

const formFields = {
  approverNotes: { fieldId: 40 },
};

const relabelForStep = async () => {
  const stepName = LFForm.step?.name ?? '';

  if (stepName.includes('Manager')) {
    await LFForm.changeFieldSettings(formFields.approverNotes, { label: 'Manager Notes' });
    return;
  }

  await LFForm.changeFieldSettings(formFields.approverNotes, { label: 'Reviewer Notes' });
};

relabelForStep().catch(console.warn);

tables

Add Table Rows and Populate Values

const formFields = {
  expenseTable: { fieldId: 30 },
  expenseItemColumn: { fieldId: 31 },
  expenseAmountColumn: { fieldId: 32 },
};

const fillExpenses = async (rows) => {
  await LFForm.addRow(formFields.expenseTable, rows.length);
  await LFForm.setFieldValues(formFields.expenseItemColumn, rows.map((r) => r.item));
  await LFForm.setFieldValues(formFields.expenseAmountColumn, rows.map((r) => r.amount));
};

fillExpenses([
  { item: 'Hotel', amount: 420 },
  { item: 'Taxi', amount: 52 },
]).catch(console.warn);

Turn Entry IDs into Document Links

Sometimes a table already contains Laserfiche entry IDs at form load, and other times those IDs arrive later through a lookup rule. A common requirement is to turn each entry ID into a clickable document link in an adjacent CustomHtml column.

Best practices:

  • Prefer LFForm.onFieldChange() on the entry ID column instead of LFForm.onLookupDone().
  • onLookupDone() does not guarantee the target field is already populated.
  • Throttle the refresh so multiple row updates collapse into one render pass.
  • Build relative URLs when possible. They are more portable across environments.

Relative URL pattern:

/laserfiche/DocView.aspx?repo=r-12345678&customerId=12345678&id=454#?openmode=PDF&lang=en-US

Full URL example when needed:

https://app.laserfiche.com/laserfiche/DocView.aspx?repo=r-12345678&customerId=12345678&id=454
const formFields = {
  entryIdColumn: { fieldId: 70 },
  docLinkColumn: { fieldId: 71 },
};

const docViewBasePath =
  '/laserfiche/DocView.aspx?repo=r-12345678&customerId=12345678&id=';

const throttle = (fn, wait = 150) => {
  let timeoutId = null;
  let pending = false;

  return () => {
    if (timeoutId) {
      pending = true;
      return;
    }

    fn().catch(console.warn);

    timeoutId = window.setTimeout(() => {
      timeoutId = null;
      if (!pending) return;
      pending = false;
      fn().catch(console.warn);
    }, wait);
  };
};

const normalizeToArray = (value) => {
  if (Array.isArray(value)) return value;
  if (value == null || value === '') return [];
  return [value];
};

const buildDocLinkHtml = (entryId) => {
  if (!entryId) return '<span class="muted">No document</span>';

  const href = `${docViewBasePath}${encodeURIComponent(entryId)}#?openmode=PDF&lang=en-US`;
  return `<a href="${href}" target="_blank" rel="noopener noreferrer">Open document ${entryId}</a>`;
};

const refreshDocLinks = async () => {
  const entryIds = normalizeToArray(LFForm.getFieldValues(formFields.entryIdColumn));
  const rowHtml = entryIds.map(buildDocLinkHtml);

  await LFForm.changeFieldSettings(formFields.docLinkColumn, {
    content: rowHtml,
  });
};

const refreshDocLinksThrottled = throttle(refreshDocLinks, 200);

LFForm.onFieldChange(refreshDocLinksThrottled, formFields.entryIdColumn);
refreshDocLinks().catch(console.warn);

Notes:

  • This pattern updates the entire CustomHtml column in one batched pass.
  • If the lookup fills several rows quickly, throttling prevents redundant renders.
  • If you need different label text, keep the relative URL shape the same and only change the anchor text or query parameters.

typescript

Use TypeScript Types for Safer Helpers

import type { LFForm, LFFormId, LFFormChangeFormSettings } from '@lfz/lf-form-types';

declare const LFForm: LFForm;

const setPageLabels = async (changes: LFFormChangeFormSettings) => {
  await LFForm.changeFormSettings(changes);
};

const readValue = (id: LFFormId) => LFForm.getFieldValues(id);

validation

Block Submission with Friendly Validation

const formFields = {
  amount: { fieldId: 20 },
  costCenter: { fieldId: 21 },
};

LFForm.onFormSubmission(() => {
  const amount = LFForm.getFieldValues(formFields.amount);
  const costCenter = LFForm.getFieldValues(formFields.costCenter);

  if (amount > 1000 && !costCenter) {
    return { error: 'Cost Center is required when Amount is over 1000.' };
  }
});

visibility

Show/Hide Fields from Dropdown Selection

const formFields = {
  requestStatus: { fieldId: 10 },
  rejectionReason: { fieldId: 11 },
};

const updateVisibility = async () => {
  const status = LFForm.getFieldValues(formFields.requestStatus);
  if (status === 'Rejected') {
    await LFForm.showFields(formFields.rejectionReason);
    await LFForm.validateFields(formFields.rejectionReason, { required: true });
    return;
  }

  await LFForm.hideFields(formFields.rejectionReason);
  await LFForm.validateFields(formFields.rejectionReason, { required: false });
};

LFForm.onFieldChange(updateVisibility, formFields.requestStatus);
updateVisibility().catch(console.warn);