İçindekiler

Tier 2 kontrolleri, native HTML girdilerinin ötesine geçer: sunucu tarafı arama, ağaç/graf seçimi, zengin metin/kod/markdown düzenleme, önizlemeli dosya yüklemeleri, çizim pedleri ve daha fazlası. Select ailesi, telefon girdisi, veri güdümlü picker'lar, editörler, upload ve görsel kontroller dahil tamamı github.com/zatrano/goui/forms paketinde yaşar.

import "github.com/zatrano/goui/forms"

Aşağıdaki her alan core.BaseComponent + forms.CommonAttrs + forms.FieldValidation'ı gömer (belirtilmediği yerler hariç) ve 05-forms-tier1.md ile 06-validation.md'de açıklanan paylaşılan Name()/RawValue()/SetRawValue(string)/Validate() bool sözleşmesini uygular. Hepsi sunucu tarafında render edilir: Go struct'ı tek doğruluk kaynağıdır, ve tarayıcı sadece aşağıda açıkça "UI-only (sadece UI)" olarak not edilen küçük UI durumu dilimine sahiptir (örn. bir takvimin şu anda hangi ayı gösterdiği).


Select ailesi (forms)

Bunların hepsi forms.BaseSelectField'i gömer:

type SelectItem struct {
    Value    string
    Label    string
    Disabled bool
}

type BaseSelectField struct {
    core.BaseComponent
    forms.CommonAttrs
    forms.FieldValidation

    Items       []SelectItem
    Filtered    []SelectItem // son sunucu tarafı filtre geçişi
    Query       string
    Open        bool
    Value       string
    Values      []string
    FilterMode  FilterMode // FilterServer (varsayılan) | FilterClient
    MaxResults  int        // varsayılan 50
    Placeholder string

    OnChange func(value string)
    OnQuery  func(query string)
}

Sunucu vs. UI-only: filtreleme varsayılan olarak sunucu taraflıdır (FilterServer) — her tuş vuruşu bir query olayı gönderir, sunucu forms.FilterItems aracılığıyla Filtered'ı yeniden hesaplar (label/value üzerinde büyük/küçük harfe duyarsız alt dize eşleşmesi, MaxResults ile sınırlandırılmış), ve yeni <li> listesini bir yama olarak geri gönderir. FilterMode: FilterClient, küçük, sabit listeler için vardır ama o durumda bile sunucu seçim durumunun sahibidir; forms'de hiçbir şey seçenek DOM'unu saf olarak JavaScript'te filtrelemez. İsteğe bağlı selectable.js istemci modülü bunu güçlendirir: sadece sunucunun zaten render ettiği listenin üzerine klavye vurgusu/Enter-ile-seçim ekler — seçenekleri istemci tarafında filtrelemez.

Searchable Select

type SearchableSelect struct {
    BaseSelectField
    EventName string // olaylar için önek, örn. "city" → city.query / city.select
}
city := forms.SearchableSelect{
    BaseSelectField: forms.BaseSelectField{
        CommonAttrs: forms.CommonAttrs{Name: "city", ID: "city"},
        Placeholder: "Select a city",
        Items: []forms.SelectItem{
            {Value: "ist", Label: "İstanbul"},
            {Value: "ank", Label: "Ankara"},
        },
    },
    EventName: "city",
}

HandleEvent eylemleri (<eventName>.<action> üzerinden dispatch edilir): toggle, open, close, query, select. Kendine ait bir istemci modülü dosyası yoktur — açma/kapama ve klavye davranışı genel olarak selectable.js tarafından kapsanır.

Multi Select

type MultiSelect struct {
    BaseSelectField
    EventName string
}

SearchableSelect ile aynı şekle sahiptir ama Values []string'i takip eder; seçili öğeleri kaldırılabilir <span class="goui-chip"> etiketleri olarak render eder. Eylemler: toggle, open, close, query, select (üyeliği açar/kapatır), remove.

cities := forms.MultiSelect{
    BaseSelectField: forms.BaseSelectField{
        CommonAttrs: forms.CommonAttrs{Name: "cities", ID: "cities"},
        Items:       cityItems,
    },
    EventName: "cities",
}

Combobox

type Combobox struct {
    BaseSelectField
    EventName      string
    RestrictToList bool // true olduğunda, serbest metni reddeder — Value yalnızca Items'tan
}

Filtrelenmiş bir öneri panelini de açan bir metin girdisi. RestrictToList ayarlanmadıkça, her tuş vuruşu ham yazılan metni Value'ya da ayarlar (serbest metin izinlidir); bir öneri seçmek Value'yu öğeye ve Query'yi onun etiketine ayarlar. Eylemler: toggle/open, close, query, select, commit.

