astwerk

Filtering a list

A two-way bound search box drives a Computed slice, and BindList renders whatever it contains. The list never knows a filter exists — it just renders the slice it's given.

5 of 5 posts

    How it works

    State

    go
    posts := []post{
    		{ID: "1", Title: "Signals track their own dependencies"},
    		{ID: "2", Title: "Keyed lists move nodes instead of rebuilding them"},
    		{ID: "3", Title: "A binding owns exactly the property it writes"},
    		{ID: "4", Title: "Computed values chain and gate"},
    		{ID: "5", Title: "The escape hatch is plain Go"},
    	}
    	query := reactive.NewSignal("")
    	reactive.BindValue(root.Find("[data-query]"), query)

    The posts are a plain slice — no signals, nothing reactive about them. The only state is the search box: BindValue is two-way, so typing writes query and setting the signal would write the field.

    The filter

    go
    matches := reactive.Computed(func() []post {
    		q := strings.ToLower(strings.TrimSpace(query.Get()))
    		if q == "" {
    			return posts
    		}
    		var out []post
    		for _, p := range posts {
    			if strings.Contains(strings.ToLower(p.Title), q) {
    				out = append(out, p)
    			}
    		}
    		return out
    	})

    Computed derives the matching slice from query — it re-runs on every keystroke because it reads query.Get(). An empty query returns everything, so the filter is off by default.

    The list and count

    go
    reactive.BindText(root.Find("[data-count]"), func() string {
    		return strconv.Itoa(len(matches.Get()))
    	})
    	reactive.BindList(root.Find("[data-results]"), matches.Get,
    		func(p post) string { return p.ID },
    		func(p post) wasmwrap.Element {
    			return wasmwrap.Create("li").SetText(p.Title)
    		})

    BindList renders the filtered slice with the same keyed patching as the keyed list; the count is a text binding over the same slice.

    Full source
    go
    posts := []post{
    		{ID: "1", Title: "Signals track their own dependencies"},
    		{ID: "2", Title: "Keyed lists move nodes instead of rebuilding them"},
    		{ID: "3", Title: "A binding owns exactly the property it writes"},
    		{ID: "4", Title: "Computed values chain and gate"},
    		{ID: "5", Title: "The escape hatch is plain Go"},
    	}
    	query := reactive.NewSignal("")
    	reactive.BindValue(root.Find("[data-query]"), query)
    
    	matches := reactive.Computed(func() []post {
    		q := strings.ToLower(strings.TrimSpace(query.Get()))
    		if q == "" {
    			return posts
    		}
    		var out []post
    		for _, p := range posts {
    			if strings.Contains(strings.ToLower(p.Title), q) {
    				out = append(out, p)
    			}
    		}
    		return out
    	})
    
    	reactive.BindText(root.Find("[data-count]"), func() string {
    		return strconv.Itoa(len(matches.Get()))
    	})
    	reactive.BindList(root.Find("[data-results]"), matches.Get,
    		func(p post) string { return p.ID },
    		func(p post) wasmwrap.Element {
    			return wasmwrap.Create("li").SetText(p.Title)
    		})
    How this demo runs

    scripts/demo.go is compiled to demo.wasm by CompileFrom and loaded by WasmScript — see Scripts & WebAssembly. Without WebAssembly the page still renders; only the results stay put.