diff --git a/_examples/tutorial/vuejs-todo-mvc/README.md b/_examples/tutorial/vuejs-todo-mvc/README.md index 09d0f55c..77141606 100644 --- a/_examples/tutorial/vuejs-todo-mvc/README.md +++ b/_examples/tutorial/vuejs-todo-mvc/README.md @@ -22,8 +22,8 @@ Many articles have been written, in the past, that lead developers not to use a You’ll need only two dependencies: -1. The Iris Web Framework, for our server-side requirements. Can be found [here](https://github.com/kataras/iris) -2. Vue.js, for our client-side requirements. Download it from [here](https://vuejs.org/) +1. The Iris Web Framework, for our server-side requirements. Can be found [here](https://github.com/kataras/iris), latest v10. +2. Vue.js, for our client-side requirements. Download it from [here](https://vuejs.org/), latest v2. > If you have Go already installed then just execute `go get -u github.com/kataras/iris` to install the Iris Web Framework. @@ -34,10 +34,530 @@ If we are all in the same page, it’s time to learn how we can create a live to We're going to use a vue.js todo application which uses browser' s local storage and doesn't have any user-specified features like live sync between browser's tabs, you can find the original version inside the vue's [docs](https://vuejs.org/v2/examples/todomvc.html). +Assuming that you know how %GOPATH% works, create an empty folder with any name in the %GOPATH%/src directory, we will create those files: + +- web/public/js/app.js +- web/public/index.html +- todo/item.go +- todo/service.go +- web/controllers/todo_controller.go +- web/main.go + +_Read the comments in the source code, they may be very helpful_ + ### The client-side (vue.js) +```js +/* file: web/public/js/app.js */ +// Full spec-compliant TodoMVC with Iris +// and hash-based routing in ~200 effective lines of JavaScript. + +var socket = new Ws("ws://localhost:8080/todos/sync"); + +socket.On("saved", function () { + // console.log("receive: on saved"); + fetchTodos(function (items) { + app.todos = items + }); +}); + + +function fetchTodos(onComplete) { + axios.get("/todos").then(response => { + if (response.data === null) { + return; + } + + onComplete(response.data); + }); +} + +var todoStorage = { + fetch: function () { + var todos = []; + fetchTodos(function (items) { + for (var i = 0; i < items.length; i++) { + todos.push(items[i]); + } + }); + return todos; + }, + save: function (todos) { + axios.post("/todos", JSON.stringify(todos)).then(response => { + if (!response.data.success) { + window.alert("saving had a failure"); + return; + } + // console.log("send: save"); + socket.Emit("save") + }); + } +} + +// visibility filters +var filters = { + all: function (todos) { + return todos + }, + active: function (todos) { + return todos.filter(function (todo) { + return !todo.completed + }) + }, + completed: function (todos) { + return todos.filter(function (todo) { + return todo.completed + }) + } +} + +// app Vue instance +var app = new Vue({ + // app initial state + data: { + todos: todoStorage.fetch(), + newTodo: '', + editedTodo: null, + visibility: 'all' + }, + + // we will not use the "watch" as it works with the fields like "hasChanges" + // and callbacks to make it true but let's keep things very simple as it's just a small getting started. + // // watch todos change for persistence + // watch: { + // todos: { + // handler: function (todos) { + // if (app.hasChanges) { + // todoStorage.save(todos); + // app.hasChanges = false; + // } + + // }, + // deep: true + // } + // }, + + // computed properties + // http://vuejs.org/guide/computed.html + computed: { + filteredTodos: function () { + return filters[this.visibility](this.todos) + }, + remaining: function () { + return filters.active(this.todos).length + }, + allDone: { + get: function () { + return this.remaining === 0 + }, + set: function (value) { + this.todos.forEach(function (todo) { + todo.completed = value + }) + this.notifyChange(); + } + } + }, + + filters: { + pluralize: function (n) { + return n === 1 ? 'item' : 'items' + } + }, + + // methods that implement data logic. + // note there's no DOM manipulation here at all. + methods: { + notifyChange: function () { + todoStorage.save(this.todos) + }, + addTodo: function () { + var value = this.newTodo && this.newTodo.trim() + if (!value) { + return + } + this.todos.push({ + id: this.todos.length + 1, // just for the client-side. + title: value, + completed: false + }) + this.newTodo = '' + this.notifyChange(); + }, + + completeTodo: function (todo) { + if (todo.completed) { + todo.completed = false; + } else { + todo.completed = true; + } + this.notifyChange(); + }, + removeTodo: function (todo) { + this.todos.splice(this.todos.indexOf(todo), 1) + this.notifyChange(); + }, + + editTodo: function (todo) { + this.beforeEditCache = todo.title + this.editedTodo = todo + }, + + doneEdit: function (todo) { + if (!this.editedTodo) { + return + } + this.editedTodo = null + todo.title = todo.title.trim(); + if (!todo.title) { + this.removeTodo(todo); + } + this.notifyChange(); + }, + + cancelEdit: function (todo) { + this.editedTodo = null + todo.title = this.beforeEditCache + }, + + removeCompleted: function () { + this.todos = filters.active(this.todos); + this.notifyChange(); + } + }, + + // a custom directive to wait for the DOM to be updated + // before focusing on the input field. + // http://vuejs.org/guide/custom-directive.html + directives: { + 'todo-focus': function (el, binding) { + if (binding.value) { + el.focus() + } + } + } +}) + +// handle routing +function onHashChange() { + var visibility = window.location.hash.replace(/#\/?/, '') + if (filters[visibility]) { + app.visibility = visibility + } else { + window.location.hash = '' + app.visibility = 'all' + } +} + +window.addEventListener('hashchange', onHashChange) +onHashChange() + +// mount +app.$mount('.todoapp') +``` + +Let's add our view, the static html. + +```html + + + + + + + Iris + Vue.js • TodoMVC + + + + + + + + + + + + + + +
+
+

todos

+ +
+
+ + +
+ +
+ + + + + + +``` + ### The server-side (iris) +Our view model. + +```go +// file: todo/item.go +package todo + +type Item struct { + SessionID string `json:"-"` + ID int64 `json:"id,omitempty"` + Title string `json:"title"` + Completed bool `json:"completed"` +} +``` + +Our service. + +```go +// file: todo/service.go +package todo + +import ( + "sync" +) + +type Service interface { + Get(owner string) []Item + Save(owner string, newItems []Item) error +} + +type MemoryService struct { + // key = session id, value the list of todo items that this session id has. + items map[string][]Item + // protected by locker for concurrent access. + mu sync.RWMutex +} + +func NewMemoryService() *MemoryService { + return &MemoryService{ + items: make(map[string][]Item, 0), + } +} + +func (s *MemoryService) Get(sessionOwner string) []Item { + s.mu.RLock() + items := s.items[sessionOwner] + s.mu.RUnlock() + + return items +} + +func (s *MemoryService) Save(sessionOwner string, newItems []Item) error { + var prevID int64 + for i := range newItems { + if newItems[i].ID == 0 { + newItems[i].ID = prevID + prevID++ + } + } + + s.mu.Lock() + s.items[sessionOwner] = newItems + s.mu.Unlock() + return nil +} +``` + +We are going to use some of the MVC functionalities of the iris web framework here but you can do the same with the standard API as well. + +```go +// file: controllers/todo_controller.go +package controllers + +import ( + "vuejs-todo-mvc/src/todo" + + "github.com/kataras/iris" + "github.com/kataras/iris/mvc" + "github.com/kataras/iris/sessions" + "github.com/kataras/iris/websocket" +) + +// TodoController is our TODO app's web controller. +type TodoController struct { + Service todo.Service + + Session *sessions.Session +} + +// BeforeActivation called once before the server ran, and before +// the routes and dependencies binded. +// You can bind custom things to the controller, add new methods, add middleware, +// add dependencies to the struct or the method(s) and more. +func (c *TodoController) BeforeActivation(b mvc.BeforeActivation) { + // this could be binded to a controller's function input argument + // if any, or struct field if any: + b.Dependencies().Add(func(ctx iris.Context) (items []todo.Item) { + ctx.ReadJSON(&items) + return + }) +} + +// Get handles the GET: /todos route. +func (c *TodoController) Get() []todo.Item { + return c.Service.Get(c.Session.ID()) +} + +// PostItemResponse the response data that will be returned as json +// after a post save action of all todo items. +type PostItemResponse struct { + Success bool `json:"success"` +} + +var emptyResponse = PostItemResponse{Success: false} + +// Post handles the POST: /todos route. +func (c *TodoController) Post(newItems []todo.Item) PostItemResponse { + if err := c.Service.Save(c.Session.ID(), newItems); err != nil { + return emptyResponse + } + + return PostItemResponse{Success: true} +} + +func (c *TodoController) GetSync(conn websocket.Connection) { + conn.Join(c.Session.ID()) + conn.On("save", func() { // "save" event from client. + conn.To(c.Session.ID()).Emit("saved", nil) // fire a "saved" event to the rest of the clients w. + }) + + conn.Wait() +} +``` + +And finally our main application's endpoint. + +```go +// file: web/main.go +package main + +import ( + "vuejs-todo-mvc/src/todo" + "vuejs-todo-mvc/src/web/controllers" + + "github.com/kataras/iris" + "github.com/kataras/iris/sessions" + "github.com/kataras/iris/websocket" + + "github.com/kataras/iris/mvc" +) + +func main() { + app := iris.New() + + // serve our app in public, public folder + // contains the client-side vue.js application, + // no need for any server-side template here, + // actually if you're going to just use vue without any + // back-end services, you can just stop afer this line and start the server. + app.StaticWeb("/", "./public") + + // configure the http sessions. + sess := sessions.New(sessions.Config{ + Cookie: "iris_session", + }) + + // configure the websocket server. + ws := websocket.New(websocket.Config{}) + + // create a sub router and register the client-side library for the iris websockets, + // you could skip it but iris websockets supports socket.io-like API. + todosRouter := app.Party("/todos") + // http://localhost:8080/todos/iris-ws.js + // serve the javascript client library to communicate with + // the iris high level websocket event system. + todosRouter.Any("/iris-ws.js", websocket.ClientHandler()) + + // create our mvc application targeted to /todos relative sub path. + todosApp := mvc.New(todosRouter) + + // any dependencies bindings here... + todosApp.Register( + todo.NewMemoryService(), + sess.Start, + ws.Upgrade, + ) + + // controllers registration here... + todosApp.Handle(new(controllers.TodoController)) + + // start the web server at http://localhost:8080 + app.Run(iris.Addr(":8080"), iris.WithoutVersionChecker) +} +``` + +Run the Iris web server you've just created by executing `go run main.go` from your current path (%GOPATH%/src/%your_folder%/web/). + +```sh +$ go run main.go +Now listening on: http://localhost:8080 +Application Started. Press CTRL+C to shut down. +_ +``` + +Open one or more browser tabs at: http://localhost:8080 and have fun! + +![](screen.png) + +### Download the Source Code + +The whole project, all the files you saw in this article are located at: https://github.com/kataras/iris/tree/master/_examples/tutorial/vuejs-todo-mvc + +## Thank you, once again + +Happy new year and thank you for your pattience, once again:) Don't hesitate to post any questions and provide feedback(I'm very active dev therefore you will be heard here!) + +Don't forget to check out my medium profile and twitter as well, I'm posting some (useful) stuff there too:) + +- https://medium.com/@kataras +- https://twitter.com/MakisMaropoulos + ## References -https://vuejs.org/v2/examples/todomvc.html (using browser's local storage) \ No newline at end of file +https://vuejs.org/v2/examples/todomvc.html (using browser's local storage) + +https://github.com/kataras/iris/tree/master/_examples/mvc (mvc examples and features overview repository) \ No newline at end of file diff --git a/_examples/tutorial/vuejs-todo-mvc/screen.png b/_examples/tutorial/vuejs-todo-mvc/screen.png new file mode 100644 index 00000000..41df240d Binary files /dev/null and b/_examples/tutorial/vuejs-todo-mvc/screen.png differ diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go index 79fa48d9..c3fd0724 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go @@ -52,6 +52,10 @@ func (c *TodoController) Post(newItems []todo.Item) PostItemResponse { } func (c *TodoController) GetSync(conn websocket.Connection) { + // join to the session in order to send "saved" + // events only to a single user, that means + // that if user has opened more than one browser window/tab + // of the same session then the changes will be reflected to one another. conn.Join(c.Session.ID()) conn.On("save", func() { // "save" event from client. conn.To(c.Session.ID()).Emit("saved", nil) // fire a "saved" event to the rest of the clients w. diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js b/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js index 92135237..ab30eee8 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js @@ -1,5 +1,5 @@ // Full spec-compliant TodoMVC with Iris -// and hash-based routing in ~120 effective lines of JavaScript. +// and hash-based routing in ~200 effective lines of JavaScript. var socket = new Ws("ws://localhost:8080/todos/sync");