role := forms.Combobox{
    BaseSelectField: forms.BaseSelectField{
        CommonAttrs: forms.CommonAttrs{Name: "role", ID: "role"},
        Items: []forms.SelectItem{{Value: "admin", Label: "Admin"}, {Value: "editor", Label: "Editor"}},
    },
    EventName: "role",
}

Autocomplete

type Autocomplete struct {
    BaseSelectField
    EventName string
}

Combobox gibidir ama yazarken Value'yu ayarlamaz — sadece bir öneri seçimi (veya seçim olmadan commit, ki bu yazılan metne geri döner) Value'yu ayarlar. Eylemler: query, select, commit, close.

suggest := forms.Autocomplete{
    BaseSelectField: forms.BaseSelectField{CommonAttrs: forms.CommonAttrs{Name: "suggest", ID: "suggest"}, Items: cityItems},
    EventName: "suggest",
}

Tag Input / Chips Input

type TagInput struct {
    core.BaseComponent
    forms.CommonAttrs
    forms.FieldValidation

    Values      []string
    Draft       string
    Placeholder string
    EventName   string
    OnChange    func(tags []string)
}

// ChipsInput, TagInput değerlerinin çip sunumunu vurgulayan bir takma addır.
type ChipsInput = TagInput

BaseSelectField üzerine inşa edilmemiştir — sabit bir Items listesi üzerinde bir seçici değil, serbest metin etiket koleksiyonudur. Büyük/küçük harfe duyarsız olarak yinelenenleri temizler. Eylemler: draft (yazarken tampon), add/commit (virgülle ayrılmış girdi desteklenir — "go, rust" her ikisini de ekler), remove.

skills := forms.TagInput{
    CommonAttrs: forms.CommonAttrs{Name: "skills", ID: "skills"},
    Placeholder: "Add a tag (Enter/blur)",
    EventName:   "skills",
}

Tree Select

type TreeNode struct {
    Value    string
    Label    string
    Disabled bool
    Children []TreeNode
}

type TreeSelect struct {
    BaseSelectField
    Nodes     []TreeNode
    Expanded  map[string]bool
    EventName string
}

Mount, Expanded'i tembel (lazily) tahsis eder. Her dal düğümü için genişlet/daralt geçişleriyle iç içe bir <ul> render eder. Eylemler: toggle (panel açma/kapama), close, expand (bir düğümün genişletme durumunu değiştirir), select.

dept := forms.TreeSelect{
    BaseSelectField: forms.BaseSelectField{CommonAttrs: forms.CommonAttrs{Name: "dept", ID: "dept"}},
    EventName:       "dept",
    Nodes: []forms.TreeNode{
        {Value: "eng", Label: "Engineering", Children: []forms.TreeNode{
            {Value: "be", Label: "Backend"}, {Value: "fe", Label: "Frontend"},
        }},
    },
}

Cascader

type CascaderLevel struct {
    Items    []SelectItem
    Selected string
}

type Cascader struct {
    BaseSelectField
    EventName    string
    Levels       []CascaderLevel
    LoadChildren func(level int, parentValue string) []SelectItem
}

Her seçimin sonraki sütunu sunucu tarafında sizin LoadChildren callback'inizle yüklediği çok sütunlu bir "içine inme" (drill-down) kontrolüdür. Mount, Levels boşsa Levels[0]Items'tan besler. RawValue(), her seviyenin seçimini / ile birleştirir. Eylem: pick (payload hem value hem level taşır); seçim yapmak herhangi bir daha derin seviyeyi temizler ve ya yeni bir sütun ekler (LoadChildren'ın döndürdüğü öğeler) ya da hiç çocuk yoksa commit eder (OnChange).

loc := forms.Cascader{
    BaseSelectField: forms.BaseSelectField{
        CommonAttrs: forms.CommonAttrs{Name: "loc", ID: "loc"},
        Items:       []forms.SelectItem{{Value: "tr", Label: "Türkiye"}, {Value: "de", Label: "Almanya"}},
    },
    EventName: "loc",
    LoadChildren: func(level int, parent string) []forms.SelectItem {
        if level == 0 && parent == "tr" {
            return []forms.SelectItem{{Value: "ist", Label: "İstanbul"}, {Value: "ank", Label: "Ankara"}}
        }
        return nil
    },
}

Dual Listbox

type DualListbox struct {
    BaseSelectField
    EventName      string
    SelectedQuery  string
    SelectedFilter []SelectItem
}

Taşıma eylemleriyle birlikte iki bağımsız olarak aranabilir sütun ("mevcut" / "seçili"). Her iki taraf da sunucu tarafında filtrelenir (ApplyAvailableQuery, ApplySelectedQuery). Eylemler: query_left/query (mevcut taraf), query_right (seçili taraf), add, remove, add_all, remove_all.

perms := forms.DualListbox{
    BaseSelectField: forms.BaseSelectField{CommonAttrs: forms.CommonAttrs{Name: "perms", ID: "perms"}, Items: permItems},
    EventName: "perms",
}

Phone Input (forms)

type PhoneInput struct {
    forms.CommonAttrs
    forms.FieldValidation

    Dial   SearchableSelect // çevirme kodu
    Number forms.TextInput  // ulusal numara
}

func NewPhoneInput(name string) *PhoneInput

Yeni bir kontrol ailesi değildir — bir SearchableSelect'i (çevirme kodu, forms.DialCodeItems()'tan önceden yüklenmiş, varsayılan +90) bir forms.TextInput (type=tel) yanına bağlayan bir kompozisyon yardımcısıdır. RawValue(), bir E.164 benzeri "<dial> <number>" string'i döndürür. HandleEvent, olay önekini ait olduğu alt alanla eşleştirerek dispatch eder.

