├── priv └── touch ├── test ├── test_helper.exs └── myweb_test.exs ├── web ├── css │ └── app.css ├── package.json ├── layout.html.eex ├── components │ ├── TodoForm.js │ ├── TodoList.js │ └── App.js └── webpack.config.js ├── config └── config.exs ├── .gitignore ├── README.md ├── mix.exs ├── lib └── reaxt_example.ex └── mix.lock /priv/touch: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /web/css/app.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /test/myweb_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MywebTest do 2 | use ExUnit.Case 3 | 4 | test "the truth" do 5 | assert 1 + 1 == 2 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :reaxt, [ 4 | otp_app: :reaxt_example, 5 | hot: Mix.env == :dev, 6 | pool_size: if Mix.env == :dev do 1 else 10 end 7 | ] 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | /web/node_modules 4 | erl_crash.dump 5 | *.ez 6 | *.swp 7 | 8 | 9 | /priv/react_servers/** 10 | /priv/static/** 11 | /priv/stderr.log 12 | /priv/webpack.stats.json 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Reaxt Example 2 | ============= 3 | 4 | Test it now (need npm and elixir): 5 | 6 | ```elixir 7 | git clone https://github.com/awetzel/reaxt-example 8 | cd reaxt-example 9 | mix deps.get 10 | iex -S mix 11 | ``` 12 | 13 | Then : 14 | - go to http://localhost:8099 to see an example application, 15 | - you can test hot loading by changing a react template in `web/components/xx`, 16 | or the css in `web/css/app.css`. 17 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ReaxtExample.Mixfile do 2 | use Mix.Project 3 | 4 | def project, do: [ 5 | app: :reaxt_example, 6 | version: "0.1.0", 7 | elixir: ">= 1.3.0", 8 | compilers: [:reaxt_webpack] ++ Mix.compilers, 9 | build_embedded: Mix.env == :prod, 10 | start_permanent: Mix.env == :prod, 11 | deps: deps() 12 | ] 13 | 14 | def application, do: [ 15 | mod: { ReaxtExample.App, [] }, 16 | extra_applications: [:logger] 17 | ] 18 | 19 | defp deps, do: [ 20 | {:plug_cowboy, "~> 2.5"}, 21 | {:reaxt, "~> 4.0"}, 22 | ] 23 | end 24 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react": "^16.12.0", 4 | "react-dom": "^16.12.0", 5 | "webpack": "^5.1.3", 6 | "webpack-cli": "4.1.0" 7 | }, 8 | "devDependencies": { 9 | "@material-ui/core": "3.3.2", 10 | "@material-ui/icons": "3.0.1", 11 | "@babel/core": "^7.12.1", 12 | "@babel/preset-env": "^7.12.1", 13 | "@babel/preset-react": "^7.12.1", 14 | "babel-loader": "^8.1.0", 15 | "css-loader": "^5.0.0", 16 | "css-minimizer-webpack-plugin": "^1.1.5", 17 | "mini-css-extract-plugin": "^1.1.1", 18 | "null-loader": "^4.0.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /web/layout.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello World 6 | 7 | 8 | <%= WebPack.header %> 9 | 10 | 11 | 12 |
<%= render.html %>
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /web/components/TodoForm.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import TextField from '@material-ui/core/TextField' 3 | 4 | function useInputState(init) { 5 | const [value, setValue] = useState(init) 6 | 7 | return { 8 | value, 9 | onChange: e => setValue(e.target.value), 10 | reset: () => setValue(init), 11 | } 12 | } 13 | 14 | export default function TodoForm({saveTodo}) { 15 | const {value, onChange, reset} = useInputState('') 16 | 17 | return ( 18 |
{ 20 | event.preventDefault() 21 | saveTodo(value) 22 | reset() 23 | }} 24 | > 25 | 32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /web/components/TodoList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import List from '@material-ui/core/List' 3 | import ListItem from '@material-ui/core/ListItem' 4 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction' 5 | import ListItemText from '@material-ui/core/ListItemText' 6 | import Checkbox from '@material-ui/core/Checkbox' 7 | import IconButton from '@material-ui/core/IconButton' 8 | import DeleteIcon from '@material-ui/icons/Delete' 9 | 10 | export default function TodoList({todos, deleteTodo}) { 11 | return ( 12 | 13 | { 14 | todos.map((todo, index) => ( 15 | 16 | 17 | 18 | 19 | deleteTodo(index)} 22 | > 23 | 24 | 25 | 26 | 27 | )) 28 | } 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /lib/reaxt_example.ex: -------------------------------------------------------------------------------- 1 | defmodule ReaxtExample.App do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | children = [ 6 | { 7 | Plug.Cowboy, 8 | scheme: :http, 9 | plug: ReaxtExample.Api, 10 | options: [ 11 | port: 8099, 12 | ] 13 | } 14 | ] 15 | _ = IO.puts("Starting server at http://localhost:8099") 16 | 17 | Reaxt.reload() 18 | Supervisor.start_link(children, [strategy: :one_for_one, name: __MODULE__]) 19 | end 20 | end 21 | 22 | defmodule ReaxtExample.Api do 23 | require EEx 24 | use Plug.Router 25 | 26 | if Mix.env == :dev do 27 | use Plug.Debugger 28 | plug WebPack.Plug.Static, at: "/public", from: :reaxt_example 29 | else 30 | plug Plug.Static, at: "/public", from: :reaxt_example 31 | end 32 | 33 | plug :match 34 | plug :dispatch 35 | 36 | EEx.function_from_file :defp, :layout, "web/layout.html.eex", [:render] 37 | 38 | get "*_" do 39 | data = %{path: conn.request_path, cookies: conn.cookies, query: conn.params, headers: conn.req_headers} 40 | render = Reaxt.render!(:App, data, 30_000) 41 | 42 | conn 43 | |> put_resp_header("content-type", "text/html;charset=utf-8") 44 | |> send_resp(render.param || 200, layout(render)) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /web/components/App.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import ReactDOM from 'react-dom' 3 | import Typography from '@material-ui/core/Typography' 4 | import '../css/app.css' 5 | 6 | import TodoForm from './TodoForm' 7 | import TodoList from './TodoList' 8 | 9 | function useTodoState(init) { 10 | const [todos, setTodos] = useState(init) 11 | 12 | 13 | return { 14 | todos, 15 | addTodo: x => setTodos(xs => [...xs, x]), 16 | removeTodo: index => setTodos(xs => xs.filter((x, i) => i !== index)), 17 | } 18 | } 19 | 20 | function App() { 21 | const {todos, addTodo, removeTodo} = useTodoState([]) 22 | 23 | const saveTodo = todoText => { 24 | const trimmedText = todoText.trim() 25 | 26 | if (trimmedText.length > 0) { 27 | addTodo(trimmedText) 28 | } 29 | } 30 | 31 | return ( 32 |
33 | 34 | Todos 35 | 36 | 37 | 38 | 39 |
40 | ) 41 | } 42 | 43 | export default { 44 | reaxt_server_render(params, render) { // server side call, should call render(ReactComp) 45 | render() 46 | }, 47 | reaxt_client_render(initialProps, render) { // initial client side call, should call render(ReactComp) 48 | render() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /web/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 4 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') 5 | 6 | const client_config = { 7 | entry: "reaxt/client_entry_addition", 8 | mode: (process.env.MIX_ENV != 'prod') ? 'development' : 'production', 9 | target: 'web', 10 | // devtool: (process.env.MIX_ENV != 'prod') ? 'source-map' : false, 11 | devtool: "inline-source-map", 12 | output: { 13 | path: path.join(__dirname, '../priv/static'), //typical output on the default directory served by Plug.Static 14 | filename: '[name].[fullhash].js', // dynamic name for long term caching, or code splitting, use WebPack.file_of(:main) to get it 15 | publicPath: '/public/' 16 | }, 17 | // use cacheGroups to aggregate all css chunks into one main.css file 18 | optimization: { 19 | splitChunks: {cacheGroups: {styles: {name: 'styles', test: /\.css$/, chunks: 'all', enforce: true}}}, 20 | minimizer: (process.env.MIX_ENV != 'prod') ? [`...`] : [`...`, new CssMinimizerPlugin()], 21 | }, 22 | plugins: [ 23 | new MiniCssExtractPlugin({insert: "", filename: 'css/[name].css'}), 24 | ], 25 | module: { 26 | rules: [ 27 | { 28 | test: /.+\.js$/, 29 | exclude: /node_modules/, 30 | use: { 31 | loader: 'babel-loader', 32 | options: { 33 | cacheDirectory: true, 34 | presets: [ 35 | ['@babel/preset-env', {targets: 'defaults'}], 36 | '@babel/preset-react', 37 | ], 38 | sourceType: 'unambiguous', 39 | }, 40 | }, 41 | }, 42 | ], 43 | } 44 | } 45 | 46 | const server_config = Object.assign(Object.assign({},client_config),{ 47 | target: "node", 48 | entry: "reaxt/react_server", 49 | externals: {}, 50 | output: { 51 | // typical output on the default directory served by Plug.Static 52 | path: path.join(__dirname, '../priv/react_servers'), 53 | filename: 'server.js', 54 | chunkFilename: 'chunk/server.[id].js', 55 | }, 56 | }) 57 | 58 | // optimisation : ONLY EMIT files for client compilation, all file-loader should not emit files on server compilation 59 | server_config.module = { 60 | rules: server_config.module.rules.map(rule => { 61 | return { 62 | ...rule, 63 | use: ((Array.isArray(rule.use)) ? rule.use : [rule.use]).map(use => ({ 64 | ...use, 65 | options: (use.loader === 'file-loader') ? {...use.options, emitFile: false} : use.options, 66 | })) 67 | } 68 | }), 69 | } 70 | 71 | // css management : MiniCssExtractPlugin on client build but ignore on server side 72 | 73 | client_config.module.rules.push( 74 | {test: /\.css$/, use: [{loader: MiniCssExtractPlugin.loader}, {loader: 'css-loader'}]} 75 | ) 76 | 77 | server_config.module.rules.push( 78 | {test: /\.css$/, use: [{loader: 'null-loader'}]} 79 | ) 80 | 81 | module.exports = [client_config,server_config] 82 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, 3 | "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, 4 | "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, 5 | "exos": {:hex, :exos, "2.0.0", "4f0dfd3f69201d7731f8824101747f9cdce979fa0324bd34e5ed4236b47be0be", [:mix], [], "hexpm", "426714ba75ad55d408617b5b40e729592a4934b3710893f1aa18bf4c761bd25e"}, 6 | "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"}, 7 | "plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.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.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"}, 8 | "plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"}, 9 | "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, 10 | "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"}, 11 | "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, 12 | "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, 13 | "reaxt": {:hex, :reaxt, "4.0.2", "cd7d66a4adf3d8342b981d1daa0e4abf63d2f4d9a9f5948243b91477301e6a4e", [:mix], [{:exos, "~> 2.0", [hex: :exos, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 4.0", [hex: :poison, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5.0", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "1cb81837ed88e4dfac5e5d4138c5bcf817a7a72a8972effd8d3c3984fda65a1a"}, 14 | "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"}, 15 | } 16 | --------------------------------------------------------------------------------