Skip to content

donseba/go-form

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go-form

Render forms in go based on struct layout, and tags.

Please note that this package is a pre-alfa release and mainly only a visualization of my initial thought. I am also trying to keep the footprint as low as possible without using third party packages.

it can convert to following data:

data := struct {
    Form   ExampleForm
    Errors []form.FieldError
}{
    Form: ExampleForm{
        Name:     "John Wick",
        Email:    "[email protected]",
        Date:     "1991-11-11",
        Password: "Secret123!",
        Address: &AddressBlock{
            Street1: "121 Mill Neck",
            City:    "Long Island",
            State:   "NY",
            Zip:     "11765",
        },
        Enums:     ExampleEnumFieldValue2,
        CheckBox:  true,
        CheckBox2: false,
        RadioGroup: &RadioGroupBlock{
            Option2: true,
        },
    },
    Errors: []form.FieldError{
        fieldError{
            Field: "Email",
            Issue: "is already taken",
        },
        fieldError{
            Field: "Address.Street1",
            Issue: "is required",
        },
    },
}

into:

Call form_render inside the template and pass it the form struct and the errors :

<form class="space-y-6" action="#" method="POST">
    {{ form_render .Form .Errors }}
    <div class="flex items-center justify-between">
        <button type="submit" class="flex w-full justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">Signup</button>
    </div>
</form>

There is currently only one template file for all the currently supported templates. I'm thinking of making a mini template for each type.

<div>
    <label {{with .Field.Id}}for="{{.}}"{{end}} class="block text-sm font-medium text-gray-700">{{.Field.Label}}{{ if eq .Field.Required true }}*{{end}}</label>
    <div class="mt-1">
        {{ if eq .Field.Type "dropdown" }}
        <select {{with .Field.Id}}id="{{.}}"{{end}} name="{{.Name}}" class="bg-white block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm">
            {{ range $k, $option := .Field.Values }}
            <option value="{{$option.Id}}">{{$option.Name}}</option>
            {{ end }}
        </select>
        {{ else if eq .Field.Type "textarea" }}
        <textarea {{with .Field.Id}}id="{{.}}"{{end}} name="{{.Field.Name}}" {{with .Field.Id}}rows="{{.}}"{{end}} {{with .Field.Cols}}cols="{{.}}"{{end}} placeholder="{{.Field.Placeholder}}" {{ if eq .Field.Required true }}required{{end}} class="block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"></textarea>
        {{ else if eq .Field.Type "dropdownmapped" }}
        <select {{with .Field.Id}}id="{{.}}"{{end}} name="{{.Field.Name}}" class="text-gray-700 dark:text-gray-200 dark:bg-gray-700 bg-white block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm">
            {{ $value := .Field.Value }}
            {{ range $k, $option := .Field.Values }}
            <option value="{{$option.Value}}" {{ if eq $value.String $option.Value }}selected{{ end }} {{ if eq $option.Disabled true }}disabled{{ end }}>{{$option.Name}}</option>
            {{ end }}
        </select>
        {{ else if eq .Type "checkbox" }}
        <input {{with .Field.Id}}id="{{.}}"{{end}} name="{{.Field.Name}}" type="checkbox" {{ if eq .Field.Required true }}required{{end}} {{ if eq .Field.Value true }}checked{{end}} class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
        {{ else }}
        <input {{with .Field.Id}}id="{{.}}"{{end}} name="{{.Field.Name}}" placeholder="{{.Field.Placeholder}}" {{with .Field.Value}}value="{{.}}"{{end}} {{ if eq .Field.Required true }}required{{end}} class="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm">
        {{ end }}
        
        {{range errors}}
        <span class="text-sm text-red-600">{{.}}</span>
        {{end}}
    </div>
</div>

Groups (nested structs) have their own template

<div class="mb-4 bg-gray-50 p-2 rounded-md">
  <label class="block text-grey-darker text-sm font-bold mb-2">{{.Name }}</label>
  {{ fields }}
</div>

please note that the errors need to implement with the FieldError interface. otherwise they will be silently skipped. You can achieve that doing so :

type fieldError struct {
    Field string
    Issue string
}

func (fe fieldError) Error() string {
    return fmt.Sprintf("%s:%s", fe.Field, fe.Issue)
}

func (fe fieldError) FieldError() (field, err string) {
    return fe.Field, fe.Issue
}

Support for sorted maps is also included.

type CountryListOption struct {
	Selected     string
	SortedValues []form.SortedMap
}

type SortedMap struct {
	SKey   string
	SValue string
}

func (s SortedMap) Key() string   { return s.SKey }
func (s SortedMap) Value() string { return s.SValue }

func (t *CountryListOption) SortedMapper() []form.SortedMap {
	return t.SortedValues
}

func (t *CountryListOption) String() string {
	return t.Selected
}

Now in the struct that contains the form you can add the following:

UserForm struct {
    Country *CountryListOption
}

however the downside is that you create a direct dependancy to the form.SortedMap interface.

supported tags

  • label
  • placeholder
  • name
  • required
  • cols
  • rows
  • step

TODO

  • better tag handling. maybe group all possible options into one tag or keep it as is.
  • add validation to process the form once posted.