The SSR to CSR model
A SiteMesh page has one authored HTML template and two stages of execution. The server resolves route data, conditions, loops, and text before returning HTML. The browser then activates the rendered controls and continues with the same initial object and list state.
Use server rendering for the first truthful view. Use browser behavior for user input and subsequent changes. A seeded model does not repeat its initial request when the browser starts.
{{ Record.Name }} is a render-time snapshot. A named SiteMesh control such as name="Record.Name" stays connected to browser state.Routes and page structure
Each configured WWW domain selects a WebRoot, so different domains can serve completely separate site folders or intentionally share the same one. Within that domain's root, HTML files become routes from their relative paths. Files beginning with an underscore are layouts or partials and do not become routes.
| File | Route |
|---|---|
| index.html | / |
| account/list.html | /account/list |
| account/{accountid}.html | /account/:accountid, with {{ accountid }} in scope |
| _layout.html | No route; used by @layout |
Page directives
Put directives at the top of the file, one per line. A layout places the route content where its expression appears.
@layout('_layout.html')
@title('Account')
@description('View and edit an account')
<sitemesh-view>
<sitemesh-pane>
<sitemesh-header>...</sitemesh-header>
<sitemesh-body>...</sitemesh-body>
</sitemesh-pane>
</sitemesh-view>Root pages and fragments
An application root route uses exactly one <sitemesh-view> with one to three direct panes. A route loaded into an existing view with View.Navigate() is a fragment: return direct header, body, and optional footer elements without another view or pane wrapper.
Template rendering
Text and attributes
Double braces render encoded text. Use direct interpolation for an attribute containing text, or :attribute when the complete attribute value comes from a path.
<h1>{{ Account.AccountName }}</h1>
<a href="/account/{{ Account.AccountID }}">Open account</a>
<a :href="Account.AccountWebsite">Website</a>
<sitemesh-button :disabled="Account.AccountLocked">Save</sitemesh-button>Conditions
Put if, else-if, and else on adjacent real elements. Paths are null-safe. Conditions support truthiness, negation, comparisons, length checks, and string containment.
<sitemesh-pill if="Account.AccountStatus == 'ACTIVE'">ACTIVE</sitemesh-pill>
<sitemesh-pill else-if="Account.AccountStatus == 'PAUSED'">PAUSED</sitemesh-pill>
<sitemesh-pill else muted>INACTIVE</sitemesh-pill>
<p if="Account.AccountPersonList.length == 0">No people assigned.</p>Loops
Repeat a real element with for-each. The forms item in items and (item, index) in items are supported. Add if to the same element to filter rows during rendering.
<sitemesh-list-row for-each="(person, index) in PersonList"
:data-key="person.PersonID"
if="person.PersonActivity">
<span>{{ index }}. {{ person.PersonName }}</span>
</sitemesh-list-row>Filters
Append filters with | and chain them from left to right.
| Expression | Use |
|---|---|
| {{ Total | currency:'$' }} | Currency with grouping and two decimals |
| {{ Created | shortdate }} | Short local date |
| {{ Created | date:'yyyy-MM-dd' }} | Explicit date format |
| {{ Updated | timesince }} | Relative time |
| {{ Name | default:'Unknown' | upper }} | Fallback and text case |
| {{ accountid | integer }} | Typed integer route parameter |
| {{ Value | json }} | JSON for controlled script interpolation |
Objects: one record of state
Create an object in a top-level declaration when browser controls or conditions need to observe a record. The compiler uses the declaration variable as its binding name. Start with null when an OBJECT endpoint will supply the record, or use an object literal for local structural state.
<script>
const Draft = SiteMesh.Object({
Title: '',
Notes: '',
Published: false,
});
</script>null or must resolve to a JSON object. Use null for an initially empty object, or use an object literal containing strings, numbers, booleans, null values, arrays, and nested objects. Do not use another JavaScript variable, a function call, or object spread inside a render declaration.Seed an object for SSR and CSR
Seed() loads during server rendering. The endpoint result is available to template expressions and becomes the browser object's initial state without another initial request.
<script>
const Account = SiteMesh.Object(null)
.Seed('/account/get', { AccountID: '{{ accountid }}' });
</script>
<h1>{{ Account.AccountName }}</h1>
<sitemesh-input name="Account.AccountName" label="Name:"></sitemesh-input>Use SiteMesh.Object(null) for a null initial value. An OBJECT endpoint such as /account/get supplies its owning schema's IntelliSense and column metadata automatically. This does not create a blank record or filter its packet; the response is retained in full.
Render-only objects
Chain .Discard() when data is needed only for the current server render. The data remains available to interpolation, conditions, and loops while rendering, then its declaration and data are omitted from the browser response.
<script>
const Summary = SiteMesh.Object(null)
.Seed('/report/summary', { Range: 'MONTH' })
.Discard();
</script>
<p>{{ Summary.Total | currency:'$' }}</p>Do not reference a discarded model from browser functions. It exists only for the current server render.
Lists: SSR rows with browser ownership
A list is declared as SiteMesh.List([]) and connects to a for-each row when its declaration variable matches the loop source. A LIST endpoint supplies its owning item schema automatically.
<sitemesh-list>
<sitemesh-list-row for-each="account in AccountList"
:data-key="account.AccountID">
<sitemesh-text>{{ account.AccountName }}</sitemesh-text>
</sitemesh-list-row>
<sitemesh-list-empty>NO ACCOUNTS</sitemesh-list-empty>
</sitemesh-list>
<script>
const AccountList = SiteMesh.List([])
.Seed('/account/list', { Limit: 50, Offset: 0 });
</script>Seeded rows render on the server. The browser receives the same list and can later refresh, paginate, add, remove, or replace rows. A route-inferred schema supplies column metadata to controls bound through the loop alias, but does not filter returned rows. Keep custom projections on generic JSON endpoints and give them an explicit stable :data-key.
Search parameters and Watch
Use an earlier named object for reactive query parameters. .Watch() subscribes the list to that object and runs a debounced search when bound values change.
<sitemesh-input name="AccountParams.AccountName" label="Search:"></sitemesh-input>
<script>
const AccountParams = SiteMesh.Object({
AccountName: null,
Limit: 50,
Offset: 0,
});
const AccountList = SiteMesh.List([])
.Seed('/account/list', AccountParams)
.Watch();
</script>Structural reactivity
Lists react to structural changes: push, pop, shift, unshift, splice, removal, replacement, refresh, and paging. Changing a property on an existing list item does not redraw its row. Replace the item or refresh the list when rendered row text must change.
The list element owns its loading and empty presentation. It shows loading rows only when loading starts without real rows, preserves current rows during refresh, and shows <sitemesh-list-empty> after a successful empty result.
Browser loads and updates
Use Load() when the browser should fetch data after the page is active. Loading replaces the model contents and retains the route for later Refresh().
<script>
const Account = SiteMesh.Object(null)
.Load('/account/get', { AccountID: SelectedAccountID });
const ActivityList = SiteMesh.List([])
.Load('/activity/list', { Limit: 25, Offset: 0 });
</script>A top-level list declaration may chain .Load(), with an optional .Watch() when its parameters are an earlier named object. Later imperative calls such as await ActivityList.Load(route, params) return a promise and replace the existing list contents.
| Operation | When to use it |
|---|---|
| .Seed(route, params) | Initial server render plus browser state and a retained query |
| .Seed(...).Discard() | Use data for the current server render, then omit it from browser state |
| .Load(route, params) | A browser request that replaces current object or list data |
| .Refresh() | Rerun a model's retained seed or load route |
| .LoadMore() | Append the next page of a retained list query |
| .Clear() | Remove object data while preserving its bindings |
Objects and lists expose Loading. Bound list and object controls reflect pending requests automatically, so application code normally does not need to manage a second loading flag.
Bind controls to browser state
Set name="Object.Property" to connect a supported control to a named object. Use a model attribute or an ancestor data-model when several controls share the same model.
<div data-model="Article">
<sitemesh-input name="ArticleTitle" label="Title:"></sitemesh-input>
<sitemesh-textarea name="ArticleBody" label="Body:"></sitemesh-textarea>
<sitemesh-toggle name="ArticlePublished" label="Published"></sitemesh-toggle>
</div>
<sitemesh-text name="Article.ArticleTitle"></sitemesh-text>| Element | Use |
|---|---|
| sitemesh-input | Text, integer, decimal, date, email, or autocomplete input |
| sitemesh-textarea | Multi-line text input |
| sitemesh-select | Value field that opens a selection workflow |
| sitemesh-radio | One value from visible options |
| sitemesh-checkbox | One boolean checkbox |
| sitemesh-toggle | One boolean switch |
| sitemesh-text | Encoded reactive text output |
Controls expose PascalCase methods including Get(), Set(value), Enable(), and Disable(). Selects also expose Label() and Options().
Requests and actions
Use SiteMesh.Request(route, params) for commands and responses that are not model loads. Pass only the route and optional parameters.
<sitemesh-button onclick="SaveArticle()">Save</sitemesh-button>
<script>
async function SaveArticle() {
await SiteMesh.Request('/article/save', Article);
await Article.Refresh();
Toast.Show({ Message: 'Article saved', Timeout: 1500 });
}
</script>Use Load() to replace a model from a read route. Use SiteMesh.Request() for saves, deletes, downloads, and other actions. Await either operation before showing success or refreshing dependent data.
Local element access
Inside a navigated fragment, prefer View.Element('ElementID') so the lookup stays within that fragment. Root pages may use ordinary DOM lookup for globally unique elements.
<sitemesh-toast id="SavedToast" timeout="1500"></sitemesh-toast>
<script>
function ShowSaved() {
View.Element('SavedToast').Show('Saved');
}
</script>Complete working pattern
This master list renders its first result on the server, watches browser search state, and opens records inside the owning view.
<sitemesh-view>
<sitemesh-pane width="340px">
<sitemesh-header>
<sitemesh-input name="PersonParams.PersonName" placeholder="Search people"></sitemesh-input>
</sitemesh-header>
<sitemesh-list>
<sitemesh-list-row for-each="person in PersonList"
:data-key="person.PersonID"
onclick="View.Navigate('/person/{{ person.PersonID }}')">
<sitemesh-text>{{ person.PersonName }}</sitemesh-text>
</sitemesh-list-row>
<sitemesh-list-empty>NO PEOPLE</sitemesh-list-empty>
</sitemesh-list>
</sitemesh-pane>
<sitemesh-pane>
<sitemesh-pane-empty>Select a person</sitemesh-pane-empty>
</sitemesh-pane>
</sitemesh-view>
<script>
const PersonParams = SiteMesh.Object({
PersonName: null,
Limit: 50,
Offset: 0,
});
const PersonList = SiteMesh.List([])
.Seed('/person/list', PersonParams)
.Watch();
</script>Pattern order
- Declare query state before the list that consumes it.
- Bind search controls to that named object.
- Seed a list whose declaration name matches the loop source and render a stable keyed row.
- Use the owning view for local navigation.
- Use
SiteMesh.Request()for commands, then refresh or replace affected state.
Public API reference
SiteMesh.Object
| Member | Behavior |
|---|---|
| Set(property, value) | Assign a property or dotted property path and notify its subscribers. |
| Subscribe(property, callback) | Observe one property. The callback receives the next and previous values. |
| Unsubscribe(property, callback) | Remove the exact subscription callback. |
| Autocomplete(property, source) | Attach an autocomplete source to a property on a named object. |
| Cookie(name) | Persist browser-writable bound fields as preference state. Do not use it for authentication. |
| Load(route, params) | Replace the complete object from a browser request and retain the route. |
| Refresh() | Run the retained seed or load route using the current object as parameters. |
| Clear() | Remove current properties while keeping bindings and subscriptions. |
| Loading | Read-only boolean indicating one or more pending loads. |
<script>
const Filters = SiteMesh.Object({ Search: '' })
.Cookie('account-filters');
Filters.Autocomplete('Search', {
search: async (text) => await SiteMesh.Request('/account/suggest', { Search: text }),
label: (item) => item.AccountName,
});
function OnSearchChanged(next, previous) {
console.log(next, previous);
}
Filters.Subscribe('Search', OnSearchChanged);
// Later: Filters.Unsubscribe('Search', OnSearchChanged);
</script>SiteMesh.List
| Member | Behavior |
|---|---|
| push / pop | Add or remove items at the end and update bound rows. |
| shift / unshift | Remove or add items at the start and update bound rows. |
| splice(...) | Insert, replace, or remove items and update the affected rows. |
| remove(itemOrKey) | Remove an item instance or an item matching the list identity key. |
| Subscribe(callback) | Observe structural and loading events. |
| Load(route, params) | Load in the browser, replace rows, and retain the query. |
| Refresh() | Replace rows from the retained seed or load query. |
| Search(delay) | Schedule a retained-query refresh; the default delay is 500 ms and 0 is immediate. |
| LoadMore() | Append the next page using the retained query and current item count. |
| Loading | Read-only boolean covering refresh and pagination requests. |
View
| Member | Behavior |
|---|---|
| Navigate(route, options) | Load a route into the next navigation pane. Options may contain State and Callback. |
| Back() | Return to the previous destination in the local view. |
| Element(id) | Find an element inside the current view or fragment scope. |
| onActivate(handler) | Receive { first, state } when the fragment becomes active. state is the value passed through navigation options. |
| Callback | Invoke the callback supplied by the route that opened this destination. |
| find(selector) | Find the first selector match inside the current view or fragment scope. |
| ShowBase() | Clear the local navigation journey and restore the authored base pane. |
Controls and feedback
| Call | Behavior |
|---|---|
| control.Get() | Read the current control value. |
| control.Set(value) | Set the value and update its model binding. |
| control.Enable() / Disable() | Change whether the control accepts input. |
| select.Options() | Return a new array of the select's resolved options. |
| Toast.Show(options) | Show non-blocking feedback with Message, optional Timeout, and optional Anchor. |
| Popup.Show(options) | Show a confirmation or blocking message with an optional callback. |
| toast.Show(message, timeout, anchor) | Show a specific declared <sitemesh-toast> element. |
| dialog.Show() / Hide() | Open or close a declared <sitemesh-dialog>. |
Common mistakes
| Symptom | Check |
|---|---|
| Object fails during SSR | Its initializer must be a JSON-safe object literal, null, or supported server interpolation. Do not reference a JavaScript variable inside it. |
| List rows do not update | The declaration name and for-each source must match. Use structural mutations, not item property assignment. |
| Initial request runs twice | Use Seed() for SSR handoff. Reserve Load() for a browser-initiated request. |
| Text does not react | {{ }} is a snapshot. Use a bound <sitemesh-text name="Object.Property"> for reactive output. |
| Refresh throws | The model needs a retained route from an earlier Seed() or Load(). |
| Empty state is incorrect | Place <sitemesh-list-empty> inside the bound list and let the list manage its visibility. |
| Navigation does nothing | A view needs a second pane before View.Navigate() has a local destination. |
| Fragment state leaks | Use the fragment's own models, actions, and View.Element() lookup instead of reaching into another pane. |
Authoring checklist
- Render the first useful state on the server whenever the data is available there.
- Use
.Discard()only when browser code does not need the model after the current render. - Use named objects and lists when controls or later actions need state.
- Keep model names, list source names, and loop sources exact.
- Give custom list rows stable identity.
- Use SiteMesh controls for reactive values and commands.
- Await requests, then refresh or replace affected state.