mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
words
This commit is contained in:
@@ -1,14 +1,38 @@
|
|||||||
# DOM Queries
|
# DOM Queries
|
||||||
|
|
||||||
In the previous chapter we introduced the [DOM](../guide/CSS.md#the-dom), which represents the widgets in a Textual app. We saw how you can apply styles to the DOM with CSS *selectors*.
|
In the previous chapter we introduced the [DOM](../guide/CSS.md#the-dom) which is how Textual apps keep track of widgets. We saw how you can apply styles to the DOM with CSS [selectors](./CSS.md#selectors).
|
||||||
|
|
||||||
Selectors are a very useful thing and can do more that apply styles. We can also modify widgets using selectors in a simple expressive way. Let's look at how!
|
Selectors are a very useful idea and can do more that apply styles. We can also find widgets in Python code with selectors, and make updates to widgets in a simple expressive way. Let's look at how!
|
||||||
|
|
||||||
|
## Query one
|
||||||
|
|
||||||
|
The [query_one][textual.dom.DOMNode.query_one] method gets a single widget in an app or other widget. If you call it with a selector it will return the first matching widget.
|
||||||
|
|
||||||
|
Let's say we have a widget with an ID of `send` and we want to get a reference to it in our app. We could do this with the following:
|
||||||
|
|
||||||
|
```python
|
||||||
|
send_button = self.query_one("#send")
|
||||||
|
```
|
||||||
|
|
||||||
|
If there is no widget with an ID of `send`, Textual will raise a [NoMatches][textual.css.query.NoMatches] exception. Otherwise it will return the matched widget.
|
||||||
|
|
||||||
|
You can also add a second parameter for the expected type.
|
||||||
|
|
||||||
|
```python
|
||||||
|
send_button = self.query_one("#send", Button)
|
||||||
|
```
|
||||||
|
|
||||||
|
If the matched widget is *not* a button (i.e. if `isinstance(widget, Button)` equals `False`), Textual will raise a [WrongType][textual.css.query.WrongType] exception.
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
|
||||||
|
The second parameter allows type-checkers like MyPy know the exact return type. Without it, MyPy would only know the result of `query_one` is a Widget (the base class).
|
||||||
|
|
||||||
## Making queries
|
## Making queries
|
||||||
|
|
||||||
Apps and widgets have a [query][textual.dom.DOMNode.query] method which finds (or queries) widgets. Calling this method will return a [DOMQuery][textual.css.query.DOMQuery] object which is a container (list-like) object with widgets you may iterate over.
|
Apps and widgets have a [query][textual.dom.DOMNode.query] method which finds (or queries) widgets. This method returns a [DOMQuery][textual.css.query.DOMQuery] object which is a list-like container of widgets.
|
||||||
|
|
||||||
If you call `query` with no arguments, you will get back a `DOMQuery` containing all widgets. This method is *recursive*, meaning it will return all child widgets.
|
If you call `query` with no arguments, you will get back a `DOMQuery` containing all widgets. This method is *recursive*, meaning it will also return child widgets (as many levels as required).
|
||||||
|
|
||||||
Here's how you might iterate over all the widgets in your app:
|
Here's how you might iterate over all the widgets in your app:
|
||||||
|
|
||||||
@@ -17,11 +41,15 @@ for widget in self.query():
|
|||||||
print(widget)
|
print(widget)
|
||||||
```
|
```
|
||||||
|
|
||||||
Called on the `app`, this will retrieve all widgets in the app. If you call the same method on a widget, it will return children of that widget.
|
Called on the `app`, this will retrieve all widgets in the app. If you call the same method on a widget, it will return the children of that widget.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
All the query and related methods work on both App and Widget sub-classes.
|
||||||
|
|
||||||
### Query selectors
|
### Query selectors
|
||||||
|
|
||||||
You can also call `query` with a CSS selector. Let's look a few examples:
|
You can call `query` with a CSS selector. Let's look a few examples:
|
||||||
|
|
||||||
If we want to find all the button widgets, we could do something like the following:
|
If we want to find all the button widgets, we could do something like the following:
|
||||||
|
|
||||||
@@ -30,32 +58,55 @@ for button in self.query("Button"):
|
|||||||
print(button)
|
print(button)
|
||||||
```
|
```
|
||||||
|
|
||||||
Any selector that works in CSS will work. For instance, if we want to find all the disabled buttons in a Dialog widget, we could do something like the following:
|
Any selector that works in CSS will work with the `query` method. For instance, if we want to find all the disabled buttons in a Dialog widget, we could do this:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
for button in self.query("Dialog > Button.disabled"):
|
for button in self.query("Dialog Button.disabled"):
|
||||||
print(button)
|
print(button)
|
||||||
```
|
```
|
||||||
|
|
||||||
### First and Last
|
!!! info
|
||||||
|
|
||||||
The [first][textual.css.query.DOMQuery.first] and [last][textual.css.query.DOMQuery.last] methods will return the first and last widgets from the selector, respectively.
|
The selector `Dialog Button.disabled` says find all the `Button` with a CSS class of `disabled` that are a child of a `Dialog` widget.
|
||||||
|
|
||||||
Here's how we might find the last button in an app.
|
### Results
|
||||||
|
|
||||||
|
Query objects have a [results][textual.css.query.DOMQuery.results] method which is an alternative way of iterating over widgets. If you supply a type (i.e. a Widget class) then this method will generate only objects of that type.
|
||||||
|
|
||||||
|
The following example queries for widgets with the `disabled` CSS class and iterates over just the Button objects.
|
||||||
|
|
||||||
|
```python
|
||||||
|
for button in self.query(".disabled").results(Button):
|
||||||
|
print(button)
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
|
||||||
|
This method allows type-checkers like MyPy to know the exact type of the object in the loop. Without it, MyPy would only know that `button` is a `Widget` (the base class).
|
||||||
|
|
||||||
|
## Query objects
|
||||||
|
|
||||||
|
We've seen that the [query][textual.dom.DOMNode.query] method returns a [DOMQuery][textual.css.query.DOMQuery] object you can iterate over in a for loop. Query objects behave like Python lists and support all of the same operations (such as `query[0]`, `len(query)` ,`reverse(query)` etc). They also have a number of other methods to simplify retrieving and modifying widgets.
|
||||||
|
|
||||||
|
## First and last
|
||||||
|
|
||||||
|
The [first][textual.css.query.DOMQuery.first] and [last][textual.css.query.DOMQuery.last] methods return the first or last matching widget from the selector, respectively.
|
||||||
|
|
||||||
|
Here's how we might find the _last_ button in an app:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
last_button = self.query("Button").last()
|
last_button = self.query("Button").last()
|
||||||
```
|
```
|
||||||
|
|
||||||
If there are no buttons, textual will raise a [NoMatchingNodesError][textual.css.query.NoMatchingNodesError] exception. Otherwise it will return a button widgets.
|
If there are no buttons, Textual will raise a [NoMatches][textual.css.query.NoMatches] exception. Otherwise it will return a button widget.
|
||||||
|
|
||||||
Both `first()` and `last()` accept an `expect_type` argument that should be the class of the widget you are expecting. For instance, lets say we want to get the last with class `.disabled`, and we want to check it really is a button. We could do this:
|
Both `first()` and `last()` accept an `expect_type` argument that should be the class of the widget you are expecting. Let's say we want to get the last widget with class `.disabled`, and we want to check it really is a button. We could do this:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
disabled_button = self.query(".disables").last(Button)
|
disabled_button = self.query(".disabled").last(Button)
|
||||||
```
|
```
|
||||||
|
|
||||||
The query selects all widgets with a `disabled` CSS class. The `last` method ensures that it is a `Button` and not any other kind of widget.
|
The query selects all widgets with a `disabled` CSS class. The `last` method gets the last disabled widget and checks it is a `Button` and not any other kind of widget.
|
||||||
|
|
||||||
If the last widget is *not* a button, Textual will raise a [WrongType][textual.css.query.WrongType] exception.
|
If the last widget is *not* a button, Textual will raise a [WrongType][textual.css.query.WrongType] exception.
|
||||||
|
|
||||||
@@ -63,5 +114,57 @@ If the last widget is *not* a button, Textual will raise a [WrongType][textual.c
|
|||||||
|
|
||||||
Specifying the expected type allows type-checkers like MyPy to know the exact return type.
|
Specifying the expected type allows type-checkers like MyPy to know the exact return type.
|
||||||
|
|
||||||
### Filtering
|
## Filter
|
||||||
|
|
||||||
|
Query objects have a [filter][textual.css.query.DOMQuery.filter] method which further refines a query. This method will return a new query object which widgets that match both the original query _and_ the new selector
|
||||||
|
|
||||||
|
Let's say we have a query which gets all the buttons in an app, and we want a new query object with just the disabled buttons. We could write something like this:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Get all the Buttons
|
||||||
|
buttons_query = self.query("Button")
|
||||||
|
# Buttons with 'disabled' CSS class
|
||||||
|
disabled_buttons = buttons_query.filter(".disabled")
|
||||||
|
```
|
||||||
|
|
||||||
|
Iterating over `disabled_buttons` will give us all the disabled buttons.
|
||||||
|
|
||||||
|
## Exclude
|
||||||
|
|
||||||
|
Query objects have a [exclude][textual.css.query.DOMQuery.exclude] method which is the logical opposite of [filter][textual.css.query.DOMQuery.filter]. The `exclude` method removes any widgets from the query object which match a selector.
|
||||||
|
|
||||||
|
Here's how we could get all the buttons which *don't* have the `disabled` class set.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Get all the Buttons
|
||||||
|
buttons_query = self.query("Button")
|
||||||
|
# Remove all the Buttons with the 'disabled' CSS class
|
||||||
|
enabled_buttons = buttons_query.exclude(".disabled")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Loop-free operations
|
||||||
|
|
||||||
|
Once you have a query object, you can loop over it to call methods on the matched widgets. Query objects also support a number of methods which make an update to every matched widget without an explicit loop.
|
||||||
|
|
||||||
|
For instance, let's say we want to disable all buttons in an app. We could do this by calling [add_class()][textual.css.query.DOMQuery.add_class] on a query object.
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.query("Button").add_class("disabled")
|
||||||
|
```
|
||||||
|
|
||||||
|
This single line is equivalent to the following:
|
||||||
|
|
||||||
|
```python
|
||||||
|
for widget in self.query("Button"):
|
||||||
|
widget.add_class("disabled")
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
- [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.
|
||||||
|
|
||||||
|
|||||||
@@ -199,7 +199,10 @@ While it's possible to set all styles for an app this way, it is rarely necessar
|
|||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
|
|
||||||
The dialect of CSS used in Textual is greatly simplified over web based CSS and much easier to learn!
|
The dialect of CSS used in Textual is greatly simplified over web based CSS and much easier to learn.
|
||||||
|
|
||||||
|
|
||||||
|
CSS makes it easy to iterate on the design of your app and enables [live-editing](./guide/devtools.md#live-editing) — you can edit CSS and see the changes without restarting the app!
|
||||||
|
|
||||||
|
|
||||||
Let's add a CSS file to our application.
|
Let's add a CSS file to our application.
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from ._context import active_app
|
|||||||
from ._event_broker import NoHandler, extract_handler_actions
|
from ._event_broker import NoHandler, extract_handler_actions
|
||||||
from ._filter import LineFilter, Monochrome
|
from ._filter import LineFilter, Monochrome
|
||||||
from .binding import Bindings, NoBinding
|
from .binding import Bindings, NoBinding
|
||||||
from .css.query import NoMatchingNodesError
|
from .css.query import NoMatches
|
||||||
from .css.stylesheet import Stylesheet
|
from .css.stylesheet import Stylesheet
|
||||||
from .design import ColorSystem
|
from .design import ColorSystem
|
||||||
from .devtools.client import DevtoolsClient, DevtoolsConnectionError, DevtoolsLog
|
from .devtools.client import DevtoolsClient, DevtoolsConnectionError, DevtoolsLog
|
||||||
@@ -774,7 +774,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
DOMNode: The first child of this node with the specified ID.
|
DOMNode: The first child of this node with the specified ID.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
NoMatchingNodesError: if no children could be found for this ID
|
NoMatches: if no children could be found for this ID
|
||||||
"""
|
"""
|
||||||
return self.screen.get_child(id)
|
return self.screen.get_child(id)
|
||||||
|
|
||||||
@@ -1553,7 +1553,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
node = self.query(f"#{widget_id}").first()
|
node = self.query(f"#{widget_id}").first()
|
||||||
except NoMatchingNodesError:
|
except NoMatches:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
if isinstance(node, Widget):
|
if isinstance(node, Widget):
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class QueryError(Exception):
|
|||||||
"""Base class for a query related error."""
|
"""Base class for a query related error."""
|
||||||
|
|
||||||
|
|
||||||
class NoMatchingNodesError(QueryError):
|
class NoMatches(QueryError):
|
||||||
"""No nodes matched the query."""
|
"""No nodes matched the query."""
|
||||||
|
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ class DOMQuery(Generic[QueryType]):
|
|||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
WrongType: If the wrong type was found.
|
WrongType: If the wrong type was found.
|
||||||
NoMatchingNodesError: If there are no matching nodes in the query.
|
NoMatches: If there are no matching nodes in the query.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Widget | ExpectType: The matching Widget.
|
Widget | ExpectType: The matching Widget.
|
||||||
@@ -194,7 +194,7 @@ class DOMQuery(Generic[QueryType]):
|
|||||||
)
|
)
|
||||||
return first
|
return first
|
||||||
else:
|
else:
|
||||||
raise NoMatchingNodesError(f"No nodes match {self!r}")
|
raise NoMatches(f"No nodes match {self!r}")
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def last(self) -> Widget:
|
def last(self) -> Widget:
|
||||||
@@ -215,7 +215,7 @@ class DOMQuery(Generic[QueryType]):
|
|||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
WrongType: If the wrong type was found.
|
WrongType: If the wrong type was found.
|
||||||
NoMatchingNodesError: If there are no matching nodes in the query.
|
NoMatches: If there are no matching nodes in the query.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Widget | ExpectType: The matching Widget.
|
Widget | ExpectType: The matching Widget.
|
||||||
@@ -229,7 +229,7 @@ class DOMQuery(Generic[QueryType]):
|
|||||||
)
|
)
|
||||||
return last
|
return last
|
||||||
else:
|
else:
|
||||||
raise NoMatchingNodesError(f"No nodes match {self!r}")
|
raise NoMatches(f"No nodes match {self!r}")
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def results(self) -> Iterator[Widget]:
|
def results(self) -> Iterator[Widget]:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ from .css.errors import StyleValueError, DeclarationError
|
|||||||
from .css.parse import parse_declarations
|
from .css.parse import parse_declarations
|
||||||
from .css.styles import Styles, RenderStyles
|
from .css.styles import Styles, RenderStyles
|
||||||
from .css.tokenize import IDENTIFIER
|
from .css.tokenize import IDENTIFIER
|
||||||
from .css.query import NoMatchingNodesError
|
from .css.query import NoMatches
|
||||||
from .message_pump import MessagePump
|
from .message_pump import MessagePump
|
||||||
from .timer import Timer
|
from .timer import Timer
|
||||||
|
|
||||||
@@ -405,6 +405,11 @@ class DOMNode(MessagePump):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def visible(self) -> bool:
|
def visible(self) -> bool:
|
||||||
|
"""Check if the node is visible or None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the node is visible.
|
||||||
|
"""
|
||||||
return self.styles.visibility != "hidden"
|
return self.styles.visibility != "hidden"
|
||||||
|
|
||||||
@visible.setter
|
@visible.setter
|
||||||
@@ -669,12 +674,12 @@ class DOMNode(MessagePump):
|
|||||||
DOMNode: The first child of this node with the ID.
|
DOMNode: The first child of this node with the ID.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
NoMatchingNodesError: if no children could be found for this ID
|
NoMatches: if no children could be found for this ID
|
||||||
"""
|
"""
|
||||||
for child in self.children:
|
for child in self.children:
|
||||||
if child.id == id:
|
if child.id == id:
|
||||||
return child
|
return child
|
||||||
raise NoMatchingNodesError(f"No child found with id={id!r}")
|
raise NoMatches(f"No child found with id={id!r}")
|
||||||
|
|
||||||
ExpectType = TypeVar("ExpectType", bound="Widget")
|
ExpectType = TypeVar("ExpectType", bound="Widget")
|
||||||
|
|
||||||
@@ -725,8 +730,8 @@ class DOMNode(MessagePump):
|
|||||||
"""Get the first Widget matching the given selector or selector type.
|
"""Get the first Widget matching the given selector or selector type.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
selector (str | type, optional): A selector.
|
selector (str | type): A selector.
|
||||||
expect_type (type, optional): Require the object be of the supplied type, or None for any type.
|
expect_type (type | None, optional): Require the object be of the supplied type, or None for any type.
|
||||||
Defaults to None.
|
Defaults to None.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from textual.css.errors import StyleValueError
|
from textual.css.errors import StyleValueError
|
||||||
from textual.css.query import NoMatchingNodesError
|
from textual.css.query import NoMatches
|
||||||
from textual.dom import DOMNode, BadIdentifier
|
from textual.dom import DOMNode, BadIdentifier
|
||||||
|
|
||||||
|
|
||||||
@@ -48,12 +48,12 @@ def test_get_child_gets_first_child(parent):
|
|||||||
|
|
||||||
|
|
||||||
def test_get_child_no_matching_child(parent):
|
def test_get_child_no_matching_child(parent):
|
||||||
with pytest.raises(NoMatchingNodesError):
|
with pytest.raises(NoMatches):
|
||||||
parent.get_child(id="doesnt-exist")
|
parent.get_child(id="doesnt-exist")
|
||||||
|
|
||||||
|
|
||||||
def test_get_child_only_immediate_descendents(parent):
|
def test_get_child_only_immediate_descendents(parent):
|
||||||
with pytest.raises(NoMatchingNodesError):
|
with pytest.raises(NoMatches):
|
||||||
parent.get_child(id="grandchild1")
|
parent.get_child(id="grandchild1")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user