Server-driven architecture#
Even though Morpheus is entirely framework and architecture agnostic, its original purpose was to enable easier development of hypermedia web app architectures, where the server owns the truth and the browser gets HTML (morph-)patches and renders them with no additional layers of abstraction; as opposed to JSON -> [vDOM ->] DOM.
Morpheus components make it easy to drive your application with just a single HTML template for each page using "fat morph" patches (see The Tao of Datastar ).
Reactive signals also allow you to easily interconnect components on a page.
Every Morpheus demo uses Datastar
In order to avoid hosting a server and to be able to deliver this site via CDN, we are using An in-browser simulator script which hijacks Datastar's fetch requests, executes the JavaScript handler and then fires a Datastar-native datastar-patch-elements event. which is similar to a regular Datastar request handler.
Data flow#
- A component dispatches an event (like
neo-dialog-open,neo-popover-open, etc.). - Datastar reacts via the
data-on:<event>="@post('/path')"event handlers and dispatches an action (an HTTP request). - The server then re-renders the entire page template and asynchronously streams the new HTML via a Brotli-compressed SSE stream (Server-Sent Events). Datastar then morph-patches that HTML into the live DOM.
- Morpheus web components observe changes in the DOM and update their internal state and presentation.
Typically, a production-grade Datastar application implements a CQRS (Command Query Responsibility Segregation) architecture. This is an example schema of how approximately this would look when implemented in the Go programming language:
Command patches#
Many Morpheus components are stateful, and server-side morph patching must only interfere in the user's client-side interactions when the server explicitly wants it to happen. For example, a fat morph of the page must not close a neo-popover after the user opened it, unless the server explicitly needs it to happen.
Therefore every Morpheus attribute follows one rule: an absent attribute is not a command. A brand-new element falls back to the attribute's default; an element already in the DOM keeps its current value. Every boolean attribute also accepts an explicit ="true" or ="false", so the server can command either state or stay silent and leave the client's alone.
Two kinds of attribute, one rule:
- Interactive state the user drives (
open,value,checked,pressed,expanded,page): an absent attribute keeps the current client state, so a fat morph that omits it can't reset what the user just did. - Configuration the server owns (
flip,hover,disabled,clamp-placement): an absent attribute falls back to the default. The client never changes these, so the server stays the source of truth.
The server can still explicitly dictate Datastar to...
- close a popover by setting
open="false", or to explicitly open it usingopen="true". - explicitly set a certain value on a
neo-textinputby setting attributevalue="specific value".
Every boolean attribute reads the same three ways. Taking open, which applies to <neo-popover>, <neo-menu>, <neo-drawer>, <neo-sidebar>, <neo-contextmenu>, and <neo-textinput>'s suggestions:
openabsent is no command. The component keeps its current state, so a morph that omitsopencan't disturb it.open="false"commands the component to close (case-insensitive).openoropen="true"commands the component to open.
Switch between the states manually or enable auto-play to see the command patches in effect:
Account
Signed in as Alice Larsson.
The selected state's markup and its scoped CSS. The CSS is wrapped in <style>@scope { ... }</style> and applies to the preview only. Edits change the preview above, applied live while Sync is on, or when you click Patch or Replace. Patch morphs the preview; Replace swaps it outright.
Signals
Datastar signals declared in the selected state's markup. Change a value to drive the preview, applied live while Sync is on, or when you click Patch.
Essentially, an absent attribute means "no command", the client may keep its current state until the server genuinely needs to take control.
Loading indication#
For components like combobox for the contents to be fetched lazily, the initial HTML must contain the placeholder slot [data-neo-async-placeholder], which the combobox displays when it has no options defined yet. The combobox then posts on open; the server streams the options into #<id>-options and the kit swaps the placeholder out. The delay keeps the placeholder visible. Open the combobox to see it.
Error handling#
If the server is temporarily unavailable due to network issues or errors, the UI must provide the user with feedback. Sometimes, that feedback can simply be provided via toasts. Morpheus offers useful template wrappers like for example datastar.ComboboxAsync:
In the following example, the server always returns a 500 Internal Server Error immediately. Datastar retries on error twice, waiting 1s between attempts; once the retry budget is exhausted, the combobox swaps in a failure state with a retry button. Async load with failure swap shows the same pattern with live retry controls.