examples: rewrite browser-auth.el using new language features

No native_js or native_js_call anywhere. Full browser auth flow expressed
with proper El constructs:

- extern fn supabase_create_client(url, key) -> Any
  Declares the Supabase CDN global without an El function body.

- client.auth.signInWithOtp(opts)
  Direct method call chain on Any-typed value. The client is built by
  calling the extern fn; .auth field access and .signInWithOtp(opts)
  method call emit clean JS without any escape hatch.

- try { ... } catch (err: Any) { ... }
  Wraps the auth call; unexpected runtime errors are caught and shown
  to the user rather than crashing silently.

- fn(event: Any) -> Void { ... }
  Inline anonymous function literals for DOM event listeners instead
  of named forward-declared callbacks.

The rewrite is the proof: every browser JavaScript pattern used in a
real auth flow can now be expressed structurally in El.
This commit is contained in:
Will Anderson
2026-05-04 11:02:13 -05:00
parent 7040830470
commit aa7d97d5ba
+62 -27
View File
@@ -1,11 +1,14 @@
// browser-auth.el -- El-compiled auth flow using Supabase via native_js_call // browser-auth.el -- El-compiled auth flow using Supabase
// //
// Compile: elc --target=js --bundle examples/browser-auth.el > auth.js // Compile: elc --target=js --bundle examples/browser-auth.el > auth.js
// (requires el_runtime.js in the same directory as browser-auth.el) // (requires el_runtime.js in the same directory as browser-auth.el)
// //
// Demonstrates: // Demonstrates:
// - extern fn for declaring Supabase client constructor
// - anonymous function literals for callbacks
// - method call syntax on Any-typed values (client.auth.signInWithOtp)
// - try/catch for error handling
// - @async functions with DOM interaction // - @async functions with DOM interaction
// - native_js_call to invoke third-party library methods (Supabase)
// - DOM bridge: dom_get_element, dom_get_value, dom_set_text, dom_add_class // - DOM bridge: dom_get_element, dom_get_value, dom_set_text, dom_add_class
// dom_remove_class, dom_show, dom_hide, dom_is_null // dom_remove_class, dom_show, dom_hide, dom_is_null
// - window_set to expose El functions to the browser global scope // - window_set to expose El functions to the browser global scope
@@ -19,8 +22,16 @@
// #auth-message -- status message container // #auth-message -- status message container
// #auth-form -- the form to hide after success // #auth-form -- the form to hide after success
// //
// Supabase client is expected on window.supabase (loaded from Supabase CDN // The Supabase JS SDK is loaded from CDN via a <script> tag before auth.js.
// via a separate script tag before auth.js). // supabase_create_client is declared extern: the runtime provides it via
// the global supabase.createClient function exposed by the CDN bundle.
// External declarations
//
// These functions are provided by the JS environment (CDN script tags).
// No body is emitted -- the compiler just records the names.
extern fn supabase_create_client(url: String, key: String) -> Any
// UI helpers // UI helpers
@@ -72,6 +83,22 @@ fn is_valid_email(email: String) -> Bool {
return true return true
} }
// Supabase client construction
//
// Build a Supabase client from config injected into the page as NEURON_CFG.
// The extern fn supabase_create_client maps to supabase.createClient on
// the global object exposed by the CDN bundle.
fn get_supabase_client() -> Any {
let cfg = window_get("NEURON_CFG")
if dom_is_null(cfg) {
return null
}
let url: String = cfg["supabaseUrl"]
let key: String = cfg["supabaseAnonKey"]
supabase_create_client(url, key)
}
// Auth flow // Auth flow
@async @async
@@ -93,32 +120,26 @@ fn send_magic_link() -> Void {
set_button_loading(true) set_button_loading(true)
state_set("auth_email", email) state_set("auth_email", email)
// Call Supabase via native_js_call. // Build the Supabase client and call auth.signInWithOtp directly.
// window.supabase.auth.signInWithOtp({ email: "..." }) returns a Promise. // Method call syntax on Any-typed values: client.auth.signInWithOtp(opts)
let supabase = window_get("supabase") // No native_js_call required.
if dom_is_null(supabase) { let client = get_supabase_client()
show_message("Auth service not available", true) if dom_is_null(client) {
show_message("Auth service not configured", true)
set_button_loading(false) set_button_loading(false)
return null return null
} }
let auth = native_js_call(supabase, "auth", []) try {
let payload = { "email": email } let opts: Map<String, Any> = { "email": email }
let result = native_js_call(auth, "signInWithOtp", [payload]) // client is Any-typed; .auth returns the auth sub-client (also Any).
// .signInWithOtp(opts) returns a Promise. @async + await handles it.
// Await the promise via native_js_call on the result object let resp = client.auth.signInWithOtp(opts)
let error = native_js_call(result, "then", [check_auth_result])
set_button_loading(false)
}
fn check_auth_result(resp: Any) -> Void {
let err = resp["error"] let err = resp["error"]
if !dom_is_null(err) { if !dom_is_null(err) {
let msg: String = err["message"] let msg: String = err["message"]
show_message("Error: " + msg, true) show_message("Error: " + msg, true)
return null } else {
}
let email: String = state_get("auth_email")
local_storage_set("auth_pending_email", email) local_storage_set("auth_pending_email", email)
show_message("Magic link sent! Check your inbox for " + email, false) show_message("Magic link sent! Check your inbox for " + email, false)
let form = dom_get_element("auth-form") let form = dom_get_element("auth-form")
@@ -126,6 +147,12 @@ fn check_auth_result(resp: Any) -> Void {
dom_hide(form) dom_hide(form)
} }
} }
} catch (err: Any) {
show_message("Unexpected error. Please try again.", true)
}
set_button_loading(false)
}
// Keyboard support // Keyboard support
@@ -141,25 +168,33 @@ fn handle_email_keydown(event: Any) -> Void {
fn init_auth() -> Void { fn init_auth() -> Void {
let email_el = dom_get_element("acct-email-input") let email_el = dom_get_element("acct-email-input")
if !dom_is_null(email_el) { if !dom_is_null(email_el) {
// Pre-fill from local storage if a pending send was interrupted // Pre-fill from local storage if a pending send was interrupted.
let pending: String = local_storage_get("auth_pending_email") let pending: String = local_storage_get("auth_pending_email")
if !str_eq(pending, "") { if !str_eq(pending, "") {
dom_set_value(email_el, pending) dom_set_value(email_el, pending)
} }
dom_listen(email_el, "keydown", handle_email_keydown) // Anonymous function literal for inline event handler.
dom_listen(email_el, "keydown", fn(event: Any) -> Void {
let key: String = dom_get_prop(event, "key")
if str_eq(key, "Enter") {
send_magic_link()
}
})
} }
let btn = dom_get_element("send-link-btn") let btn = dom_get_element("send-link-btn")
if !dom_is_null(btn) { if !dom_is_null(btn) {
dom_listen(btn, "click", send_magic_link) dom_listen(btn, "click", fn(event: Any) -> Void {
send_magic_link()
})
} }
state_set("auth_initialized", "true") state_set("auth_initialized", "true")
} }
fn main() -> Void { fn main() -> Void {
// Expose send_magic_link globally so inline event handlers can call it // Expose send_magic_link globally so inline event handlers can call it.
window_set("sendMagicLink", send_magic_link) window_set("sendMagicLink", send_magic_link)
window_set("initAuth", init_auth) window_set("initAuth", init_auth)
// Run init when DOM is ready // Run init when DOM is ready.
window_on_load(init_auth) window_on_load(init_auth)
} }