8000 Pydom add better support for select element (#1887) · sugruedes/pyscript@c0d45d3 · GitHub
[go: up one dir, main page]

Skip to content

Commit c0d45d3

Browse files
fpligerpre-commit-ci[bot]WebReflection
authored
Pydom add better support for select element (pyscript#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>
1 parent f18ec3d commit c0d45d3

File tree

3 files changed

+259
-0
lines changed

3 files changed

+259
-0
lines changed

pyscript.core/src/stdlib/pyweb/pydom.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def __init__(self, js_element):
1414
self._js = js_element
1515
self._parent = None
1616
self.style = StyleProxy(self)
17+
self._proxies = {}
1718

1819
def __eq__(self, obj):
1920
"""Check if the element is the same as the other element by comparing
@@ -129,6 +130,18 @@ def id(self):
129130
def id(self, value):
130131
self._js.id = value
131132

133+
@property
134+
def options(self):
135+
if "options" in self._proxies:
136+
return self._proxies["options"]
137+
138+
if not self._js.tagName.lower() in {"select", "datalist", "optgroup"}:
139+
raise AttributeError(
140+
f"Element {self._js.tagName} has no options attribute."
141+
)
142+
self._proxies["options"] = OptionsProxy(self)
143+
return self._proxies["options"]
144+
132145
@property
133146
def value(self):
134147
return self._js.value
@@ -145,6 +158,22 @@ def value(self, value):
145158
)
146159
self._js.value = value
147160

161+
@property
162+
def selected(self):
163+
return self._js.selected
164+
165+
@selected.setter
166+
def selected(self, value):
167+
# in order to avoid confusion to the user, we don't allow setting the
168+
# value of elements that don't have a value attribute
169+
if not hasattr(self._js, "selected"):
170+
raise AttributeError(
171+
f"Element {self._js.tagName} has no value attribute. If you want to "
172+
"force a value attribute, set it directly using the `_js.value = <value>` "
173+
"javascript API attribute instead."
174+
)
175+
self._js.selected = value
176+
148177
def clone(self, new_id=None):
149178
clone = Element(self._js.cloneNode(True))
150179
clone.id = new_id
@@ -176,6 +205,77 @@ def show_me(self):
176205
self._js.scrollIntoView()
177206

178207

208+
class OptionsProxy:
209+
"""This class represents the options of a select element. It
210+
allows to access to add and remove options by using the `add` and `remove` methods.
211+
"""
212+
213+
def __init__(self, element: Element) -> None:
214+
self._element = element
215+
if self._element._js.tagName.lower() != "select":
216+
raise AttributeError(
217+
f"Element {self._element._js.tagName} has no options attribute."
218+
)
219+
220+
def add(
221+
self,
222+
value: Any = None,
223+
html: str = None,
224+
text: str = None,
225+
before: Element | int = None,
226+
**kws,
227+
) -> None:
228+
"""Add a new option to the select element"""
229+
# create the option element and set the attributes
230+
option = document.createElement("option")
231+
if value is not None:
232+
kws["value"] = value
233+
if html is not None:
234+
option.innerHTML = html
235+
if text is not None:
236+
kws["text"] = text
237+
238+
for key, value in kws.items():
239+
option.setAttribute(key, value)
240+
241+
if before:
242+
if isinstance(before, Element):
243+
before = before._js
244+
245+
self._element._js.add(option, before)
246+
247+
def remove(self, item: int) -> None:
248+
"""Remove the option at the specified index"""
249+
self._element._js.remove(item)
250+
251+
def clear(self) -> None:
252+
"""Remove all the options"""
253+
for i in range(len(self)):
254+
self.remove(0)
255+
256+
@property
257+
def options(self):
258+
"""Return the list of options"""
259+
return [Element(opt) for opt in self._element._js.options]
260+
261+
@property
262+
def selected(self):
263+
"""Return the selected option"""
264+
return self.options[self._element._js.selectedIndex]
265+
266+
def __iter__(self):
267+
yield from self.options
268+
269+
def __len__(self):
270+
return len(self.options)
271+
272+
def __repr__(self):
273+
return f"{self.__class__.__name__} (length: {len(self)}) {self.options}"
274+
275+
def __getitem__(self, key):
276+
return self.options[key]
277+
278+
179279
class StyleProxy(dict):
180280
def __init__(self, element: Element) -> None:
181281
self._element = element

pyscript.core/test/pyscript_dom/index.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,24 @@ <h2 id="multi-elem-h2" class="multi-elems">Content multi-elem-h2</h2>
7070
<input id="test_rr_input_password" type="password" value="Content test_rr_input_password">
7171
</form>
7272

73+
<select id="test_select_element"></select>
74+
<select id="test_select_element_w_options">
75+
<option value="1">Option 1</option>
76+
<option value="2" selected="selected">Option 2</option>
77+
</select>
78+
<select id="test_select_element_to_clear">
79+
<option value="1">Option 1</option>
80+
<option value="2">Option 2</option>
81+
<option value="4">Option 4</option>
82+
</select>
83+
84+
<select id="test_select_element_to_remove">
85+
<option value="1">Option 1</option>
86+
<option value="2">Option 2</option>
87+
<option value="3">Option 3</option>
88+
<option value="4">Option 4</option>
89+
</select>
90+
7391
<div id="element-creation-test"></div>
7492

7593
<button id="a-test-button">I'm a button to be clicked</button>

pyscript.core/test/pyscript_dom/tests/test_dom.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,3 +292,144 @@ def test_element_without_collection(self):
292292
result = pydom[f"#tests-terminal"]
293293
with pytest.raises(AttributeError):
294294
result.value = "some value"
295+
296+
def test_element_without_collection(self):
297+
result = pydom[f"#tests-terminal"]
298+
with pytest.raises(AttributeError):
299+
result.value = "some value"
300+
301+
302+
class TestSelect:
303+
def test_select_options_iter(self):
304+
select = pydom[f"#test_select_element_w_options"][0]
305+
306+
for i, option in enumerate(select.options, 1):
307+
assert option.value == f"{i}"
308+
assert option.html == f"Option {i}"
309+
310+
def test_select_options_len(self):
311+
select = pydom[f"#test_select_element_w_options"][0]
312+
assert len(select.options) == 2
313+
314+
def test_select_options_clear(self):
315+
select = pydom[f"#test_select_element_to_clear"][0]
316+
assert len(select.options) == 3
317+
318+
select.options.clear()
319+
320+
assert len(select.options) == 0
321+
322+
def test_select_element_add(self):
323+
# GIVEN the existing select element with no options
324+
select = pydom[f"#test_select_element"][0]
325+
326+
# EXPECT the select element to have no options
327+
assert len(select.options) == 0
328+
329+
# WHEN we add an option
330+
select.options.add(value="1", html="Option 1")
331+
332+
# EXPECT the select element to have 1 option matching the attributes
333+
# we passed in
334+
assert len(select.options) == 1
335+
assert select.options[0].value == "1"
336+
assert select.options[0].html == "Option 1"
337+
338+
# WHEN we add another option (blank this time)
339+
select.options.add()
340+
341+
# EXPECT the select element to have 2 options
342+
assert len(select.options) == 2
343+
344+
# EXPECT the last option to have an empty value and html
345+
assert select.options[1].value == ""
346+
assert select.options[1].html == ""
347+
348+
# WHEN we add another option (this time adding it in between the other 2
349+
# options by using an integer index)
350+
select.options.add(value="2", html="Option 2", before=1)
351+
352+
# EXPECT the select element to have 3 options
353+
assert len(select.options) == 3
354+
355+
# EXPECT the middle option to have the value and html we passed in
356+
assert select.options[0].value == "1"
357+
assert select.options[0].html == "Option 1"
358+
assert select.options[1].value == "2"
359+
assert select.options[1].html == "Option 2"
360+
assert select.options[2].value == ""
361+
assert select.options[2].html == ""
362+
363+
# WHEN we add another option (this time adding it in between the other 2
364+
# options but using the option itself)
365+
select.options.add(
366+
value="3", html="Option 3", before=select.options[2], selected=True
367+
)
368+
369+
# EXPECT the select element to have 3 options
370+
assert len(select.options) == 4
371+
372+
# EXPECT the middle option to have the value and html we passed in
373+
assert select.options[0].value == "1"
374+
assert select.options[0].html == "Option 1"
375+
assert select.options[0].selected == select.options[0]._js.selected == False
376+
assert select.options[1].value == "2"
377+
assert select.options[1].html == "Option 2"
378+
assert select.options[2].value == "3"
379+
assert select.options[2].html == "Option 3"
380+
assert select.options[2].selected == select.options[2]._js.selected == True
381+
assert select.options[3].value == ""
382+
assert select.options[3].html == ""
383+
384+
# WHEN we add another option (this time adding it in between the other 2
385+
# options but using the JS element of the option itself)
386+
select.options.add(value="2a", html="Option 2a", before=select.options[2]._js)
387+
388+
# EXPECT the select element to have 3 options
389+
assert len(select.options) == 5
390+
391+
# EXPECT the middle option to have the value and html we passed in
392+
assert select.options[0].value == "1"
393+
assert select.options[0].html == "Option 1"
394+
assert select.options[1].value == "2"
395+
assert select.options[1].html == "Option 2"
396+
assert select.options[2].value == "2a"
397+
assert select.options[2].html == "Option 2a"
398+
assert select.options[3].value == "3"
399+
assert select.options[3].html == "Option 3"
400+
assert select.options[4].value == ""
401+
assert select.options[4].html == ""
402+
403+
def test_select_options_remove(self):
404+
# GIVEN the existing select element with 3 options
405+
select = pydom[f"#test_select_element_to_remove"][0]
406+
407+
# EXPECT the select element to have 3 options
408+
assert len(select.options) == 4
409+
# EXPECT the options to have the values originally set
410+
assert select.options[0].value == "1"
411+
assert select.options[1].value == "2"
412+
assert select.options[2].value == "3"
413+
assert select.options[3].value == "4"
414+
415+
# WHEN we remove the second option (index starts at 0)
416+
select.options.remove(1)
417+
418+
# EXPECT the select element to have 2 options
419+
assert len(select.options) == 3
420+
# EXPECT the options to have the values originally set but the second
421+
assert select.options[0].value == "1"
422+
assert select.options[1].value == "3"
423+
assert select.options[2].value == "4"
424+
425+
def test_select_get_selected_option(self):
426+
# GIVEN the existing select element with one selected option
427+
select = pydom[f"#test_select_element_w_options"][0]
428+
429+
# WHEN we get the selected option
430+
selected_option = select.options.selected
431+
432+
# EXPECT the selected option to be correct
433+
assert selected_option.value == "2"
434+
assert selected_option.html == "Option 2"
435+
assert selected_option.selected == selected_option._js.selected == True

0 commit comments

Comments
 (0)
0