add inline edit / fix copy button

This commit is contained in:
maddalax 2024-10-28 18:47:00 -05:00
parent db3322c3d8
commit 7666186f83
10 changed files with 304 additions and 45 deletions

View file

@ -14,6 +14,15 @@ func Unique[T any](slice []T, key func(item T) string) []T {
return result
}
func Find[T any](slice []T, predicate func(item *T) bool) *T {
for _, v := range slice {
if predicate(&v) {
return &v
}
}
return nil
}
// Filter returns a new slice with only items that match the predicate.
func Filter[T any](slice []T, predicate func(item T) bool) []T {
var result []T

View file

@ -62,7 +62,7 @@ func RenderMarkdown(reader io.Reader) bytes.Buffer {
highlighting.WithFormatOptions(
chromahtml.WithLineNumbers(true),
chromahtml.WithCustomCSS(map[chroma.TokenType]string{
chroma.PreWrapper: "padding: 12px; overflow: auto; background-color: rgb(245, 245, 245) !important;",
chroma.PreWrapper: "font-size: 14px; padding: 12px; overflow: auto; background-color: rgb(245, 245, 245) !important;",
}),
),
highlighting.WithStyle("github"),

View file

@ -0,0 +1,10 @@
package examples
import (
"github.com/maddalax/htmgo/framework/h"
)
func ClickToEditExample(ctx *h.RequestContext) *h.Page {
SetSnippet(ctx, &ClickToEditSnippet)
return Index(ctx)
}

View file

@ -8,6 +8,9 @@ import (
"io"
"log/slog"
"net/http"
"os"
"reflect"
"runtime"
"strings"
"time"
)
@ -29,20 +32,38 @@ var RenderCodeToStringCached = h.CachedPerKeyT(time.Minute*30, func(snippet *Sni
})
func renderCodeToString(snippet *Snippet) *h.Element {
url := GetGithubRawPath(snippet.path)
slog.Info("getting snippet source code", slog.String("url", url))
resp, err := http.Get(url)
if err != nil {
return h.Empty()
source := ""
// in development, use the local file
if h.IsDevelopment() {
ptr := reflect.ValueOf(snippet.partial).Pointer()
fnInfo := runtime.FuncForPC(ptr)
if fnInfo == nil {
return h.Empty()
}
file, _ := fnInfo.FileLine(ptr)
b, err := os.ReadFile(file)
if err != nil {
return h.Empty()
}
source = string(b)
} else {
url := GetGithubRawPath(snippet.path)
slog.Info("getting snippet source code", slog.String("url", url))
resp, err := http.Get(url)
if err != nil {
return h.Empty()
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return h.Empty()
}
out := bytes.NewBuffer(nil)
_, err = io.Copy(out, resp.Body)
if err != nil {
return h.Empty()
}
source = out.String()
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return h.Empty()
}
out := bytes.NewBuffer(nil)
_, err = io.Copy(out, resp.Body)
if err != nil {
return h.Empty()
}
return ui.CodeSnippet(out.String(), "border-radius: 0.5rem;")
return ui.CodeSnippet(source, "border-radius: 0.5rem;")
}

View file

@ -5,7 +5,7 @@ import "htmgo-site/partials/snippets"
var FormWithLoadingStateSnippet = Snippet{
name: "Form",
description: "A simple form submission example with a loading state",
sidebarName: "Form with loading state",
sidebarName: "Form With Loading State",
path: "/examples/form",
partial: snippets.FormExample,
}
@ -22,16 +22,16 @@ var UserAuthSnippet = Snippet{
var ChatSnippet = Snippet{
name: "Chat App",
description: "A simple chat application built with htmgo using SSE for real-time updates",
sidebarName: "Chat App",
sidebarName: "Chat App Using SSE",
path: "/examples/chat",
externalRoute: "https://chat-example.htmgo.dev",
sourceCodePath: "https://github.com/maddalax/htmgo/tree/master/examples/chat",
}
var HackerNewsSnippet = Snippet{
name: "Hacker News Clone",
name: "HackerNews Clone",
description: "A hacker news reader clone built with htmgo",
sidebarName: "Hacker News Clone",
sidebarName: "HackerNews Clone",
path: "/examples/hackernews",
externalRoute: "https://hn.htmgo.dev",
sourceCodePath: "https://github.com/maddalax/htmgo/tree/master/examples/hackernews",
@ -55,8 +55,17 @@ var TodoListSnippet = Snippet{
sourceCodePath: "https://github.com/maddalax/htmgo/tree/master/examples/todo-list",
}
var ClickToEditSnippet = Snippet{
name: "Inline Click To Edit",
description: "List view of items with a click to edit button and persistence",
sidebarName: "Inline click to edit",
path: "/examples/click-to-edit",
partial: snippets.ClickToEdit,
}
var examples = []Snippet{
FormWithLoadingStateSnippet,
ClickToEditSnippet,
UserAuthSnippet,
ChatSnippet,
HackerNewsSnippet,

View file

@ -17,6 +17,7 @@ func HtmlToGoPage(ctx *h.RequestContext) *h.Page {
h.Div(
h.Class("h-full w-full flex gap-4 p-8"),
partials.HtmlInput(),
partials.HiddenCopyOutput(""),
partials.GoOutput(""),
),
),

View file

@ -2,18 +2,20 @@ package partials
import (
"github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/js"
"github.com/maddalax/htmgo/tools/html-to-htmgo/htmltogo"
"htmgo-site/ui"
)
func ConvertHtmlToGo(ctx *h.RequestContext) *h.Partial {
value := ctx.FormValue("html-input")
parsed := htmltogo.Parse([]byte(value))
parsed := string(htmltogo.Parse([]byte(value)))
formatted := ui.FormatCode(string(parsed), "height: 100%;")
formatted := ui.FormatCode(parsed, "height: 100%;")
return h.SwapPartial(ctx, GoOutput(formatted))
return h.SwapManyPartial(ctx,
GoOutput(formatted),
HiddenCopyOutput(parsed),
)
}
func HtmlInput() *h.Element {
@ -30,6 +32,14 @@ func HtmlInput() *h.Element {
)
}
func HiddenCopyOutput(content string) *h.Element {
return h.Div(
h.Class("hidden"),
h.Id("go-output-raw"),
h.UnsafeRaw(content),
)
}
func GoOutput(content string) *h.Element {
return h.Div(
h.Class("h-full w-1/2 min-w-1/2"),
@ -43,25 +53,7 @@ func GoOutput(content string) *h.Element {
),
h.If(
content != "",
h.Div(
h.Class("absolute top-0 right-0 p-2 bg-slate-800 text-white rounded-bl-md cursor-pointer"),
h.Text("Copy"),
h.OnClick(
// language=JavaScript
js.EvalJs(`
if(!navigator.clipboard) {
alert("Clipboard API not supported");
return;
}
let text = self.parentElement.querySelector("#go-output-content").innerText;
navigator.clipboard.writeText(text);
self.innerText = "Copied!";
setTimeout(() => {
self.innerText = "Copy";
}, 1000);
`),
),
),
ui.CopyButton("#go-output-raw"),
),
),
)

View file

@ -0,0 +1,178 @@
package snippets
import (
"fmt"
"github.com/maddalax/htmgo/framework/h"
)
// RowClasses defined here for simplicity of the example
var RowClasses = "whitespace-nowrap px-4 py-4 font-medium text-gray-900 text-left"
var ButtonClasses = "inline-block rounded bg-slate-900 px-4 py-2 text-xs font-medium text-white hover:bg-slate-800"
var InputClasses = "-ml-2 max-w-[125px] border p-2 rounded focus:outline-none focus:ring focus:ring-slate-800"
type Record struct {
Id string
Name string
Birthday string
Role string
Salary string
}
type TableProps struct {
EditingId string
}
var records = []Record{
{
Id: "1",
Name: "John Doe",
Birthday: "24/05/1995",
Role: "htmgo developer",
Salary: "$250,000",
},
{
Id: "2",
Name: "Jake Smith",
Birthday: "24/05/1995",
Role: "htmx developer",
Salary: "$100,000",
},
}
func ClickToEdit(ctx *h.RequestContext) *h.Partial {
return h.NewPartial(
h.Div(
h.Class("flex gap-2 items-center w-full"),
Table(TableProps{
// no record is being edited initially
EditingId: "",
}),
),
)
}
// StartEditing is a partial that is called when the user clicks on the edit button,
// it will swap in the form for editing for the given record
func StartEditing(ctx *h.RequestContext) *h.Partial {
id := ctx.QueryParam("id")
record := h.Find(records, func(record *Record) bool {
return record.Id == id
})
if record == nil {
return h.EmptyPartial()
}
return h.SwapManyPartial(
ctx,
TableRow(record, true),
)
}
// SaveEditing is a partial that is called when the user clicks on the save button while editing,
// it will update the record with the new values and swap it back out
// note: in the example, we are just creating a new record in memory instead of updating the existing one,
// normally you would update a persistent record in a database
func SaveEditing(ctx *h.RequestContext) *h.Partial {
id := ctx.QueryParam("id")
// just for the example, create a new record so it doesn't affect the global original
record := Record{
Id: id,
Name: ctx.FormValue("name"),
Birthday: ctx.FormValue("birthday"),
Role: ctx.FormValue("role"),
Salary: ctx.FormValue("salary"),
}
return h.SwapPartial(ctx, TableRow(&record, false))
}
func Table(props TableProps) *h.Element {
return h.Div(
h.Class("overflow-x-auto w-full"),
h.Table(
h.Class("divide-y divide-gray-200 bg-white table-fixed"),
h.THead(
h.Tr(
h.Th(
h.Class(RowClasses),
h.Text("Name"),
),
h.Th(
h.Class(RowClasses),
h.Text("Date of Birth"),
),
h.Th(
h.Class(RowClasses),
h.Text("Role"),
),
h.Th(
h.Class(RowClasses),
h.Text("Salary"),
),
h.Th(
h.Class("px-4 py-2"),
),
),
),
h.TBody(
h.Class("divide-y divide-gray-200"),
h.List(records, func(record Record, index int) *h.Element {
editing := props.EditingId == record.Id
return TableRow(&record, editing)
}),
),
),
)
}
func TableRow(record *Record, editing bool) *h.Element {
recordId := fmt.Sprintf("record-%s", record.Id)
var Cell = func(name string, value string) *h.Element {
return h.Td(
h.Class(RowClasses, "h-[75px]"),
h.IfElse(
!editing,
h.Pf(
value,
h.Class("w-[125px]"),
),
h.Input(
"text",
h.Class(InputClasses),
h.Value(value),
h.Name(name),
),
),
)
}
return h.Tr(
h.If(
editing,
// this is important to make sure the inputs are included in the form submission
h.HxInclude("input"),
),
h.Id(recordId),
Cell("name", record.Name),
Cell("birthday", record.Birthday),
Cell("role", record.Role),
Cell("salary", record.Salary),
// Edit button
h.Td(
h.Button(
h.Class(ButtonClasses),
h.PostPartialWithQs(
h.Ternary(!editing, StartEditing, SaveEditing),
h.NewQs("id", record.Id),
),
h.Text(
h.Ternary(!editing, "Edit", "Save"),
),
),
),
)
}

28
htmgo-site/ui/copy.go Normal file
View file

@ -0,0 +1,28 @@
package ui
import (
"fmt"
"github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/js"
)
func CopyButton(selector string) *h.Element {
return h.Div(
h.Class("absolute top-0 right-0 p-2 bg-slate-800 text-white rounded-bl-md cursor-pointer"),
h.Text("Copy"),
h.OnClick(
// language=JavaScript
js.EvalJs(fmt.Sprintf(`
if(!navigator.clipboard) {
return;
}
let text = document.querySelector("%s").innerText;
navigator.clipboard.writeText(text);
self.innerText = "Copied!";
setTimeout(() => {
self.innerText = "Copy";
}, 1000);
`, selector)),
),
)
}

View file

@ -7,6 +7,7 @@ import (
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
"github.com/google/uuid"
"github.com/maddalax/htmgo/framework/h"
"strings"
)
@ -19,7 +20,7 @@ func FormatCode(code string, customStyles ...string) string {
html.WrapLongLines(true),
html.WithLineNumbers(true),
html.WithCustomCSS(map[chroma.TokenType]string{
chroma.PreWrapper: fmt.Sprintf("padding: 12px; overflow: auto; background-color: rgb(245, 245, 245) !important; %s", strings.Join(customStyles, ";")),
chroma.PreWrapper: fmt.Sprintf("font-size: 14px; padding: 12px; overflow: auto; background-color: rgb(245, 245, 245) !important; %s", strings.Join(customStyles, ";")),
}))
iterator, err := lexer.Tokenise(nil, code)
if err != nil {
@ -30,7 +31,17 @@ func FormatCode(code string, customStyles ...string) string {
}
func CodeSnippet(code string, customStyles ...string) *h.Element {
id := fmt.Sprintf("code-snippet-%s", uuid.NewString())
return h.Div(
h.UnsafeRaw(FormatCode(code, customStyles...)),
h.Class("relative"),
h.Div(
h.UnsafeRaw(code),
h.Class("hidden"),
h.Id(id),
),
CopyButton("#"+id),
h.UnsafeRaw(
FormatCode(code, customStyles...),
),
)
}