phone := forms.NewPhoneInput("phone") // *PhoneInput

Country / Language / Timezone / Currency Picker (forms)

Bunlar ayrı struct türleri değildir — curated (derlenmiş) []SelectItem verisiyle (forms.CountryItems(), LanguageItems(), TimezoneItems(), CurrencyItems()) önceden yüklenmiş SearchableSelect factory fonksiyonlarıdır:

func NewCountryPicker(name, event string) SearchableSelect
func NewLanguagePicker(name, event string) SearchableSelect
func NewTimezonePicker(name, event string) SearchableSelect
func NewCurrencyPicker(name, event string) SearchableSelect
country := forms.NewCountryPicker("country", "country")   // SearchableSelect
language := forms.NewLanguagePicker("lang", "lang")
tz := forms.NewTimezonePicker("tz", "tz")
currency := forms.NewCurrencyPicker("cur", "cur")

SearchableSelect için yukarıda belgelenen her şey (sunucu tarafı filtre, sadece selectable.js klavye navigasyonu, istemci tarafında filtreleme yok) değişmeden geçerlidir.


Emoji / Icon / Font Picker (forms)

Yukarıdaki seçicilerle aynı desen — curated öğe kümeleri (EmojiItems(), IconItems(), FontItems()) üzerinde SearchableSelect factory'leri:

func NewEmojiPicker(name, event string) SearchableSelect
func NewIconPicker(name, event string) SearchableSelect
func NewFontPicker(name, event string) SearchableSelect
emoji := forms.NewEmojiPicker("emoji", "emoji")
icon := forms.NewIconPicker("icon", "icon")
font := forms.NewFontPicker("font", "font")

FontItems(), Value olarak tam CSS font-family yığınlarını döndürür (örn. "Georgia, serif"), böylece Value'yu doğrudan satır içi style="font-family:..." olarak canlı bir önizleme için uygulayabilirsiniz, examples/misc-controls'un yaptığı gibi.

forms.MentionUsers() — örnek kullanıcıların küçük, curated bir []SelectItem dizini — aynı dosyada yaşar ve kendisi bir seçici olarak render edilmek için değil, aşağıdaki forms.MentionTextarea için MentionUser listelerini beslemek içindir.


Currency Input (forms)

type CurrencyInput struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation

    Value     float64
    Currency  string // ISO kodu, varsayılan TRY
    Locale    string // varsayılan "tr"
    Decimals  int    // varsayılan 2
    Draft     string // yazarken ham metin
    EventName string
    OnChange  func(value float64)
}

Bir float64 saklar; tüm görüntüleme biçimlendirmesi sunucu taraflıdır (forms.NumberFormat/forms.ParseLocalizedNumbertr1.234,56 tarzı gruplama kullanır, en1,234.56 kullanır). Yazarken, ham metin Draft'ta tutulur ve sadece ayrıştırılabiliyorsa blur/change'de Value'ya commit edilir; aksi halde ayrıştırılamayan Draft görünür kalır, böylece kullanıcı bir yazım hatasını düzeltebilir.

price := forms.CurrencyInput{
    CommonAttrs: forms.CommonAttrs{Name: "price", ID: "price"},
    Currency:    "TRY",
    Locale:      "tr",
    Value:       1250.5,
    EventName:   "price",
}

Percentage Input (forms)

