From 73cd276a7216d03eeac2091e23105e935159dc72 Mon Sep 17 00:00:00 2001
From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com>
Date: Mon, 10 Aug 2026 20:03:58 -0500
Subject: [PATCH] Add JavaScript Interop Docs Page
---
.vitepress/sidebar.mts | 1 +
guide/topics/javascript-interop.md | 72 ++++++++++++++++++++++++++
swift/Sources/Snippets/ClickRace.swift | 44 ++++++++++++++++
swift/Sources/Snippets/run.swift | 2 +
4 files changed, 119 insertions(+)
create mode 100644 guide/topics/javascript-interop.md
create mode 100644 swift/Sources/Snippets/ClickRace.swift
diff --git a/.vitepress/sidebar.mts b/.vitepress/sidebar.mts
index a13f328..6b2171b 100644
--- a/.vitepress/sidebar.mts
+++ b/.vitepress/sidebar.mts
@@ -51,6 +51,7 @@ export default {
collapsed: false,
base: "/guide/topics/",
items: [
+ { text: "JavaScript Interop", link: "javascript-interop" },
{ text: "Server-Side Rendering", link: "server-side-rendering" },
],
},
diff --git a/guide/topics/javascript-interop.md b/guide/topics/javascript-interop.md
new file mode 100644
index 0000000..11a7a02
--- /dev/null
+++ b/guide/topics/javascript-interop.md
@@ -0,0 +1,72 @@
+# JavaScript Interop
+
+ElementaryUI interfaces with JavaScript using the [JavaScriptKit](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/main/documentation/javascriptkit) package. JavaScriptKit can be used to access JavaScript objects and functions at runtime, convert between Swift and JavaScript data types, and bridge JavaScript Promises and Swift's structured concurrency.
+
+## JSObject
+
+`JSObject` represents a JavaScript object in Swift. It supports dynamic member lookup, so accesses like `object.foo` will dynamically request the corresponding JavaScript member `foo` on the object. You can use `JSObject.global` as an entry point for web APIs.
+
+```swift
+let console = JSObject.global.console.object!
+let document = JSObject.global.document.object!
+let window = JSObject.global.window.object!
+```
+
+To construct new JavaScript objects, `JSObject` provides a specialized `new` call to call the associated constructor.
+
+```swift
+// Create a new URLSearchParams instance
+let URLSearchParams = JSObject.global.URLSearchParams.object!
+let params = URLSearchParams.new(window.location.search)
+```
+
+This type also supports `callAsFunction` in Swift, allowing you to call JavaScript functions in a similar manner.
+
+```swift
+// Find the `?language=` query parameter for the page
+let query = params.get?("language")
+```
+
+## JSValue
+
+`JSValue` represents a single value in JavaScript. It can be used to convert values from Swift to JavaScript.
+
+```swift
+// Extract the JavaScript value
+let languageJS: JSValue = query?.jsValue
+// Convert the query into a Swift String
+let language: String? = query?.jsValue.string
+```
+
+Types conforming to `ConvertibleToJSValue` and `ConstructibleFromJSValue` can be converted to and from their corresponding JavaScript type. Most standard Swift types conform to `ConvertibleToJSValue` and `ConstructibleFromJSValue`. The [JavaScriptKit Documentation](https://swiftpackageindex.com/swiftwasm/javascriptkit/main/documentation/javascriptkit/javascript-interop-cheat-sheet#Convert-Between-Swift-and-JavaScript) has a detailed map of each Swift and JavaScript type conversion.
+
+## JSClosure
+
+This type represents a JavaScript closure whose function is written in Swift. It can be passed as a callback handler to JavaScript functions.
+
+```swift
+let button = document.createElement!("button").object!
+let handler = JSClosure { args in
+ console.log!("Clicked", args[0])
+ return .undefined
+}
+button.addEventListener!("click", handler)
+```
+
+For async closures, use the `JSClosure.async` static method to bridge the async Swift function to a JavaScript promise.
+
+```swift
+let asyncHandler = JSClosure.async { _ async throws(JSException) -> JSValue in
+ try! await Task.sleep(nanoseconds: 1_000_000)
+ console.log!("Async closure finished")
+ return .undefined
+}
+```
+
+## Example
+
+A short example calling `setTimeout` in ElementaryUI to build a game to determine how fast you can click.
+
+<<< @/swift/Sources/Snippets/ClickRace.swift#snippet
+
+
\ No newline at end of file
diff --git a/swift/Sources/Snippets/ClickRace.swift b/swift/Sources/Snippets/ClickRace.swift
new file mode 100644
index 0000000..17409b0
--- /dev/null
+++ b/swift/Sources/Snippets/ClickRace.swift
@@ -0,0 +1,44 @@
+import ElementaryUI
+import JavaScriptKit
+
+@View
+struct ClickRace: SnippetContentView {
+ static let file: SnippetFile = "ClickRace"
+ // #region snippet
+ @State var clicks: Int?
+ @State var lastCount: Int?
+ @State var startTimerValue: JSValue?
+
+ var body: some View {
+ div {
+ p { "How fast can you click?" }
+ button { clicks == nil ? "Start!" : "Click!" }
+ .onClick { clicked() }
+ div {
+ if let clicks {
+ p { "Clicks: \(clicks)" }
+ }
+ if let lastCount {
+ p { "Your Record: \(Double(lastCount) / 3.0) clicks per second" }
+ }
+ }
+ }
+ }
+
+ func clicked() {
+ clicks = (clicks ?? 0) + 1
+ guard startTimerValue == nil else { return }
+
+ let callback = JSClosure { _ -> JSValue in
+ if clicks ?? 0 > lastCount ?? 0 {
+ lastCount = clicks
+ }
+ clicks = nil
+ startTimerValue = nil
+ return .undefined
+ }
+
+ startTimerValue = JSObject.global.setTimeout!(callback, 3000)
+ }
+ // #endregion snippet
+}
diff --git a/swift/Sources/Snippets/run.swift b/swift/Sources/Snippets/run.swift
index 74b26db..a60cd81 100644
--- a/swift/Sources/Snippets/run.swift
+++ b/swift/Sources/Snippets/run.swift
@@ -43,6 +43,8 @@ struct SnippetContainerView {
WithAnimationView()
case AnimateLayoutView.file:
AnimateLayoutView()
+ case ClickRace.file:
+ ClickRace()
default:
"Unknown snippet \(file?.value ?? "")"
}