mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
Pydom add better support for select element (#1887)
* add tests for select options * add classes to support select and options management * fix add methond and implement clear on options * fix optionsproxy.add * fix select.add method * add test adding a second option to select * add tests around adding options in multiple flavors * add test to add an option by passing the option it wants to be added before * complete test around adding options * add select to add test on remove * add tests and support for selected item * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
This commit is contained in:
@@ -14,6 +14,7 @@ class BaseElement:
|
||||
self._js = js_element
|
||||
self._parent = None
|
||||
self.style = StyleProxy(self)
|
||||
self._proxies = {}
|
||||
|
||||
def __eq__(self, obj):
|
||||
"""Check if the element is the same as the other element by comparing
|
||||
@@ -129,6 +130,18 @@ class Element(BaseElement):
|
||||
def id(self, value):
|
||||
self._js.id = value
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
if "options" in self._proxies:
|
||||
return self._proxies["options"]
|
||||
|
||||
if not self._js.tagName.lower() in {"select", "datalist", "optgroup"}:
|
||||
raise AttributeError(
|
||||
f"Element {self._js.tagName} has no options attribute."
|
||||
)
|
||||
self._proxies["options"] = OptionsProxy(self)
|
||||
return self._proxies["options"]
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._js.value
|
||||
@@ -145,6 +158,22 @@ class Element(BaseElement):
|
||||
)
|
||||
self._js.value = value
|
||||
|
||||
@property
|
||||
def selected(self):
|
||||
return self._js.selected
|
||||
|
||||
@selected.setter
|
||||
def selected(self, value):
|
||||
# in order to avoid confusion to the user, we don't allow setting the
|
||||
# value of elements that don't have a value attribute
|
||||
if not hasattr(self._js, "selected"):
|
||||
raise AttributeError(
|
||||
f"Element {self._js.tagName} has no value attribute. If you want to "
|
||||
"force a value attribute, set it directly using the `_js.value = <value>` "
|
||||
"javascript API attribute instead."
|
||||
)
|
||||
self._js.selected = value
|
||||
|
||||
def clone(self, new_id=None):
|
||||
clone = Element(self._js.cloneNode(True))
|
||||
clone.id = new_id
|
||||
@@ -176,6 +205,77 @@ class Element(BaseElement):
|
||||
self._js.scrollIntoView()
|
||||
|
||||
|
||||
class OptionsProxy:
|
||||
"""This class represents the options of a select element. It
|
||||
allows to access to add and remove options by using the `add` and `remove` methods.
|
||||
"""
|
||||
|
||||
def __init__(self, element: Element) -> None:
|
||||
self._element = element
|
||||
if self._element._js.tagName.lower() != "select":
|
||||
raise AttributeError(
|
||||
f"Element {self._element._js.tagName} has no options attribute."
|
||||
)
|
||||
|
||||
def add(
|
||||
self,
|
||||
value: Any = None,
|
||||
html: str = None,
|
||||
text: str = None,
|
||||
before: Element | int = None,
|
||||
**kws,
|
||||
) -> None:
|
||||
"""Add a new option to the select element"""
|
||||
# create the option element and set the attributes
|
||||
option = document.createElement("option")
|
||||
if value is not None:
|
||||
kws["value"] = value
|
||||
if html is not None:
|
||||
option.innerHTML = html
|
||||
if text is not None:
|
||||
kws["text"] = text
|
||||
|
||||
for key, value in kws.items():
|
||||
option.setAttribute(key, value)
|
||||
|
||||
if before:
|
||||
if isinstance(before, Element):
|
||||
before = before._js
|
||||
|
||||
self._element._js.add(option, before)
|
||||
|
||||
def remove(self, item: int) -> None:
|
||||
"""Remove the option at the specified index"""
|
||||
self._element._js.remove(item)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Remove all the options"""
|
||||
for i in range(len(self)):
|
||||
self.remove(0)
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
"""Return the list of options"""
|
||||
return [Element(opt) for opt in self._element._js.options]
|
||||
|
||||
@property
|
||||
def selected(self):
|
||||
"""Return the selected option"""
|
||||
return self.options[self._element._js.selectedIndex]
|
||||
|
||||
def __iter__(self):
|
||||
yield from self.options
|
||||
|
||||
def __len__(self):
|
||||
return len(self.options)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__} (length: {len(self)}) {self.options}"
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.options[key]
|
||||
|
||||
|
||||
class StyleProxy(dict):
|
||||
def __init__(self, element: Element) -> None:
|
||||
self._element = element
|
||||
|
||||
@@ -70,6 +70,24 @@
|
||||
<input id="test_rr_input_password" type="password" value="Content test_rr_input_password">
|
||||
</form>
|
||||
|
||||
<select id="test_select_element"></select>
|
||||
<select id="test_select_element_w_options">
|
||||
<option value="1">Option 1</option>
|
||||
<option value="2" selected="selected">Option 2</option>
|
||||
</select>
|
||||
<select id="test_select_element_to_clear">
|
||||
<option value="1">Option 1</option>
|
||||
<option value="2">Option 2</option>
|
||||
<option value="4">Option 4</option>
|
||||
</select>
|
||||
|
||||
<select id="test_select_element_to_remove">
|
||||
<option value="1">Option 1</option>
|
||||
<option value="2">Option 2</option>
|
||||
<option value="3">Option 3</option>
|
||||
<option value="4">Option 4</option>
|
||||
</select>
|
||||
|
||||
<div id="element-creation-test"></div>
|
||||
|
||||
<button id="a-test-button">I'm a button to be clicked</button>
|
||||
|
||||
@@ -292,3 +292,144 @@ class TestInput:
|
||||
result = pydom[f"#tests-terminal"]
|
||||
with pytest.raises(AttributeError):
|
||||
result.value = "some value"
|
||||
|
||||
def test_element_without_collection(self):
|
||||
result = pydom[f"#tests-terminal"]
|
||||
with pytest.raises(AttributeError):
|
||||
result.value = "some value"
|
||||
|
||||
|
||||
class TestSelect:
|
||||
def test_select_options_iter(self):
|
||||
select = pydom[f"#test_select_element_w_options"][0]
|
||||
|
||||
for i, option in enumerate(select.options, 1):
|
||||
assert option.value == f"{i}"
|
||||
assert option.html == f"Option {i}"
|
||||
|
||||
def test_select_options_len(self):
|
||||
select = pydom[f"#test_select_element_w_options"][0]
|
||||
assert len(select.options) == 2
|
||||
|
||||
def test_select_options_clear(self):
|
||||
select = pydom[f"#test_select_element_to_clear"][0]
|
||||
assert len(select.options) == 3
|
||||
|
||||
select.options.clear()
|
||||
|
||||
assert len(select.options) == 0
|
||||
|
||||
def test_select_element_add(self):
|
||||
# GIVEN the existing select element with no options
|
||||
select = pydom[f"#test_select_element"][0]
|
||||
|
||||
# EXPECT the select element to have no options
|
||||
assert len(select.options) == 0
|
||||
|
||||
# WHEN we add an option
|
||||
select.options.add(value="1", html="Option 1")
|
||||
|
||||
# EXPECT the select element to have 1 option matching the attributes
|
||||
# we passed in
|
||||
assert len(select.options) == 1
|
||||
assert select.options[0].value == "1"
|
||||
assert select.options[0].html == "Option 1"
|
||||
|
||||
# WHEN we add another option (blank this time)
|
||||
select.options.add()
|
||||
|
||||
# EXPECT the select element to have 2 options
|
||||
assert len(select.options) == 2
|
||||
|
||||
# EXPECT the last option to have an empty value and html
|
||||
assert select.options[1].value == ""
|
||||
assert select.options[1].html == ""
|
||||
|
||||
# WHEN we add another option (this time adding it in between the other 2
|
||||
# options by using an integer index)
|
||||
select.options.add(value="2", html="Option 2", before=1)
|
||||
|
||||
# EXPECT the select element to have 3 options
|
||||
assert len(select.options) == 3
|
||||
|
||||
# EXPECT the middle option to have the value and html we passed in
|
||||
assert select.options[0].value == "1"
|
||||
assert select.options[0].html == "Option 1"
|
||||
assert select.options[1].value == "2"
|
||||
assert select.options[1].html == "Option 2"
|
||||
assert select.options[2].value == ""
|
||||
assert select.options[2].html == ""
|
||||
|
||||
# WHEN we add another option (this time adding it in between the other 2
|
||||
# options but using the option itself)
|
||||
select.options.add(
|
||||
value="3", html="Option 3", before=select.options[2], selected=True
|
||||
)
|
||||
|
||||
# EXPECT the select element to have 3 options
|
||||
assert len(select.options) == 4
|
||||
|
||||
# EXPECT the middle option to have the value and html we passed in
|
||||
assert select.options[0].value == "1"
|
||||
assert select.options[0].html == "Option 1"
|
||||
assert select.options[0].selected == select.options[0]._js.selected == False
|
||||
assert select.options[1].value == "2"
|
||||
assert select.options[1].html == "Option 2"
|
||||
assert select.options[2].value == "3"
|
||||
assert select.options[2].html == "Option 3"
|
||||
assert select.options[2].selected == select.options[2]._js.selected == True
|
||||
assert select.options[3].value == ""
|
||||
assert select.options[3].html == ""
|
||||
|
||||
# WHEN we add another option (this time adding it in between the other 2
|
||||
# options but using the JS element of the option itself)
|
||||
select.options.add(value="2a", html="Option 2a", before=select.options[2]._js)
|
||||
|
||||
# EXPECT the select element to have 3 options
|
||||
assert len(select.options) == 5
|
||||
|
||||
# EXPECT the middle option to have the value and html we passed in
|
||||
assert select.options[0].value == "1"
|
||||
assert select.options[0].html == "Option 1"
|
||||
assert select.options[1].value == "2"
|
||||
assert select.options[1].html == "Option 2"
|
||||
assert select.options[2].value == "2a"
|
||||
assert select.options[2].html == "Option 2a"
|
||||
assert select.options[3].value == "3"
|
||||
assert select.options[3].html == "Option 3"
|
||||
assert select.options[4].value == ""
|
||||
assert select.options[4].html == ""
|
||||
|
||||
def test_select_options_remove(self):
|
||||
# GIVEN the existing select element with 3 options
|
||||
select = pydom[f"#test_select_element_to_remove"][0]
|
||||
|
||||
# EXPECT the select element to have 3 options
|
||||
assert len(select.options) == 4
|
||||
# EXPECT the options to have the values originally set
|
||||
assert select.options[0].value == "1"
|
||||
assert select.options[1].value == "2"
|
||||
assert select.options[2].value == "3"
|
||||
assert select.options[3].value == "4"
|
||||
|
||||
# WHEN we remove the second option (index starts at 0)
|
||||
select.options.remove(1)
|
||||
|
||||
# EXPECT the select element to have 2 options
|
||||
assert len(select.options) == 3
|
||||
# EXPECT the options to have the values originally set but the second
|
||||
assert select.options[0].value == "1"
|
||||
assert select.options[1].value == "3"
|
||||
assert select.options[2].value == "4"
|
||||
|
||||
def test_select_get_selected_option(self):
|
||||
# GIVEN the existing select element with one selected option
|
||||
select = pydom[f"#test_select_element_w_options"][0]
|
||||
|
||||
# WHEN we get the selected option
|
||||
selected_option = select.options.selected
|
||||
|
||||
# EXPECT the selected option to be correct
|
||||
assert selected_option.value == "2"
|
||||
assert selected_option.html == "Option 2"
|
||||
assert selected_option.selected == selected_option._js.selected == True
|
||||
|
||||
Reference in New Issue
Block a user