├── .env ├── .gitignore ├── Makefile ├── README.md ├── docs └── man ├── scripts ├── create-component └── create-model ├── server ├── api-routes.go ├── server.go ├── src │ └── ui │ │ ├── cache.go │ │ ├── compile.go │ │ ├── eval.go │ │ ├── router.go │ │ ├── runtime.go │ │ ├── state.go │ │ ├── ui.go │ │ └── watch-changes.go └── ui-routes.go └── ui ├── app.css ├── app.js ├── client-side.js ├── components └── example │ ├── index.js │ ├── reducers.js │ ├── state.js │ ├── style.css │ └── view.js ├── index.html ├── package.json ├── routes.js └── server-side.js /.env: -------------------------------------------------------------------------------- 1 | ADDR=:9000 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | public/dist.js 5 | public/dist.css 6 | server/vendor 7 | server/pkg 8 | server/bin 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include .env 2 | 3 | PROJECTNAME=$(shell basename "$(PWD)") 4 | GOBASE=$(shell pwd)/server 5 | GOPATH=$(GOBASE)/vendor:$(GOBASE) 6 | GOBIN=$(GOBASE)/bin 7 | GOFILES=$(wildcard server/*.go) 8 | PID=/tmp/go-$(PROJECTNAME).pid 9 | UIBASE=$(shell pwd)/ui 10 | UIBIN=$(UIBASE)/node_modules/.bin 11 | 12 | all: develop 13 | 14 | start: 15 | @echo " Starting $(PROJECTNAME) at $(ADDR)" 16 | @-$(GOBIN)/$(PROJECTNAME) & echo $$! > $(PID) 17 | @echo " Process ID: "$(shell cat $(PID)) 18 | 19 | stop: 20 | @echo " Stopping $(PROJECTNAME)" 21 | @-touch $(PID) 22 | @-kill `cat $(PID)` 2> /dev/null || true 23 | 24 | restart: stop start 25 | build: go-build ui-build 26 | clean: go-clean ui-clean 27 | 28 | develop: setup build 29 | @DEVELOP=1 LOG=* $(MAKE) restart 30 | @echo " Watching for changes..." 31 | @fswatch server/. -e "server/bin" -e "server/pkg" | (while read; do DEVELOP=1 LOG=* make setup clean build restart; done) 32 | 33 | setup: 34 | @echo " Please wait while I'm getting the dependencies of $(PROJECTNAME) from internet." 35 | @$(MAKE) go-get 36 | @$(MAKE) ui-install 37 | 38 | go-build: 39 | @echo " Building server into $(GOBIN)" 40 | @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go build -o $(GOBIN)/$(PROJECTNAME) $(GOFILES) 41 | 42 | go-get: 43 | @cd server && GOPATH=$(GOPATH) GOBIN=$(GOBIN) go get . 44 | 45 | go-install: 46 | @cd server && GOPATH=$(GOPATH) GOBIN=$(GOBIN) go install $(GOFILES) 47 | 48 | go-run: 49 | @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go run $(GOFILES) 50 | 51 | go-clean: 52 | @echo " Cleaning Go build cache" 53 | @cd server && GOPATH=$(GOPATH) GOBIN=$(GOBIN) go clean 54 | 55 | go-rebuild: go-clean go-build 56 | 57 | ui-install: 58 | @cd ui && npm install --silent 59 | 60 | ui-build: 61 | @echo " Building UI into ./public" 62 | @mkdir -p ./public 63 | @cd ui && ./node_modules/.bin/browserify -r min-document --igv __filename,__dirname,_process -t [ babelify --presets [ es2015 ] ] client-side.js > ../public/dist.js 64 | @cd ui && cat *.css components/**/*.css > ../public/dist.css 65 | 66 | ui-clean: 67 | @echo " Cleaning UI builds" 68 | @-rm public/{dist.js,dist.css} 2> /dev/null || true 69 | 70 | ui-build-serverside: 71 | @cd ui && ./node_modules/.bin/browserify -r min-document --igv __filename,__dirname,_process -t [ babelify --presets [ es2015 ] ] --debug server-side.js 72 | 73 | export COMPONENT_INDEX 74 | export COMPONENT_VIEW 75 | create-component: 76 | define COMPONENT_INDEX 77 | import view from './view' 78 | import state from './state' 79 | import * as reducers from './reducers' 80 | import * as effects from './effects' 81 | 82 | export default { 83 | namespace: '$(name)', 84 | view, 85 | state, 86 | reducers, 87 | effects 88 | } 89 | endef 90 | 91 | define COMPONENT_VIEW 92 | import html from "choo/html" 93 | 94 | const view = (state, prev, send) => html` 95 | $(name) 96 | ` 97 | 98 | export default view 99 | endef 100 | 101 | @mkdir ui/components/${name} 102 | @echo "$$COMPONENT_INDEX" > ui/components/${name}/index.js 103 | @echo "$$COMPONENT_VIEW" > ui/components/${name}/view.js 104 | @echo "export default {}" > ui/components/${name}/reducers.js 105 | @echo "export default {}" > ui/components/${name}/effects.js 106 | @echo "export default {}" > ui/components/${name}/state.js 107 | 108 | commands: 109 | @cat docs/man 110 | 111 | usage: commands 112 | help: commands 113 | 114 | .PHONY: default go-build go-get go-install go-run go-rebuild go-clean ui-build ui-clean ui-build-serverside start stop restart clean commands help usage 115 | 116 | ifndef VERBOSE 117 | .SILENT: 118 | endif 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## go-choo-starter 2 | 3 | Starter for [choo](https://github.com/yoshuawuyts/choo) projects with [Go](http://golang.org) backend. No configuration needed. 4 | 5 | ### How It Works? 6 | 7 | * Renders [choo](https://github.com/yoshuawuyts/choo) views on server-side with [go-duktape](https://github.com/olebedev/go-duktape). 8 | * Provides a simple Makefile to manage your project; 9 | * `make start` starts everything 10 | * `make stop` stops all 11 | * `make develop` starts the server and watches everything (Go, JS, CSS) for changes. 12 | * `make setup` installs all dependencies (Go and JS) 13 | 14 | ### Install 15 | 16 | Clone the repo and install the dependencies: 17 | 18 | ```bash 19 | git clone git@github.com:azer/go-choo-starter.git hello-world 20 | cd hello-world 21 | make setup # Runs `go get` and `npm install` for you. 22 | ``` 23 | 24 | ### First Steps 25 | 26 | Here is how you start the server: 27 | 28 | ```bash 29 | make develop 30 | ``` 31 | 32 | `develop` watches your code (Go, JS and CSS) and applies changes immediately. If you don't need that, you can run `make start` and `make stop` commands. You should use these two commands when you're not actively changing your code. 33 | 34 | ### Coding 35 | 36 | * Create UI components under `ui/components/` folder and route them at `ui/app.js` 37 | * Create API endpoints at `server/api.go` like the example there. 38 | * Use `make go-get` to fetch dependencies when you have new dependencies in the backend. 39 | * Any file under `./public` directory will be online at same path. 40 | * Run `make build` to build everything. Run `make go-build` and `make ui-build` to build each separately. 41 | * Run `make clean` to clean everything. Run `make go-clean` and `make ui-clean` to clean each separately. 42 | -------------------------------------------------------------------------------- /docs/man: -------------------------------------------------------------------------------- 1 | 2 | USAGE 3 | 4 | make [command] 5 | 6 | COMMANDS 7 | 8 | develop 9 | start 10 | stop 11 | restart 12 | setup 13 | clean 14 | help 15 | 16 | go-build 17 | go-get 18 | go-install 19 | go-run 20 | go-rebuild 21 | go-clean 22 | 23 | ui-install 24 | ui-build 25 | ui-clean 26 | 27 | create-component 28 | -------------------------------------------------------------------------------- /scripts/create-component: -------------------------------------------------------------------------------- 1 | createComponent () { 2 | mkdir ui/components/$1 3 | cat > ui/components/$1/index.js <<- EOM 4 | import view from './view' 5 | import state from './state' 6 | import * as reducers from './reducers' 7 | import * as effects from './effects' 8 | 9 | export default { 10 | namespace: '$1', 11 | view, 12 | state, 13 | reducers, 14 | effects 15 | } 16 | EOM 17 | 18 | 19 | cat > ui/components/$1/view.js <<- EOM 20 | import html from "choo/html" 21 | 22 | export default (state, prev, send) => html\` 23 |
24 |
25 | \` 26 | EOM 27 | 28 | @mkdir ui/components/${name} 29 | @echo "$$COMPONENT_INDEX" > ui/components/${name}/index.js 30 | @echo "$$COMPONENT_VIEW" > ui/components/${name}/view.js 31 | @echo "export default {}" > ui/components/${name}/reducers.js 32 | @echo "export default {}" > ui/components/${name}/effects.js 33 | @echo "export default {}" > ui/components/${name}/state.js 34 | } 35 | 36 | createComponent $1 37 | -------------------------------------------------------------------------------- /scripts/create-model: -------------------------------------------------------------------------------- 1 | createModel () { 2 | mkdir src/ui/models/$1 3 | cat > src/ui/models/$1/index.js <<- EOM 4 | import state from './state' 5 | import * as reducers from './reducers' 6 | import * as effects from './effects' 7 | 8 | export default { 9 | namespace: '$(name)', 10 | state, 11 | reducers, 12 | effects 13 | } 14 | EOM 15 | 16 | echo "module.exports = {}" > ui/models/$1/state.js 17 | echo "module.exports = {}" > ui/models/$1/reducers.js 18 | echo "module.exports = {}" > ui/models/$1/effects.js 19 | } 20 | 21 | createModel $1 22 | -------------------------------------------------------------------------------- /server/api-routes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/labstack/echo" 5 | "net/http" 6 | ) 7 | 8 | // Set all your API endpoints in this function, on top of the file 9 | func SetAPIRoutes(server *echo.Echo) { 10 | server.GET("/api/hi", Hi) 11 | } 12 | 13 | // And create the handlers after that 14 | func Hi(c echo.Context) error { 15 | return c.JSON(http.StatusOK, &struct { 16 | Hi string 17 | }{"there"}) 18 | } 19 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/joho/godotenv" 5 | "github.com/labstack/echo" 6 | "os" 7 | "ui" 8 | ) 9 | 10 | func init() { 11 | // Read env variables from .env file in the root directory 12 | if err := godotenv.Load(); err != nil { 13 | panic(err) 14 | } 15 | } 16 | 17 | func main() { 18 | server := echo.New() 19 | SetUIRoutes(server) 20 | SetAPIRoutes(server) 21 | 22 | // Setup static files 23 | server.Static("/public", "./public") 24 | server.GET("/favicon.ico", func(c echo.Context) error { 25 | c.Redirect(301, "/public/favicon.ico") 26 | return nil 27 | }) 28 | 29 | // Watch & build front-end stuff if develop mode is enabled 30 | if len(os.Getenv("DEVELOP")) > 0 { 31 | go ui.WatchCodeChanges() 32 | } 33 | 34 | // And finally, run the server. You can edit the ADDR from .env file on the project folder. 35 | server.Start(os.Getenv("ADDR")) 36 | } 37 | -------------------------------------------------------------------------------- /server/src/ui/cache.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | type Cache struct { 4 | Content map[string]string 5 | } 6 | 7 | func (cache *Cache) Get(path string) (string, bool) { 8 | if _, ok := cache.Content[path]; !ok { 9 | return "", false 10 | } 11 | 12 | return cache.Content[path], true 13 | } 14 | 15 | func (cache *Cache) Set(path, html string) { 16 | cache.Content[path] = html 17 | } 18 | 19 | func NewCache() *Cache { 20 | return &Cache{ 21 | Content: map[string]string{}, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/src/ui/compile.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os/exec" 7 | ) 8 | 9 | func CompileJS() (string, error) { 10 | var stderr bytes.Buffer 11 | cmd := exec.Command("make", "ui-build-serverside") 12 | cmd.Stderr = &stderr 13 | 14 | source, err := cmd.Output() 15 | if err != nil { 16 | return "", fmt.Errorf("%s: %s", err, stderr.String()) 17 | } 18 | 19 | return string(source), nil 20 | } 21 | 22 | func BuildUI() error { 23 | var stderr bytes.Buffer 24 | cmd := exec.Command("make", "ui-build") 25 | cmd.Stderr = &stderr 26 | 27 | _, err := cmd.Output() 28 | if err != nil { 29 | return fmt.Errorf("%s: %s", err, stderr.String()) 30 | } 31 | 32 | log.Info("UI assets has been compiled and saved into ./public directory.") 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /server/src/ui/eval.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | . "github.com/azer/go-style" 6 | "gopkg.in/olebedev/go-duktape.v2" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | const headers = ` 12 | require = undefined 13 | console = { log: print, info: print, error: print, warn: print, trace: print } 14 | ` 15 | 16 | func EvalJS(code string) (string, error) { 17 | ctx := duktape.New() 18 | code = fmt.Sprintf("%s\n%s", headers, code) 19 | 20 | if err := ctx.PevalString(code); err != nil { 21 | derr := err.(*duktape.Error) 22 | PrintJSError(code, derr.Message, derr.LineNumber) 23 | return "", err 24 | } 25 | 26 | result := ctx.GetString(-1) 27 | ctx.DestroyHeap() 28 | 29 | return result, nil 30 | } 31 | 32 | func PrintJSError(code, message string, lineno int) { 33 | lines := strings.Split(code, "\n") 34 | fmt.Println(Style("bold", fmt.Sprintf("\n 📍 JavaScript Error: %s", Style("red", message)))) 35 | 36 | prev := lineno - 2 37 | for prev > 0 && (lines[prev] == "" || isBrowserifyCode(lines[prev])) { 38 | prev-- 39 | } 40 | 41 | next := lineno 42 | for next < len(lines) && (lines[next] == "" || isBrowserifyCode(lines[next])) { 43 | next++ 44 | } 45 | 46 | fmt.Println(Style("reset", fmt.Sprintf("\n %d. %s", prev+1, lines[prev]))) 47 | fmt.Println(Style("red", fmt.Sprintf(" %d. %s", lineno, lines[lineno-1]))) 48 | fmt.Println(Style("reset", fmt.Sprintf(" %d. %s\n", next+1, lines[next]))) 49 | } 50 | 51 | func isBrowserifyCode(line string) bool { 52 | r, _ := regexp.Compile(`^\s*\}\,\{"[^\"]+":\d+`) 53 | return r.MatchString(line) 54 | } 55 | -------------------------------------------------------------------------------- /server/src/ui/router.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "github.com/labstack/echo" 6 | "net/http" 7 | ) 8 | 9 | func Render(c echo.Context, path string, state *State) error { 10 | body, err := runtime.Render(path, state) 11 | if err != nil { 12 | log.Error("Can not render. Error: %v", err) 13 | return err 14 | } 15 | 16 | html := fmt.Sprintf(indexhtml, body) 17 | cache.Set(path, html) 18 | 19 | return c.HTML(http.StatusOK, html) 20 | } 21 | 22 | func HTTPHandler(next echo.HandlerFunc) echo.HandlerFunc { 23 | return func(c echo.Context) error { 24 | path := c.Request().URL.Path 25 | 26 | if html, ok := cache.Get(path); ok { 27 | return c.HTML(http.StatusOK, html) 28 | } 29 | 30 | if err := runtime.SyncRoutes(); err != nil { 31 | return err 32 | } 33 | 34 | match := runtime.Routes.Match(path) 35 | 36 | if match == nil { 37 | return next(c) 38 | } 39 | 40 | log.Info("%s is being automatically routed to UI component", path) 41 | 42 | state := &State{} 43 | state.Location.Pathname = path 44 | state.Params = match.Params 45 | 46 | return Render(c, path, state) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/src/ui/runtime.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/azer/url-router" 7 | "strings" 8 | ) 9 | 10 | type Runtime struct { 11 | CachedSourceCode string 12 | Routes *urlrouter.Router 13 | } 14 | 15 | func (runtime *Runtime) Browserify() error { 16 | code, err := CompileJS() 17 | if err != nil { 18 | return err 19 | } 20 | 21 | runtime.CachedSourceCode = code 22 | return nil 23 | } 24 | 25 | func (runtime *Runtime) CleanCache() { 26 | log.Info("Clearing JS runtime cache...") 27 | 28 | runtime.CachedSourceCode = "" 29 | runtime.Routes = nil 30 | } 31 | 32 | func (runtime *Runtime) CheckForErrors() error { 33 | _, err := runtime.Render("/", nil) 34 | return err 35 | } 36 | 37 | func (runtime *Runtime) Render(route string, state interface{}) (string, error) { 38 | body, err := runtime.SourceCode() 39 | if err != nil { 40 | return "", err 41 | } 42 | 43 | encodedState, err := EncodeState(state) 44 | if err != nil { 45 | return "", err 46 | } 47 | 48 | html, err := EvalJS(fmt.Sprintf(` 49 | var app; 50 | %s 51 | 52 | app.toString("%s", %s) 53 | 54 | function start (_app) { 55 | app = _app; 56 | }`, 57 | body, 58 | route, 59 | encodedState, 60 | )) 61 | 62 | if err != nil { 63 | return "", err 64 | } 65 | 66 | return html, nil 67 | } 68 | 69 | func (runtime *Runtime) SyncRoutes() error { 70 | if runtime.Routes != nil { 71 | return nil 72 | } 73 | 74 | log.Info("Syncing UI and server-side routes...") 75 | 76 | body, err := runtime.SourceCode() 77 | if err != nil { 78 | return err 79 | } 80 | 81 | routes, err := EvalJS(fmt.Sprintf(` 82 | var app; 83 | var routes; 84 | %s 85 | 86 | Object.keys(routes).join(',') 87 | 88 | function start (_app, _routes) { 89 | app = _app; 90 | routes = _routes 91 | }`, 92 | body, 93 | )) 94 | 95 | if err != nil { 96 | return err 97 | } 98 | 99 | runtime.Routes = urlrouter.New() 100 | paths := strings.Split(routes, ",") 101 | 102 | for _, path := range paths { 103 | runtime.Routes.Add(path) 104 | } 105 | 106 | return nil 107 | } 108 | 109 | func (runtime *Runtime) SourceCode() (string, error) { 110 | if runtime.CachedSourceCode != "" { 111 | return runtime.CachedSourceCode, nil 112 | } 113 | 114 | if err := runtime.Browserify(); err != nil { 115 | return "", err 116 | } 117 | 118 | return runtime.CachedSourceCode, nil 119 | } 120 | 121 | func EncodeState(state interface{}) (string, error) { 122 | encoded, err := json.Marshal(state) 123 | if err != nil { 124 | return "", err 125 | } 126 | 127 | return string(encoded), nil 128 | } 129 | -------------------------------------------------------------------------------- /server/src/ui/state.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | type State struct { 4 | Params map[string]string `json:"params"` 5 | Location struct { 6 | Pathname string `json:"pathname"` 7 | } `json:"location"` 8 | } 9 | -------------------------------------------------------------------------------- /server/src/ui/ui.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/azer/logger" 5 | "io/ioutil" 6 | ) 7 | 8 | var cache = NewCache() 9 | var log = logger.New("ui") 10 | var runtime *Runtime 11 | var indexhtml string 12 | 13 | func init() { 14 | runtime = &Runtime{} 15 | loadIndexHTML() 16 | } 17 | 18 | func loadIndexHTML() { 19 | indexbyt, err := ioutil.ReadFile("./ui/index.html") 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | indexhtml = string(indexbyt) 25 | } 26 | -------------------------------------------------------------------------------- /server/src/ui/watch-changes.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/azer/logger" 5 | . "github.com/azer/on-tree-change" 6 | "os" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | const CHANGE_THRESHOLD_MS = 250 12 | 13 | func CheckRuntimeErrors() { 14 | if err := runtime.CheckForErrors(); err == nil { 15 | log.Info("🏁 JavaScript runtime is ready for server-side rendering.") 16 | } else { 17 | log.Error("JavaScript runtime could not be compiled.", logger.Attrs{ 18 | "error": err, 19 | }) 20 | } 21 | } 22 | 23 | func WatchCodeChanges() { 24 | go CheckRuntimeErrors() 25 | 26 | var lastchange = 0 27 | 28 | OnTreeChange("ui", filter, func(name string) { 29 | now := int(time.Now().UnixNano() / 1000000) 30 | 31 | if now-lastchange < CHANGE_THRESHOLD_MS { 32 | return 33 | } 34 | 35 | lastchange = now 36 | 37 | BuildUI() 38 | 39 | runtime.CleanCache() 40 | CheckRuntimeErrors() 41 | 42 | loadIndexHTML() 43 | }) 44 | } 45 | 46 | func filter(name string, info os.FileInfo) bool { 47 | return (info != nil && !info.IsDir()) && !strings.Contains(name, "node_modules/") 48 | } 49 | -------------------------------------------------------------------------------- /server/ui-routes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/labstack/echo" 5 | "ui" 6 | ) 7 | 8 | func SetUIRoutes(server *echo.Echo) { 9 | // Client-side routes will already be compiled and routed by the server automatically. 10 | // But you may want to do call the UI manually in some cases like passing a custom state. 11 | // Check this ExampleForm function to see how we accomplish that. 12 | // You can try commenting out following line and visiting the same URL again. 13 | // server.Get("/", ExampleForm) 14 | 15 | // If there is a UI route matching the URL path, it'll respond first. Otherwise, server will try to match other endpoints. 16 | server.Use(ui.HTTPHandler) 17 | } 18 | 19 | /*func ExampleForm(c echo.Context) error { 20 | return ui.Render(c, "/", &struct { 21 | ExampleTitle string `json:"title"` 22 | }{"Hello From The Server-side"}) 23 | }*/ 24 | -------------------------------------------------------------------------------- /ui/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | padding: 0; 4 | margin: 0; 5 | } 6 | -------------------------------------------------------------------------------- /ui/app.js: -------------------------------------------------------------------------------- 1 | import choo from 'choo' 2 | import example from './components/example' 3 | import routes from "./routes" 4 | 5 | const app = choo() 6 | app.model(example) 7 | router(routes) 8 | 9 | export default app 10 | 11 | // We need this extra function until Choo lets us get list of the routes easily 12 | function router (routes) { 13 | app.router(route => { 14 | const result = [] 15 | 16 | var key 17 | for (key in routes) { 18 | result.push(route(key, routes[key])) 19 | } 20 | 21 | return result 22 | }) 23 | 24 | return Object.keys(routes) 25 | } 26 | -------------------------------------------------------------------------------- /ui/client-side.js: -------------------------------------------------------------------------------- 1 | import app from './app' 2 | 3 | const newTree = app.start() 4 | const oldTree = document.querySelector('main') 5 | 6 | if (oldTree) { 7 | document.body.replaceChild(newTree, oldTree) 8 | } else { 9 | document.body.appendChild(newTree) 10 | } 11 | -------------------------------------------------------------------------------- /ui/components/example/index.js: -------------------------------------------------------------------------------- 1 | import view from './view' 2 | import state from './state' 3 | import * as reducers from './reducers' 4 | 5 | export default { 6 | // namespace: 'example', 7 | view, 8 | state, 9 | reducers 10 | } 11 | -------------------------------------------------------------------------------- /ui/components/example/reducers.js: -------------------------------------------------------------------------------- 1 | export function setTitle (title, state) { 2 | return { 3 | title 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /ui/components/example/state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'hello world' 3 | } 4 | -------------------------------------------------------------------------------- /ui/components/example/style.css: -------------------------------------------------------------------------------- 1 | .example { 2 | padding: 25px; 3 | color: #333; 4 | text-shadow: 2px 2px pink; 5 | } 6 | -------------------------------------------------------------------------------- /ui/components/example/view.js: -------------------------------------------------------------------------------- 1 | import choo from "choo" 2 | import html from "choo/html" 3 | 4 | const view = (state, prev, send) => html` 5 |
6 |

Title: ${state.title}

7 | send('setTitle', e.target.value)}> 8 |
9 | ` 10 | 11 | export default view 12 | -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello World 6 | 7 | 8 | 9 | %s 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "babel-preset-es2015": "^6.18.0", 4 | "babelify": "^7.3.0", 5 | "browserify": "^13.1.1", 6 | "choo": "^3.3.0", 7 | "exorcist": "^0.4.0", 8 | "watchify": "^3.7.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ui/routes.js: -------------------------------------------------------------------------------- 1 | import example from './components/example' 2 | 3 | // Setup all the routes we need and export the list of the paths. 4 | export default { 5 | '/': example.view 6 | } 7 | -------------------------------------------------------------------------------- /ui/server-side.js: -------------------------------------------------------------------------------- 1 | import app from './app' 2 | import routes from './routes' 3 | 4 | start(app, routes) 5 | --------------------------------------------------------------------------------