diff --git a/_examples/tutorial/vuejs-todo-mvc/src/todo/item.go b/_examples/tutorial/vuejs-todo-mvc/src/todo/item.go new file mode 100644 index 00000000..744880c4 --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/todo/item.go @@ -0,0 +1,24 @@ +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 +} diff --git a/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go b/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go new file mode 100644 index 00000000..3a8f75a6 --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go @@ -0,0 +1,53 @@ +package todo + +type Service interface { + GetByID(id int64) (Item, bool) + GetByOwner(owner string) []Item + Complete(item Item) bool + Save(newItem Item) error +} + +type MemoryService struct { + items map[int64]Item +} + +func (s *MemoryService) getLatestID() (id int64) { + for k := range s.items { + if k > id { + id = k + } + } + + 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 simplicy) + s.items[newItem.ID] = newItem + return nil +} 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 new file mode 100644 index 00000000..69923ea2 --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go @@ -0,0 +1,73 @@ +package controllers + +import ( + "github.com/kataras/iris/_examples/tutorial/vuejs-todo-mvc/src/todo" + + "github.com/kataras/iris" + mvc "github.com/kataras/iris/mvc2" + "github.com/kataras/iris/sessions" +) + +// TodoController is our TODO app's web controller. +type TodoController struct { + service todo.Service + + session *sessions.Session +} + +// OnActivate called once before the server ran, can bind custom +// things to the controller. +func (c *TodoController) OnActivate(ca *mvc.ControllerActivator) { + // this could be binded to a controller's function input argument + // if any, or struct field if any: + ca.Bind(func(ctx iris.Context) todo.Item { + // ctx.ReadForm(&item) + var ( + owner = ctx.PostValue("owner") + body = ctx.PostValue("body") + state = ctx.PostValue("state") + ) + + return todo.Item{ + OwnerID: owner, + Body: body, + CurrentState: todo.ParseState(state), + } + }) + +} + +// 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 +} diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/main.go b/_examples/tutorial/vuejs-todo-mvc/src/web/main.go new file mode 100644 index 00000000..ee3c875e --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "github.com/kataras/iris/_examples/tutorial/vuejs-todo-mvc/src/todo" + "github.com/kataras/iris/_examples/tutorial/vuejs-todo-mvc/src/web/controllers" + + "github.com/kataras/iris" + mvc "github.com/kataras/iris/mvc2" + "github.com/kataras/iris/sessions" +) + +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") + + sess := sessions.New(sessions.Config{ + Cookie: "_iris_session", + }) + + m := mvc.New() + + // any bindings here... + m.Bind(mvc.Session(sess)) + + m.Bind(new(todo.MemoryService)) + // controllers registration here... + m.Controller(app.Party("/todo"), new(controllers.TodoController)) + + // start the web server at http://localhost:8080 + app.Run(iris.Addr(":8080"), iris.WithoutVersionChecker, iris.WithOptimizations) +} diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/public/css/index b/_examples/tutorial/vuejs-todo-mvc/src/web/public/css/index new file mode 100644 index 00000000..3aea0097 --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/public/css/index @@ -0,0 +1,2 @@ +index.css is not here to reduce the disk space for the examples. +https://unpkg.com/todomvc-app-css@2.0.4/index.css is used instead. \ No newline at end of file diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html b/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html new file mode 100644 index 00000000..f282a65d --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html @@ -0,0 +1,63 @@ + + + + + Iris + Vue.js • TodoMVC + + + + + + + +
+
+

todos

