1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-18 18:37:05 +00:00

finish the first state of the vuejs todo mvc example, a simple rest api - todo: websocket and live updates between browser tabs with the same session id

Former-commit-id: 0bd859420cff87014479c44a471ec273c621c1a2
This commit is contained in:
Gerasimos (Makis) Maropoulos
2017-12-23 17:07:39 +02:00
parent a2f217be17
commit e1c65d23fb
11 changed files with 232 additions and 190 deletions

View File

@@ -1,24 +1,8 @@
package todo
type State uint32
const (
StateActive State = iota
StateCompleted
)
func ParseState(s string) State {
switch s {
case "completed":
return StateCompleted
default:
return StateActive
}
}
type Item struct {
OwnerID string
ID int64
Body string
CurrentState State
SessionID string `json:"-"`
ID int64 `json:"id,omitempty"`
Title string `json:"title"`
Completed bool `json:"completed"`
}

View File

@@ -1,53 +1,31 @@
package todo
type Service interface {
GetByID(id int64) (Item, bool)
GetByOwner(owner string) []Item
Complete(item Item) bool
Save(newItem Item) error
Get(owner string) []Item
Save(owner string, newItems []Item) error
}
type MemoryService struct {
items map[int64]Item
items map[string][]Item
}
func (s *MemoryService) getLatestID() (id int64) {
for k := range s.items {
if k > id {
id = k
func NewMemoryService() *MemoryService {
return &MemoryService{make(map[string][]Item, 0)}
}
func (s *MemoryService) Get(sessionOwner string) (items []Item) {
return s.items[sessionOwner]
}
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++
}
}
return
}
func (s *MemoryService) GetByID(id int64) (Item, bool) {
item, found := s.items[id]
return item, found
}
func (s *MemoryService) GetByOwner(owner string) (items []Item) {
for _, item := range s.items {
if item.OwnerID != owner {
continue
}
items = append(items, item)
}
return
}
func (s *MemoryService) Complete(item Item) bool {
item.CurrentState = StateCompleted
return s.Save(item) == nil
}
func (s *MemoryService) Save(newItem Item) error {
if newItem.ID == 0 {
// create
newItem.ID = s.getLatestID() + 1
}
// full replace here for the shake of simplicity)
s.items[newItem.ID] = newItem
s.items[sessionOwner] = newItems
return nil
}

View File

@@ -4,9 +4,9 @@ import (
"github.com/kataras/iris/_examples/tutorial/vuejs-todo-mvc/src/todo"
"github.com/kataras/iris"
"github.com/kataras/iris/sessions"
"github.com/kataras/iris/mvc"
"github.com/kataras/iris/sessions"
"github.com/kataras/iris/websocket"
)
// TodoController is our TODO app's web controller.
@@ -23,55 +23,39 @@ type TodoController struct {
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) todo.Item {
// ctx.ReadForm(&item)
var (
owner = ctx.PostValue("owner")
body = ctx.PostValue("body")
state = ctx.PostValue("state")
)
b.Dependencies().Add(func(ctx iris.Context) (items []todo.Item) {
ctx.ReadJSON(&items)
return
})
}
return todo.Item{
OwnerID: owner,
Body: body,
CurrentState: todo.ParseState(state),
}
// 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 "save" event to the rest of the clients.
})
// ca.Router.Use(...).Done(...).Layout(...)
}
// Get handles the GET: /todo route.
func (c *TodoController) Get() []todo.Item {
return c.Service.GetByOwner(c.Session.ID())
}
// PutCompleteBy handles the PUT: /todo/complete/{id:long} route.
func (c *TodoController) PutCompleteBy(id int64) int {
item, found := c.Service.GetByID(id)
if !found {
return iris.StatusNotFound
}
if item.OwnerID != c.Session.ID() {
return iris.StatusForbidden
}
if !c.Service.Complete(item) {
return iris.StatusBadRequest
}
return iris.StatusOK
}
// Post handles the POST: /todo route.
func (c *TodoController) Post(newItem todo.Item) int {
if newItem.OwnerID != c.Session.ID() {
return iris.StatusForbidden
}
if err := c.Service.Save(newItem); err != nil {
return iris.StatusBadRequest
}
return iris.StatusOK
conn.Wait()
}

