Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data binding and more #4075

Merged
merged 38 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a6514f8
added cancelled event to worker
willmcgugan Jan 29, 2024
5a4cc51
data binding
willmcgugan Jan 30, 2024
c4fb356
Merge branch 'main' into cancelled-event
willmcgugan Jan 30, 2024
da9dae7
refresh line
willmcgugan Jan 31, 2024
7e27a33
reactive fix
willmcgugan Jan 31, 2024
8bab37f
tests
willmcgugan Jan 31, 2024
e2facd3
test
willmcgugan Jan 31, 2024
24ef676
more tests
willmcgugan Jan 31, 2024
6c2e867
error test
willmcgugan Jan 31, 2024
6838e87
fux test
willmcgugan Jan 31, 2024
c66f1d4
Tidy
willmcgugan Jan 31, 2024
04e5ce2
superfluous var
willmcgugan Jan 31, 2024
260bab0
superfluous vars
willmcgugan Jan 31, 2024
72e54d9
test fix
willmcgugan Jan 31, 2024
ef790af
merge
willmcgugan Feb 1, 2024
2ee8631
api change
willmcgugan Feb 1, 2024
db05bf5
api change
willmcgugan Feb 1, 2024
e90c76e
Added query.set
willmcgugan Feb 1, 2024
322120a
Merge branch 'main' into cancelled-event
willmcgugan Feb 1, 2024
1ed6139
simplify bind API
willmcgugan Feb 1, 2024
cf04b3c
World clock examples
willmcgugan Feb 1, 2024
aeeb93d
reactive init
willmcgugan Feb 2, 2024
3e59e2c
setter
willmcgugan Feb 2, 2024
9ee308b
merge changelog
willmcgugan Feb 2, 2024
0755e89
revert init change
willmcgugan Feb 2, 2024
6b82aef
Added set_reactive
willmcgugan Feb 2, 2024
e91699f
Merge branch 'main' into cancelled-event
willmcgugan Feb 2, 2024
fa8c0e8
fix refresh lines
willmcgugan Feb 5, 2024
e620ff8
simplify
willmcgugan Feb 5, 2024
2dc534e
fix tint
willmcgugan Feb 6, 2024
f20515d
reactive docs
willmcgugan Feb 6, 2024
11ba94e
changelog
willmcgugan Feb 6, 2024
cea5a49
changelog
willmcgugan Feb 6, 2024
34e7721
examples
willmcgugan Feb 6, 2024
a817ecb
docs
willmcgugan Feb 6, 2024
52ca178
no need for this import
willmcgugan Feb 6, 2024
06394ed
test
willmcgugan Feb 6, 2024
f39a7c9
Update docs/guide/reactivity.md
willmcgugan Feb 6, 2024
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fixed a hang in the Linux driver when connected to a pipe https://github.com/Textualize/textual/issues/4104
- Fixed broken `OptionList` `Option.id` mappings https://github.com/Textualize/textual/issues/4101

### Added

- Added DOMQuery.set https://github.com/Textualize/textual/pull/4075
- Added DOMNode.set_reactive https://github.com/Textualize/textual/pull/4075
- Added DOMNode.data_bind https://github.com/Textualize/textual/pull/4075
- Added DOMNode.action_toggle https://github.com/Textualize/textual/pull/4075
- Added Worker.cancelled_event https://github.com/Textualize/textual/pull/4075

### Changed

- Breaking change: keyboard navigation in `RadioSet`, `ListView`, `OptionList`, and `SelectionList`, no longer allows highlighting disabled items https://github.com/Textualize/textual/issues/3881
Expand Down
67 changes: 67 additions & 0 deletions docs/examples/guide/reactivity/set_reactive01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from textual.app import App, ComposeResult
from textual.containers import Horizontal
from textual.reactive import reactive, var
from textual.widgets import Label

GREETINGS = [
"Bonjour",
"Hola",
"こんにちは",
"你好",
"안녕하세요",
"Hello",
]


class Greeter(Horizontal):
"""Display a greeting and a name."""

DEFAULT_CSS = """
Greeter {
width: auto;
height: 1;
& Label {
margin: 0 1;
}
}
"""
greeting: reactive[str] = reactive("")
who: reactive[str] = reactive("")

