add pagination engine to render

This commit is contained in:
Pedro Pérez 2024-11-19 15:54:29 +01:00
parent aa16e20958
commit c777ef7056
7 changed files with 509 additions and 30 deletions

View File

@ -8,6 +8,7 @@ todo te será familiar.
- Sin dependencias - Sin dependencias
- Procesamiento y salida de ficheros HTML - Procesamiento y salida de ficheros HTML
- Paginación lista para usar
- Vinculación entrada formulario y JSON a tipos estructurados. - Vinculación entrada formulario y JSON a tipos estructurados.
## Motivación ## Motivación

View File

@ -6,17 +6,73 @@ import (
"ron" "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() { func main() {
r := ron.New(func(e *ron.Engine) { r := ron.New(func(e *ron.Engine) {
e.LogLevel = slog.LevelDebug e.LogLevel = slog.LevelDebug
}) })
htmlRender := ron.NewHTMLRender() htmlRender := ron.NewHTMLRender()
r.Renderer = htmlRender r.Render = htmlRender
r.Static("static", "static") r.Static("static", "static")
//r.GET("/", helloWorld)
r.GET("/json", helloWorldJSON) r.GET("/json", helloWorldJSON)
r.POST("/another", anotherHelloWorld) r.POST("/another", anotherHelloWorld)
r.GET("/html", helloWorldHTML) r.GET("/html", helloWorldHTML)
@ -39,12 +95,23 @@ func helloWorldJSON(c *ron.Context) {
} }
func helloWorldHTML(c *ron.Context) { func helloWorldHTML(c *ron.Context) {
c.HTML(200, "page.index.gohtml", ron.Data{
"title": "hello world", pages := ron.Pages{
"message": "hello world from html", 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) { func componentHTML(c *ron.Context) {
c.HTML(200, "component.list.gohtml", ron.Data{}) c.HTML(200, "component.list.gohtml", nil)
} }

View File

@ -12,6 +12,40 @@
{{ .Data.message }} {{ .Data.message }}
{{ range .Data.elements }}
<ul>
<li>
{{ .Name }}
{{ .Description }}
</li>
</ul>
{{ end }}
{{ if not .Pages.HasPrevious }}
primera página
{{ else }}
<a href="html?page={{ .Pages.First }}">primera</a>
<a href="html?page={{ .Pages.Previous }}">anterior</a>
{{ end }}
{{ range .Pages.PageRange 5 }}
{{ if .Active }}
<strong>{{ .Number }}</strong>
{{ else }}
<a href="html?page={{ .Number }}">{{ .Number }}</a>
{{ end }}
{{ end }}
{{ if not .Pages.HasNext }}
última página
{{ else }}
<a href="html?page={{ .Pages.Next }}">siguiente</a>
<a href="html?page={{ .Pages.Last }}">última</a>
{{ end }}
</body> </body>

8
ron.go
View File

@ -27,7 +27,7 @@ type (
Engine struct { Engine struct {
mux *http.ServeMux mux *http.ServeMux
LogLevel slog.Level 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.WriteHeader(code)
c.W.Header().Set("Content-Type", "text/html; charset=utf-8") c.W.Header().Set("Content-Type", "text/html; charset=utf-8")
err := c.E.Renderer.Template(c.W, name, &TemplateData{ err := c.E.Render.Template(c.W, name, td)
Data: data,
})
if err != nil { if err != nil {
http.Error(c.W, err.Error(), http.StatusInternalServerError) http.Error(c.W, err.Error(), http.StatusInternalServerError)
} }

View File

@ -19,17 +19,17 @@ func Test_New(t *testing.T) {
if e == nil { if e == nil {
t.Error("Expected Engine, Actual: nil") t.Error("Expected Engine, Actual: nil")
} }
if e.Renderer != nil { if e.Render != nil {
t.Error("No expected Renderer, Actual: Renderer") t.Error("No expected Renderer, Actual: Renderer")
} }
} }
func Test_applyEngineConfig(t *testing.T) { func Test_applyEngineConfig(t *testing.T) {
e := New(func(e *Engine) { e := New(func(e *Engine) {
e.Renderer = NewHTMLRender() e.Render = NewHTMLRender()
e.LogLevel = 1 e.LogLevel = 1
}) })
if e.Renderer == nil { if e.Render == nil {
t.Error("Expected Renderer, Actual: nil") t.Error("Expected Renderer, Actual: nil")
} }
if e.LogLevel != 1 { if e.LogLevel != 1 {
@ -151,15 +151,17 @@ func Test_HTML(t *testing.T) {
c := &Context{ c := &Context{
W: rr, W: rr,
E: &Engine{ E: &Engine{
Renderer: NewHTMLRender(), Render: NewHTMLRender(),
}, },
} }
expected := `<h1>foo</h1><h2>bar</h2>` expected := `<h1>foo</h1><h2>bar</h2>`
c.HTML(http.StatusOK, "page.index.gohtml", Data{ c.HTML(http.StatusOK, "page.index.gohtml", &TemplateData{
Data: Data{
"heading1": "foo", "heading1": "foo",
"heading2": "bar", "heading2": "bar",
},
}) })
if status := rr.Code; status != http.StatusOK { if status := rr.Code; status != http.StatusOK {

View File

@ -7,6 +7,8 @@ import (
"io/fs" "io/fs"
"net/http" "net/http"
"path/filepath" "path/filepath"
"reflect"
"strconv"
"strings" "strings"
) )
@ -15,6 +17,7 @@ type (
TemplateData struct { TemplateData struct {
Data Data Data Data
Pages Pages
} }
RenderOptions func(*Render) RenderOptions func(*Render)
@ -22,6 +25,7 @@ type (
EnableCache bool EnableCache bool
TemplatesPath string TemplatesPath string
Functions template.FuncMap Functions template.FuncMap
TemplateData TemplateData
templateCache templateCache templateCache templateCache
} }
) )
@ -30,8 +34,9 @@ func defaultHTMLRender() *Render {
return &Render{ return &Render{
EnableCache: false, EnableCache: false,
TemplatesPath: "templates", TemplatesPath: "templates",
Functions: make(template.FuncMap), TemplateData: TemplateData{},
templateCache: make(templateCache), Functions: template.FuncMap{},
templateCache: templateCache{},
} }
} }
@ -139,3 +144,173 @@ func (re *Render) createTemplateCache() (templateCache, error) {
return cache, nil 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
}

View File

@ -2,6 +2,7 @@ package ron
import ( import (
"html/template" "html/template"
"net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"reflect" "reflect"
@ -13,8 +14,9 @@ func Test_DefaultHTMLRender(t *testing.T) {
expected := &Render{ expected := &Render{
EnableCache: false, EnableCache: false,
TemplatesPath: "templates", TemplatesPath: "templates",
Functions: make(template.FuncMap), TemplateData: TemplateData{},
templateCache: make(templateCache), Functions: template.FuncMap{},
templateCache: templateCache{},
} }
actual := defaultHTMLRender() actual := defaultHTMLRender()
@ -27,8 +29,9 @@ func Test_HTMLRender(t *testing.T) {
expected := &Render{ expected := &Render{
EnableCache: false, EnableCache: false,
TemplatesPath: "templates", TemplatesPath: "templates",
Functions: make(template.FuncMap), TemplateData: TemplateData{},
templateCache: make(templateCache), Functions: template.FuncMap{},
templateCache: templateCache{},
} }
tests := []struct { tests := []struct {
@ -58,15 +61,17 @@ func Test_applyRenderConfig(t *testing.T) {
{"Empty OptionFunc", &Render{ {"Empty OptionFunc", &Render{
EnableCache: false, EnableCache: false,
TemplatesPath: "templates", TemplatesPath: "templates",
Functions: make(template.FuncMap), TemplateData: TemplateData{},
templateCache: make(templateCache), Functions: template.FuncMap{},
templateCache: templateCache{},
}, defaultHTMLRender()}, }, defaultHTMLRender()},
{ {
name: "Two OptionFunc", expected: &Render{ name: "Two OptionFunc", expected: &Render{
EnableCache: true, EnableCache: true,
TemplatesPath: "foobar", TemplatesPath: "foobar",
Functions: make(template.FuncMap), TemplateData: TemplateData{},
templateCache: make(templateCache), Functions: template.FuncMap{},
templateCache: templateCache{},
}, },
actual: NewHTMLRender(func(r *Render) { actual: NewHTMLRender(func(r *Render) {
r.EnableCache = true 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)
}
})
}
}