Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion examples/gno.land/r/docs/markdown/markdown.gno
Original file line number Diff line number Diff line change
Expand Up @@ -411,16 +411,19 @@ Create a form using the ±<gno-form>± tag and add inputs with ±<gno-input>±:
- Must start with ±<gno-form>± and end with ±</gno-form>±
- Optional attributes:
- ±path±: Render path after form submission (e.g. ±path="user"± will redirect to ±:user?[params]±)
- ±exec±: Auto-generate fields from a realm function signature (e.g. ±exec="Render"±).
When set, manual fields are not allowed; inputs are derived from the function parameters.
- Form data is always sent as query parameters
- Cannot be nested (forms inside forms are not allowed)
- Automatically includes a header showing the realm name

2. **Input Fields**
- Created with ±<gno-input>± tag
- Required attributes:
- Attributes:
- ±name±: Unique identifier for the input (required)
- ±type±: Type of input (optional, defaults to "text")
- ±placeholder±: Hint text shown in the input (optional, defaults to "Enter value")
- ±value±: Default value for non-selectable inputs (optional)
- ±description±: Description of the input as title (optional - can be used as title for group of one or multiple inputs)
- Supported input types:
- ±type="text"± (default): For text input
Expand All @@ -444,6 +447,7 @@ Create a form using the ±<gno-form>± tag and add inputs with ±<gno-input>±:
- ±name±: Unique identifier for the textarea (required)
- ±placeholder±: Hint text shown in the textarea (optional, defaults to "Enter value")
- Optional attributes:
- ±value±: Default content for the textarea (optional)
- ±rows±: Number of visible rows (2-10, defaults to 4)
- Perfect for longer text input like messages, descriptions, or comments
- Each textarea must have a unique name
Expand Down Expand Up @@ -608,6 +612,32 @@ Select options work like radio buttons but with a more semantic meaning. All ±<

This example shows how to combine all form element types in a single form.

8. Form with Default Values
±±±markdown
<gno-form>
<gno-input name="username" type="text" placeholder="Enter username" value="Alice" />
<gno-input name="age" type="number" placeholder="Your age" value="42" />
<gno-textarea name="bio" placeholder="Short bio" rows="4" value="Super builder" />
</gno-form>
±±±

<gno-form>
<gno-input name="username" type="text" placeholder="Enter username" value="Alice" />
<gno-input name="age" type="number" placeholder="Your age" value="42" />
<gno-textarea name="bio" placeholder="Short bio" rows="4" value="Super builder" />
</gno-form>

9. Exec-Generated Form
When using the ±exec± attribute, the form inputs are automatically generated from the realm function signature, and a command preview appears. Manual fields are not allowed in this mode.

±±±markdown
<gno-form exec="Render">
</gno-form>
±±±

<gno-form exec="Render">
</gno-form>

#### Important Rules

1. **Validation Rules**:
Expand Down
2 changes: 1 addition & 1 deletion gno.land/pkg/gnoweb/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func NewRouter(logger *slog.Logger, cfg *AppConfig) (http.Handler, error) {
mdhtml.WithXHTML(), mdhtml.WithUnsafe(),
))
}
renderer := NewHTMLRenderer(logger, rcfg)
renderer := NewHTMLRenderer(logger, rcfg, adpcli)