def __init__(self, greeting: str = "Hello", who: str = "World!") -> None:
super().__init__()
self.greeting = greeting # (1)!
self.who = who

def compose(self) -> ComposeResult:
yield Label(self.greeting, id="greeting")
yield Label(self.who, id="name")

def watch_greeting(self, greeting: str) -> None:
self.query_one("#greeting", Label).update(greeting) # (2)!

def watch_who(self, who: str) -> None:
self.query_one("#who", Label).update(who)


class NameApp(App):

CSS = """
Screen {
align: center middle;
}
"""
greeting_no: var[int] = var(0)
BINDINGS = [("space", "greeting")]

def compose(self) -> ComposeResult:
yield Greeter(who="Textual")

def action_greeting(self) -> None:
self.greeting_no = (self.greeting_no + 1) % len(GREETINGS)
self.query_one(Greeter).greeting = GREETINGS[self.greeting_no]


if __name__ == "__main__":
app = NameApp()
app.run()
67 changes: 67 additions & 0 deletions docs/examples/guide/reactivity/set_reactive02.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from textual.app import App, ComposeResult
from textual.containers import Horizontal
from textual.reactive import reactive, var
from textual.widgets import Label

GREETINGS = [
"Bonjour",
"Hola",
"こんにちは",
"你好",
"안녕하세요",
"Hello",
]


class Greeter(Horizontal):
"""Display a greeting and a name."""

DEFAULT_CSS = """
Greeter {
width: auto;
height: 1;
& Label {
margin: 0 1;
}
}
"""
greeting: reactive[str] = reactive("")
who: reactive[str] = reactive("")

def __init__(self, greeting: str = "Hello", who: str = "World!") -> None:
super().__init__()
self.set_reactive(Greeter.greeting, greeting) # (1)!
self.set_reactive(Greeter.who, who)

def compose(self) -> ComposeResult:
yield Label(self.greeting, id="greeting")
yield Label(self.who, id="name")

def watch_greeting(self, greeting: str) -> None:
self.query_one("#greeting", Label).update(greeting)

def watch_who(self, who: str) -> None:
self.query_one("#who", Label).update(who)


class NameApp(App):

CSS = """
Screen {
align: center middle;
}
"""
greeting_no: var[int] = var(0)
BINDINGS = [("space", "greeting")]

def compose(self) -> ComposeResult:
yield Greeter(who="Textual")

def action_greeting(self) -> None:
self.greeting_no = (self.greeting_no + 1) % len(GREETINGS)
self.query_one(Greeter).greeting = GREETINGS[self.greeting_no]


if __name__ == "__main__":
app = NameApp()
app.run()
52 changes: 52 additions & 0 deletions docs/examples/guide/reactivity/world_clock01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from datetime import datetime

from pytz import timezone

from textual.app import App, ComposeResult
from textual.reactive import reactive
from textual.widget import Widget
from textual.widgets import Digits, Label


class WorldClock(Widget):

time: reactive[datetime] = reactive(datetime.now)

def __init__(self, timezone: str) -> None:
self.timezone = timezone
super().__init__()

def compose(self) -> ComposeResult:
yield Label(self.timezone)
yield Digits()

def watch_time(self, time: datetime) -> None:
localized_time = time.astimezone(timezone(self.timezone))
self.query_one(Digits).update(localized_time.strftime("%H:%M:%S"))


class WorldClockApp(App):
CSS_PATH = "world_clock01.tcss"

time: reactive[datetime] = reactive(datetime.now)

def compose(self) -> ComposeResult:
yield WorldClock("Europe/London")
yield WorldClock("Europe/Paris")
yield WorldClock("Asia/Tokyo")

def update_time(self) -> None:
self.time = datetime.now()

def watch_time(self, time: datetime) -> None:
for world_clock in self.query(WorldClock): # (1)!
world_clock.time = time

def on_mount(self) -> None:
self.update_time()
self.set_interval(1, self.update_time)


if __name__ == "__main__":
app = WorldClockApp()
app.run()
16 changes: 16 additions & 0 deletions docs/examples/guide/reactivity/world_clock01.tcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Screen {
align: center middle;
}

WorldClock {
width: auto;
height: auto;
padding: 1 2;
background: $panel;
border: wide $background;

& Digits {
width: auto;
color: $secondary;
}
}
47 changes: 47 additions & 0 deletions docs/examples/guide/reactivity/world_clock02.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from datetime import datetime

