Repository: Rezact/Rezact (ts)

Issue:
FEATURE: element ref's attribute
At the moment the best way to get a reference to an element is to assign an element to a variable like 

```tsx
let elmRef = <div></div>
elmRef.innerText = "Hello World"

return <>
  <p>Other Elms</p>
  {elmRef}
</>
```

But it would be nice to be able to do something roughly the equivalent of this:

```tsx
let elmRef = ref();
const doSomethingLater = () => {
  elmRef.current.innerText = "Hello World"
}

return <>
  <p>Other Elms</p>
  <div ref={elmRef}></div>
</>
```

Pull request:
Close #45 add refs attribute to elements
Close #45 

Add Refs attributes to DOM Elements:

Example Usage:

```tsx
import { MyLayout } from "../Layout/nestedLayout";

function Canvas({ ref: incomingRef }) {
  incomingRef.clear = () => console.log("clearing this canvas");
  const ref = {};  // this will get a new property "elm" which you can access at ref.elm

  return (
    <canvas
      ref={ref}
      onClick={() => console.log("testing ref attribute", ref.elm)}
    ></canvas>
  );
}

export function Page() {
  const canvasRef: any = {};

  return (
    <>
      <Canvas ref={canvasRef} />
      <button onClick={() => canvasRef.clear()}>Clear Canvas</button>
    </>
  );
}

export const Layout = MyLayout;

```

Patch:
diff --git a/src/components/appRouter.ts b/src/components/appRouter.ts
index 54aa40e..61b35ee 100644
--- a/src/components/appRouter.ts
+++ b/src/components/appRouter.ts
@@ -11,62 +11,64 @@ const router = useRouter();
 
 router.addRoute("/404", FourOhFour);
 
+router.addRoute("/refs", () => import("src/examples/Refs/Refs"));
+
 router.addRoute(
   "/array-state-persistent-across-route-changes",
-  () => import("src/components/HomePageStatePersistsAcrossRouteChanges")
+  () => import("src/components/HomePageStatePersistsAcrossRouteChanges"),
 );
 router.addRoute(
   "/benchmark",
-  () => import("src/examples/BenchmarkRezact/Benchmark")
+  () => import("src/examples/BenchmarkRezact/Benchmark"),
 );
 router.addRoute("/counter", () => import("src/examples/CounterRezact/Counter"));
 router.addRoute(
   "/counter-multiple",
-  () => import("src/examples/CounterMultipleRezact/CounterMultiple")
+  () => import("src/examples/CounterMultipleRezact/CounterMultiple"),
 );
 router.addRoute("/list", () => import("src/examples/ListRezact/List"));
 router.addRoute(
   "/nested",
-  () => import("src/examples/NestedRezact/NestedChildren")
+  () => import("src/examples/NestedRezact/NestedChildren"),
 );
 router.addRoute(
   "/hello-world",
-  () => import("src/examples/HelloWorldRezact/HelloWorld")
+  () => import("src/examples/HelloWorldRezact/HelloWorld"),
 );
 router.addRoute(
   "/hello-world-multiple",
-  () => import("src/examples/HelloWorldMultipleRezact/HelloWorldMultiple")
+  () => import("src/examples/HelloWorldMultipleRezact/HelloWorldMultiple"),
 );
 router.addRoute("/mdx", () => import("src/components/Test.mdx"));
 
 router.addRoute(
   "/post/:id/something/:test",
-  () => import("src/examples/RouteWithPathParams/index")
+  () => import("src/examples/RouteWithPathParams/index"),
 );
 
 router.addRoute(
   "/shadow-dom",
-  () => import("src/examples/ShadowDom/ShadowDomForExample")
+  () => import("src/examples/ShadowDom/ShadowDomForExample"),
 );
 
 router.addRoute(
   "/data-fetching",
-  () => import("src/examples/DataFetching/DataFetching")
+  () => import("src/examples/DataFetching/DataFetching"),
 );
 
 router.addRoute(
   "/uncontrolled-forms",
-  () => import("src/examples/Forms/UncontrolledForms")
+  () => import("src/examples/Forms/UncontrolledForms"),
 );
 
 router.addRoute(
   "/controlled-forms",
-  () => import("src/examples/Forms/ControlledForms")
+  () => import("src/examples/Forms/ControlledForms"),
 );
 
 router.addRoute(
   "/escape-hatches",
-  () => import("src/examples/EscapeHatches/EscapeHatches")
+  () => import("src/examples/EscapeHatches/EscapeHatches"),
 );
 
 router.addRoute("/todos", () => import("src/examples/Todo/Todo"));
