├── test ├── test_helper.exs └── phoenix_live_react_test.exs ├── .formatter.exs ├── assets ├── babel.config.js ├── webpack.config.js ├── package.json └── js │ └── phoenix_live_react.js ├── package.json ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── mix.exs ├── mix.lock ├── priv └── static │ └── phoenix_live_react.js ├── lib └── phoenix_live_react.ex └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /test/phoenix_live_react_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixLiveReactTest do 2 | use ExUnit.Case 3 | doctest PhoenixLiveReact 4 | 5 | test "none yet" do 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /assets/babel.config.js: -------------------------------------------------------------------------------- 1 | // babel.config.js 2 | module.exports = { 3 | presets: ["@babel/preset-env"], 4 | env: { 5 | test: { 6 | presets: [ 7 | [ 8 | "@babel/preset-env", 9 | { 10 | targets: { 11 | node: "10" 12 | } 13 | } 14 | ] 15 | ] 16 | } 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phoenix_live_react", 3 | "version": "0.4.2", 4 | "description": "Hook to render live react components in Phoenix LiveView.", 5 | "license": "MIT", 6 | "main": "./priv/static/phoenix_live_react.js", 7 | "author": "Robin Fidder", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/fidr/phoenix_live_react.git" 11 | }, 12 | "files": [ 13 | "README.md", 14 | "LICENSE.md", 15 | "package.json", 16 | "priv/static/phoenix_live_react.js", 17 | "assets/js/phoenix_live_react.js" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /assets/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './js/phoenix_live_react.js', 5 | output: { 6 | filename: 'phoenix_live_react.js', 7 | path: path.resolve(__dirname, '../priv/static'), 8 | library: 'phoenix_live_react', 9 | libraryTarget: 'umd', 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.js$/, 15 | exclude: /node_modules/, 16 | use: { 17 | loader: 'babel-loader', 18 | }, 19 | }, 20 | ], 21 | }, 22 | plugins: [], 23 | externals: { 24 | react: 'react', 25 | 'react-dom/client': 'react-dom/client', 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phoenix_live_react", 3 | "version": "0.1.0", 4 | "description": "Hook to render live react components in Phoenix LiveView.", 5 | "license": "MIT", 6 | "main": "./priv/static/phoenix_live_react.js", 7 | "repository": {}, 8 | "scripts": { 9 | "build": "webpack --mode production", 10 | "watch": "webpack --mode development --watch" 11 | }, 12 | "dependencies": {}, 13 | "devDependencies": { 14 | "@babel/core": "^7.25.9", 15 | "@babel/preset-env": "^7.25.9", 16 | "babel-loader": "^9.2.1", 17 | "webpack": "5.95.0", 18 | "webpack-cli": "^5.1.4" 19 | }, 20 | "peerDependencies": { 21 | "react": ">=18", 22 | "react-dom": ">=18" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | phoenix_live_react-*.tar 24 | 25 | node_modules 26 | 27 | .elixir_ls/ 28 | 29 | .DS_Store 30 | 31 | dummy/ 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for PhoenixLiveReact 2 | 3 | ## v0.4.2 4 | * Removes usage of eval so it works with CSP. by @aglassman 5 | 6 | ## v0.4.1 7 | * Allow `phoenix_html` v3 in deps 8 | 9 | ## v0.4.0 10 | * Add `id` option to set the id's on the `phx-hook` and `phx-update="ignore` containers 11 | * Add `merge_props` option to merge updated props with the last known props 12 | * Pass `handleEvent` as a prop 13 | 14 | ## v0.3.0 15 | 16 | * Add support for live components (pushEventTo) 17 | * Support keyword list props in `live_react_component` 18 | 19 | ## v0.2.1 20 | 21 | * Add option to override the default LiveView binding prefix 22 | 23 | ## v0.2.0 24 | 25 | * Add option to also render the components on page load 26 | 27 | ## v0.1.0 28 | 29 | Initial version 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2019 Robin Fidder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixLiveReact.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :phoenix_live_react, 7 | version: "0.6.0", 8 | elixir: "~> 1.9", 9 | start_permanent: Mix.env() == :prod, 10 | package: package(), 11 | description: description(), 12 | docs: docs(), 13 | deps: deps() 14 | ] 15 | end 16 | 17 | def application do 18 | [ 19 | extra_applications: [:logger] 20 | ] 21 | end 22 | 23 | defp deps do 24 | [ 25 | {:ex_doc, "~> 0.21", only: :dev, runtime: false}, 26 | {:phoenix_html, "~> 4.0"}, 27 | {:phoenix_html_helpers, "~> 1.0"}, 28 | {:jason, "~> 1.1"} 29 | ] 30 | end 31 | 32 | def description do 33 | """ 34 | A helper library for easily rendering React components in 35 | Phoenix LiveView views. 36 | """ 37 | end 38 | 39 | defp package do 40 | [ 41 | name: :phoenix_live_react, 42 | files: ["lib", "priv", "mix.exs", "package.json", "README*", "LICENSE*"], 43 | maintainers: ["Robin Fidder"], 44 | licenses: ["MIT"], 45 | links: %{"GitHub" => "https://github.com/fidr/phoenix_live_react"} 46 | ] 47 | end 48 | 49 | defp docs do 50 | [ 51 | name: "PhoenixLiveReact", 52 | source_url: "https://github.com/fidr/phoenix_live_react", 53 | homepage_url: "https://github.com/fidr/phoenix_live_react", 54 | main: "readme", 55 | extras: ["README.md"] 56 | ] 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark": {:hex, :earmark, "1.4.4", "4821b8d05cda507189d51f2caeef370cf1e18ca5d7dfb7d31e9cafe6688106a4", [:mix], [], "hexpm", "1f93aba7340574847c0f609da787f0d79efcab51b044bb6e242cae5aca9d264d"}, 3 | "ex_doc": {:hex, :ex_doc, "0.22.0", "fb0495cd70849bc4d7bc716d4e740aebfaddb77bb1074addf357912468c831d6", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f9b36237b220c8262d561489967b6a604832593f0dc1fb1608662909457e91aa"}, 4 | "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"}, 5 | "makeup": {:hex, :makeup, "1.0.1", "82f332e461dc6c79dbd82fbe2a9c10d48ed07146f0a478286e590c83c52010b5", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49736fe5b66a08d8575bf5321d716bac5da20c8e6b97714fec2bcd6febcfa1f8"}, 6 | "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, 7 | "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, 8 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, 9 | "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, 10 | "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, 11 | "plug": {:hex, :plug, "1.10.1", "c56a6d9da7042d581159bcbaef873ba9d87f15dce85420b0d287bca19f40f9bd", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "b5cd52259817eb8a31f2454912ba1cff4990bca7811918878091cb2ab9e52cb8"}, 12 | "plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"}, 13 | } 14 | -------------------------------------------------------------------------------- /assets/js/phoenix_live_react.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | 4 | /** 5 | * Renders a React component with the given props 6 | * @param {HTMLElement} el - The element containing React props 7 | * @param {HTMLElement} target - The target element to render the component into 8 | * @param {React.ComponentType} componentClass - The React component class 9 | * @param {Object} additionalProps - Additional props to pass to the component 10 | * @param {Object} previousProps - Previous props for merging 11 | * @returns {Object} The final props used for rendering 12 | */ 13 | const render = function ( 14 | el, 15 | target, 16 | componentClass, 17 | additionalProps = {}, 18 | previousProps = {} 19 | ) { 20 | let props = el.dataset.liveReactProps 21 | ? JSON.parse(el.dataset.liveReactProps) 22 | : {}; 23 | if (el.dataset.liveReactMerge) { 24 | props = { ...previousProps, ...props, ...additionalProps }; 25 | } else { 26 | props = { ...props, ...additionalProps }; 27 | } 28 | const reactElement = React.createElement(componentClass, props); 29 | if (!target._reactRoot) { 30 | target._reactRoot = ReactDOM.createRoot(target); 31 | } 32 | target._reactRoot.render(reactElement); 33 | return props; 34 | }; 35 | 36 | const initLiveReactElement = function (el, additionalProps) { 37 | const target = el.nextElementSibling; 38 | const componentClass = Array.prototype.reduce.call( 39 | el.dataset.liveReactClass.split('.'), 40 | (acc, el) => { 41 | return acc[el]; 42 | }, 43 | window 44 | ); 45 | render(el, target, componentClass, additionalProps); 46 | return { target: target, componentClass: componentClass }; 47 | }; 48 | 49 | const initLiveReact = function () { 50 | const elements = document.querySelectorAll('[data-live-react-class]'); 51 | Array.prototype.forEach.call(elements, (el) => { 52 | initLiveReactElement(el); 53 | }); 54 | }; 55 | 56 | const LiveReact = { 57 | mounted() { 58 | const { el } = this; 59 | const pushEvent = this.pushEvent.bind(this); 60 | const pushEventTo = this.pushEventTo && this.pushEventTo.bind(this); 61 | const handleEvent = this.handleEvent && this.handleEvent.bind(this); 62 | const { target, componentClass } = initLiveReactElement(el, { pushEvent }); 63 | const props = render(el, target, componentClass, { 64 | pushEvent, 65 | pushEventTo, 66 | handleEvent, 67 | }); 68 | if (el.dataset.liveReactMerge) this.props = props; 69 | Object.assign(this, { target, componentClass }); 70 | }, 71 | 72 | updated() { 73 | const { el, target, componentClass } = this; 74 | const pushEvent = this.pushEvent.bind(this); 75 | const pushEventTo = this.pushEventTo && this.pushEventTo.bind(this); 76 | const handleEvent = this.handleEvent; 77 | const previousProps = this.props; 78 | const props = render( 79 | el, 80 | target, 81 | componentClass, 82 | { pushEvent, pushEventTo }, 83 | previousProps 84 | ); 85 | if (el.dataset.liveReactMerge) this.props = props; 86 | }, 87 | 88 | destroyed() { 89 | const { target } = this; 90 | if (target._reactRoot) { 91 | target._reactRoot.unmount(); 92 | } 93 | }, 94 | }; 95 | 96 | export { LiveReact as default, initLiveReact, initLiveReactElement }; 97 | -------------------------------------------------------------------------------- /priv/static/phoenix_live_react.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react"),require("react-dom/client")):"function"==typeof define&&define.amd?define(["react","react-dom/client"],t):"object"==typeof exports?exports.phoenix_live_react=t(require("react"),require("react-dom/client")):e.phoenix_live_react=t(e.react,e["react-dom/client"])}(self,((e,t)=>(()=>{"use strict";var r={155:t=>{t.exports=e},236:e=>{e.exports=t}},o={};function n(e){var t=o[e];if(void 0!==t)return t.exports;var i=o[e]={exports:{}};return r[e](i,i.exports,n),i.exports}n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var i={};n.r(i),n.d(i,{default:()=>y,initLiveReact:()=>b,initLiveReactElement:()=>h});var a=n(155),c=n.n(a),s=n(236),u=n.n(s);function l(e){return l="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},l(e)}function p(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);t&&(o=o.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,o)}return r}function f(e){for(var t=1;t3&&void 0!==arguments[3]?arguments[3]:{},n=arguments.length>4&&void 0!==arguments[4]?arguments[4]:{},i=e.dataset.liveReactProps?JSON.parse(e.dataset.liveReactProps):{};i=e.dataset.liveReactMerge?f(f(f({},n),i),o):f(f({},i),o);var a=c().createElement(r,i);return t._reactRoot||(t._reactRoot=u().createRoot(t)),t._reactRoot.render(a),i},h=function(e,t){var r=e.nextElementSibling,o=Array.prototype.reduce.call(e.dataset.liveReactClass.split("."),(function(e,t){return e[t]}),window);return d(e,r,o,t),{target:r,componentClass:o}},b=function(){var e=document.querySelectorAll("[data-live-react-class]");Array.prototype.forEach.call(e,(function(e){h(e)}))},y={mounted:function(){var e=this.el,t=this.pushEvent.bind(this),r=this.pushEventTo&&this.pushEventTo.bind(this),o=this.handleEvent&&this.handleEvent.bind(this),n=h(e,{pushEvent:t}),i=n.target,a=n.componentClass,c=d(e,i,a,{pushEvent:t,pushEventTo:r,handleEvent:o});e.dataset.liveReactMerge&&(this.props=c),Object.assign(this,{target:i,componentClass:a})},updated:function(){var e=this.el,t=this.target,r=this.componentClass,o=this.pushEvent.bind(this),n=this.pushEventTo&&this.pushEventTo.bind(this),i=(this.handleEvent,this.props),a=d(e,t,r,{pushEvent:o,pushEventTo:n},i);e.dataset.liveReactMerge&&(this.props=a)},destroyed:function(){var e=this.target;e._reactRoot&&e._reactRoot.unmount()}};return i})())); -------------------------------------------------------------------------------- /lib/phoenix_live_react.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixLiveReact do 2 | @moduledoc """ 3 | Render React.js components in Phoenix LiveView views. 4 | """ 5 | 6 | import Phoenix.HTML 7 | use PhoenixHTMLHelpers 8 | 9 | @doc """ 10 | Render a react component in a live view. 11 | 12 | ``` 13 | <%= PhoenixLiveReact.live_react_component("Components.MyComponent", %{name: "Bob"}, id: "my-component-1") %> 14 | ``` 15 | 16 | ## Events 17 | 18 | To push events back to the liveview the `pushEvent` and `pushEventTo` functions from 19 | Phoenix LiveView are passed as props to the component. 20 | 21 | * pushEvent(event, payload, (reply, ref) => ...) - push an event from the client to the LiveView 22 | * pushEventTo(selector, event, payload, (reply, ref) => ...) - push an event from the client to a specific LiveView component 23 | * handleEvent(event, handler) - (phoenix_live_view >= 0.14) receive data directly through liveview `push_event` 24 | 25 | ``` 26 | const { pushEvent, pushEventTo, handleEvent } = this.props; 27 | pushEvent("button_click"); 28 | pushEvent("myevent", {"var": "value"}); 29 | pushEventTo("#component-1", "do_something") 30 | 31 | handleEvent("some-event", (payload) => console.log(payload)) 32 | ``` 33 | 34 | ## Parameters 35 | 36 | - name: String with the module name of the component 37 | - props: Map or keyword list with the props for the react component 38 | - options: Keyword list with render options 39 | 40 | It is possible to override both the receiver and the container div's attributes by passing 41 | a keyword list as `:container` and `:receiver` options. 42 | 43 | You can also override the tag type with the `:container_tag` and `:receiver_tag` options 44 | 45 | By default, LiveView uses `phx-` as the binding prefix. You can override this with the 46 | `:binding_prefix` option. 47 | 48 | ``` 49 | <%= 50 | PhoenixLiveReact.live_react_component("Components.MyComponent", %{}, 51 | id: "my-component-1", 52 | container: [class: "my-component"], 53 | container_tag: :p 54 | ) 55 | %> 56 | ``` 57 | """ 58 | def live_react_component(name, props \\ %{}, options \\ []) 59 | 60 | def live_react_component(name, props_list, options) when is_list(props_list) do 61 | live_react_component(name, Map.new(props_list), options) 62 | end 63 | 64 | def live_react_component(name, props, options) do 65 | html_escape([ 66 | receiver_element(name, props, options), 67 | container_element(options) 68 | ]) 69 | end 70 | 71 | defp receiver_element(name, props, options) do 72 | attr = Keyword.get(options, :receiver, []) 73 | tag = Keyword.get(options, :receiver_tag, :div) 74 | binding_prefix = Keyword.get(options, :binding_prefix, "phx-") 75 | 76 | default_attr = [ 77 | style: "display: none;", 78 | id: Keyword.get(options, :id), 79 | data: [ 80 | live_react_class: name, 81 | live_react_props: Jason.encode!(props), 82 | live_react_merge: options[:merge_props] == true 83 | ], 84 | "#{binding_prefix}hook": "LiveReact" 85 | ] 86 | 87 | content_tag(tag, "", Keyword.merge(default_attr, attr)) 88 | end 89 | 90 | defp container_element(options) do 91 | attr = Keyword.get(options, :container, []) 92 | tag = Keyword.get(options, :container_tag, :div) 93 | binding_prefix = Keyword.get(options, :binding_prefix, "phx-") 94 | id = 95 | case Keyword.get(options, :id) do 96 | nil -> nil 97 | id -> "#{id}-container" 98 | end 99 | 100 | default_attr = ["#{binding_prefix}update": "ignore", id: id] 101 | 102 | content_tag(tag, "", Keyword.merge(default_attr, attr)) 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PhoenixLiveReact 2 | 3 | Render React.js components in Phoenix LiveView views. 4 | 5 | ## Installation 6 | 7 | Add to your `mix.exs` and run `mix deps.get`: 8 | 9 | ```elixir 10 | def deps do 11 | [ 12 | {:phoenix_live_react, "~> 0.5"} 13 | ] 14 | end 15 | ``` 16 | 17 | 18 | If you're using Phoenix 1.5 or older, then add to your `assets/package.json` and run `npm i` or `yarn`: 19 | 20 | ``` 21 | { 22 | ... 23 | "dependencies": { 24 | ... 25 | "phoenix": "file:../deps/phoenix", 26 | "phoenix_html": "file:../deps/phoenix_html", 27 | "phoenix_live_view": "file:../deps/phoenix_live_view", 28 | "phoenix_live_react": "file:../deps/phoenix_live_react", # <-- ADD THIS! 29 | ... 30 | }, 31 | ... 32 | } 33 | ``` 34 | 35 | Note for umbrella projects the relative file paths should look like `"file:../../../deps/phoenix_live_react"` 36 | 37 | Connect the hooks to your liveview (`app.js`): 38 | 39 | ```javascript 40 | import LiveReact, { initLiveReact } from "phoenix_live_react" 41 | 42 | let hooks = { LiveReact } 43 | 44 | let liveSocket = new LiveSocket("/live", Socket, { hooks, params: { _csrf_token: csrfToken } }) 45 | 46 | // Optionally render the React components on page load as 47 | // well to speed up the initial time to render. 48 | // The pushEvent, pushEventTo and handleEvent props will not be passed here. 49 | document.addEventListener("DOMContentLoaded", e => { 50 | initLiveReact() 51 | }) 52 | ``` 53 | 54 | Add the helper to your `MyAppWeb` file, `lib/MyAppWeb.ex`: 55 | 56 | ```elixir 57 | def live_view do 58 | quote do 59 | # ... 60 | import PhoenixLiveReact 61 | # ... 62 | end 63 | end 64 | ``` 65 | 66 | Add your react components to the window scope (`app.js`): 67 | 68 | ```javascript 69 | import { MyComponent } from "./components/MyComponent" 70 | 71 | window.Components = { 72 | MyComponent 73 | } 74 | ``` 75 | 76 | ## Usage 77 | 78 | Use in your live view: 79 | 80 | ```elixir 81 | <%= live_react_component("Components.MyComponent", [name: @name], id: "my-component-1") %> 82 | ``` 83 | 84 | ### Events 85 | 86 | To push events back to the liveview the `pushEvent`, `pushEventTo` and `handleEvent` functions from 87 | Phoenix LiveView are passed as props to the component. 88 | 89 | * pushEvent(event, payload, (reply, ref) => ...) - push an event from the client to the LiveView 90 | * pushEventTo(selector, event, payload, (reply, ref) => ...) - push an event from the client to a specific LiveView component 91 | * handleEvent(event, handler) - (phoenix_live_view >= 0.14) receive data directly through liveview `push_event` 92 | 93 | ```javascript 94 | const { pushEvent, pushEventTo, handleEvent } = this.props; 95 | 96 | pushEvent("button_click"); 97 | pushEvent("myevent", {"var": "value"}); 98 | pushEventTo("#component-1", "do_something") 99 | 100 | handleEvent("some-event", (payload) => console.log(payload)) 101 | ``` 102 | 103 | ## How to add React to Phoenix 1.6 app 104 | 105 | ### Add NPM 106 | 107 | In your assets dir: 108 | 109 | ```bash 110 | npm init # press enter until its done 111 | ``` 112 | 113 | In your `config.exs`: 114 | 115 | Change the NODE_PATH to include node_modules for the :esbuild / :default entry. 116 | 117 | ```elixir 118 | env: %{"NODE_PATH" => Enum.join([Path.expand("../deps", __DIR__), Path.expand("../assets/node_modules", __DIR__)], ":")} 119 | ``` 120 | 121 | ### Add react 122 | 123 | In your assets dir: 124 | 125 | ```bash 126 | npm add react react-dom 127 | ``` 128 | 129 | ## How to add react to Phoenix 1.5 or older 130 | 131 | In your assets dir: 132 | 133 | ```bash 134 | npm install react react-dom --save 135 | npm install @babel/preset-env @babel/preset-react --save-dev 136 | ``` 137 | 138 | Create an `assets/.babelrc` file: 139 | 140 | ``` 141 | { 142 | "presets": [ 143 | "@babel/preset-env", 144 | "@babel/preset-react" 145 | ] 146 | } 147 | ``` 148 | 149 | For NPM users, you might need the add the following to your `assets/webpack.config.js` file: 150 | ``` 151 | module.exports = (env, options) => ({ 152 | // add: 153 | resolve: { 154 | alias: { 155 | react: path.resolve(__dirname, './node_modules/react'), 156 | 'react-dom': path.resolve(__dirname, './node_modules/react-dom') 157 | } 158 | } 159 | // 160 | }); 161 | ``` 162 | 163 | ## React phoenix 164 | 165 | This library is inspired by [react-phoenix](https://github.com/geolessel/react-phoenix). 166 | 167 | Check it out if you want to use react components in regular views. 168 | --------------------------------------------------------------------------------