View File

@@ -6,12 +6,14 @@ import (
"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,
@@ -20,20 +22,28 @@ func main() {
app.StaticWeb("/", "./public")
sess := sessions.New(sessions.Config{
Cookie: "_iris_session",
Cookie: "iris_session",
})
m := mvc.New(app.Party("/todo"))
ws := websocket.New(websocket.Config{})
// create our mvc application targeted to /todos relative sub path.
m := mvc.New(app.Party("/todos"))
// any dependencies bindings here...
m.AddDependencies(
todo.NewMemoryService(),
mvc.Session(sess),
new(todo.MemoryService),
ws.Upgrade,
)
// http://localhost:8080/iris-ws.js
// serve the javascript client library to communicate with
// the iris high level websocket event system.
m.Router.Any("/iris-ws.js", websocket.ClientHandler())
// controllers registration here...
m.Register(new(controllers.TodoController))
// start the web server at http://localhost:8080
app.Run(iris.Addr(":8080"), iris.WithoutVersionChecker, iris.WithOptimizations)
app.Run(iris.Addr(":8080"), iris.WithoutVersionChecker)
}

View File

@@ -1,63 +1,71 @@
<!doctype html>
<html data-framework="vue">
<head>
<meta charset="utf-8">
<title>Iris + Vue.js • TodoMVC</title>
<link rel="stylesheet" href="https://unpkg.com/todomvc-app-css@2.0.4/index.css">
<!-- this needs to be loaded before guide's inline scripts -->
<script src="https://vuejs.org/js/vue.js"></script>
<script src="https://unpkg.com/director@1.2.8/build/director.js"></script>
<style>[v-cloak] { display: none; }</style>
</head>
<body>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo"
autofocus autocomplete="off"
placeholder="What needs to be done?"
v-model="newTodo"
@keyup.enter="addTodo">
</header>
<section class="main" v-show="todos.length" v-cloak>
<input class="toggle-all" type="checkbox" v-model="allDone">
<ul class="todo-list">
<li v-for="todo in filteredTodos"
class="todo"
:key="todo.id"
:class="{ completed: todo.completed, editing: todo == editedTodo }">
<div class="view">
<input class="toggle" type="checkbox" v-model="todo.completed">
<label @dblclick="editTodo(todo)">{{ todo.title }}</label>
<button class="destroy" @click="removeTodo(todo)"></button>
</div>
<input class="edit" type="text"
v-model="todo.title"
v-todo-focus="todo == editedTodo"
@blur="doneEdit(todo)"
@keyup.enter="doneEdit(todo)"
@keyup.esc="cancelEdit(todo)">
</li>
</ul>
</section>
<footer class="footer" v-show="todos.length" v-cloak>
<span class="todo-count">
<strong>{{ remaining }}</strong> {{ remaining | pluralize }} left
</span>
<ul class="filters">
<li><a href="#/all" :class="{ selected: visibility == 'all' }">All</a></li>
<li><a href="#/active" :class="{ selected: visibility == 'active' }">Active</a></li>
<li><a href="#/completed" :class="{ selected: visibility == 'completed' }">Completed</a></li>
</ul>
<button class="clear-completed" @click="removeCompleted" v-show="todos.length > remaining">
Clear completed
</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
</footer>
<script src="/js/app.js"></script>
</body>
<head>
<meta charset="utf-8">
<title>Iris + Vue.js • TodoMVC</title>
<link rel="stylesheet" href="https://unpkg.com/todomvc-app-css@2.0.4/index.css">
<!-- this needs to be loaded before guide's inline scripts -->
<script src="https://vuejs.org/js/vue.js"></script>
<!-- $http -->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<!-- -->
<script src="https://unpkg.com/director@1.2.8/build/director.js"></script>
<!-- websocket sync between multiple tabs -->
<script src="/todos/iris-ws.js"></script>
<!-- -->
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" autofocus autocomplete="off" placeholder="What needs to be done?" v-model="newTodo" @keyup.enter="addTodo">
</header>
<section class="main" v-show="todos.length" v-cloak>
<input class="toggle-all" type="checkbox" v-model="allDone">
<ul class="todo-list">
<li v-for="todo in filteredTodos" class="todo" :key="todo.id" :class="{ completed: todo.completed, editing: todo == editedTodo }">
<div class="view">
<input class="toggle" type="checkbox" v-model="todo.completed">
<label @dblclick="editTodo(todo)">{{ todo.title }}</label>
<button class="destroy" @click="removeTodo(todo)"></button>
</div>
<input class="edit" type="text" v-model="todo.title" v-todo-focus="todo == editedTodo" @blur="doneEdit(todo)" @keyup.enter="doneEdit(todo)"
@keyup.esc="cancelEdit(todo)">
</li>
</ul>
</section>
<footer class="footer" v-show="todos.length" v-cloak>
<span class="todo-count">
<strong>{{ remaining }}</strong> {{ remaining | pluralize }} left
</span>
<ul class="filters">
<li>
<a href="#/all" :class="{ selected: visibility == 'all' }">All</a>
</li>
<li>
<a href="#/active" :class="{ selected: visibility == 'active' }">Active</a>
</li>
<li>
<a href="#/completed" :class="{ selected: visibility == 'completed' }">Completed</a>
</li>
</ul>
<button class="clear-completed" @click="removeCompleted" v-show="todos.length > remaining">
Clear completed
</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
</footer>
<script src="/js/app.js"></script>
</body>
</html>

View File

@@ -1,19 +1,42 @@
// Full spec-compliant TodoMVC with localStorage persistence
// Full spec-compliant TodoMVC with Iris
// and hash-based routing in ~120 effective lines of JavaScript.
// localStorage persistence
var STORAGE_KEY = 'todos-vuejs-2.0'
// var socket = new Ws("ws://localhost:8080/todos/sync");
// socket.On("saved", function () {
// console.log("receive: on saved");
// todoStorage.fetch();
// });
var todos = [];
var todoStorage = {
fetch: function () {
var todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
todos.forEach(function (todo, index) {
todo.id = index
})
todoStorage.uid = todos.length
axios.get("/todos").then(response => {
if (response.data == null) {
return;
}
for (var i = 0; i < response.data.length; i++) {
// if (todos.length <=i || todos[i] === null) {
// todos.push(response.data[i]);
// } else {
// todos[i] = response.data[i];
// }
todos.push(response.data[i]);
}
});
return todos
},
save: function (todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(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")
});
}
}
@@ -44,11 +67,18 @@ var app = new Vue({
visibility: 'all'
},
// watch todos change for localStorage persistence
// watch todos change for persistence
watch: {
todos: {
handler: function (todos) {
todoStorage.save(todos)
// // saved by this client.
// if (todos[todos.length - 1].id === 0) {
// todoStorage.save(todos);
// } else {
// console.log("item cannot be saved, already exists.");
// console.log(todos[todos.length - 1]);
// }
todoStorage.save(todos);
},
deep: true
}
@@ -90,7 +120,7 @@ var app = new Vue({
return
}
this.todos.push({
id: todoStorage.uid++,
id: 0, // just for the client-side.
title: value,
completed: false
})
@@ -140,7 +170,7 @@ var app = new Vue({
})
// handle routing
function onHashChange () {
function onHashChange() {
var visibility = window.location.hash.replace(/#\/?/, '')
if (filters[visibility]) {
app.visibility = visibility