@@ -75,62 +77,62 @@ router.addRoute("/list-redraw", () => import("src/examples/ListReRender/App"));
 
 router.addRoute(
   "/reactive-computations",
-  () => import("src/examples/ReactiveComputations/ReactiveComp")
+  () => import("src/examples/ReactiveComputations/ReactiveComp"),
 );
 
 router.addRoute("/store-test", () => import("src/examples/StoreTest/App"));
 
 router.addRoute(
   "/simple-string-list",
-  () => import("src/examples/SimpleStringListState/SimpleStringList")
+  () => import("src/examples/SimpleStringListState/SimpleStringList"),
 );
 
 router.addRoute(
   "/form-validation",
-  () => import("src/examples/FormValidation/FormValidation")
+  () => import("src/examples/FormValidation/FormValidation"),
 );
 
 router.addRoute(
   "/value-attribute-dot-notation",
-  () => import("src/examples/ValueAttributeDotNotation/ValueAttributeDot")
+  () => import("src/examples/ValueAttributeDotNotation/ValueAttributeDot"),
 );
 
 router.addRoute(
   "/component-context",
-  () => import("src/examples/ComponentContext/ComponentContext")
+  () => import("src/examples/ComponentContext/ComponentContext"),
 );
 
 router.addRoute(
   "/jsx-signals",
-  () => import("src/examples/JsxSignals/JsxSignals")
+  () => import("src/examples/JsxSignals/JsxSignals"),
 );
 
 router.addRoute(
   "/payments/:id",
-  () => import("src/examples/RouteWithPathParams/AmbiguousTest1")
+  () => import("src/examples/RouteWithPathParams/AmbiguousTest1"),
 );
 
 router.addRoute(
   "/payments/ach/:id",
-  () => import("src/examples/RouteWithPathParams/AmbiguousTest2")
+  () => import("src/examples/RouteWithPathParams/AmbiguousTest2"),
 );
 
 router.addRoute(
   "/users",
   () => import("src/examples/NestedRoutes/Users"),
-  nestedRoot
+  nestedRoot,
 );
 router.addRoute(
   "/users/:id",
-  () => import("src/examples/NestedRoutes/UsersId")
+  () => import("src/examples/NestedRoutes/UsersId"),
 );
 router.addRoute(
   "/users/:id/settings",
-  () => import("src/examples/NestedRoutes/UsersIdSettings")
+  () => import("src/examples/NestedRoutes/UsersIdSettings"),
 );
 router.addRoute(
   "/users/:id/settings/*",
-  () => import("src/examples/NestedRoutes/UsersIdSettingsWildCard")
+  () => import("src/examples/NestedRoutes/UsersIdSettingsWildCard"),
 );
 
 const routes = [
diff --git a/src/examples/Refs/Refs.tsx b/src/examples/Refs/Refs.tsx
index 64783e0..8136871 100644
--- a/src/examples/Refs/Refs.tsx
+++ b/src/examples/Refs/Refs.tsx
@@ -1,10 +1,18 @@
-function Canvas({ ref }) {
-  ref.clear = () => console.log("clearing this canvas");
+import { MyLayout } from "../Layout/nestedLayout";
 
-  return <canvas></canvas>;
+function Canvas({ ref: incomingRef }) {
+  incomingRef.clear = () => console.log("clearing this canvas");
+  const ref = {};
+
+  return (
+    <canvas
+      ref={ref}
+      onClick={() => console.log("testing ref attribute", ref)}
+    ></canvas>
+  );
 }
 
-function App() {
+export function Page() {
   const canvasRef: any = {};
 
   return (
@@ -14,3 +22,5 @@ function App() {
     </>
   );
 }
+
+export const Layout = MyLayout;
diff --git a/src/lib/rezact/index.ts b/src/lib/rezact/index.ts
index a7f720f..988681c 100644
--- a/src/lib/rezact/index.ts
+++ b/src/lib/rezact/index.ts
@@ -32,11 +32,11 @@ export function xCreateElement(tagName, attributes, ...children) {
 
     const hookContext = {};
     preCreateComponentHooks.forEach((func) =>
-      func(tagName, attributes, hookContext)
+      func(tagName, attributes, hookContext),
     );
     const newComp = createComponent(tagName, attributes);
     postCreateComponentHooks.forEach((func) =>
-      func(newComp, tagName, attributes, hookContext)
+      func(newComp, tagName, attributes, hookContext),
     );
 
     return newComp;
@@ -54,6 +54,18 @@ export let attributeHandlers = [];
 
 export const addAttributeHandler = (item) => attributeHandlers.unshift(item);
 
+const attributeInputValueHandler = {
+  matches: (_attrs, key, attrVal) =>
+    key === "ref" && typeof attrVal === "object",
+  handler: (elm, _key, attrVal) => handleRef(elm, attrVal),
+};
+
+addAttributeHandler(attributeInputValueHandler);
+
+function handleRef(elm, attrVal) {
+  attrVal.elm = elm;
+}
+
 const skipEvents = new Set(["onMount", "onUnmount"]);
 function handleAttributes(elm, attrs) {
   const keys = Object.keys(attrs);
@@ -114,7 +126,7 @@ function appendChildNode(
   parentNode: any,
   childNode: any,
   insertAfter: boolean = false,
-  removeElm: boolean = false
+  removeElm: boolean = false,
 ) {
   if (removeElm) return childNode.remove();
   if (parentNode instanceof Comment) insertAfter = true;
@@ -212,7 +224,7 @@ export function useContext() {
     (newComp, _tagName, _attributes, hookContext) => {
       hookContext.contextRoot = newComp[0] || newComp;
       hookContext.contextRoot.rezactContext = hookContext.componentContext;
-    }
+    },
   );
 }
 
@@ -246,7 +258,7 @@ export function useInputs() {
         if (element.value === newVal) return;
         setInputVal(element, newVal);
       },
-      { elm: element }
+      { elm: element },
     );
 
     if (


Test patch:
diff --git a/src/examples/Refs/refs.test.tsx b/src/examples/Refs/refs.test.tsx
new file mode 100644
index 0000000..0c6bbd0
--- /dev/null
+++ b/src/examples/Refs/refs.test.tsx
@@ -0,0 +1,36 @@
+import { describe, expect, it } from "vitest";
+import { render } from "rezact";
+
+let canvasClicked = false;
+let clearedCanvasClicked = false;
+const ref: any = {};
+
+function Canvas({ ref: incomingRef }) {
+  incomingRef.clear = () => (clearedCanvasClicked = true);
+
+  return <canvas ref={ref} onClick={() => (canvasClicked = true)}></canvas>;
+}
+
+function Page() {
+  const canvasRef: any = {};
+
+  return (
+    <>
+      <Canvas ref={canvasRef} />
+      <button onClick={() => canvasRef.clear()}>Clear Canvas</button>
+    </>
+  );
+}
+
+describe("Refs Test Suite", () => {
+  it("Test Refs", async () => {
+    render(document.body, Page);
+    const button = document.querySelector("button");
+    const canvas = document.querySelector("canvas");
+    button.click();
+    canvas.click();
+    expect(ref.elm).toBe(canvas);
+    expect(canvasClicked).toBe(true);
+    expect(clearedCanvasClicked).toBe(true);
+  });
+});