diff --git a/README.md b/README.md index 4944563..7f08727 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ todo te será familiar. - Sin dependencias - Procesamiento y salida de ficheros HTML +- Paginación lista para usar - Vinculación entrada formulario y JSON a tipos estructurados. ## Motivación diff --git a/example/main.go b/example/main.go index d9a188a..949ba36 100644 --- a/example/main.go +++ b/example/main.go @@ -6,17 +6,73 @@ import ( "ron" ) +type SomethingElements struct { + Name string + Description string +} + +var elements = []SomethingElements{ + {"element 1", "description 1"}, + {"element 2", "description 2"}, + {"element 3", "description 3"}, + {"element 4", "description 4"}, + {"element 5", "description 5"}, + {"element 6", "description 6"}, + {"element 7", "description 7"}, + {"element 8", "description 8"}, + {"element 9", "description 9"}, + {"element 10", "description 10"}, + {"element 11", "description 11"}, + {"element 12", "description 12"}, + {"element 13", "description 13"}, + {"element 14", "description 14"}, + {"element 15", "description 15"}, + {"element 16", "description 16"}, + {"element 17", "description 17"}, + {"element 18", "description 18"}, + {"element 19", "description 19"}, + {"element 20", "description 20"}, + {"element 21", "description 21"}, + {"element 22", "description 22"}, + {"element 23", "description 23"}, + {"element 24", "description 24"}, + {"element 25", "description 25"}, + {"element 26", "description 26"}, + {"element 27", "description 27"}, + {"element 28", "description 28"}, + {"element 29", "description 29"}, + {"element 30", "description 30"}, + {"element 31", "description 31"}, + {"element 32", "description 32"}, + {"element 33", "description 33"}, + {"element 34", "description 34"}, + {"element 35", "description 35"}, + {"element 36", "description 36"}, + {"element 37", "description 37"}, + {"element 38", "description 38"}, + {"element 39", "description 39"}, + {"element 40", "description 40"}, + {"element 41", "description 41"}, + {"element 42", "description 42"}, + {"element 43", "description 43"}, + {"element 44", "description 44"}, + {"element 45", "description 45"}, + {"element 46", "description 46"}, + {"element 47", "description 47"}, + {"element 48", "description 48"}, + {"element 49", "description 49"}, +} + func main() { r := ron.New(func(e *ron.Engine) { e.LogLevel = slog.LevelDebug }) htmlRender := ron.NewHTMLRender() - r.Renderer = htmlRender + r.Render = htmlRender r.Static("static", "static") - //r.GET("/", helloWorld) r.GET("/json", helloWorldJSON) r.POST("/another", anotherHelloWorld) r.GET("/html", helloWorldHTML) @@ -39,12 +95,23 @@ func helloWorldJSON(c *ron.Context) { } func helloWorldHTML(c *ron.Context) { - c.HTML(200, "page.index.gohtml", ron.Data{ - "title": "hello world", - "message": "hello world from html", - }) + + pages := ron.Pages{ + TotalElements: len(elements), + ElementsPerPage: 5, + } + + pages.PaginationParams(c.R) + elementsPaginated := pages.PaginateArray(elements) + + td := &ron.TemplateData{ + Data: ron.Data{"title": "hello world", "message": "hello world from html", "elements": elementsPaginated}, + Pages: pages, + } + + c.HTML(200, "page.index.gohtml", td) } func componentHTML(c *ron.Context) { - c.HTML(200, "component.list.gohtml", ron.Data{}) + c.HTML(200, "component.list.gohtml", nil) } diff --git a/example/templates/page.index.gohtml b/example/templates/page.index.gohtml index 8d43466..f1eef8a 100644 --- a/example/templates/page.index.gohtml +++ b/example/templates/page.index.gohtml @@ -12,6 +12,40 @@ {{ .Data.message }} +{{ range .Data.elements }} + + +{{ end }} + +{{ if not .Pages.HasPrevious }} +primera página +{{ else }} +primera +anterior +{{ end }} + +{{ range .Pages.PageRange 5 }} +{{ if .Active }} +{{ .Number }} +{{ else }} +{{ .Number }} +{{ end }} +{{ end }} + +{{ if not .Pages.HasNext }} +última página +{{ else }} +siguiente +última +{{ end }} + + diff --git a/ron.go b/ron.go index 6547301..aeea964 100644 --- a/ron.go +++ b/ron.go @@ -27,7 +27,7 @@ type ( Engine struct { mux *http.ServeMux LogLevel slog.Level - Renderer *Render + Render *Render } ) @@ -120,12 +120,10 @@ func (c *Context) JSON(code int, data any) { } } -func (c *Context) HTML(code int, name string, data Data) { +func (c *Context) HTML(code int, name string, td *TemplateData) { c.W.WriteHeader(code) c.W.Header().Set("Content-Type", "text/html; charset=utf-8") - err := c.E.Renderer.Template(c.W, name, &TemplateData{ - Data: data, - }) + err := c.E.Render.Template(c.W, name, td) if err != nil { http.Error(c.W, err.Error(), http.StatusInternalServerError) } diff --git a/ron_test.go b/ron_test.go index 511f6fa..05593f1 100644 --- a/ron_test.go +++ b/ron_test.go @@ -19,17 +19,17 @@ func Test_New(t *testing.T) { if e == nil { t.Error("Expected Engine, Actual: nil") } - if e.Renderer != nil { + if e.Render != nil { t.Error("No expected Renderer, Actual: Renderer") } } func Test_applyEngineConfig(t *testing.T) { e := New(func(e *Engine) { - e.Renderer = NewHTMLRender() + e.Render = NewHTMLRender() e.LogLevel = 1 }) - if e.Renderer == nil { + if e.Render == nil { t.Error("Expected Renderer, Actual: nil") } if e.LogLevel != 1 { @@ -151,15 +151,17 @@ func Test_HTML(t *testing.T) { c := &Context{ W: rr, E: &Engine{ - Renderer: NewHTMLRender(), + Render: NewHTMLRender(), }, } expected := `

