Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions assets/js/live_react/context.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React, { createContext, useContext } from "react";

export const LiveReactContext = createContext(null);

export function LiveReactProvider({ children, ...props }) {
return (
<LiveReactContext.Provider value={props}>
{children}
</LiveReactContext.Provider>
);
}

export function useLiveReact() {
return useContext(LiveReactContext);
}
25 changes: 11 additions & 14 deletions assets/js/live_react/hooks.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { getComponentTree } from "./utils";

function getAttributeJson(el, attributeName) {
const data = el.getAttribute(attributeName);
Expand All @@ -23,7 +24,6 @@ function getChildren(hook) {
function getProps(hook) {
return {
...getAttributeJson(hook.el, "data-props"),
// pass the hook callbacks to the component
pushEvent: hook.pushEvent.bind(hook),
pushEventTo: hook.pushEventTo.bind(hook),
handleEvent: hook.handleEvent.bind(hook),
Expand All @@ -36,13 +36,12 @@ function getProps(hook) {
export function getHooks(components) {
const ReactHook = {
_render() {
this._root.render(
React.createElement(
this._Component,
getProps(this),
...getChildren(this),
),
const tree = getComponentTree(
this._Component,
getProps(this),
getChildren(this),
);
this._root.render(tree);
},
mounted() {
const componentName = this.el.getAttribute("data-name");
Expand All @@ -55,14 +54,12 @@ export function getHooks(components) {
const isSSR = this.el.hasAttribute("data-ssr");

if (isSSR) {
this._root = ReactDOM.hydrateRoot(
this.el,
React.createElement(
this._Component,
getProps(this),
...getChildren(this),
),
const tree = getComponentTree(
this._Component,
getProps(this),
getChildren(this),
);
this._root = ReactDOM.hydrateRoot(this.el, tree);
} else {
this._root = ReactDOM.createRoot(this.el);
this._render();
Expand Down
1 change: 1 addition & 0 deletions assets/js/live_react/index.mjs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { getHooks } from "./hooks";
export { useLiveReact } from "./context";
34 changes: 14 additions & 20 deletions assets/js/live_react/server.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import React from "react";
import { renderToString } from "react-dom/server";
import { getComponentTree } from "./utils";

function Wrapper({ children }) {
return React.createElement(React.Fragment, null, children);
function getChildren(slots) {
if (!slots?.default) {
return [];
}

return [
React.createElement("div", {
dangerouslySetInnerHTML: { __html: slots.default.trim() },
}),
];
}

export function getRender(components) {
Expand All @@ -11,25 +20,10 @@ export function getRender(components) {
if (!Component) {
throw new Error(`Component "${name}" not found`);
}

let children = [];
if (slots?.default) {
children.push(
React.createElement("div", {
dangerouslySetInnerHTML: { __html: slots.default.trim() },
}),
);
}

// The Component need to be wrapped to prevent useState useEffect error which can't be root component
const componentInstance = React.createElement(
Component,
props,
...children,
);
const content = React.createElement(Wrapper, null, componentInstance);
const children = getChildren(slots);
const tree = getComponentTree(Component, props, children);

// https://react.dev/reference/react-dom/server/renderToString
return renderToString(content);
return renderToString(tree);
};
}
23 changes: 23 additions & 0 deletions assets/js/live_react/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from "react";
import { LiveReactProvider } from "./context";

function getHooks(props) {
return {
pushEvent: props.pushEvent,
pushEventTo: props.pushEventTo,
handleEvent: props.handleEvent,
removeHandleEvent: props.removeHandleEvent,
upload: props.upload,
uploadTo: props.uploadTo,
};
}

export function getComponentTree(Component, props, children) {
const componentInstance = React.createElement(Component, props, ...children);

return React.createElement(
LiveReactProvider,
getHooks(props),
componentInstance,
);
}
38 changes: 38 additions & 0 deletions live_react_examples/assets/react-components/context.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { useState } from "react";
import { useLiveReact } from "live_react";

export function Context({ count }) {
const [amount, setAmount] = useState(1);
const { pushEvent, ...rest } = useLiveReact();
console.log(rest);

return (
<div className="flex flex-col justify-center items-center gap-4">
<div className="flex flex-row items-center justify-center gap-10">
<button
className="px-4 py-2 rounded bg-red-500 text-white"
onClick={() => pushEvent("set_count", { value: count - amount })}
>
-{amount}
</button>
<span className="text-xl">{count}</span>
<button
className="px-4 py-2 rounded bg-green-500 text-white"
onClick={() => pushEvent("set_count", { value: count + amount })}
>
+{amount}
</button>
</div>
<label>
Amount:
<input
onChange={(e) => setAmount(parseInt(e.target.value, 10))}
type="number"
className="rounded"
value={amount}
min="1"
/>
</label>
</div>
);
}
2 changes: 1 addition & 1 deletion live_react_examples/assets/react-components/counter.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from "react";

export function Counter({ count, onInc, onDec }) {
export function Counter({ count }) {
const [amount, setAmount] = useState(1);

return (
Expand Down
2 changes: 2 additions & 0 deletions live_react_examples/assets/react-components/index.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// polyfill recommended by Vite https://vitejs.dev/config/build-options#build-modulepreload
import "vite/modulepreload-polyfill";

import { Context } from "./context";
import { Counter } from "./counter";
import { DelaySlider } from "./delay-slider";
import { FlashSonner } from "./flash-sonner";
Expand All @@ -14,6 +15,7 @@ import { Slot } from "./slot";
import { Typescript } from "./typescript";

export default {
Context,
Counter,
DelaySlider,
FlashSonner,
Expand Down
10 changes: 10 additions & 0 deletions live_react_examples/lib/live_react_examples.ex
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ defmodule LiveReactExamples do
}
end

def demo(:context) do
%{
raw_view_url: "#{@raw_url}#{@live_views}/context.ex",
view_url: "#{@url}#{@live_views}/context.ex",
view_language: "elixir",
raw_react_url: "#{@raw_url}#{@react}/context.jsx",
react_url: "#{@url}#{@react}/context.jsx"
}
end

def demo(demo) do
raise ArgumentError, "Unknown demo: #{inspect(demo)}"
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@
>
Slot
</.link>
<.link
class="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline font-medium text-zinc-700"
navigate={~p"/context"}
>
Context
</.link>
</div>
</div>
</nav>
Expand Down
23 changes: 23 additions & 0 deletions live_react_examples/lib/live_react_examples_web/live/context.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule LiveReactExamplesWeb.LiveContext do
use LiveReactExamplesWeb, :live_view

def render(assigns) do
~H"""
<h1 class="flex justify-center mb-10 font-bold">Hybrid: LiveView + React</h1>
<.react name="Context" count={@count} socket={@socket} ssr={true} />
"""
end

def mount(_session, _params, socket) do
{:ok, assign(socket, :count, 10)}
end

# def handle_event("set_count", params, socket) do
# IO.inspect(params)
# {:noreply, socket}
# end

def handle_event("set_count", %{"value" => number}, socket) do
{:noreply, assign(socket, :count, number)}
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ defmodule LiveReactExamplesWeb.LiveDemoAssigns do
{LiveReactExamplesWeb.LiveSlot, _} ->
:slot

{LiveReactExamplesWeb.LiveContext, _} ->
:context

{_view, _live_action} ->
nil
end
Expand Down
1 change: 1 addition & 0 deletions live_react_examples/lib/live_react_examples_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ defmodule LiveReactExamplesWeb.Router do
get "/typescript", PageController, :typescript

live "/live-counter", LiveCounter
live "/context", LiveContext
live "/log-list", LiveLogList
live "/flash-sonner", LiveFlashSonner
live "/ssr", LiveSSR
Expand Down