Skip to content

Commit

Permalink
Merge pull request #356 from washad/main
Browse files Browse the repository at this point in the history
Added documentation and type-hinting to base element class.
  • Loading branch information
falkoschindler authored Feb 13, 2023
2 parents decee11 + 0fd9a27 commit 849e3a5
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 23 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ dist
/test.py
*.pickle
tests/screenshots/

# ignore local virtual environments
venv
.idea
107 changes: 84 additions & 23 deletions nicegui/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from copy import deepcopy
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union

from typing_extensions import Self

from . import binding, events, globals, outbox
from .elements.mixins.visibility import Visibility
from .event_listener import EventListener
Expand All @@ -20,6 +22,13 @@
class Element(ABC, Visibility):

def __init__(self, tag: str, *, _client: Optional[Client] = None) -> None:
"""Generic Element
This class is also the base class for all other elements.
:param tag: HTML tag of the element
:param _client: client for this element (for internal use only)
"""
super().__init__()
self.client = _client or globals.get_client()
self.id = self.client.next_element_id
Expand All @@ -45,10 +54,15 @@ def __init__(self, tag: str, *, _client: Optional[Client] = None) -> None:
outbox.enqueue_update(self.parent_slot.parent)

def add_slot(self, name: str) -> Slot:
"""Add a slot to the element.
:param name: name of the slot
:return: the slot
"""
self.slots[name] = Slot(self, name)
return self.slots[name]

def __enter__(self):
def __enter__(self) -> Self:
self.default_slot.__enter__()
return self

Expand Down Expand Up @@ -111,12 +125,18 @@ def to_dict(self, *keys: str) -> Dict:
raise ValueError(f'Unknown key {key}')
return dict_

def classes(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None):
'''HTML classes to modify the look of the element.
Every class in the `remove` parameter will be removed from the element.
Classes are separated with a blank space.
This can be helpful if the predefined classes by NiceGUI are not wanted in a particular styling.
'''
def classes(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None) \
-> Self:
"""Apply, remove, or replace HTML classes.
This allows modifying the look of the element or its layout using `Tailwind <https://tailwindcss.com/>`_ or `Quasar <https://quasar.dev/>`_ classes.
Removing or replacing classes can be helpful if predefined classes are not desired.
:param add: whitespace-delimited string of classes
:param remove: whitespace-delimited string of classes to remove from the element
:param replace: whitespace-delimited string of classes to use instead of existing ones
"""
class_list = self._classes if replace is None else []
class_list = [c for c in class_list if c not in (remove or '').split()]
class_list += (add or '').split()
Expand All @@ -137,12 +157,19 @@ def _parse_style(text: Optional[str]) -> Dict[str, str]:
result[key.strip()] = value.strip()
return result

def style(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None):
'''CSS style sheet definitions to modify the look of the element.
Every style in the `remove` parameter will be removed from the element.
Styles are separated with a semicolon.
This can be helpful if the predefined style sheet definitions by NiceGUI are not wanted in a particular styling.
'''
def style(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None) -> Self:
"""Apply, remove, or replace CSS definitions.
Removing or replacing styles can be helpful if the predefined style is not desired.
.. codeblock:: python
ui.button('Click me').style('color: #6E93D6; font-size: 200%', remove='font-weight; background-color')
:param add: semicolon-separated list of styles to add to the element
:param remove: semicolon-separated list of styles to remove from the element
:param replace: semicolon-separated list of styles to use instead of existing ones
"""
style_dict = deepcopy(self._style) if replace is None else {}
for key in self._parse_style(remove):
if key in style_dict:
Expand All @@ -165,13 +192,21 @@ def _parse_props(text: Optional[str]) -> Dict[str, Any]:
dictionary[key] = value or True
return dictionary

def props(self, add: Optional[str] = None, *, remove: Optional[str] = None):
'''Quasar props https://quasar.dev/vue-components/button#design to modify the look of the element.
Boolean props will automatically activated if they appear in the list of the `add` property.
Props are separated with a blank space. String values must be quoted.
Every prop passed to the `remove` parameter will be removed from the element.
This can be helpful if the predefined props by NiceGUI are not wanted in a particular styling.
'''
def props(self, add: Optional[str] = None, *, remove: Optional[str] = None) -> Self:
"""Add or remove props.
This allows modifying the look of the element or its layout using `Quasar <https://quasar.dev/>`_ props.
Since props are simply applied as HTML attributes, they can be used with any HTML element.
.. codeblock:: python
ui.button('Open menu').props('outline icon=menu')
Boolean properties are assumed ``True`` if no value is specified.
:param add: whitespace-delimited list of either boolean values or key=value pair to add
:param remove: whitespace-delimited list of property keys to remove
"""
needs_update = False
for key in self._parse_props(remove):
if key in self._props:
Expand All @@ -185,13 +220,25 @@ def props(self, add: Optional[str] = None, *, remove: Optional[str] = None):
self.update()
return self

def tooltip(self, text: str):
def tooltip(self, text: str) -> Self:
"""Add a tooltip to the element.
:param text: text of the tooltip
"""
with self:
tooltip = Element('q-tooltip')
tooltip._text = text
return self

def on(self, type: str, handler: Optional[Callable], args: Optional[List[str]] = None, *, throttle: float = 0.0):
def on(self, type: str, handler: Optional[Callable], args: Optional[List[str]] = None, *, throttle: float = 0.0) \
-> Self:
"""Subscribe to an event.
:param type: name of the event (without the "on" prefix, e.g. "click" or "mousedown")
:param handler: callback that is called upon occurrence of the event
:param args: arguments included in the event message sent to the event handler (default: `None` meaning all)
:param throttle: minimum time (in seconds) between event occurrences (default: 0.0)
"""
if handler:
args = args if args is not None else ['*']
listener = EventListener(element_id=self.id, type=type, args=args, handler=handler, throttle=throttle)
Expand All @@ -204,23 +251,33 @@ def handle_event(self, msg: Dict) -> None:
events.handle_event(listener.handler, msg, sender=self)

def collect_descendant_ids(self) -> List[int]:
'''includes own ID as first element'''
"""Return a list of IDs of the element and each of its descendants.
The first ID in the list is that of the element itself.
"""
ids: List[int] = [self.id]
for slot in self.slots.values():
for child in slot.children:
ids.extend(child.collect_descendant_ids())
return ids

def update(self) -> None:
"""Update the element on the client side."""
outbox.enqueue_update(self)

def run_method(self, name: str, *args: Any) -> None:
"""Run a method on the client side.
:param name: name of the method
:param args: arguments to pass to the method
"""
if not globals.loop:
return
data = {'id': self.id, 'name': name, 'args': args}
outbox.enqueue_message('run_method', data, globals._socket_id or self.client.id)

def clear(self) -> None:
"""Remove all child elements."""
descendants = [self.client.elements[id] for id in self.collect_descendant_ids()[1:]]
binding.remove(descendants, Element)
for element in descendants:
Expand All @@ -230,6 +287,10 @@ def clear(self) -> None:
self.update()

def remove(self, element: Union[Element, int]) -> None:
"""Remove a child element.
:param element: either the element instance or its ID
"""
if isinstance(element, int):
children = [child for slot in self.slots.values() for child in slot.children]
element = children[element]
Expand Down

0 comments on commit 849e3a5

Please sign in to comment.