switch to a Document trait and introduce Script/Head/Style/Meta components (#2635)

* switch to a Document trait and introduce Script/Head/Style/Meta components

* Fix desktop title

* Insert special elements into the head during the inital SSR render

* Make all head component attributes optional

* hydrate head elements

* improve the server streaming head warning

* Document fullstack head hydration approach

* deduplicate head elements by href

* move Link into head::Link

* document head components

* add meta and title examples

* Fix a few doc examples

* fix formatting

* Add title to playwright tests

* serde is optional on web, but it is enabled by hydrate

* remove leftover console log
This commit is contained in:
Evan Almloff authored and GitHub committed 2024-07-17 20:54:03 -05:00
1 parent a490a94ae5
commit 176e67e5b7
65 files changed
+1703 -495

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
# please dont accidentally run tsc and commit your js in this dir.
*.js
+108
View File
@@ -0,0 +1,108 @@
// Handle communication between rust and evaluating javascript
export class Channel {
pending: any[];
waiting: ((data: any) => void)[];
constructor() {
this.pending = [];
this.waiting = [];
}
send(data: any) {
// If there's a waiting callback, call it
if (this.waiting.length > 0) {
this.waiting.shift()(data);
return;
}
// Otherwise queue the data
this.pending.push(data);
}
async recv(): Promise<any> {
return new Promise((resolve, _reject) => {
// If data already exists, resolve immediately
if (this.pending.length > 0) {
resolve(this.pending.shift());
return;
}
// Otherwise queue the resolve callback
this.waiting.push(resolve);
});
}
}
export class WeakDioxusChannel {
inner: WeakRef<DioxusChannel>;
constructor(channel: DioxusChannel) {
this.inner = new WeakRef(channel);
}
// Send data from rust to javascript
rustSend(data: any) {
let channel = this.inner.deref();
if (channel) {
channel.rustSend(data);
}
}
// Receive data sent from javascript in rust
async rustRecv(): Promise<any> {
let channel = this.inner.deref();
if (channel) {
return await channel.rustRecv();
}
}
}
export abstract class DioxusChannel {
// Return a weak reference to this channel
weak(): WeakDioxusChannel {
return new WeakDioxusChannel(this);
}
// Send data from rust to javascript
abstract rustSend(data: any): void;
// Receive data sent from javascript in rust
abstract rustRecv(): Promise<any>;
}
export class WebDioxusChannel extends DioxusChannel {
js_to_rust: Channel;
rust_to_js: Channel;
owner: any;
constructor(owner: any) {
super();
this.owner = owner;
this.js_to_rust = new Channel();
this.rust_to_js = new Channel();
}
// Return a weak reference to this channel
weak(): WeakDioxusChannel {
return new WeakDioxusChannel(this);
}
// Receive message from Rust
async recv() {
return await this.rust_to_js.recv();
}
// Send message to rust.
send(data: any) {
this.js_to_rust.send(data);
}
// Send data from rust to javascript
rustSend(data: any) {
this.rust_to_js.send(data);
}
// Receive data sent from javascript in rust
async rustRecv(): Promise<any> {
return await this.js_to_rust.recv();
}
}
+19
View File
@@ -0,0 +1,19 @@
// Helper functions for working with the document head
function createElementInHead(
tag: string,
attributes: [string, string][],
children: string | null
): void {
const element = document.createElement(tag);
for (const [key, value] of attributes) {
element.setAttribute(key, value);
}
if (children) {
element.appendChild(document.createTextNode(children));
}
document.head.appendChild(element);
}
// @ts-ignore
window.createElementInHead = createElementInHead;
+86
View File
@@ -0,0 +1,86 @@
import { Channel, DioxusChannel, WeakDioxusChannel } from "./eval";
// In dioxus desktop, eval needs to use the window object to store global state because we evaluate separate snippets of javascript in the browser
declare global {
interface Window {
__msg_queues: WeakDioxusChannel[];
finalizationRegistry: FinalizationRegistry<{ id: number }>;
getQuery(request_id: number): WeakDioxusChannel;
createQuery(request_id: number): NativeDioxusChannel;
}
}
// A message that can be sent to the desktop renderer about a query
class QueryParams {
id: number;
data: { method: "drop" | "send"; data?: any };
constructor(id: number, method: "drop" | "send", data?: any) {
this.id = id;
this.data = { method, data };
}
}
window.__msg_queues = window.__msg_queues || [];
// In dioxus desktop, eval is copy so we cannot run a drop handler. Instead, the drop handler is run after the channel is garbage collected in the javascript side
window.finalizationRegistry =
window.finalizationRegistry ||
new FinalizationRegistry(({ id }) => {
// @ts-ignore - wry gives us this
window.ipc.postMessage(
JSON.stringify({
method: "query",
params: new QueryParams(id, "drop"),
})
);
});
// Get a query from the global state
window.getQuery = function (request_id: number): WeakDioxusChannel {
return window.__msg_queues[request_id];
};
// Create a new query (and insert it into the global state)
window.createQuery = function (request_id: number): NativeDioxusChannel {
return new NativeDioxusChannel(request_id);
};
export class NativeDioxusChannel extends DioxusChannel {
rust_to_js: Channel;
request_id: number;
constructor(request_id: number) {
super();
this.rust_to_js = new Channel();
this.request_id = request_id;
window.__msg_queues[request_id] = this.weak();
window.finalizationRegistry.register(this, { id: request_id });
}
// Receive message from Rust
async recv() {
return await this.rust_to_js.recv();
}
// Send message to rust.
send(data: any) {
// @ts-ignore - wry gives us this
window.ipc.postMessage(
JSON.stringify({
method: "query",
params: new QueryParams(this.request_id, "send", data),
})
);
}
// Send data from rust to javascript
rustSend(data: any) {
this.rust_to_js.send(data);
}
// Receive data sent from javascript in rust. This is a no-op in the native interpreter because the rust code runs remotely
async rustRecv(): Promise<any> {}
}