// Configure HTTPHandler
if cfg.Aliases == nil {
Expand Down
27 changes: 24 additions & 3 deletions gno.land/pkg/gnoweb/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,13 @@ type ClientAdapter interface {
// package path and filename.
File(ctx context.Context, path, filename string) ([]byte, FileMeta, error)

// Sources lists all source files available in a specified
// ListFiles lists all source files available in a specified
// package path.
ListFiles(ctx context.Context, path string) ([]string, error)

// ListFuncs lists all exposed funcs in a specified package path.
ListFuncs(ctx context.Context, path string) (vm.FunctionSignatures, error)

// QueryPath list any path given the specified prefix
ListPaths(ctx context.Context, prefix string, limit int) ([]string, error)

Expand Down Expand Up @@ -159,18 +162,36 @@ func (c *rpcClient) Doc(ctx context.Context, pkgPath string) (*doc.JSONDocumenta
args := fmt.Sprintf("%s/%s", c.domain, strings.Trim(pkgPath, "/"))
res, err := c.query(ctx, qpath, []byte(args))
if err != nil {
return nil, fmt.Errorf("unable to query qdoc: %w", err)
return nil, fmt.Errorf("unable to query qdoc for %s: %w", pkgPath, err)
}

jdoc := &doc.JSONDocumentation{}
if err := amino.UnmarshalJSON(res, jdoc); err != nil {
c.logger.Warn("unable to unmarshal qdoc, client is probably outdated")
return nil, fmt.Errorf("unable to unmarshal qdoc: %w", err)
return nil, fmt.Errorf("unable to unmarshal qdoc for %s: %w", pkgPath, err)
}

return jdoc, nil
}

func (c *rpcClient) ListFuncs(ctx context.Context, pkgPath string) (vm.FunctionSignatures, error) {
const qpath = "vm/qfuncs"

args := fmt.Sprintf("%s/%s", c.domain, strings.Trim(pkgPath, "/"))
res, err := c.query(ctx, qpath, []byte(args))
if err != nil {
return nil, fmt.Errorf("unable to query qfuncs for %s: %w", pkgPath, err)
}

fsigs := vm.FunctionSignatures{}
if err := amino.UnmarshalJSON(res, &fsigs); err != nil {
c.logger.Warn("unable to unmarshal qfuncs, client is probably outdated")
return nil, fmt.Errorf("unable to unmarshal qfuncs for %s: %w", pkgPath, err)
}

return fsigs, nil
}

// query sends a query to the RPC client and returns the response
// data.
func (c *rpcClient) query(ctx context.Context, qpath string, data []byte) ([]byte, error) {
Expand Down
41 changes: 41 additions & 0 deletions gno.land/pkg/gnoweb/client_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"sort"
"strings"

"github.com/gnolang/gno/gno.land/pkg/sdk/vm"
"github.com/gnolang/gno/gnovm/pkg/doc"
)

Expand Down Expand Up @@ -138,6 +139,46 @@ func (m *MockClient) Doc(ctx context.Context, path string) (*doc.JSONDocumentati
return &doc.JSONDocumentation{Funcs: pkg.Functions}, nil
}

// ListFuncs retrieves the function signatures for a specified package path.
func (m *MockClient) ListFuncs(ctx context.Context, path string) (vm.FunctionSignatures, error) {
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("context error: %w", err)
}

pkg, exists := m.Packages[path]
if !exists {
return nil, ErrClientPackageNotFound
}

// Convert JSONFunc to FunctionSignature
var sigs vm.FunctionSignatures
for _, fn := range pkg.Functions {
sig := vm.FunctionSignature{
FuncName: fn.Name,
}

// Convert parameters
for _, param := range fn.Params {
sig.Params = append(sig.Params, vm.NamedType{
Name: param.Name,
Type: param.Type,
})
}

// Convert results
for _, result := range fn.Results {
sig.Results = append(sig.Results, vm.NamedType{
Name: result.Name,
Type: result.Type,
})
}

sigs = append(sigs, sig)
}

return sigs, nil
}

// Helper: check if package has a Render(string) string function.
func pkgHasRender(pkg *MockPackage) bool {
if len(pkg.Functions) == 0 {
Expand Down
4 changes: 2 additions & 2 deletions gno.land/pkg/gnoweb/components/layouts/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<img src="/public/imgs/gnoland.svg" alt="Gno username profile pic" width="40px" height="40px" />
</a>

<div class="b-main-navivation">
<div class="b-main-navigation">
<div class="inner c-reel u-no-scrollbar" data-controller="searchbar">
<form class="searchbar" data-action="submit->searchbar#searchUrl">
<label for="header-input-search" class="u-sr-only">
Expand Down Expand Up @@ -146,4 +146,4 @@
</ol>
<button type="submit" class="u-sr-only">Update Breadcrumb</button>
</form>
{{ end }}
{{ end }}
15 changes: 15 additions & 0 deletions gno.land/pkg/gnoweb/components/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ func registerCommonFuncs(funcs template.FuncMap) {
return vals.Has(key)
}
funcs["FormatRelativeTime"] = FormatRelativeTimeSince
// dict creates a map from key-value pairs for passing multiple values to templates
funcs["dict"] = func(kv ...any) (map[string]any, error) {
if len(kv)%2 != 0 {
return nil, fmt.Errorf("dict requires an even number of arguments")
}
result := make(map[string]any)
for i := 0; i < len(kv); i += 2 {
key, ok := kv[i].(string)
if !ok {
return nil, fmt.Errorf("dict keys must be strings")
}
result[key] = kv[i+1]
}
return result, nil
}
}

func init() {
Expand Down
22 changes: 22 additions & 0 deletions gno.land/pkg/gnoweb/components/ui/command.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{{ define "ui/command" }}
<div class="b-code">
<button data-controller="copy" data-action="click->copy#copy"
data-copy-remote-value="action-function-{{.FuncName}}" data-copy-clean-value class="btn-copy"
aria-label="Copy Command">
<svg class="c-icon">
<use href="#ico-copy" data-copy-target="icon"></use>
<use href="#ico-check" class="u-hidden u-color-valid" data-copy-target="icon"></use>
</svg>
</button>
<pre><code><span data-action-function-target="mode" data-action-function-mode-value="fast" class="u-hidden" data-copy-target="action-function-{{.FuncName}}"># WARNING: This command is running in an INSECURE mode.
# It is strongly recommended to use a hardware device for signing
# and avoid trusting any computer connected to the internet,
# as your private keys could be exposed.

gnokey maketx call -pkgpath "{{.PkgPath}}" -func "{{.FuncName}}"{{range .ParamNames}} -args "<span data-action-function-target="arg" data-action-function-arg-value="{{.}}"></span>"{{end}} -gas-fee 1000000ugnot -gas-wanted 5000000 -send "<span data-action-function-target="send-code"></span>" -broadcast -chainid "{{.ChainId}}" -remote "{{.Remote}}" <span data-action-function-target="address">ADDRESS</span></span><span data-action-function-target="mode" data-action-function-mode-value="secure" data-copy-target="action-function-{{.FuncName}}" class="u-inline">gnokey query -remote "{{.Remote}}" auth/accounts/<span data-action-function-target="address">ADDRESS</span>
gnokey maketx call -pkgpath "{{.PkgPath}}" -func "{{.FuncName}}"{{range .ParamNames}} -args "<span data-action-function-target="arg" data-action-function-arg-value="{{.}}"></span>"{{end}} -gas-fee 1000000ugnot -gas-wanted 5000000 -send "<span data-action-function-target="send-code"></span>" <span data-action-function-target="address">ADDRESS</span> > call.tx
gnokey sign -tx-path call.tx -chainid "{{.ChainId}}" -account-number ACCOUNTNUMBER -account-sequence SEQUENCENUMBER <span data-action-function-target="address">ADDRESS</span>
gnokey broadcast -remote "{{.Remote}}" call.tx
</span></code></pre>
</div>
{{ end }}
2 changes: 1 addition & 1 deletion gno.land/pkg/gnoweb/components/ui_breadcrumb.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ type BreadcrumbData struct {
Queries []QueryParam
}

func RenderBreadcrumpComponent(w io.Writer, data BreadcrumbData) error {
func RenderBreadcrumbComponent(w io.Writer, data BreadcrumbData) error {
return tmpl.ExecuteTemplate(w, "Breadcrumb", data)
}
25 changes: 25 additions & 0 deletions gno.land/pkg/gnoweb/components/view_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ type HelpTocItem struct {
Text string
}

type CommandData struct {
FuncName string
PkgPath string
ParamNames []string
ChainId string
Remote string
}

type helpViewParams struct {
HelpData
Article ArticleData
Expand Down Expand Up @@ -68,6 +76,23 @@ func registerHelpFuncs(funcs template.FuncMap) {
}
return url
}

funcs["buildCommandData"] = func(data HelpData, fn *doc.JSONFunc) CommandData {
// Extract parameter names
paramNames := make([]string, len(fn.Params))

for i, param := range fn.Params {
paramNames[i] = param.Name
}

return CommandData{
FuncName: fn.Name,
PkgPath: data.PkgPath,
ParamNames: paramNames,
ChainId: data.ChainId,
Remote: data.Remote,
}
}
}

func HelpView(data HelpData) *View {
Expand Down
4 changes: 2 additions & 2 deletions gno.land/pkg/gnoweb/components/view_realm.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package components

import (
"github.com/gnolang/gno/gno.land/pkg/gnoweb/markdown"
ti "github.com/gnolang/gno/gno.land/pkg/gnoweb/markdown/tocitem"
)

const RealmViewType ViewType = "realm-view"

type RealmTOCData struct {
Items []*markdown.TocItem
Items []*ti.TocItem
}

type RealmData struct {
Expand Down
4 changes: 2 additions & 2 deletions gno.land/pkg/gnoweb/components/view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"strings"
"testing"

"github.com/gnolang/gno/gno.land/pkg/gnoweb/markdown"
ti "github.com/gnolang/gno/gno.land/pkg/gnoweb/markdown/tocitem"
"github.com/gnolang/gno/gnovm/pkg/doc"
"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -144,7 +144,7 @@ func (m *mockWriter) Write(p []byte) (n int, err error) {
func TestRealmView(t *testing.T) {
content := NewReaderComponent(strings.NewReader("testdata"))
tocItems := &RealmTOCData{
Items: []*markdown.TocItem{
Items: []*ti.TocItem{
{Title: []byte("Introduction"), ID: []byte("introduction")},
},
}
Expand Down
21 changes: 2 additions & 19 deletions gno.land/pkg/gnoweb/components/views/action.html
Original file line number Diff line number Diff line change
Expand Up @@ -147,26 +147,9 @@ <h3 class="alert-title c-with-icon">
</form>
<div>
<h3 class="title">Command</h3>
<div class="b-code">
<button data-controller="copy" data-action="click->copy#copy"
data-copy-remote-value="action-function-{{ .Name }}" data-copy-clean-value class="btn-copy"
aria-label="Copy Command">
{{ template "ui/copy" }}
</button>
{{/* prettier-ignore-start */}}
<pre><code><span data-action-function-target="mode" data-action-function-mode-value="fast" class="u-hidden" data-copy-target="action-function-{{ .Name }}"># WARNING: This command is running in an INSECURE mode.
# It is strongly recommended to use a hardware device for signing
# and avoid trusting any computer connected to the internet,
# as your private keys could be exposed.

gnokey maketx call -pkgpath "{{ $.PkgPath }}" -func "{{ .Name }}"{{ range .Params }} -args "<span data-action-function-target="arg" data-action-function-arg-value="{{ .Name }}"></span>"{{ end }} -gas-fee 1000000ugnot -gas-wanted 5000000 -send "<span data-action-function-target="send-code"></span>" -broadcast -chainid "{{ $.ChainId }}" -remote "{{ $.Remote }}" <span data-action-function-target="address">ADDRESS</span></span><span data-action-function-target="mode" data-action-function-mode-value="secure" data-copy-target="action-function-{{ .Name }}" class="u-inline">gnokey query -remote "{{ $.Remote }}" auth/accounts/<span data-action-function-target="address">ADDRESS</span>
gnokey maketx call -pkgpath "{{ $.PkgPath }}" -func "{{ .Name }}"{{ range .Params }} -args "<span data-action-function-target="arg" data-action-function-arg-value="{{ .Name }}"></span>"{{ end }} -gas-fee 1000000ugnot -gas-wanted 5000000 -send "<span data-action-function-target="send-code"></span>" <span data-action-function-target="address">ADDRESS</span> > call.tx
gnokey sign -tx-path call.tx -chainid "{{ $.ChainId }}" -account-number ACCOUNTNUMBER -account-sequence SEQUENCENUMBER <span data-action-function-target="address">ADDRESS</span>
gnokey broadcast -remote "{{ $.Remote }}" call.tx</span></code></pre>
{{/* prettier-ignore-end */}}
</div>
{{ template "ui/command" (buildCommandData $data .) }}
</div>
</article>
{{ end }}
</div>
{{ end }}
{{ end }}
12 changes: 12 additions & 0 deletions gno.land/pkg/gnoweb/frontend/css/05-composition.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@
margin-block-start: var(--g-space-4);
}

.c-inline {
display: inline-flex;
align-items: center;
gap: var(--g-space-3);
}

.c-between {
display: flex;
justify-content: space-between;
align-items: center;
}

/* Center layout */
.c-center {
box-sizing: border-box;
Expand Down
Loading
Loading