type PercentageInput struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation

    Value     float64 // yüzde puanı, örn. 45.5, 45,5% anlamına gelir
    Locale    string
    Decimals  int      // varsayılan 1
    Min, Max  *float64
    Draft     string
    EventName string
    OnChange  func(value float64)
}

CurrencyInput ile aynı draft/commit/locale-biçimlendirme deseni, isteğe bağlı Min/Max sınırlamasıyla.

max, min := 100.0, 0.0
vat := forms.PercentageInput{
    CommonAttrs: forms.CommonAttrs{Name: "vat", ID: "vat"},
    Value: 20, Min: &min, Max: &max, EventName: "vat",
}

Rating (forms)

type Rating struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation

    Value     int // 0..Max
    Max       int // varsayılan 5
    Icon      string // varsayılan ★
    EmptyIcon string // varsayılan ☆
    EventName string
    OnChange  func(value int)
}

Max<button> simgesi render eder; şu anda seçili yıldıza tıklamak onu 0'a geri döndürür ("un-rating" — puanı geri almaya olanak tanır). İstemci modülü yok — her yıldız için data-goui-value ile saf g-click.

score := forms.Rating{CommonAttrs: forms.CommonAttrs{Name: "score", ID: "score"}, Value: 3, Max: 5, EventName: "score"}

Date Range / Time Range Picker (forms)

type DateRangePicker struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    Start, End, Min, Max string
    EventName             string
    OnChange              func(start, end string)
}

type TimeRangePicker struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    Start, End, Min, Max, Step string
    EventName                   string
    OnChange                    func(start, end string)
}

Yan yana iki native <input type="date">/<input type="time"> elemanı; End < Start olduğunda Validate() ekstra bir hata ekler (forms.date_range.invalid / forms.time_range.invalid). İstemci modülü yok — her ikisi de g-change ile düz native girdilerdir.

leave := forms.DateRangePicker{
    CommonAttrs: forms.CommonAttrs{Name: "leave", ID: "leave"},
    Start: "2026-07-10", End: "2026-07-15", EventName: "leave",
}
shift := forms.TimeRangePicker{
    CommonAttrs: forms.CommonAttrs{Name: "shift", ID: "shift"},
    Start: "09:00", End: "17:30", EventName: "shift",
}

Calendar Date Picker (forms)

type CalendarDatePicker struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    Value, Min, Max string
    Open            bool
    Placeholder     string
    EventName       string
    OnChange        func(value string)
}

Sunucu vs. UI-only: seçili Value (YYYY-MM-DD), Min/Max sınırları, ve açık/kapalı durum sunucu sahiplidir. Ay/yıl navigasyonu yalnızca istemci taraflıdırclient/modules/calendar.js'deki (enhanceCalendar) / başlık düğmeleri yerel bir view değişkenini hareket ettirir ve ızgarayı tamamen tarayıcıda yeniden render eder, ay değişikliği başına hiçbir ağ gidiş-dönüşü olmadan. Sadece nihai gün tıklaması, panelde data-select-event içindeki olay adı aracılığıyla sunucuya bir g-click (data-goui-value="<ymd>") geri gönderir. Bu yüzden sunucu tarafında render edilen panel, calendar.js mount olup devralana kadar sadece bir yer tutucudur (<div class="goui-calendar-placeholder">Loading…</div>) — bu, o alt ağacı sunucudan kasıtlı olarak asla yeniden render etmez.

day := forms.CalendarDatePicker{
    CommonAttrs: forms.CommonAttrs{Name: "day", ID: "day"},
    Value: "2026-07-16", Placeholder: "Pick a date", EventName: "day",
}

İstemci modülü: client/modules/calendar.js (enhanceCalendar(root)).


OTP / PIN Input (forms)

type OTPInput struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    Length    int // varsayılan 6
    Value     string
    Masked    bool // parola tarzı hücreler (PIN)
    EventName string
    OnChange  func(value string)
}

// PINInput bir takma addır; PIN UX'i için Masked: true ayarlayın.
type PINInput = OTPInput

Length tek karakterli <input> hücresi render eder (Masked olduğunda type=password). Tam kod sunucu tarafında Value'da yaşar; hücre başına düzenlemeler digit eylemini kullanır (payload index + value taşır), tam değiştirme commit/paste/change/input kullanır. Toplanan uzunluk Length ile eşleşmediğinde Validate(), forms.otp.incomplete'i ekler.

otp := forms.OTPInput{CommonAttrs: forms.CommonAttrs{Name: "otp", ID: "otp"}, Length: 6, EventName: "otp"}
pin := forms.PINInput{CommonAttrs: forms.CommonAttrs{Name: "pin", ID: "pin"}, Length: 4, Masked: true, EventName: "pin"}