+ +
+
+ + +
+ +
+ + + + + \ No newline at end of file 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 new file mode 100644 index 00000000..248c5333 --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js @@ -0,0 +1,157 @@ +// Full spec-compliant TodoMVC with localStorage persistence +// and hash-based routing in ~120 effective lines of JavaScript. + +// localStorage persistence +var STORAGE_KEY = 'todos-vuejs-2.0' +var todoStorage = { + fetch: function () { + var todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]') + todos.forEach(function (todo, index) { + todo.id = index + }) + todoStorage.uid = todos.length + return todos + }, + save: function (todos) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)) + } +} + +// 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' + }, + + // watch todos change for localStorage persistence + watch: { + todos: { + handler: function (todos) { + todoStorage.save(todos) + }, + 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 + }) + } + } + }, + + filters: { + pluralize: function (n) { + return n === 1 ? 'item' : 'items' + } + }, + + // methods that implement data logic. + // note there's no DOM manipulation here at all. + methods: { + addTodo: function () { + var value = this.newTodo && this.newTodo.trim() + if (!value) { + return + } + this.todos.push({ + id: todoStorage.uid++, + title: value, + completed: false + }) + this.newTodo = '' + }, + + removeTodo: function (todo) { + this.todos.splice(this.todos.indexOf(todo), 1) + }, + + 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) + } + }, + + cancelEdit: function (todo) { + this.editedTodo = null + todo.title = this.beforeEditCache + }, + + removeCompleted: function () { + this.todos = filters.active(this.todos) + } + }, + + // 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') \ No newline at end of file diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/lib/vue b/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/lib/vue new file mode 100644 index 00000000..8da03f48 --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/lib/vue @@ -0,0 +1,2 @@ +vue.js is not here to reduce the disk space for the examples. +Instead https://vuejs.org/js/vue.js is used instead. \ No newline at end of file diff --git a/mvc2/controller.go b/mvc2/controller.go index e024841c..4afad118 100644 --- a/mvc2/controller.go +++ b/mvc2/controller.go @@ -160,7 +160,6 @@ func (c *ControllerActivator) isReservedMethod(name string) bool { // register all available, exported methods to handlers if possible. func (c *ControllerActivator) parseMethods() { - n := c.Type.NumMethod() for i := 0; i < n; i++ { m := c.Type.Method(i) diff --git a/mvc2/controller_test.go b/mvc2/controller_test.go index 3c48c6a9..d0079f6b 100644 --- a/mvc2/controller_test.go +++ b/mvc2/controller_test.go @@ -260,6 +260,12 @@ func (t *testControllerBindStruct) Get() { t.Ctx.Writef(t.TitlePointer.title + t.TitleValue.title + t.Other) } +// test if context can be binded to the controller's function +// without need to declare it to a struct if not needed. +func (t *testControllerBindStruct) GetCtx(ctx iris.Context) { + ctx.StatusCode(iris.StatusContinue) +} + type testControllerBindDeep struct { testControllerBindStruct } @@ -268,6 +274,7 @@ func (t *testControllerBindDeep) Get() { // t.testControllerBindStruct.Get() t.Ctx.Writef(t.TitlePointer.title + t.TitleValue.title + t.Other) } + func TestControllerBind(t *testing.T) { app := iris.New() // app.Logger().SetLevel("debug") @@ -287,6 +294,8 @@ func TestControllerBind(t *testing.T) { expected := t1 + t2 e.GET("/").Expect().Status(iris.StatusOK). Body().Equal(expected) + e.GET("/ctx").Expect().Status(iris.StatusContinue) + e.GET("/deep").Expect().Status(iris.StatusOK). Body().Equal(expected) } diff --git a/mvc2/session_binder.go b/mvc2/session_binder.go new file mode 100644 index 00000000..173a3c8c --- /dev/null +++ b/mvc2/session_binder.go @@ -0,0 +1,17 @@ +package mvc2 + +import ( + "github.com/kataras/iris/context" + "github.com/kataras/iris/sessions" +) + +// Session -> TODO: think of move all bindings to +// a different folder like "bindings" +// so it will be used as .Bind(bindings.Session(manager)) +// or let it here but change the rest of the binding names as well +// because they are not "binders", their result are binders to be percise. +func Session(sess *sessions.Sessions) func(context.Context) *sessions.Session { + return func(ctx context.Context) *sessions.Session { + return sess.Start(ctx) + } +} diff --git a/mvc2/session_controller.go b/mvc2/session_controller.go index 8c88f863..e369e877 100644 --- a/mvc2/session_controller.go +++ b/mvc2/session_controller.go @@ -5,7 +5,7 @@ import ( "github.com/kataras/iris/sessions" ) -var defaultManager = sessions.New(sessions.Config{}) +var defaultSessionManager = sessions.New(sessions.Config{}) // SessionController is a simple `Controller` implementation // which requires a binded session manager in order to give @@ -22,7 +22,7 @@ type SessionController struct { // It makes sure that its "Manager" field is filled // even if the caller didn't provide any sessions manager via the `app.Controller` function. func (s *SessionController) OnActivate(ca *ControllerActivator) { - if didntBindManually := ca.BindIfNotExists(defaultManager); didntBindManually { + if didntBindManually := ca.BindIfNotExists(defaultSessionManager); didntBindManually { ca.Router.GetReporter().Add( `MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field, therefore this controller is using the default sessions manager instead.