In a UIX app, the user interface is provided by default exports from the entrypoint.ts
/entrypoint.tsx
files located at the root of the backend
or frontend
directories.
There are variety of values that can be exported from an entrypoint for display in the browser, including strings, HTML Elements, Blobs and more.
Example entrypoints:
export default "Hello, this is a simple text displayed on a website and loaded from the backend entrypoint";
export default
<section>
<h1>Title</h1>
<p>Description...</p>
</section>;
If there are no backend entrypoint exports, the UI is generated directly from the frontend entrypoint on each frontend client. This configuration is useful for complex web applications with user-specific UI and also when the UI content should not be available on the backend.
UI generated on the backend entrypoint is "moved" to the frontend client. UIX supports multiple methods for backend rendering.
If entrypoint exports exist for both the frontend and the backend, they will be merged automatically. This configuration is especially useful when combined with Entrypoint Routes.
The following diagram visualizes the concept of route merging in UIX:
Backend routes are always prioritized over frontend routes.
In this example, the route /support/technical
follows the frontend entrypoint route and resolves to Tech Support
.
The route /home/about
is resolved on the backend to About us
.
HTML elements are appended directly to the body of the document. They can be created using the built-in DOM APIs (document.createElement()
) or using JSX syntax:
import { Entrypoint } from "uix/providers/entrypoints.ts";
export default <div>Content</div> satisfies Entrypoint;
Like other entrypoint values, HTML elements are DATEX compatible and their content can be synchronized.
Keep in mind that the content is not updated when it is provided with renderStatic
.
const counter = $(0);
setInterval(()=>counter.val++,1000);
export default <div>Count: {counter}</div> satisfies Entrypoint;
Strings are displayed as text appended to the document body (color and background color depend on the current app theme).
Examples:
export default 'Hi World' satisfies Entrypoint;
const content = $("content");
export default content satisfies Entrypoint;
content.val = 'new content';
If you only want to display plain text without a parent HTML document and CSS styles, you can use provideContent('text content')
.
Route maps are simple JavaScript objects with route patterns as keys and entrypoint values as values.
When a route is requested from the backend or loaded on the frontend, the most specific (longest) matching route entry is resolved to an entrypoint value.
A simple route map might look like this:
export default {
'/home': <HomePage/>,
'/about': <div>About us...</div>
} satisfies Entrypoint;
Since route maps are valid Entrypoint
values, multiple route maps can be nested. Since a route part must match the route pattern key exactly, the parent route key must end with *
for the route to be resolved recursively.
export default {
'/articles/*': {
'/first': 'First Article...',
'/second': 'Second Article...'
}
} satisfies Entrypoint;
Besides the *
syntax, many more patterns, such as Regular Expressions, are supported in route map keys. Internally, route maps use the URLPattern API.
Matches can be accessed via the second argument in the callback function. The raw URLPatternResult
can be acessed via ctx.urlPattern
.
export default {
// Match user route with name
'/user/:name/': (_, {name}) => `Hello ${name}!`
// Match page route using a Regular Expression
'/page/(1|2|3)/': (ctx) => `This is page ${ctx.urlPattern.pathname.groups[0]}`
// Fallback if nothing else matches
'*': 'Not found'
} satisfies Entrypoint;
Route Maps also accept special symbols, called filters as keys. They can be used to follow a specific route only if a certain condition is met.
An important use case for filters is the RequestMethod
filter, which can be used to route depending on the HTTP request method:
import { RequestMethod } from "uix/routing/request-methods.ts"
export default {
'/login': {
// Provide login page
[RequestMethod.GET]: provideFile("common/index.html"),
// Handle POST method triggered from login page
[RequestMethod.POST]: (ctx) => handleLogin(ctx)
}
} satisfies Entrypoint;
Custom route filters can be created with the createFilter()
method from:
import { createFilter } from "uix/routing/route-filter.ts";
const isAdmin = createFilter((ctx: Context) =>
ctx.privateData.isAdmin);
const isPayingCustomer = createFilter((ctx: Context) =>
ctx.privateData.isPayingCustomer);
export default {
'/api/*': {
[isAdmin]: ctx => handleAPICall(ctx, {rateLimit: Infinity}).
[isPayingCustomer]: ctx => handleAPICall(ctx, {rateLimit: 1000}),
'*' : ctx => handleAPICall(ctx, {rateLimit: 10}),
}
} satisfies Entrypoint;
In this example, API calls are triggered with different rate limits depending on the type of the requesting client.
The wildcard ('*'
) selector can be used as with normal routes to provide fallback behavior if none of the other cases match.
Blobs are displayed directly as files in the browser (creating a file response with the correct mime type).
Example:
export default datex.get('./image.png') satisfies Entrypoint;
In a Deno environment, Deno.FSFile
values can be returned as entrypoint values. They create a file response with the correct mime type.
The provideFile()
function can also be used to return files from the local file system.
import { provideFile } from "uix/providers/common.tsx";
export default provideFile('./image.png') satisfies Entrypoint;
The provideImage()
method allows you to generate dynamic images using JSX and CSS. This is useful for generating social media images such as Open Graph images, Twitter cards, and more.
The following options are available for provideImage
:
provideImage(
element: Element,
options: {
width?: number = 200
height?: number = 200
fonts?: {
name: string,
data: Uint8Array | ArrayBuffer,
weight?: number,
style?: 'normal' | 'italic',
lang?: string
}[]
contentType?: 'png' | 'svg' = 'svg',
// Options that will be passed to the UIX HTTP response
status?: number = 200
}
)
You can render your favicon using following code:
import type { Entrypoint } from "uix/providers/entrypoints.ts";
import { provideImage } from "uix/providers/image.ts";
import { app } from "uix/app/app.ts";
export default {
'/favicon.png': provideImage(<div style={{
color: 'white',
height: '100%',
fontSize: 30,
background: app.stage === 'dev' ?
'orange' :
'black',
display: 'flex',
lineHeight: 2,
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column'
}}>
<h1>My App</h1>
<span>
<i style={{color: app.stage === 'dev' ?
'black' :
'orange'}}>
{app.stage}
</i>-Stage
</span>
</div>, {
width: 250,
height: 250,
contentType: 'png'
})
)
} satisfies Entrypoint;
Warning
Not all CSS features can be used to create dynamic images from JSX, as Satori only supports a subset of CSS. Please refer to Satori's documentation for a list of supported properties and elements.
URL
objects result in a redirect response (HTTP Status Code 304) to the given URL.
This can also be achieved with provideRedirect()
:
import { provideRedirect } from "uix/providers/common.tsx";
export default {
'/github': new URL('https://github.com/unyt-org'),
'/to/:id': (_, { id }) =>
provideRedirect(`https://${id}.unyt.app`),
} satisfies Entrypoint;
Virtual redirects are similar to normal redirects, but they directly return a response with the content of the redirect URL, not a redirect response (HTTP Status 304).
import { provideVirtualRedirect } from "uix/providers/common.tsx";
export default provideVirtualRedirect('/example/home') satisfies Entrypoint;
In the example above, an entrypoint function is used to return custom content based on the context of a route. Entrypoint functions take a single argument, a Context
object and return a Entrypoint
or Promise<Entrypoint>
.
Example:
export default (ctx: Context) => {
return `You visited this page from ${ctx.request.address} and your language is ${ctx.language}`;
} satisfies Entrypoint;
When an entrypoint function throws an error, the error value is returned like a normal return value, but with an HTTP status code 500.
UIX components implement the Route Manager interface and can handle routes internally.
When a component is encountered in the route chain, the onRoute
method is called on the component.
class Component {
// return the child element to which the route is resolved
// if the route contains more sections, onRoute is called on this child element with the next route section as the identifier
onRoute(identifier: string, is_initial_route: boolean): Component | boolean | void;
// return internal state of last resolved route
getInternalRoute(): Path.route_representation | Promise<Path.route_representation>;
}
Component routing can be used to display or focus on different child components depending on the route:
@template()
class Parent extends Component {
#activeChild?: HTMLElement
override onRoute(identifier:string) {
// find the child that has the same id as the identifier
this.#activeChild = this.querySelector(`#${identifier}`);
this.#activeChild?.focus();
return this.#activeChild;
}
override getInternalRoute() {
return [this.#activeChild.id]
}
}
export default {
// content for /about
'/about': 'About us',
// content for /version
'/version': 1,
// content for all other routes
// e.g. /a -> div#a is focused
'*': <Parent>
<div id="a">A</div>
<div id="b">B</div>
<div id="c">C</div>
</Parent>
};
Values that are thrown with throw
from an entrypoint function are treated similarly to returned values - the value is still rendered in the browser. There is only one difference: The response will have an error status code instead of the default 200 status code.
export default {
'/:id': (_, { id }) => {
if (id !== '4269420')
throw 'Invalid login'; // displays 'Invalid login' with status code 500
return 'The secret is 42!'; // displays 'The secret is 42!' with status code 200
}
} satisfies Entrypoint;
Instances of Error
that are thrown or returned b an entrypoint function will be rendered in the browser as an error info box including a stack trace when running in dev
stage.
export default {
'/:id': (_, { id }) => {
if (id !== '4269420')
throw new Error('Invalid login'); // displays an error info box (with stack trace)
return 'The secret is 42!';
}
} satisfies Entrypoint;
HTTPStatus
values can be returned or thrown to create a response with a specific status code.
Additionally, custom content can be returned using the with
method:
import { HTTPStatus } from "uix/html/http-status.ts";
export default {
'/:id': (_, { id }) => {
if (id !== '4269420')
throw HTTPStatus.BAD_REQUEST.with('MyCustomMessage'); // displays "MyCustomMessage" with status code 400 (Bad Request)
return "The secret is 42!";
}
} satisfies Entrypoint;
UIX provider utility functions allow the backend entrypoint to return HTTP responses directly from an entrypoint.
-
This function returns a HTTP Response with the mime type
function provideValue(value: unknown, options?: { type?: Datex.DATEX_FILE_TYPE, formatted?: boolean})
application/json
,application/datex
ortext/datex
, containing the serialized value. When theoptions.type
isDatex.DATEX_FILE_TYPE.JSON
, the value must be serializable with JSON.stringify. The default value foroptions.type
isDatex.FILE_TYPE.DATEX_SCRIPT
. In this configuration, any DATEX-compatible value can be provided. -
This function returns a HTTP Response with the mime type
function provideJSON(value: unknown, options?: { formatted?: boolean })
application/json
, containing the serialized value. The value must be JSON-compatible. -
Returns a HTTP Response with custom content and a custom mime type and status code.
function provideContent(content: string | ArrayBuffer, type: mime_type = 'text/plain;charset=utf-8', status?: number)
Route handlers are similar to Dynamic Entrypoint Functions, but they are represented with an interface.
Unlike a dynamic entrypoint function, which only takes a UIX context as a parameter, the getRoute
method of a route handler additionally takes the remaining route as an argument.
export interface RouteHandler {
// return entrypoint for a route
getRoute(route: Path.Route, context: Context): Entrypoint | Promise<Entrypoint>;
}
The RouteManager
interface represents an entity with an internal route state.
In contrast to other entrypoints, it has the ability to modify the requested route.
When a route manager is encountered while resolving a route, the resolveRoute
method is called with the remaining part of the current route.
The route manager decides how to update its internal state and returns the part of the route that it was able to resolve.
The actual route on the client is updated to contain only this portion.
The getInternalRoute
should always return the route represented by the current state, and should match the route part returned by resolveRoute
.
The RouteManager
interface is implemented by UIX components.
interface RouteManager {
// return part of route that could be resolved
resolveRoute(route: Path.Route, context: Context): Path.route_representation | Promise<Path.route_representation>;
// return internal state of last resolved route
getInternalRoute(): Path.route_representation | Promise<Path.route_representation>;
}
An entrypoint proxy can be wrapped around any entrypoint value to intercept routing and add custom functionality.
The abstract EntrypointProxy
class has two methods that can be implemented:
abstract class EntrypointProxy implements RouteHandler {
/**
* This method is called before a route is resolved by the entrypoint
* It can be used to implement a custom routing behavior
* for some or all routes, overriding the entrypoint routing
*
* The returned value replaces the entrypoint, if not null
*
* @param route requested route
* @param context UIX context
* @returns entrypoint override or null
*/
abstract intercept?(route: Path.Route, context: Context): void | Entrypoint | Promise<void | Entrypoint>;
/**
* This method is called after a route was resolved by the entrypoint
* It can be used to override the content provided for a route by returning
* a different entrypoint value.
* When null is returned, the route content is not changed
*
* @param content content as resolved by entrypoint
* @param render_method render method as resolved by entrypoint
* @param route the requested route
* @param context UIX context
* @returns entrypoint override or null
*/
abstract transform?(content: Entrypoint, render_method: RenderMethod, route: Path.Route, context: Context): void | Entrypoint | Promise<void | Entrypoint>;
}
A UIX Context
is created for each entrypoint request (when requesting a URL from a backend entrypoint or when redirecting to a URL on the frontend) and can be accessed in Dynamic Entrypoint Functions, Route Managers and Route Handlers.
It contains information about the client, about the route, and about the HTTP request (for backend entrypoints only).
interface Context {
request?: Request
requestData = {
address: string | null
}
path: string
params: Record<string,string>
urlPattern?: URLPatternResult
searchParams: URLSearchParams
language: string
endpoint: Datex.Endpoint
getSharedData(): Promise<Record<string, unknown>>
getPrivateData(): Promise<Record<string, unknown>>
}
UIX allows you to add auto generated OpenGraph meta tags to your DOM to improve SEO and social media sharing. Meta tags such as title, description, and preview images are generated when rendering the DOM of an element or component where the meta tags are applied to on the backend. The tags will be picked up by platforms like Twitter, Instagram, etc., automatically.
To apply custom OpenGraph metadata, create a instance of the OpenGraphInformation
class and add override the [OPEN_GRAPH]
symbol of the Class component or JSX Element:
class OpenGraphInformation {
constructor(
openGraphData: {
title?: string;
description?: string;
},
openGraphOptions?: {
getImageURL: () => string;
}
);
}
Here’s an example of how to integrate the auto generated metadata on a backend route's entrypoint value that is a HTMLElement:
import { OPEN_GRAPH, OpenGraphInformation } from "uix/base/open-graph.ts";
export default {
'/home': () => {
const dom = <div>Hello, UIX!</div>;
dom[OPEN_GRAPH] = new OpenGraphInformation({
title: "My custom title",
description: "My description ..."
});
return dom;
}
}
Here’s an example of how to implement it within a custom class component:
import { OPEN_GRAPH, OpenGraphInformation } from "uix/base/open-graph.ts";
import { Component } from "uix/components/Component.ts";
class MyCustomComponent extends Component {
override [OPEN_GRAPH] = new OpenGraphInformation({
title: "Hello, UIX!",
description: "This is the description of my UIX page."
}, {
getImageURL() {
return "https://example.com/preview.png";
}
});
}