Skip to content

0.4.0 — Signal binding on fields

Choose a tag to compare

@github-actions github-actions released this 29 Aug 22:20
· 24 commits to main since this release

Added

  • A Signal can now hold an empty value, so clearing a field is no longer silent. A signal takes its type from the value you create it with and had no way to say "nothing", so clearing a bound field left the signal on its last value and nothing watching it heard. Pass allow_empty=True when the value can also be empty — bs.Signal(date.today(), allow_empty=True) — and clear() empties it, in both directions between field and signal. A signal can also start empty, in which case name its type: bs.Signal(None, allow_empty=True, dtype=date). Empty reads as None, except where the signal is the widget's own variable, as for a text field or radio group, where it reads as '', and a signal holding a set, as a multi-select toggle group does, which empties to the empty set — a falsiness check covers all three. Signals you already have are unchanged: without allow_empty=True, clear() and set(None) raise. Binding one that allows empty to a checkbox, switch, toggle button, slider or progress bar raises, since none of them has an empty state to hold. (#390)
  • Select gains the validation and addon surface the other fields already had. select.valid and select.error, both bindable Signals; the on_valid, on_invalid and on_validate events; and insert_addon, update_addon, remove_addon and addons. select.signal also reads back the Signal the widget was built with, as NumberField, DateField and TimeField already allowed. (#465, #458)

Changed

  • A keyword a widget does not recognize is now reported instead of ignored. bs.TextField(bogus=1) used to construct as if nothing had happened, and so did a typo of a real parameter: bs.DataTable(densty="compact") silently used default spacing. Every widget you place in a layout now raises TypeError naming itself and the keyword — TextField() got unexpected keyword argument(s): bogus. The top-level windows (App, AppShell, Workbench, Window) report it through the toolkit they are built on, so their message names that rather than the window. Check this when you upgrade: an application that starts today can fail to start after the upgrade — but only where a keyword was already doing nothing, so you get a message pointing at the typo instead of a setting that never applied. Placement keywords (grow=, horizontal=, row=, column= and the rest) are unaffected, and Chart, MenuButton, Picture, StatusBar and Toolbar still pass extra options through to what they wrap. (#472)
  • A SelectButton bound to a Signal now carries the option's value, not the label shown for it. Wherever a label differed from its value, seeding the signal with a value displayed that raw value and left selection empty for good, while selecting Three put the label 'Three' into the signal where value reported '3'. The signal now carries the value in both directions, and button.signal returns the Signal you passed rather than a separate one tracking it. Check this when you upgrade if you seed a SelectButton signal with anything other than one of its option values — a label, or any other string, now raises; seed it with the option's value instead. This reaches buttons built from plain strings, where label and value are the same text. A button with no signal=, or one whose seed already names an option, is unaffected. Clearing the button in code leaves an ordinary signal untouched; declare it allow_empty=True and the clear reaches it. (#461)

Fixed

  • A validation field no longer stops validating when a custom rule cannot judge a value. A custom rule runs a function you supply, and if it raised — comparing a number against text, or meeting an empty field it did not handle — the exception escaped into the event loop and the field silently kept whatever validity it already had. It now reports the value invalid, and the first raise per rule writes one line to stderr naming the exception; set BOOTSTACK_DEBUG for the traceback. The field shows "Could not check this value (expected: must be over 5)", carrying the message you gave the rule as an expectation rather than as a verdict: a function that raised judged nothing, so your message alone can be false of the value — "must be over 5" on a field holding 6. It is shown alone, as the verdict, only when the function returns False. This also covers validate(), your own call and the one a FormDialog submit makes for you, which returns False rather than raising. Check this when you upgrade if a custom rule is attached to an optional field and your function does not handle an empty value: that field now reports invalid where it previously appeared valid. (#467)
  • A Signal bound to a TextArea or CodeEditor now works in both directions, and .signal gives you the signal back. Both widgets take a textsignal= documented as a two-way binding, but only one direction was wired: the widget followed the signal, while typing left the signal on its old value forever. .signal returned None in every case, even when a signal was bound. Edits now travel back as you make them, and .signal returns what you bound (still None when nothing is). Writing to a bound signal while a TextArea's placeholder was showing also left the widget stuck — the text appeared, but value returned '' and on_input/on_change stopped firing for everything typed afterwards. Both behave normally now. Check this when you upgrade if you subscribe to a signal bound to one of these two widgets: that subscriber starts firing on user edits, where before it only heard writes your own code made. Binding a signal holding something other than text — bs.TextArea(textsignal=bs.Signal(123)) — now raises TypeError, the same refusal single-line fields have always given. (#486)
  • A Select bound to a Signal now tracks the selection, not the text on screen. Seeding the signal with an option's value displayed that raw value instead of the option's label, and setting it later moved the displayed text without moving the selection — so the field showed one option while value and selection reported the previous one, no change event fired, and it did not correct itself. That second half applied to plain string options too. The signal now carries the option's value in both directions, matching value= and the signal= on NumberField, DateField and TimeField. Two things to check when you upgrade: what a Select writes into its signal changes from the option's label to its value, and textsignal= on a Select now raises instead of being silently discarded. Clearing the field leaves an ordinary signal untouched; declare it allow_empty=True and the clear reaches it. SelectButton had the same defect and is fixed with it. (#458)
  • A validation rule on a Select now has somewhere to report. Select accepted add_validation_rule() and ran the rule, but exposed no valid or error to read the outcome from and emitted no valid/invalid events, so a failing rule could reach neither your code nor the screen. It now carries the field family's full validation surface (see Added), so bs.Label(textsignal=select.error) works and on_valid/on_invalid fire as they do on TextField. The rules you attach today run exactly as before — this only gives their outcome somewhere to go. Check this when you upgrade: Select now shares the family's add_validation_rule(), so passing anything other than a rule-type string raises TypeError instead of being accepted and silently ignored. (#465)
  • A modal bs.Window now hands the grab back to whatever was modal underneath it. Opening a modal window from inside another modal — an "Advanced…" button on a dialog, say — left the dialog underneath on screen and still blocking the code that opened it, but no longer modal: the user could click straight past it into the main window. A closing modal window now returns the grab to its previous holder, and as the same kind, so an application-modal window underneath stays application-modal rather than quietly narrowing. This was the same defect fixed for dialogs in 0.3.1, on the one path that fix did not cover; a window with no modal opener, and any non-modal window, are unaffected. (#444)
  • A SelectButton no longer reports the same selection twice. Every change fired on_change two times, so a handler that saves a record, sends a request or increments a counter did all of it twice for one selection. This applied to every SelectButton, whichever way the options were written, and both to a selection made from the menu and to one set in code. A selection is now announced exactly once; the event, its payload and its timing are unchanged. (#476)
  • A TimeField bound to a Signal no longer reports a change on startup. Seeding the field from its signal announced a change event while the field was being built, for a time nobody had picked — so an application reacting to a time change ran that reaction once at startup. A handler registered on the line after the constructor still received it, because the event waited in the queue until the application began running. TimeField(signal=…) is now silent, matching TimeField(value=…) and the already-silent NumberField and DateField. Every change after construction is announced exactly as before. (#459)
  • DataTable's context_menus option now works. It was documented and shown in the widget guide but had no effect: every table offered both right-click menus whatever you asked for, so context_menus="none" still opened the column-header menu and the row menu. The option is now honored as documented — 'all' (the default), 'headers' or 'rows' for one menu only, 'none' for neither — and a misspelled value is reported rather than ignored. on_row_right_click fires no matter which menus you turn off: the option chooses which menus the table offers, not whether a right-click reaches your code. (#456)
  • .signal no longer looks like it might hand back nothing on seven widgets. TextField, PasswordField, PathField, SpinnerField, Checkbox, Switch and ToggleButton documented .signal as returning a Signal or None, and none of them can return None — they create a signal on first read whether or not you bound one, so a type checker made you guard a case that cannot happen. The seven now say what they do. Widgets that really can return None are unchanged and still say so: TextArea, CodeEditor, NumberField, DateField, TimeField, Select and SelectButton. Behavior is identical either way. (#460)

What's Changed

Full Changelog: v0.3.2...v0.4.0