foo

bar

` - c.HTML(http.StatusOK, "page.index.gohtml", Data{ - "heading1": "foo", - "heading2": "bar", + c.HTML(http.StatusOK, "page.index.gohtml", &TemplateData{ + Data: Data{ + "heading1": "foo", + "heading2": "bar", + }, }) if status := rr.Code; status != http.StatusOK { diff --git a/template.go b/template.go index 7e876dd..8f3e438 100644 --- a/template.go +++ b/template.go @@ -7,6 +7,8 @@ import ( "io/fs" "net/http" "path/filepath" + "reflect" + "strconv" "strings" ) @@ -14,7 +16,8 @@ type ( templateCache map[string]*template.Template TemplateData struct { - Data Data + Data Data + Pages Pages } RenderOptions func(*Render) @@ -22,6 +25,7 @@ type ( EnableCache bool TemplatesPath string Functions template.FuncMap + TemplateData TemplateData templateCache templateCache } ) @@ -30,8 +34,9 @@ func defaultHTMLRender() *Render { return &Render{ EnableCache: false, TemplatesPath: "templates", - Functions: make(template.FuncMap), - templateCache: make(templateCache), + TemplateData: TemplateData{}, + Functions: template.FuncMap{}, + templateCache: templateCache{}, } } @@ -139,3 +144,173 @@ func (re *Render) createTemplateCache() (templateCache, error) { return cache, nil } + +// Pages contiene la información de paginación. +type Pages struct { + // TotalElements son la cantidad de elementos totales a paginar. Pueden ser + // total de filas o total de páginas de blog. + TotalElements int + // ElementsPerPage muestra la cantidad máxima de elementos a mostrar en una + // página. + ElementsPerPage int + // ActualPage es la página actual, utilizado como ayuda para mostrar la + // página activa. + ActualPage int +} + +func (p *Pages) PaginationParams(r *http.Request) { + limit := r.FormValue("limit") + page := r.FormValue("page") + + if limit == "" { + if p.ElementsPerPage != 0 { + limit = strconv.Itoa(p.ElementsPerPage) + } else { + limit = "20" + } + } + + if page == "" || page == "0" { + if p.ActualPage != 0 { + page = strconv.Itoa(p.ActualPage) + } else { + page = "1" + } + } + + limitInt, _ := strconv.Atoi(limit) + pageInt, _ := strconv.Atoi(page) + offset := (pageInt - 1) * limitInt + currentPage := offset/limitInt + 1 + + p.ElementsPerPage = limitInt + p.ActualPage = currentPage +} + +func (p Pages) PaginateArray(elements any) any { + itemsValue := reflect.ValueOf(elements) + + if p.ActualPage < 1 { + p.ActualPage = 1 + } + + if p.ActualPage > p.TotalPages() { + p.ActualPage = p.TotalPages() + } + + startIndex := (p.ActualPage - 1) * p.ElementsPerPage + endIndex := startIndex + p.ElementsPerPage + + return itemsValue.Slice(startIndex, endIndex).Interface() +} + +func (p Pages) CurrentPage() int { + return p.ActualPage +} + +// TotalPages devuelve la cantidad total de páginas. +func (p Pages) TotalPages() int { + return (p.TotalElements + p.ElementsPerPage - 1) / p.ElementsPerPage +} + +// IsFirst indica si la página actual es la primera. +func (p Pages) IsFirst() bool { + return p.ActualPage == 1 +} + +// IsLast indica si la página actual es la última. +func (p Pages) IsLast() bool { + return p.ActualPage == p.TotalPages() +} + +// HasPrevious indica si hay una página anterior. +func (p Pages) HasPrevious() bool { + return p.ActualPage > 1 +} + +// HasNext indica si hay una página siguiente. +func (p Pages) HasNext() bool { + return p.ActualPage < p.TotalPages() +} + +// Previous devuelve el número de la página anterior. +func (p Pages) Previous() int { + if p.ActualPage > p.TotalPages() { + return p.TotalPages() + } + return p.ActualPage - 1 +} + +// Next devuelve el número de la página siguiente. +func (p Pages) Next() int { + if p.ActualPage < 1 { + return 1 + } + return p.ActualPage + 1 +} + +func (p Pages) GoToPage(page int) int { + if page < 1 { + page = 1 + } else if page > p.TotalPages() { + page = p.TotalPages() + } + return page +} + +func (p Pages) First() int { + return p.GoToPage(1) +} + +func (p Pages) Last() int { + return p.GoToPage(p.TotalPages()) +} + +// Page contiene la información de una página. Utilizado para la barra de +// paginación que suelen mostrarse en la parte inferior de una lista o tabla. +type Page struct { + // Number es el número de página. + Number int + // Active es un dato lógico que indica si la página es la actual. + Active bool +} + +func (p Page) NumberOfPage() int { + return p.Number +} + +// IsActive indica si la página es la actual. +func (p Page) IsActive() bool { + return p.Active +} + +// PagesRange muestra un rango de páginas a mostrar en la paginación. +func (p Pages) PageRange(maxPagesToShow int) []Page { + var pages []Page + totalPages := p.TotalPages() + + startPage := p.ActualPage - (maxPagesToShow / 2) + endPage := p.ActualPage + (maxPagesToShow / 2) + + if startPage < 1 { + startPage = 1 + endPage = maxPagesToShow + } + + if endPage > totalPages { + endPage = totalPages + startPage = totalPages - maxPagesToShow + 1 + if startPage < 1 { + startPage = 1 + } + } + + for i := startPage; i <= endPage; i++ { + pages = append(pages, Page{ + Number: i, + Active: i == p.ActualPage, + }) + } + + return pages +} diff --git a/template_test.go b/template_test.go index 0cc9661..e03305e 100644 --- a/template_test.go +++ b/template_test.go @@ -2,6 +2,7 @@ package ron import ( "html/template" + "net/http" "net/http/httptest" "os" "reflect" @@ -13,8 +14,9 @@ func Test_DefaultHTMLRender(t *testing.T) { expected := &Render{ EnableCache: false, TemplatesPath: "templates", - Functions: make(template.FuncMap), - templateCache: make(templateCache), + TemplateData: TemplateData{}, + Functions: template.FuncMap{}, + templateCache: templateCache{}, } actual := defaultHTMLRender() @@ -27,8 +29,9 @@ func Test_HTMLRender(t *testing.T) { expected := &Render{ EnableCache: false, TemplatesPath: "templates", - Functions: make(template.FuncMap), - templateCache: make(templateCache), + TemplateData: TemplateData{}, + Functions: template.FuncMap{}, + templateCache: templateCache{}, } tests := []struct { @@ -58,15 +61,17 @@ func Test_applyRenderConfig(t *testing.T) { {"Empty OptionFunc", &Render{ EnableCache: false, TemplatesPath: "templates", - Functions: make(template.FuncMap), - templateCache: make(templateCache), + TemplateData: TemplateData{}, + Functions: template.FuncMap{}, + templateCache: templateCache{}, }, defaultHTMLRender()}, { name: "Two OptionFunc", expected: &Render{ EnableCache: true, TemplatesPath: "foobar", - Functions: make(template.FuncMap), - templateCache: make(templateCache), + TemplateData: TemplateData{}, + Functions: template.FuncMap{}, + templateCache: templateCache{}, }, actual: NewHTMLRender(func(r *Render) { r.EnableCache = true @@ -193,3 +198,200 @@ func Test_TemplateDefault(t *testing.T) { }) } } + +type SomethingElements struct { + Name string + Description string +} + +func createDummyElements() []SomethingElements { + return []SomethingElements{ + {"element 1", "description 1"}, + {"element 2", "description 2"}, + {"element 3", "description 3"}, + {"element 4", "description 4"}, + {"element 5", "description 5"}, + {"element 6", "description 6"}, + {"element 7", "description 7"}, + {"element 8", "description 8"}, + {"element 9", "description 9"}, + {"element 10", "description 10"}, + {"element 11", "description 11"}, + {"element 12", "description 12"}, + {"element 13", "description 13"}, + {"element 14", "description 14"}, + {"element 15", "description 15"}, + {"element 16", "description 16"}, + {"element 17", "description 17"}, + {"element 18", "description 18"}, + {"element 19", "description 19"}, + {"element 20", "description 20"}, + {"element 21", "description 21"}, + {"element 22", "description 22"}, + {"element 23", "description 23"}, + {"element 24", "description 24"}, + {"element 25", "description 25"}, + {"element 26", "description 26"}, + {"element 27", "description 27"}, + {"element 28", "description 28"}, + {"element 29", "description 29"}, + {"element 30", "description 30"}, + {"element 31", "description 31"}, + {"element 32", "description 32"}, + {"element 33", "description 33"}, + {"element 34", "description 34"}, + {"element 35", "description 35"}, + {"element 36", "description 36"}, + {"element 37", "description 37"}, + {"element 38", "description 38"}, + {"element 39", "description 39"}, + {"element 40", "description 40"}, + {"element 41", "description 41"}, + {"element 42", "description 42"}, + {"element 43", "description 43"}, + {"element 44", "description 44"}, + {"element 45", "description 45"}, + {"element 46", "description 46"}, + {"element 47", "description 47"}, + {"element 48", "description 48"}, + {"element 49", "description 49"}, + {"element 50", "description 50"}, + } +} + +func Test_PaginationParams(t *testing.T) { + elements := createDummyElements() + tests := []struct { + name string + req *http.Request + given *Pages + expected *Pages + }{ + { + name: "All defaults", + req: httptest.NewRequest("GET", "/", nil), + given: &Pages{ + TotalElements: len(elements), + }, + expected: &Pages{ + TotalElements: len(elements), + ElementsPerPage: 20, + ActualPage: 1, + }, + }, + { + name: "Without params", + req: httptest.NewRequest("GET", "/", nil), + given: &Pages{ + TotalElements: len(elements), + ElementsPerPage: 10, + ActualPage: 3, + }, + expected: &Pages{ + TotalElements: len(elements), + ElementsPerPage: 10, + ActualPage: 3, + }, + }, + { + name: "With page param", + req: httptest.NewRequest("GET", "/?page=2", nil), + given: &Pages{ + TotalElements: len(elements), + }, + expected: &Pages{ + TotalElements: len(elements), + ElementsPerPage: 20, + ActualPage: 2, + }, + }, + { + name: "With limit param", + req: httptest.NewRequest("GET", "/?limit=10", nil), + given: &Pages{ + TotalElements: len(elements), + }, + expected: &Pages{ + TotalElements: len(elements), + ElementsPerPage: 10, + ActualPage: 1, + }, + }, + { + name: "With page and limit param", + req: httptest.NewRequest("GET", "/?page=2&limit=10", nil), + given: &Pages{ + TotalElements: len(elements), + }, + expected: &Pages{ + TotalElements: len(elements), + ElementsPerPage: 10, + ActualPage: 2, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.given.PaginationParams(tt.req) + if !reflect.DeepEqual(tt.expected, tt.given) { + t.Errorf("Expected: %v, Actual: %v", tt.expected, tt.given) + } + }) + } +} + +func Test_PaginateArray(t *testing.T) { + elements := createDummyElements() + tests := []struct { + name string + given *Pages + expected []SomethingElements + }{ + { + name: "First page", + given: &Pages{ + TotalElements: len(elements), + ElementsPerPage: 10, + ActualPage: 1, + }, + expected: elements[:10], + }, + { + name: "Second page", + given: &Pages{ + TotalElements: len(elements), + ElementsPerPage: 10, + ActualPage: 2, + }, + expected: elements[10:20], + }, + { + name: "Out of range superior", + given: &Pages{ + TotalElements: len(elements), + ElementsPerPage: 10, + ActualPage: 999, + }, + expected: elements[40:50], + }, + { + name: "Out of range inferior", + given: &Pages{ + TotalElements: len(elements), + ElementsPerPage: 10, + ActualPage: -1, + }, + expected: elements[:10], + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := tt.given.PaginateArray(elements) + if !reflect.DeepEqual(tt.expected, actual) { + t.Errorf("Expected: %v, Actual: %v", tt.expected, actual) + } + }) + } +}