UseFormTemplate API¶
Returns the rendered HTML form for a content template. The response is the complete HTML that the user's browser can display and submit to create a new document in the specified destination folder.
Endpoint¶
Methods¶
- GET
/srv.asmx/UseFormTemplate?authenticationTicket=...&targetFolderPath=...&templatePath=...&submitUrl=... - POST
/srv.asmx/UseFormTemplate(form data) - SOAP Action:
http://tempuri.org/UseFormTemplate
Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
authenticationTicket |
string | Yes | Authentication ticket obtained from AuthenticateUser. |
targetFolderPath |
string | Yes | Full infoRouter path of the destination folder where the new document will be created (e.g. /Finance/Reports). |
templatePath |
string | Yes | Full infoRouter path of the template document (e.g. /Templates/ExpenseForm.htm), or ~D<id> short form (e.g. ~D42). Use ~D999 for the built-in HTML document type. |
submitUrl |
string | Yes | URL to set as the HTML form action attribute. Pass an empty string "" to use the default legacy IRDOC.ASPX handler. Pass the route of a React/SPA page to intercept the submission client-side instead. This parameter must always be present in the request; omitting it entirely from the query string or form body will cause a server error. |
Response¶
Success Response¶
The element content is the complete rendered HTML form wrapped in a CDATA section to preserve HTML characters (<, >, &) without XML encoding.
Error Response¶
Required Permissions¶
- Read permission on the template document.
- Add Document permission on the destination folder (
targetFolderPath).
Example¶
POST Request¶
POST /srv.asmx/UseFormTemplate HTTP/1.1
Content-Type: application/x-www-form-urlencoded
authenticationTicket=3f2504e0-4f89-11d3-9a0c-0305e82c3301
&targetFolderPath=/Finance/Reports
&templatePath=/Templates/ExpenseReport.htm
&submitUrl=
Using ~D999 for HTML Documents¶
To render the built-in HTML document form (not tied to a specific template file):
POST /srv.asmx/UseFormTemplate HTTP/1.1
Content-Type: application/x-www-form-urlencoded
authenticationTicket=3f2504e0-4f89-11d3-9a0c-0305e82c3301
&targetFolderPath=/Finance/Reports
&templatePath=~D999
&submitUrl=
Using submitUrl for React/SPA Integration¶
Do not use submitUrl when embedding inside a React app via iframe — see the React section below. The submitUrl parameter is only useful when an external page submits to a known server-side route.
React Integration¶
The rendered HTML is a complete standalone page (<html>, <head>, <style>, <body>). The correct way to embed it in a React application is inside an <iframe srcdoc>, not via dangerouslySetInnerHTML.
Why iframe and not dangerouslySetInnerHTML:
dangerouslySetInnerHTML |
<iframe srcdoc> |
|
|---|---|---|
<script> tags execute |
No | Yes |
<style> isolated from React app |
No — leaks into host page | Yes |
| Full HTML document supported | No — strips <html>/<head>/<body> |
Yes |
| Submit interception | Requires querySelector on container |
contentDocument.querySelector on load |
srcdoc iframes are always same-origin with their parent, so contentDocument access is always permitted regardless of the host the React app is served from.
The rendered HTML has InfoRouter_Ticket set to an empty string. The React app must inject the live session ticket in the onLoad handler before the user submits.
Step 1 — Fetch the form¶
const res = await fetch(
`/srv.asmx/UseFormTemplate?authenticationTicket=${ticket}` +
`&targetFolderPath=${encodeURIComponent(targetFolderPath)}` +
`&templatePath=${encodeURIComponent(templatePath)}` +
`&submitUrl=`
);
const xml = new DOMParser().parseFromString(await res.text(), 'text/xml');
const renderedHtml = xml.querySelector('root').textContent;
Step 2 — Render inside an iframe and intercept submit¶
import { useRef } from 'react';
function InfoRouterForm({ renderedHtml, authTicket, targetFolderPath, documentName }) {
const iframeRef = useRef(null);
const handleLoad = () => {
const doc = iframeRef.current.contentDocument;
const form = doc.querySelector('form');
if (!form) return;
// Inject the live ticket — the rendered HTML has this field empty
const ticketField = doc.getElementById('InfoRouter_Ticket');
if (ticketField) ticketField.value = authTicket;
form.addEventListener('submit', async (e) => {
e.preventDefault();
// Parse InfoRouter_Fields to discover field names and build xmlContent
const fieldsInput = doc.getElementById('InfoRouter_Fields');
const tokens = fieldsInput ? fieldsInput.value.split(',') : [];
let xmlContent = '<FORMDATA>';
for (let i = 0; i + 3 < tokens.length; i += 4) {
const raw = tokens[i].trim(); // e.g. "'IR_title'"
const name = raw.slice(4, raw.length - 1); // strip 'IR_ prefix and trailing '
const value = (form.elements[name]?.value ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
xmlContent += `<Prompt Name="${name}">${value}</Prompt>`;
}
xmlContent += '</FORMDATA>';
const body = new URLSearchParams({
AuthenticationTicket: authTicket,
Path: targetFolderPath.replace(/\/+$/, '') + '/' + documentName,
TemplatePath: templatePath,
xmlContent,
});
const res = await fetch('/srv.asmx/CreateDocumentUsingTemplate', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
});
const xml = new DOMParser().parseFromString(await res.text(), 'text/xml');
const root = xml.querySelector('root') ?? xml.querySelector('response');
if (root?.getAttribute('success') === 'true') {
// handle success — root may have DocumentID and DocumentName attributes
} else {
console.error('Submit failed:', root?.getAttribute('error'));
}
});
};
return (
<iframe
ref={iframeRef}
srcdoc={renderedHtml}
onLoad={handleLoad}
style={{ width: '100%', height: '600px', border: 'none' }}
title="Document Form"
/>
);
}
The InfoRouter_Fields hidden input¶
The rendered HTML always contains a hidden input named InfoRouter_Fields. Its value is a comma-separated list of 4-token descriptors — one group per template field:
Each group of 4 tokens:
| Position in group | Meaning |
|---|---|
0 — 'IR_{fieldname}' |
HTML input name with 'IR_ prefix and trailing '. Strip those to get the bare field name. |
1 — 'CHAR' | 'DATE' | 'NUMBER' | 'BOOLEAN' |
Data type. |
2 — 'Y' | 'N' |
Required flag. |
3 — 'N' |
Reserved. |
You must parse this value to know which form inputs to collect and to build the correct xmlContent for CreateDocumentUsingTemplate.
Key points¶
submitUrlmust always be included in the request. Pass an empty string""when embedding via iframe — the iframe'sonLoadhandler takes over submit interception entirely, so the form'sactionattribute is irrelevant.e.preventDefault()must be called to stop the browser from navigating away when the user clicks Submit.- The
InfoRouter_Tickethidden field is empty in the rendered HTML by design. Always overwrite it with the current session ticket inhandleLoadand again in the submit handler. - Read
InfoRouter_FieldsfromiframeDocin theonLoadhandler to discover all template field names and their data types before buildingxmlContent. - If
renderedHtmlchanges (user picks a different template), the iframe re-renders andonLoadfires again, re-attaching the listener cleanly.
Notes¶
- The
templatePathaccepts either a full document path or the~D<id>short form.~D999is the reserved identifier for the built-in HTML document type and does not correspond to a physical document in the repository. - The rendered HTML is returned inside a CDATA section. Extract the element's text content before rendering it in a browser.
submitUrlmust always be present in the request. When passed as an empty string"", the formactiondefaults toIRDOC.ASPX(legacy handler). This parameter is only meaningful for non-React integrations where a server-side route must receive the POST. In a React app using<iframe srcdoc>, passsubmitUrl=""and intercept submit viacontentDocumentinstead.- The
InfoRouter_Tickethidden field inside the rendered form is always empty. The React app must inject the live session ticket in the iframeonLoadhandler. - Use this API to embed infoRouter form templates inside custom applications or portals.
Related APIs¶
- CreateHtmlDocument — Store a completed HTML document in a folder.
- Search — Find documents rendered from a specific template using the
TEMPLATEPATHcriterion.
Error Codes¶
| Error | Description |
|---|---|
[900] Authentication failed |
Invalid or missing authentication ticket. |
[901] Session expired or Invalid ticket |
The ticket has expired or does not exist. |
| Folder not found | The targetFolderPath does not exist or is not accessible. |
| Document not found | The templatePath does not resolve to an existing document. |
| Access denied | The user lacks Read permission on the template or Add Document permission on the folder. |
SystemError:... |
An unexpected server-side error occurred. |
React Implementer Guide¶
Production-ready patterns derived from the reference demo at IRWebCore/wwwRoot/form-template-demo.html.
Shared helper functions¶
These functions are used across all three form APIs (UseFormTemplate, GetFormFromDocument, CreateDocumentUsingTemplate). Define them once in a shared module.
// XML-escape a value before placing it inside a <Prompt> element
function escapeXml(s) {
return String(s ?? '')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// Parse the InfoRouter_Fields hidden input from a rendered form inside an iframe.
// Returns an array of { name, value, dataType, required }.
// InfoRouter_Fields format: 'IR_field1','CHAR','N','N','IR_field2','DATE','Y','N',...
function parseInfoRouterFields(iframeDoc, form) {
const input = iframeDoc.getElementById('InfoRouter_Fields');
if (!input || !input.value.trim()) return [];
const tokens = input.value.split(',');
const fields = [];
for (let i = 0; i + 3 < tokens.length; i += 4) {
const raw = tokens[i].trim(); // e.g. "'IR_title'"
if (raw.length < 5) continue;
const name = raw.slice(4, raw.length - 1); // strip 'IR_ prefix + trailing '
const dataType = tokens[i + 1].trim().replace(/'/g, ''); // CHAR | DATE | NUMBER | BOOLEAN
const required = tokens[i + 2].trim() === "'Y'";
const el = form.elements[name];
const value = el ? el.value : '';
fields.push({ name, value, dataType, required });
}
return fields;
}
// Build the <FORMDATA> XML required by CreateDocumentUsingTemplate
function buildXmlContent(fields) {
if (!fields || fields.length === 0) return '';
return '<FORMDATA>' +
fields.map(f => `<Prompt Name="${f.name}">${escapeXml(f.value)}</Prompt>`).join('') +
'</FORMDATA>';
}
// Parse an infoRouter XML response — handles both <root> and <response> root elements
function parseXml(xmlText) {
const doc = new DOMParser().parseFromString(xmlText, 'text/xml');
const root = doc.querySelector('root') ?? doc.querySelector('response');
if (!root) return { ok: false, error: 'Could not parse response' };
return { ok: root.getAttribute('success') === 'true', error: root.getAttribute('error') ?? '', el: root };
}
Step 1 — Fetch the form¶
async function loadForm(apiBase, ticket, targetFolderPath, templatePath) {
const body = new URLSearchParams({
authenticationTicket: ticket,
targetFolderPath,
templatePath,
submitUrl: '', // Always include; pass '' when using iframe submit interception
});
const res = await fetch(`${apiBase}/UseFormTemplate`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
});
const r = parseXml(await res.text());
if (!r.ok) throw new Error(r.error);
return r.el.textContent; // Raw HTML extracted from CDATA
}
r.el.textContent extracts the HTML from inside the CDATA wrapper. submitUrl must always be present — pass '' to let the iframe onLoad handler own submit interception.
Step 2 — Render in React and intercept submit¶
import { useState, useRef, useCallback } from 'react';
function CreateDocumentForm({ ticket, targetFolderPath, templatePath, apiBase, onSubmit }) {
const [renderedHtml, setRenderedHtml] = useState('');
const iframeRef = useRef(null);
async function handleLoadForm() {
const html = await loadForm(apiBase, ticket, targetFolderPath, templatePath);
setRenderedHtml(html);
}
// useCallback with full dependency array prevents stale closures in the submit handler
const handleIframeLoad = useCallback(() => {
const iframe = iframeRef.current;
if (!iframe || !renderedHtml) return;
const doc = iframe.contentDocument;
// Always inject the live ticket — the rendered form leaves InfoRouter_Ticket empty
const ticketField = doc.getElementById('InfoRouter_Ticket');
if (ticketField) ticketField.value = ticket;
const form = doc.querySelector('form');
if (!form) return;
form.addEventListener('submit', e => {
e.preventDefault();
e.stopPropagation();
// Collect all template fields and their current values
const fields = parseInfoRouterFields(doc, form);
// Hand off to the parent — it will call CreateDocumentUsingTemplate
onSubmit({ fields, templatePath });
});
}, [renderedHtml, ticket, templatePath]);
return (
<>
<button onClick={handleLoadForm}>Load Form</button>
{renderedHtml && (
<iframe
ref={iframeRef}
srcDoc={renderedHtml}
onLoad={handleIframeLoad}
sandbox="allow-scripts allow-forms allow-same-origin"
style={{ width: '100%', height: 600, border: 'none' }}
title="Document Form"
/>
)}
</>
);
}
Critical rules¶
| Rule | Reason |
|---|---|
Use srcDoc (not src) on the iframe |
Renders the HTML string directly without a round-trip |
sandbox="allow-scripts allow-forms allow-same-origin" |
Scripts inside the form execute; contentDocument access is permitted |
e.preventDefault() + e.stopPropagation() |
Prevents the browser from navigating away on submit |
Inject InfoRouter_Ticket in onLoad |
The rendered HTML always has this field empty by design |
List [renderedHtml, ticket, templatePath] in useCallback deps |
Prevents the submit handler from capturing stale values when inputs change |
Call CreateDocumentUsingTemplate from the onSubmit handler |
UseFormTemplate only renders the form; the actual save is a separate call |