İstemci modülü: client/modules/otp.js (enhanceOTP) — sadece UI'ye ait otomatik-sonraki-hücreye-geçme, backspace-ile-öncekine-dönme, ok tuşu navigasyonu, ve yapıştırma-hücreler-arasında-bölünür. Her hücre için native input olayları ateşler, böylece mevcut g-input delegasyonu her rakamı sunucuya göndermeye devam eder; kendisi WebSocket ile doğrudan konuşmaz.


Rich Text Editor (forms)

type RichTextEditor struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    Value      string // HTML
    EventName  string
    DebounceMS int    // varsayılan 350
    OnChange   func(value string)
}

Sunucu vs. UI-only:Value (HTML içeriği) sunucuda yetkilidir, ama düzenleme yüzeyinin kendisi tamamen istemciye aittir — bir CDN'den yüklenen bir Quill örneği (client/modules/richtext.js, enhanceRichText/mountQuill). Render edilen markup, sarmalayıcı üzerinde data-goui-ignore taşır, böylece diff-yama istemcisi onun içine asla uzlaştırma (reconcile) yapmaz (bkz. client/goui.js'deki applyPatch'in isGoUIIgnored kontrolü) — Quill'in canlı DOM'unu yamalamak, imleç konumunu, geri alma geçmişini ve seçimi bozardı.

İçerik senkronizasyonu, Quill'in her text-change'de yazdığı ve üzerinde g-debounce ile debounce edilmiş sentetik bir input olayı ateşlediği gizli bir <textarea class="goui-editor-sync"> üzerinden çalışır. Sunucu tarafında, HandleEvent'in sync eylemi, Value'yu MarkDirty()'yi çağırmadan güncelleyerek — ve karşılık gelen demo ek olarak bu kontrol için üst bileşenin HandleEvent'inden core.ErrSkipRender döndürerek — bu şekilde rich-text senkronizasyon olayları için hiçbir render frame'i asla geri gönderilmez:

case strings.HasPrefix(event, "rt."):
    _ = d.Rich.HandleEvent(ctx, event, payload)
    // Quill DOM'un sahibidir — yamalamak yeniden mount eder ve HTML'i iki kez escape eder.
    return core.ErrSkipRender
rich := forms.RichTextEditor{CommonAttrs: forms.CommonAttrs{Name: "rt", ID: "rt"}, Value: "<p>Hello</p>", EventName: "rt"}

İstemci modülü: client/modules/richtext.js.

Markdown Editor (forms)

type MarkdownEditor struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    Value       string
    PreviewHTML string
    Rows        int    // varsayılan 10
    Placeholder string
    EventName   string
    DebounceMS  int    // varsayılan 250
    OnChange    func(value string)
}

Sunucu vs. UI-only: kaynak <textarea>, normal bir sunucu tarafında render edilen Tier-1-tarzı kontroldür (istemci modülü yok, data-goui-ignore yok) — her tuş vuruşu normal bir Textarea gibi g-input/sync üzerinden gidiş-dönüş yapar. Canlı önizleme paneli, dışa açık yardımcı aracılığıyla goldmark kullanılarak tamamen sunucuda render edilir:

func RenderMarkdown(source string) string

Mount ve her syncHandleEvent çağrısı, PreviewHTML = RenderMarkdown(Value)'yu ayarlayan refreshPreview()'i çağırır; Render() bu HTML'i doğrudan bir <div class="goui-markdown-preview"> içine yayar. Bu normal (yok sayılmamış) bir alt ağaç olduğundan, diff motoru onu diğer herhangi bir sunucu tarafında render edilmiş HTML gibi seve seve yamalar.

md := forms.MarkdownEditor{
    CommonAttrs: forms.CommonAttrs{Name: "md", ID: "md"},
    Value:       "# Hello\n\n**Markdown** rendered server-side.",
    Rows:        12,
    EventName:   "md",
}

Code Editor (forms)

type CodeEditor struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    Value      string
    Language   string // örn. javascript, go, htmlmixed — varsayılan javascript
    EventName  string
    DebounceMS int    // varsayılan 350
    OnChange   func(value string)
}

Sunucu vs. UI-only:RichTextEditor ile aynı desen — bir CDN'den CodeMirror 5 örneği (client/modules/codeeditor.js, enhanceCodeEditor/mountCM) düzenleme yüzeyinin sahibidir, yamaların onu asla dokunmaması için data-goui-ignore işaretlidir, ve g-debounce ile debounce edilmiş gizli bir <textarea class="goui-editor-sync"> aracılığıyla senkronize olur. Tam olarak rich text gibi, üst bileşenin HandleEvent'i code.* senkronizasyon olayları için core.ErrSkipRender döndürmelidir:

case strings.HasPrefix(event, "code."):
    _ = d.Code.HandleEvent(ctx, event, payload)
    return core.ErrSkipRender
code := forms.CodeEditor{
    CommonAttrs: forms.CommonAttrs{Name: "code", ID: "code"},
    Value:       "function hello() {\n  return 'GoUI';\n}\n",
    Language:    "javascript",
    EventName:   "code",
}

İstemci modülü: client/modules/codeeditor.js.


Drag & Drop Upload / Image Upload (forms)

type UploadedRef struct {
    ID          string
    Name        string
    URL         string
    ContentType string
    Size        int64
}

type DragDropUpload struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    Files      []UploadedRef
    Accept     string
    Multiple   bool
    ShowThumbs bool
    UploadURL  string // varsayılan /goui/upload
    EventName  string
    OnChange   func(files []UploadedRef)
}

// ImageUpload preset'i: Accept "image/*", ShowThumbs true.
func NewImageUpload(name, event string) DragDropUpload

Sunucu vs. UI-only: ikili baytlar WebSocket üzerinden asla yolculuk etmez. client/modules/upload.js (enhanceUpload), sürükle/bırak ve dosya girdisi change'ini yakalar, ham dosyayı data-upload-url'e (varsayılan olarak) POST eder (adapter'ınızın Store seçeneği veya upload.Mount aracılığıyla, ki bu bir upload.Storage'a — örn. upload.LocalStore — yazar ve JSON Meta'yı döndürür), ardından metadata'yı data-goui-* özniteliklerinde taşıyan gizli bir <button class="goui-upload-carrier" g-click="<event>.uploaded"> üzerinde sentetik bir tıklama üretir, böylece mevcut g-click/collectPayload delegasyonu id, name, url, size, contentType içeren bir event frame'i gönderir — soket üzerinden sadece küçük JSON referansı yolculuk eder. Sunucu tarafında, uploaded eylemi bir forms.UploadedRef'i ekler/değiştirir; remove eylemi ID ile bir tanesini düşürür.

docs := forms.DragDropUpload{
    CommonAttrs: forms.CommonAttrs{Name: "docs", ID: "docs"},
    Multiple:    true,
    Accept:      ".pdf,.txt,.png,.jpg",
    ShowThumbs:  true,
    EventName:   "docs",
}
images := forms.NewImageUpload("images", "images") // DragDropUpload preset'i

HTTP tarafını uygulama başına bir kez kaydedin:

store, err := upload.NewLocalStore("./.goui-uploads", "/goui/files", 8<<20)
gouifiber.Register(app, gouifiber.Options{Server: server, Store: store})
// POST /goui/upload, GET /goui/files/:id

İstemci modülü: client/modules/upload.js (kendi postFile/notifyUploaded çağırıları için aşağıdaki avatar.js ve signature.js tarafından da import edilir).

Avatar Upload + Image Cropper (forms)

type AvatarUpload struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    Avatar    UploadedRef
    UploadURL string
    EventName string
    OnChange  func(ref UploadedRef)
}

Sunucu vs. UI-only: nihai saklanan Avatar referansı sunucu durumudur; kırpma etkileşiminin kendisi tamamen istemci taraflıdır. client/modules/avatar.js (enhanceAvatar), dosya seçiminde bir <canvas> katmanı açar, kullanıcının 1:1 bir kareyi kaydırmasına (pointerdown/pointermove) izin verir, ve "Kırp & Yükle" (Crop & Upload) üzerinde canvas.toBlob(...)'u çağırarak kırpmayı istemci tarafında bir PNG Blob'una rasterize eder, bunu upload.js'in sunduğu aynı postFile/notifyUploaded yardımcılarıyla yükler, ardından katmanı gizler. Sunucu asla kırpılmamış pikselleri veya kırpma koordinatlarını görmez — sadece nihai kırpılmış PNG dosya referansını (action: "uploaded") veya onu kaldırmak için bir "clear" eylemini görür.

avatar := forms.AvatarUpload{CommonAttrs: forms.CommonAttrs{Name: "avatar", ID: "avatar"}, EventName: "avatar"}

İstemci modülü: client/modules/avatar.js.

Signature Pad (forms)

type SignaturePad struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    File      UploadedRef
    UploadURL string
    EventName string
    OnChange  func(ref UploadedRef)
}