from pytz import timezone

from textual.app import App, ComposeResult
from textual.reactive import reactive
from textual.widget import Widget
from textual.widgets import Digits, Label


class WorldClock(Widget):

time: reactive[datetime] = reactive(datetime.now)

def __init__(self, timezone: str) -> None:
self.timezone = timezone
super().__init__()

def compose(self) -> ComposeResult:
yield Label(self.timezone)
yield Digits()

def watch_time(self, time: datetime) -> None:
localized_time = time.astimezone(timezone(self.timezone))
self.query_one(Digits).update(localized_time.strftime("%H:%M:%S"))


class WorldClockApp(App):
CSS_PATH = "world_clock01.tcss"

time: reactive[datetime] = reactive(datetime.now)

def compose(self) -> ComposeResult:
yield WorldClock("Europe/London").data_bind(WorldClockApp.time) # (1)!
yield WorldClock("Europe/Paris").data_bind(WorldClockApp.time)
yield WorldClock("Asia/Tokyo").data_bind(WorldClockApp.time)

def update_time(self) -> None:
self.time = datetime.now()

def on_mount(self) -> None:
self.update_time()
self.set_interval(1, self.update_time)


if __name__ == "__main__":
WorldClockApp().run()
49 changes: 49 additions & 0 deletions docs/examples/guide/reactivity/world_clock03.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from datetime import datetime

from pytz import timezone

from textual.app import App, ComposeResult
from textual.reactive import reactive
from textual.widget import Widget
from textual.widgets import Digits, Label


class WorldClock(Widget):

clock_time: reactive[datetime] = reactive(datetime.now)

def __init__(self, timezone: str) -> None:
self.timezone = timezone
super().__init__()

def compose(self) -> ComposeResult:
yield Label(self.timezone)
yield Digits()

def watch_clock_time(self, time: datetime) -> None:
localized_time = time.astimezone(timezone(self.timezone))
self.query_one(Digits).update(localized_time.strftime("%H:%M:%S"))


class WorldClockApp(App):
CSS_PATH = "world_clock01.tcss"

time: reactive[datetime] = reactive(datetime.now)

def compose(self) -> ComposeResult:
yield WorldClock("Europe/London").data_bind(
clock_time=WorldClockApp.time # (1)!
)
yield WorldClock("Europe/Paris").data_bind(clock_time=WorldClockApp.time)
yield WorldClock("Asia/Tokyo").data_bind(clock_time=WorldClockApp.time)

def update_time(self) -> None:
self.time = datetime.now()

def on_mount(self) -> None:
self.update_time()
self.set_interval(1, self.update_time)


if __name__ == "__main__":
WorldClockApp().run()
10 changes: 6 additions & 4 deletions docs/guide/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,12 @@ for widget in self.query("Button"):

Here are the other loop-free methods on query objects:

- [set_class][textual.css.query.DOMQuery.set_class] Sets a CSS class (or classes) on matched widgets.
- [add_class][textual.css.query.DOMQuery.add_class] Adds a CSS class (or classes) to matched widgets.
- [blur][textual.css.query.DOMQuery.focus] Blurs (removes focus) from matching widgets.
- [focus][textual.css.query.DOMQuery.focus] Focuses the first matching widgets.
- [refresh][textual.css.query.DOMQuery.refresh] Refreshes matched widgets.
- [remove_class][textual.css.query.DOMQuery.remove_class] Removes a CSS class (or classes) from matched widgets.
- [toggle_class][textual.css.query.DOMQuery.toggle_class] Sets a CSS class (or classes) if it is not set, or removes the class (or classes) if they are set on the matched widgets.
- [remove][textual.css.query.DOMQuery.remove] Removes matched widgets from the DOM.
- [refresh][textual.css.query.DOMQuery.refresh] Refreshes matched widgets.

- [set_class][textual.css.query.DOMQuery.set_class] Sets a CSS class (or classes) on matched widgets.
- [set][textual.css.query.DOMQuery.set] Sets common attributes on a widget.
- [toggle_class][textual.css.query.DOMQuery.toggle_class] Sets a CSS class (or classes) if it is not set, or removes the class (or classes) if they are set on the matched widgets.
Loading
Loading