Remote content and lazy Data: remote and dataRemote
remote makes the content of an element a Source fragment built on the server.
dataRemote puts at a Data path a value that an endpoint computes when the path is
read. Both name a server method, as dataRpc does
(Calling the server), and follow the same request rule. The
Controllers family of the gallery of
gramlot-examples has lessons on
remote content, cached remote content and remote Data.
remote, dataRemote and the request rule
remote makes the content of an element a Source fragment built on the server
by a @source method (Python) or a method declared with registerSource(name)
(JavaScript). The element carries the attribute remote with the method name; the
attributes remote_<k> are the call parameters. The node method writes them:
remote(name, **params)
The fragment is requested when the element starts (lifecycle) and again when a
^pointer ofremoteor of aremote_<k>parameter changes.=parameters are only read. The fragment replaces the content of the element; the authored children stay until the first fragment arrives.The request goes through
gramlot.src.remoteSource(target, method, params)and follows the request rule below. Removing the element cancels its request._cachedRemote=Truemakes no request when the method name and the parameters equal those of the request in flight or, with none in flight, those of the fragment mounted last. A fragment mounted byremoteSourcefrom code replaces that record. Parameters are compared by their TYTX serialization, captured when the request is made, so later mutations cannot alter the comparison. A different object-key order can cause another request even when the values are equal._ifis inline code with the parameters as locals. It is a readiness condition: false makes no request, cancels nothing and leaves the content as it is;_elsethen runs as inline code. Example:_if="record_id". To clear the content when the condition turns false, write_else="this.setValue(null)"; when_ifis true again, the fragment is mounted again._onCallingruns as inline code at each request, with the parameters as locals, before the request rule below decides whether the request is sent, waits or does nothing; a resultfalsemakes no request._onRemoteruns as inline code after the fragment is mounted, with the parameters as locals._delay(milliseconds),_timing(seconds) and_userChangesapply as fordataRpc._delaydelays the request after a change, and a new change in the meantime replaces it; it never cancels the request in flight. There is no default delay; set_delaywhen a parameter changes at every keystroke.A parameter starting with
_other than the eight control attributes (_cachedRemote,_if,_else,_onRemote,_onCalling,_delay,_timing,_userChanges) is an error. A failed request reaches the browser console as an unhandled rejection that names the element.A fragment can carry elements with
remoteof their own.
The method name of remote, dataRpc, dataRemote and remoteSource is a
string:
"details": a method of the page;"orders.details"or"a.b.c.details": a path read from the page. Each segment before the last is a public attribute: its name does not start with_. A property is read, so its getter runs when a request names it. The last segment is a method declared with@sourceor@endpoint(Python), or withregisterSourceorregisterEndpoint(JavaScript), on the class of the object before it. Itsauthrule is the only authorization of the request.The syntax of the name belongs to the server:
GramlotServer.name_syntax(JavaScriptnameSyntax) declares it as a portable regex, and the bootstrap carries it to the browser, where theremote(…)helper and a name read from Data are checked with it (Server protocol). With the core server each segment is an ASCII letter followed by ASCII letters, digits and_. Any other name answersnot_found.In JavaScript, every object on the path is an instance of a class that extends
Exposed, the base ofPagethat holdsregisterSourceandregisterEndpoint. A member inherited fromObject, such asconstructor, never leads to a method. In Python the objects on the path are ordinary objects.The name can be a
^or=pointer: the method is then read from Data. Use it with care: a reader of the Source no longer sees which method runs. An empty ornullname rejects the request with an error, as a name outside the syntax does. To make no request while no method is chosen, write_if.The page decides which objects a path reaches. For an object with state, a cached property is the recommended form.
The request rule is the same for remote, dataRpc, dataRemote and
gramlot.src.remoteSource:
one request is in flight for each Source node;
a request in flight always finishes, and its answer is applied: the fragment is mounted, the value is written,
_onRemoteor_onResultruns;the first change during the flight waits, with its method and parameters of that moment; later changes do nothing while one waits;
when the answer arrives, the waiting request is sent.
The content can therefore stay behind the Data until the next change. Only the
removal of the element and the close of the page abort a request; timeout of
dataRpc aborts its call.
Inline code reaches the state of the requests through this.requests, where this
is the Source node:
this.requests.inflightis the request in flight,{method, params, sentAt, cancel()}, ornull;paramsare the parameters sent andsentAtthe send time in milliseconds (Date.now());this.requests.pendingis the waiting request,{method, params}, ornull;inflight.cancel()makes the node ignore the answer of the request in flight. The server may still compute it. Called inside_onCalling, the new request is sent at once and replaces the waiting one; called elsewhere, the waiting request is sent.
root.div().remote("details", record_id="^.record_id",
_onCalling="if (this.requests.inflight) this.requests.inflight.cancel()")
from gramlot import Page as BasePage
from gramlot import source
class Orders:
@source
def details(self, root, record_id=None, **kwargs):
root.span(f"Order {record_id}")
class Page(BasePage):
@property
def orders(self):
return Orders()
def main(self, root):
root.input(type="number", value="^.record_id")
root.div().remote("details", record_id="^.record_id", _cachedRemote=True)
root.div().remote("orders.details", record_id="^.record_id")
@source
def details(self, root, record_id=None, **kwargs):
root.span(f"Record {record_id}")
import {Exposed, Page as BasePage} from '@gramlot/gramlot/page';
class Orders extends Exposed {
details(root, {record_id}) { root.span(`Order ${record_id}`); }
}
Orders.registerSource('details');
export class Page extends BasePage {
get orders() { return new Orders(); }
main(root) {
root.input({type: 'number', value: '^.record_id'});
root.div().remote('details', {record_id: '^.record_id', _cachedRemote: true});
root.div().remote('orders.details', {record_id: '^.record_id'});
}
details(root, {record_id}) { root.span(`Record ${record_id}`); }
}
Page.registerSource('details');
dataRemote installs at a Data path a resolver that computes the value with an
endpoint (Calling the server). The endpoint is
called when the path is read, not when the declaration is installed.
dataRemote(destination_path, method, cacheTime=None, sync=True, **params)
destination_pathis the Data path of the value; it does not accept?attr.methodis the endpoint name, a path from the page or a^/=pointer read from Data, as forremote(above, “The method name”).cacheTimeis in seconds: a negative value orNonecalls once;0calls at every read; a positive value calls again on the first read after that many seconds.sync, true by default as in the legacy, makes the read wait when the transport of the page sends synchronous calls (HTTP, Server protocol): a read whose cache has expired calls the endpoint synchronously and returns the answer; the value is stored at the path without notifying the consumers, because the reader receives it. A failed call raises the error, naming the declaration, to the reader; the next read calls again. Browsers deprecate the synchronous request on the main thread, and the page is blocked until the answer arrives.With
sync=False, and on a transport without synchronous calls (the serverless Worker), a read never waits. It returns the current value,Nonebefore the first call, and starts the call when the cache has expired. A read during a call starts no other call. On the Worker this also holds forsync=True.When an asynchronous call answers, the value is written at the path and every
^consumer of the path is projected with it. WithcacheTime=0the value is written without notifying the consumers, as in the legacy: it is a value per explicit read, and a^consumer would call again at each projection.The other keyword arguments are the endpoint arguments. A change of a
^argument installs a new resolver. When the previous resolver had called, the new one calls at once, asynchronously also withsync, and the consumers receive the new value.The calls follow the request rule above: a call in flight is not aborted by a new resolver and writes its value; the call of the new resolver waits for it.
A synchronous and an asynchronous call fill the same cache. The first read after the asynchronous call of a new resolver returns the stored value while the cache is valid and sends no call.
_onCallingruns as inline code before each call, with the arguments of the declaration; a resultfalsemakes no call, and a later read tries again.A false
_ifremoves the resolver: the path keeps its last value, and a call in flight still writes its own._elsethen runs as inline code. When_ifis true again, the new resolver calls at once if the removed one had called._delay,_timingand_userChangesapply as fordataRpc._init,_onBuiltand_onStartare an error ondataRemote.Removing the declaration removes the resolver and aborts its call; an aborted call writes nothing.
An endpoint failure leaves the stored value unchanged. It reaches the browser console as an unhandled rejection that names the declaration.
from gramlot import Page as BasePage
from gramlot import endpoint
class Page(BasePage):
def main(self, root):
root.dataRemote(".menu", "load_menu", cacheTime=5, table="dishes")
@endpoint
def load_menu(self, table=None, **kwargs):
return f"menu of {table}"
import {Page as BasePage} from '@gramlot/gramlot/page';
export class Page extends BasePage {
main(root) {
root.dataRemote({destination_path: '.menu', method: 'load_menu', cacheTime: 5,
table: 'dishes'});
}
load_menu({table}) { return `menu of ${table}`; }
}
Page.registerEndpoint('load_menu');
Intended differences from the legacy:
remoteis an attribute of the element itself, written by the node methodremote(name, …). Legacyremotewas a child element that wrote its attributes on the parent.No
lazyloading by visibility, no_waitingMessage, notbodymerge, no_resolvedand no_fired.The legacy alias
data(path, remote=…)is not accepted: writedataRemote.