Sunucu vs. UI-only: çizmenin kendisi (bir <canvas> üzerinde pointerdown/pointermove darbeleri) %100 istemci taraflıdır (client/modules/signature.js, enhanceSignature/mountPad). "Kaydet"e tıklamak, canvas'ı bir PNG blob'una rasterize eder ve onu tam olarak AvatarUpload'ın yaptığı gibi yükler, başarıda action: "uploaded"'ı ateşler; "Temizle" (yerel temizleme) sadece sunucu gidiş-dönüşü olmadan canvas piksellerini temizler; ayrı bir sunucuya bağlı "Kaydı sil" düğmesi (sadece File.ID ayarlandığında render edilir) saklanan referansı düşürmek için action: "clear" gönderir.

sig := forms.SignaturePad{CommonAttrs: forms.CommonAttrs{Name: "sig", ID: "sig"}, EventName: "sig"}

İstemci modülü: client/modules/signature.js.


Mention (forms)

type MentionUser struct {
    ID    string
    Label string
}

type MentionTextarea struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    Value       string
    Placeholder string
    Rows        int // varsayılan 4
    Users       []MentionUser // tam dizin
    Filtered    []MentionUser
    Query       string        // @'dan sonraki metin
    Open        bool
    EventName   string
    OnChange    func(value string)
}

İmleç konumunda tamamlanmamış bir @fragment algılayan (mentionQuery aracılığıyla, string tabanlı — gerçek imleç konumuna değil tüm Value'ya bakar, dolayısıyla basitleştirilmiş bir "son @" sezgiseli (heuristic)) ve sunucu tarafında filtrelenmiş bir öneri listesi (filterUsers, ID/etiket üzerinde alt dize eşleşmesi, 8 ile sınırlandırılmış) açan bir <textarea>. Bir öneri seçmek (pick eylemi), @fragment'i @<id> ile değiştirir. İstemci modülü yok — bir düz Textarea-tarzı kontrol, ardından koşullu olarak render edilmiş bir <ul>.

mention := forms.MentionTextarea{
    CommonAttrs: forms.CommonAttrs{Name: "mention", ID: "mention"},
    Placeholder: "Tag someone with @...",
    Users:       []forms.MentionUser{{ID: "ayse", Label: "Ayşe Yılmaz"}},
    EventName:   "mention",
}

Color (Swatch) Picker (forms)

type SwatchColorPicker struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    Value     string // #rrggbb
    Swatches  []string
    EventName string
    OnChange  func(value string)
}

