Skip to content

Commit

Permalink
Merge pull request #1689 from natankeddem/defaulting_poc
Browse files Browse the repository at this point in the history
Defaulting Element Styling
  • Loading branch information
falkoschindler authored Oct 2, 2023
2 parents 6d6af6b + ff3f6e5 commit 45c1c80
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 0 deletions.
69 changes: 69 additions & 0 deletions nicegui/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ class Element(Visibility):
libraries: List[Library] = []
extra_libraries: List[Library] = []
exposed_libraries: List[Library] = []
_default_props: Dict[str, Any] = {}
_default_classes: List[str] = []
_default_style: Dict[str, str] = {}

def __init__(self, tag: Optional[str] = None, *, _client: Optional[Client] = None) -> None:
"""Generic Element
Expand All @@ -44,8 +47,11 @@ def __init__(self, tag: Optional[str] = None, *, _client: Optional[Client] = Non
self.client.next_element_id += 1
self.tag = tag if tag else self.component.tag if self.component else 'div'
self._classes: List[str] = []
self._classes.extend(self._default_classes)
self._style: Dict[str, str] = {}
self._style.update(self._default_style)
self._props: Dict[str, Any] = {'key': self.id} # HACK: workaround for #600 and #898
self._props.update(self._default_props)
self._event_listeners: Dict[str, EventListener] = {}
self._text: Optional[str] = None
self.slots: Dict[str, Slot] = {}
Expand Down Expand Up @@ -97,6 +103,10 @@ def glob_absolute_paths(file: Union[str, Path]) -> List[Path]:
for path in glob_absolute_paths(library):
cls.exposed_libraries.append(register_library(path, expose=True))

cls._default_props = copy(cls._default_props)
cls._default_classes = copy(cls._default_classes)
cls._default_style = copy(cls._default_style)

def add_slot(self, name: str, template: Optional[str] = None) -> Slot:
"""Add a slot to the element.
Expand Down Expand Up @@ -176,6 +186,24 @@ def classes(self, add: Optional[str] = None, *, remove: Optional[str] = None, re
self.update()
return self

@classmethod
def default_classes(cls, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None) \
-> Self:
"""Apply, remove, or replace default 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.
All elements of this class will share these HTML classes.
These must be defined before element instantiation.
: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
"""
cls._default_classes = cls._update_classes_list(cls._default_classes, add, remove, replace)
return cls

@staticmethod
def _parse_style(text: Optional[str]) -> Dict[str, str]:
result = {}
Expand Down Expand Up @@ -205,6 +233,26 @@ def style(self, add: Optional[str] = None, *, remove: Optional[str] = None, repl
self.update()
return self

@classmethod
def default_style(cls, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None) -> Self:
"""Apply, remove, or replace default CSS definitions.
Removing or replacing styles can be helpful if the predefined style is not desired.
All elements of this class will share these CSS definitions.
These must be defined before element instantiation.
: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
"""
if replace is not None:
cls._default_style.clear()
for key in cls._parse_style(remove):
cls._default_style.pop(key, None)
cls._default_style.update(cls._parse_style(add))
cls._default_style.update(cls._parse_style(replace))
return cls

@staticmethod
def _parse_props(text: Optional[str]) -> Dict[str, Any]:
dictionary = {}
Expand Down Expand Up @@ -240,6 +288,27 @@ def props(self, add: Optional[str] = None, *, remove: Optional[str] = None) -> S
self.update()
return self

@classmethod
def default_props(cls, add: Optional[str] = None, *, remove: Optional[str] = None) -> Self:
"""Add or remove default 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.
All elements of this class will share these props.
These must be defined before element instantiation.
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
"""
for key in cls._parse_props(remove):
if key in cls._default_props:
del cls._default_props[key]
for key, value in cls._parse_props(add).items():
cls._default_props[key] = value
return cls

def tooltip(self, text: str) -> Self:
"""Add a tooltip to the element.
Expand Down
105 changes: 105 additions & 0 deletions tests/test_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,108 @@ def test_xss(screen: Screen):
screen.should_contain('</script><script>alert(2)</script>')
screen.should_contain('<b>Bold 1</b>, `code`, copy&paste, multi\nline')
screen.should_contain('<b>Bold 2</b>, `code`, copy&paste, multi\nline')


def test_default_props():
ui.button.default_props('rounded outline')
button_a = ui.button('Button A')
button_b = ui.button('Button B')
assert button_a._props.get('rounded') is True, 'default props are set'
assert button_a._props.get('outline') is True
assert button_b._props.get('rounded') is True
assert button_b._props.get('outline') is True

ui.button.default_props(remove='outline')
button_c = ui.button('Button C')
assert button_c._props.get('outline') is None, '"outline" prop was removed'
assert button_c._props.get('rounded') is True, 'other props are still there'

ui.input.default_props('filled')
input_a = ui.input()
assert input_a._props.get('filled') is True
assert input_a._props.get('rounded') is None, 'default props of ui.button do not affect ui.input'

class MyButton(ui.button):
pass
MyButton.default_props('flat')
button_d = MyButton()
button_e = ui.button()
assert button_d._props.get('flat') is True
assert button_d._props.get('rounded') is True, 'default props are inherited'
assert button_e._props.get('flat') is None, 'default props of MyButton do not affect ui.button'
assert button_e._props.get('rounded') is True

ui.button.default_props('no-caps').default_props('no-wrap')
button_f = ui.button()
assert button_f._props.get('no-caps') is True
assert button_f._props.get('no-wrap') is True


def test_default_classes():
ui.button.default_classes('bg-white text-green')
button_a = ui.button('Button A')
button_b = ui.button('Button B')
assert 'bg-white' in button_a._classes, 'default classes are set'
assert 'text-green' in button_a._classes
assert 'bg-white' in button_b._classes
assert 'text-green' in button_b._classes

ui.button.default_classes(remove='text-green')
button_c = ui.button('Button C')
assert 'text-green' not in button_c._classes, '"text-green" class was removed'
assert 'bg-white' in button_c._classes, 'other classes are still there'

ui.input.default_classes('text-black')
input_a = ui.input()
assert 'text-black' in input_a._classes
assert 'bg-white' not in input_a._classes, 'default classes of ui.button do not affect ui.input'

class MyButton(ui.button):
pass
MyButton.default_classes('w-full')
button_d = MyButton()
button_e = ui.button()
assert 'w-full' in button_d._classes
assert 'bg-white' in button_d._classes, 'default classes are inherited'
assert 'w-full' not in button_e._classes, 'default classes of MyButton do not affect ui.button'
assert 'bg-white' in button_e._classes

ui.button.default_classes('h-40').default_classes('max-h-80')
button_f = ui.button()
assert 'h-40' in button_f._classes
assert 'max-h-80' in button_f._classes


def test_default_style():
ui.button.default_style('color: green; font-size: 200%')
button_a = ui.button('Button A')
button_b = ui.button('Button B')
assert button_a._style.get('color') == 'green', 'default style is set'
assert button_a._style.get('font-size') == '200%'
assert button_b._style.get('color') == 'green'
assert button_b._style.get('font-size') == '200%'

ui.button.default_style(remove='color: green')
button_c = ui.button('Button C')
assert button_c._style.get('color') is None, '"color" style was removed'
assert button_c._style.get('font-size') == '200%', 'other style are still there'

ui.input.default_style('font-weight: 300')
input_a = ui.input()
assert input_a._style.get('font-weight') == '300'
assert input_a._style.get('font-size') is None, 'default style of ui.button does not affect ui.input'

class MyButton(ui.button):
pass
MyButton.default_style('font-family: courier')
button_d = MyButton()
button_e = ui.button()
assert button_d._style.get('font-family') == 'courier'
assert button_d._style.get('font-size') == '200%', 'default style is inherited'
assert button_e._style.get('font-family') is None, 'default style of MyButton does not affect ui.button'
assert button_e._style.get('font-size') == '200%'

ui.button.default_style('border: 2px').default_style('padding: 30px')
button_f = ui.button()
assert button_f._style.get('border') == '2px'
assert button_f._style.get('padding') == '30px'
42 changes: 42 additions & 0 deletions website/more_documentation/element_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,45 @@ def move_elements() -> None:
ui.button('Move X to A', on_click=lambda: x.move(a))
ui.button('Move X to B', on_click=lambda: x.move(b))
ui.button('Move X to top', on_click=lambda: x.move(target_index=0))

@text_demo('Default props', '''
You can set default props for all elements of a certain class.
This way you can avoid repeating the same props over and over again.
Default props only apply to elements created after the default props were set.
Subclasses inherit the default props of their parent class.
''')
def default_props() -> None:
ui.button.default_props('rounded outline')
ui.button('Button A')
ui.button('Button B')
# END OF DEMO
ui.button.default_props(remove='rounded outline')

@text_demo('Default classes', '''
You can set default classes for all elements of a certain class.
This way you can avoid repeating the same classes over and over again.
Default classes only apply to elements created after the default classes were set.
Subclasses inherit the default classes of their parent class.
''')
def default_classes() -> None:
ui.label.default_classes('bg-blue-100 p-2')
ui.label('Label A')
ui.label('Label B')
# END OF DEMO
ui.label.default_classes(remove='bg-blue-100 p-2')

@text_demo('Default style', '''
You can set a default style for all elements of a certain class.
This way you can avoid repeating the same style over and over again.
A default style only applies to elements created after the default style was set.
Subclasses inherit the default style of their parent class.
''')
def default_style() -> None:
ui.label.default_style('color: tomato')
ui.label('Label A')
ui.label('Label B')
# END OF DEMO
ui.label.default_style(remove='color: tomato')

0 comments on commit 45c1c80

Please sign in to comment.