21694b79d2
Iteration 5: ? nil-propagation: Field and Index handlers in js_cg_expr now detect when the object expression is a Try node (the AST node for postfix `?`). When detected, emit JS optional chaining: `(expr)?.["field"] ?? null`. The `?? null` normalizes JS undefined to El's null. A bare `expr?` not followed by field/index still passes through unchanged. browser-auth.el: a realistic 130-line example demonstrating: - @async function with Supabase via native_js_call - DOM bridge: get/set value/text/attr, add/remove class, show/hide - local_storage_get/set for session hints - window_on_load for initialization - window_set to expose functions to the browser global scope - set_timeout for transient state, is_valid_email for input validation Compiles cleanly with elc --target=js --bundle Spec updated: status promoted to Phase 4 / ~80% coverage, nil-prop status updated, new example referenced.
166 lines
5.8 KiB
EmacsLisp
166 lines
5.8 KiB
EmacsLisp
// browser-auth.el -- El-compiled auth flow using Supabase via native_js_call
|
|
//
|
|
// Compile: elc --target=js --bundle examples/browser-auth.el > auth.js
|
|
// (requires el_runtime.js in the same directory as browser-auth.el)
|
|
//
|
|
// Demonstrates:
|
|
// - @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_remove_class, dom_show, dom_hide, dom_is_null
|
|
// - window_set to expose El functions to the browser global scope
|
|
// - local_storage_set/get for session hints
|
|
// - set_timeout for transient UI state
|
|
// - state_set/get for component state
|
|
//
|
|
// Expected HTML elements:
|
|
// #acct-email-input -- email text input
|
|
// #send-link-btn -- submit button
|
|
// #auth-message -- status message container
|
|
// #auth-form -- the form to hide after success
|
|
//
|
|
// Supabase client is expected on window.supabase (loaded from Supabase CDN
|
|
// via a separate script tag before auth.js).
|
|
|
|
// ── UI helpers ─────────────────────────────────────────────────────────────
|
|
|
|
fn show_message(text: String, is_error: Bool) -> Void {
|
|
let msg_el = dom_get_element("auth-message")
|
|
if !dom_is_null(msg_el) {
|
|
dom_set_text(msg_el, text)
|
|
dom_remove_class(msg_el, "hidden")
|
|
if is_error {
|
|
dom_add_class(msg_el, "error")
|
|
dom_remove_class(msg_el, "success")
|
|
} else {
|
|
dom_add_class(msg_el, "success")
|
|
dom_remove_class(msg_el, "error")
|
|
}
|
|
}
|
|
}
|
|
|
|
fn set_button_loading(loading: Bool) -> Void {
|
|
let btn = dom_get_element("send-link-btn")
|
|
if !dom_is_null(btn) {
|
|
if loading {
|
|
dom_set_text(btn, "Sending...")
|
|
dom_set_attr(btn, "disabled", "true")
|
|
} else {
|
|
dom_set_text(btn, "Send Magic Link")
|
|
dom_remove_attr(btn, "disabled")
|
|
}
|
|
}
|
|
}
|
|
|
|
fn clear_message() -> Void {
|
|
let msg_el = dom_get_element("auth-message")
|
|
if !dom_is_null(msg_el) {
|
|
dom_add_class(msg_el, "hidden")
|
|
dom_set_text(msg_el, "")
|
|
}
|
|
}
|
|
|
|
// ── Email validation ───────────────────────────────────────────────────────
|
|
|
|
fn is_valid_email(email: String) -> Bool {
|
|
let trimmed: String = str_trim(email)
|
|
if str_len(trimmed) < 5 { return false }
|
|
let at_pos: Int = str_index_of(trimmed, "@")
|
|
if at_pos < 1 { return false }
|
|
let dot_pos: Int = str_index_of(trimmed, ".")
|
|
if dot_pos < at_pos + 2 { return false }
|
|
return true
|
|
}
|
|
|
|
// ── Auth flow ──────────────────────────────────────────────────────────────
|
|
|
|
@async
|
|
fn send_magic_link() -> Void {
|
|
let email_el = dom_get_element("acct-email-input")
|
|
if dom_is_null(email_el) {
|
|
show_message("Could not find email input", true)
|
|
return null
|
|
}
|
|
|
|
let email: String = str_trim(dom_get_value(email_el))
|
|
|
|
if !is_valid_email(email) {
|
|
show_message("Please enter a valid email address", true)
|
|
return null
|
|
}
|
|
|
|
clear_message()
|
|
set_button_loading(true)
|
|
state_set("auth_email", email)
|
|
|
|
// Call Supabase via native_js_call.
|
|
// window.supabase.auth.signInWithOtp({ email: "..." }) returns a Promise.
|
|
let supabase = window_get("supabase")
|
|
if dom_is_null(supabase) {
|
|
show_message("Auth service not available", true)
|
|
set_button_loading(false)
|
|
return null
|
|
}
|
|
|
|
let auth = native_js_call(supabase, "auth", [])
|
|
let payload = { "email": email }
|
|
let result = native_js_call(auth, "signInWithOtp", [payload])
|
|
|
|
// Await the promise via native_js_call on the result object
|
|
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"]
|
|
if !dom_is_null(err) {
|
|
let msg: String = err["message"]
|
|
show_message("Error: " + msg, true)
|
|
return null
|
|
}
|
|
let email: String = state_get("auth_email")
|
|
local_storage_set("auth_pending_email", email)
|
|
show_message("Magic link sent! Check your inbox for " + email, false)
|
|
let form = dom_get_element("auth-form")
|
|
if !dom_is_null(form) {
|
|
dom_hide(form)
|
|
}
|
|
}
|
|
|
|
// ── Keyboard support ───────────────────────────────────────────────────────
|
|
|
|
fn handle_email_keydown(event: Any) -> Void {
|
|
let key: String = dom_get_prop(event, "key")
|
|
if str_eq(key, "Enter") {
|
|
send_magic_link()
|
|
}
|
|
}
|
|
|
|
// ── Initialization ─────────────────────────────────────────────────────────
|
|
|
|
fn init_auth() -> Void {
|
|
let email_el = dom_get_element("acct-email-input")
|
|
if !dom_is_null(email_el) {
|
|
// Pre-fill from local storage if a pending send was interrupted
|
|
let pending: String = local_storage_get("auth_pending_email")
|
|
if !str_eq(pending, "") {
|
|
dom_set_value(email_el, pending)
|
|
}
|
|
dom_listen(email_el, "keydown", handle_email_keydown)
|
|
}
|
|
let btn = dom_get_element("send-link-btn")
|
|
if !dom_is_null(btn) {
|
|
dom_listen(btn, "click", send_magic_link)
|
|
}
|
|
state_set("auth_initialized", "true")
|
|
}
|
|
|
|
fn main() -> Void {
|
|
// Expose send_magic_link globally so inline event handlers can call it
|
|
window_set("sendMagicLink", send_magic_link)
|
|
window_set("initAuth", init_auth)
|
|
|
|
// Run init when DOM is ready
|
|
window_on_load(init_auth)
|
|
}
|