Tier 1'deki native forms.ColorInput'a gelişmiş bir alternatif: bir sıra önceden ayarlı swatch düğmesi artı serbest metin bir hex alanı. Swatches boşsa 10 önceden ayarlı swatch'a varsayılan olur. Eylemler: pick/select (bir swatch'tan), hex/change/input (metin alanından, normalizeHex aracılığıyla normalize edilmiş — küçük harfli, #-önekli).

color := forms.SwatchColorPicker{
    CommonAttrs: forms.CommonAttrs{Name: "color", ID: "color"},
    Value:       "#2563eb",
    EventName:   "color",
}

Gradient Picker (forms)

type GradientPicker struct {
    core.BaseComponent
    CommonAttrs
    FieldValidation
    From, To, Angle string // örn. Angle "135deg"
    EventName       string
    OnChange        func(css string)
}

func (g *GradientPicker) CSS() string // "linear-gradient(<angle>, <from>, <to>)"

İki native <input type="color"> swatch'ı artı serbest metin bir açı alanı; Render(), canlı bir önizleme <div>'i ve üretilen CSS'i <code> olarak gösterir. Eylemler: from, to, angle.

grad := forms.GradientPicker{
    CommonAttrs: forms.CommonAttrs{Name: "grad", ID: "grad"},
    From: "#2563eb", To: "#db2777", Angle: "135deg", EventName: "grad",
}

Character Counter (ShowCharCount)

Ayrı bir struct değildir — mevcut Tier 1 kontrolleri üzerinde bir alan: forms.TextInput.ShowCharCount ve forms.Textarea.ShowCharCount. true olduğunda, Render(), MaxLength'i aştığında hata olarak renklendirilmiş, len(value) / MaxLength'i (rune sayılmış) gösteren bir <p class="goui-char-count"> ekler. Her iki bayrağı ayarlamak da, kendiniz DebounceMS'i ayarlamadıysanız g-debounce'u 100'e varsayılan yapar, böylece sayaç her tuş vuruşunda olayları spamlamadan duyarlı bir şekilde güncellenir.

bio := forms.Textarea{
    CommonAttrs:   forms.CommonAttrs{Name: "bio", ID: "bio"},
    Rows:          4,
    MaxLength:     120,
    ShowCharCount: true,
    HelperText:    "Up to 120 characters",
}

Password Strength (ShowStrength)

Ayrıca forms.TextInput üzerinde bir alandır: ShowStrength bool, ki bu sadece Type == "password" olduğunda render edilir. Puanlama, küçük bir sunucu tarafı sezgiseldir (forms.PasswordStrength, 0–4: uzunluk ≥8/≥12, karakter sınıfı çeşitliliği) ve şu şekilde açığa çıkarılmıştır:

type PasswordStrengthLevel int
const (
    StrengthEmpty PasswordStrengthLevel = iota
    StrengthWeak
    StrengthFair
    StrengthGood
    StrengthStrong
)
func PasswordStrength(password string) PasswordStrengthLevel

Render(), bir çevrilmiş etiketle (forms.password_strength.* i18n key'leri — bkz. 03-i18n.md) birlikte bir <div class="goui-password-strength <level>"> çubuğu ekler (genişlik = level*25%).

pw := forms.TextInput{
    CommonAttrs:  forms.CommonAttrs{Name: "pw", ID: "pw"},
    Type:         "password",
    ShowStrength: true,
}

Özet tablo

KontrolPaketStructİstemci modülüNotlar
Searchable SelectformsSearchableSelect— (selectable.js kullanır)sunucu tarafı filtre
Multi SelectformsMultiSelect— (selectable.js kullanır)Values'ın çipleri
ComboboxformsCombobox— (selectable.js kullanır)RestrictToList olmadıkça serbest metin
AutocompleteformsAutocomplete— (selectable.js kullanır)Value yalnızca seçim/commit'te ayarlanır
Tag Input / Chips InputformsTagInput / ChipsInput (takma ad)yinelenen temizleme, virgülle bölme
Tree SelectformsTreeSelectsunucu sahipli Expanded map'i
CascaderformsCascaderLoadChildren callback'i
Dual ListboxformsDualListboxiki bağımsız filtrelenmiş taraf
PhoneformsPhoneInputSearchableSelect + TextInput'i kompoze eder
Country/Language/Timezone/Currency PickerformsSearchableSelect (NewXPicker üzerinden)curated SelectItem verisi
Emoji/Icon/Font PickerformsSearchableSelect (NewXPicker üzerinden)curated SelectItem verisi
Currency InputformsCurrencyInputsunucu locale biçimlendirmesi
Percentage InputformsPercentageInputsunucu locale biçimlendirmesi
RatingformsRatingsaf g-click
Date RangeformsDateRangePickeriki native <input type=date>
Time RangeformsTimeRangePickeriki native <input type=time>
CalendarformsCalendarDatePickercalendar.jsay navigasyonu yalnızca istemci taraflıdır
OTP / PINformsOTPInput / PINInput (takma ad)otp.jssadece UI otomatik-ilerleme/yapıştırma
Rich TextformsRichTextEditorrichtext.jsQuill; ErrSkipRender + data-goui-ignore
MarkdownformsMarkdownEditorgoldmark aracılığıyla sunucu tarafında render edilir
Code EditorformsCodeEditorcodeeditor.jsCodeMirror; ErrSkipRender + data-goui-ignore
DragDrop UploadformsDragDropUploadupload.jsikili HTTP üzerinden, referans WS üzerinden
Image UploadformsDragDropUpload (NewImageUpload üzerinden)upload.jspreset: image/* + küçük resimler
Avatar UploadformsAvatarUploadavatar.jskırpma katmanını içerir
Image Cropperforms(AvatarUpload'ın bir parçası)avatar.jsistemci tarafı canvas kırpma
Color (Swatch)formsSwatchColorPickerswatch'lar + hex alanı
GradientformsGradientPickeriki renk + açı
SignatureformsSignaturePadsignature.jscanvas çizim → PNG yükleme
MentionformsMentionTextareasunucu tarafında filtrelenmiş @ önerileri
Character CounterformsTextInput.ShowCharCount / Textarea.ShowCharCountalan bayrağı, struct değil
Password StrengthformsTextInput.ShowStrengthalan bayrağı, Type: "password" gerektirir

Yukarıdaki her kontrolün tam olarak bağlanmış, çalıştırılabilir versiyonları için examples/ dizinine bakın (portlar ve eşleme 01-getting-started.md'de belgelenmiştir) — özellikle searchable-select (3002), numeric-controls (3003), field-meta (3004), date-controls (3005), identity-inputs (3006), editors (3007), media-upload (3008), ve misc-controls (3009).