diff --git a/manifest.toml b/manifest.toml index 356bf5d..b47df9c 100644 --- a/manifest.toml +++ b/manifest.toml @@ -8,6 +8,6 @@ packages = [ ] [requirements] -gleam_javascript = { version = ">= 0.11.0 and < 1.0.0"} +gleam_javascript = { version = ">= 0.11.0 and < 1.0.0" } gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/src/events.ffi.mjs b/src/events.ffi.mjs index 7339551..956fc89 100644 --- a/src/events.ffi.mjs +++ b/src/events.ffi.mjs @@ -29,3 +29,238 @@ export function target(event) { export function timeStamp(event) { return event.timeStamp } + +export function nativeEvent(event) { + return event.nativeEvent +} + +export function animationName(event) { + return event.animationName +} + +export function elapsedTime(event) { + return event.elapsedTime +} + +export function pseudoElement(event) { + return event.pseudoElement +} + +export function clipboardData(event) { + return event.clipboardData +} + +export function detail(event) { + return event.detail +} + +export function view(event) { + return event.view +} + +export function dataTransfer(event) { + return event.dataTransfer +} + +export function relatedTarget(event) { + return event.relatedTarget +} + +export function data(event) { + return event.data +} + +export function altKey(event) { + return event.altKey +} + +export function charCode(event) { + return event.charCode +} + +export function code(event) { + return event.code +} + +export function button(event) { + return event.button +} + +export function buttons(event) { + return event.buttons +} + +export function ctrlKey(event) { + return event.ctrlKey +} + +export function clientX(event) { + return event.clientX +} + +export function clientY(event) { + return event.clientY +} + +export function metaKey(event) { + return event.metaKey +} + +export function key(event) { + return event.key +} + +export function keyCode(event) { + return event.keyCode +} + +export function locale(event) { + return event.locale +} + +export function location(event) { + return event.location +} + +export function repeat(event) { + return event.repeat +} + +export function movementX(event) { + return event.movementX +} + +export function movementY(event) { + return event.movementY +} + +export function pageX(event) { + return event.pageX +} + +export function pageY(event) { + return event.pageY +} + +export function relatedTarget(event) { + return event.relatedTarget +} + +export function screenX(event) { + return event.screenX +} + +export function screenY(event) { + return event.screenY +} + +export function shiftKey(event) { + return event.shiftKey +} + +export function which(event) { + return event.which +} + +export function height(event) { + return event.height +} + +export function width(event) { + return event.width +} + +export function isPrimary(event) { + return event.isPrimary +} + +export function pointerId(event) { + return event.pointerId +} + +export function pointerType(event) { + return event.pointerType +} + +export function pressure(event) { + return event.pressure +} + +export function tangentialPressure(event) { + return event.tangentialPressure +} + +export function tiltX(event) { + return event.tiltX +} + +export function tiltY(event) { + return event.tiltY +} + +export function twist(event) { + return event.twist +} + +export function changedTouches(event) { + return event.changedTouches +} + +export function touches(event) { + return event.touches +} + +export function targetTouches(event) { + return event.targetTouches +} + +export function propertyName(event) { + return event.propertyName +} + +export function deltaMode(event) { + return event.deltaMode +} + +export function deltaX(event) { + return event.deltaX +} + +export function deltaY(event) { + return event.deltaY +} + +export function deltaZ(event) { + return event.deltaZ +} + +export function isDefaultPrevented(event) { + return event.isDefaultPrevented() +} + +export function isPropagationStopped(event) { + return event.isPropagationStopped() +} + +export function isPersistent(event) { + return event.isPersistent() +} + +export function preventDefault(event) { + event.preventDefault() + return event +} + +export function stopPropagation(event) { + event.stopPropagation() + return event +} + +export function persist(event) { + event.persist() + return event +} + +export function getModifierState(event, key) { + return event.getModifierState(key) +} diff --git a/src/main.gleam b/src/main.gleam index c79605e..c691997 100644 --- a/src/main.gleam +++ b/src/main.gleam @@ -26,7 +26,7 @@ pub fn root() { fn counter() { use <- react.component__("Counter") let #(counting, set_counting) = react.use_state_(0) - html.button([e.on_click(fn() { set_counting(fn(count) { count + 1 }) })], [ + html.button([e.on_click(fn(_) { set_counting(fn(count) { count + 1 }) })], [ html.text("count is " <> int.to_string(counting)), ]) } diff --git a/src/react.ffi.mjs b/src/react.ffi.mjs index 84e4a98..ef8b4a9 100644 --- a/src/react.ffi.mjs +++ b/src/react.ffi.mjs @@ -1,25 +1,35 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import runtime from "react/jsx-runtime"; -import * as gleam from "./gleam.mjs"; +import React from "react" +import ReactDOM from "react-dom/client" +import runtime from "react/jsx-runtime" +import * as gleam from "./gleam.mjs" export function createRoot(value) { - const node = document.getElementById(value); - return ReactDOM.createRoot(node); + const node = document.getElementById(value) + return ReactDOM.createRoot(node) +} + +export function hydrateRoot(value, content) { + const node = document.getElementById(value) + return ReactDOM.hydrateRoot(node, content) +} + +export function createPortal(children, root) { + const node = document.getElementById(root) + return ReactDOM.createPortal(children, node) } export function render(root, children) { - return root.render(children); + return root.render(children) } export function withChildren(Component) { return new Proxy(Component, { apply(target, _, argumentsList) { - const props = argumentsList[0]; - const children = gleam.List.fromArray(props.children); - return target(props, children); + const props = argumentsList[0] + const children = gleam.List.fromArray(props.children) + return target(props, children) }, - }); + }) } // Extract children from props to give it to function. @@ -27,105 +37,121 @@ export function withChildren(Component) { export function addChildrenProxy(Component) { return new Proxy(Component, { apply(target, _this, argumentsList) { - const props = argumentsList[0]; - const children = argumentsList[1]; - return jsx(withChildren(target), props, children); + const props = argumentsList[0] + const children = argumentsList[1] + return jsx(withChildren(target), props, children) }, - }); + }) } export function addEmptyProxy(Component) { return new Proxy(Component, { apply(target) { - const props = {}; - return jsx(target, props); + const props = {} + return jsx(target, props) }, - }); + }) } export function addProxy(Component) { return new Proxy(Component, { apply(target, _, argumentsList) { - const props = argumentsList[0]; - return jsx(target, props); + const props = argumentsList[0] + return jsx(target, props) }, - }); + }) } // Generate JSX using the JSX factory. // jsx is for dynamic components, while jsxs is for static components. export function jsx(value, props_, children_) { - if (value === "text_") return children_; - let children = children_?.toArray?.(); - let isStatic = true; + if (value === "text_") return children_ + let children = children_?.toArray?.() + let isStatic = true // Handle keyed elements like lustre does. // This allow to have a similar interface between lustre and greact. if (Array.isArray(children?.[0])) { children = children.map((c) => { - const [key, node] = c; - if ("key" in node) return React.cloneElement(node, { key }); - return node; - }); - isStatic = false; + const [key, node] = c + if ("key" in node) return React.cloneElement(node, { key }) + return node + }) + isStatic = false } // Props creation. // Uses the existing props, and add children if needed. - const props = {}; - Object.assign(props, props_); - if (children?.length > 0) props.children = children; + const props = {} + Object.assign(props, props_) + if (children?.length > 0) props.children = children if (isStatic) { - return runtime.jsxs(value, props); + return runtime.jsxs(value, props) } else { - return runtime.jsx(value, props); + return runtime.jsx(value, props) } } // Set the display name function. Essential to display the correct name in // the devtools. export function setFunctionName(component, name) { - component.displayName = name; - return component; + component.displayName = name + return component } export function strictMode(children) { - return jsx(React.StrictMode, {}, children); + return jsx(React.StrictMode, {}, children) } export function fragment(children) { - return jsx(React.Fragment, {}, children); + return jsx(React.Fragment, {}, children) } export function profiler(children) { - return jsx(React.Profiler, {}, children); + return jsx(React.Profiler, {}, children) } export function suspense(props, children) { - return jsx(React.Suspense, props, children); + return jsx(React.Suspense, props, children) } export function coerce(value) { - return value; + return value } export function contextProvider(context, value, children) { - return jsx(context.Provider, { value }, children); + return jsx(context.Provider, { value }, children) } export function setCurrent(ref, value) { - ref.current = value; + ref.current = value } export function getCurrent(ref) { - return ref.current; + return ref.current } export function toProps(attributes) { - const props = {}; + const props = {} for (const item of attributes) { - props[item.key] = item.content; + props[item.key] = item.content } - return props; + return props +} + +function camelize(key) { + return key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()) +} + +export function convertStyle(styles) { + const styles_ = {} + for (const style of styles) { + styles_[camelize(style[0])] = style[1] + } + return styles_ +} + +export function innerHTML(html) { + return { __html: html } } diff --git a/src/react.gleam b/src/react.gleam index db12662..89ebb96 100644 --- a/src/react.gleam +++ b/src/react.gleam @@ -1,5 +1,6 @@ import gleam/javascript/promise.{type Promise} import gleam/option.{type Option} +import react/internals/coerce.{coerce} // Component creation @@ -176,6 +177,3 @@ fn add_empty_proxy(a: fn() -> Component) -> fn() -> Component fn add_children_proxy( a: fn(props, List(Component)) -> Component, ) -> fn(props, List(Component)) -> Component - -@external(javascript, "./react.ffi.mjs", "coerce") -fn coerce(a: a) -> b diff --git a/src/react/attribute.gleam b/src/react/attribute.gleam index d2e29cb..ab64b1d 100644 --- a/src/react/attribute.gleam +++ b/src/react/attribute.gleam @@ -1,25 +1,192 @@ import gleam/dynamic -import react/internals/attribute.{Attribute} +import react +import react/internals/attribute pub type Attribute = attribute.Attribute pub fn href(url: String) { - Attribute("href", dynamic.from(url)) + attribute.attribute("href", url) } pub fn target(value: String) { - Attribute("target", dynamic.from(value)) + attribute.attribute("target", value) } pub fn src(value: String) { - Attribute("src", dynamic.from(value)) + attribute.attribute("src", value) } pub fn class(value: String) { - Attribute("className", dynamic.from(value)) + attribute.attribute("className", value) } pub fn alt(value: String) { - Attribute("alt", dynamic.from(value)) + attribute.attribute("alt", value) +} + +pub type InnerHTML + +@external(javascript, "../react.ffi.mjs", "innerHTML") +pub fn inner_html(html: String) -> InnerHTML + +/// Overrides the innerHTML property of the DOM node and displays the passed HTML inside. This should be used with extreme caution! If the HTML inside isn’t trusted (for example, if it’s based on user data), you risk introducing an XSS vulnerability. +pub fn dangerously_set_inner_html(inner_html: InnerHTML) { + attribute.attribute("dangerouslySetInnerHTML", inner_html) +} + +/// A ref object from `react.use_ref`. Your ref will be filled with the DOM element for this node. +pub fn ref(ref: react.Ref(a)) { + attribute.attribute("ref", ref) +} + +/// A ref callback function. The callback will be provided with the DOM element for this node. +pub fn ref_(ref: fn(dynamic.Dynamic) -> Nil) { + attribute.attribute("ref", ref) +} + +pub fn suppress_content_editable_warning(value: Bool) { + attribute.attribute("suppressContentEditableWarning", value) +} + +pub fn suppress_hydration_warning(value: Bool) { + attribute.attribute("suppressHydrationWarning", value) +} + +@external(javascript, "../react.ffi.mjs", "convertStyle") +fn convert_style(styles: List(#(String, String))) -> a + +pub fn style(styles: List(#(String, String))) { + attribute.attribute("style", convert_style(styles)) +} + +/// Set aria attribute on the node. Should be used like `aria("valuenow", "75")`. +pub fn aria(key: String, value: String) { + attribute.attribute("aria-" <> key, value) +} + +pub fn access_key(value: String) { + attribute.attribute("accessKey", value) +} + +pub fn auto_capitalize(value: String) { + attribute.attribute("autoCapitalize", value) +} + +/// Alias of `class`. +pub fn class_name(value: String) { + attribute.attribute("className", value) +} + +/// If true, the browser lets the user edit the rendered element directly. This is used to implement rich text input libraries like Lexical. React warns if you try to pass React children to an element with contentEditable={true} because React will not be able to update its content after user edits. +pub fn content_editable(value: Bool) { + attribute.attribute("contentEditable", value) +} + +/// Data attributes let you attach some string data to the element, for example data-fruit="banana". In React, they are not commonly used because you would usually read data from props or state instead. +pub fn data(key: String, value: String) { + attribute.attribute("data-" <> key, value) +} + +pub type Dir { + Ltr + Rtl +} + +/// Specifies the text direction of the element. +pub fn dir(value: Dir) { + let value = case value { + Ltr -> "ltr" + Rtl -> "rtl" + } + attribute.attribute("dir", value) +} + +/// Specifies whether the element is draggable. Part of HTML Drag and Drop API. +pub fn draggable(value: Bool) { + attribute.attribute("draggable", value) +} + +/// Specifies which action to present for the enter key on virtual keyboards. +pub fn enter_key_hint(value: String) { + attribute.attribute("enterKeyHint", value) +} + +/// For