Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add possibility to nest CSS and use "at rules" #10

Merged
merged 2 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class Page(Component[AnyChildren, NoAttrs]):
return html(
head(
title("Ludic Example"),
style.load(),
style.load(cache=True),
),
body(
main(*self.children),
Expand Down Expand Up @@ -73,6 +73,7 @@ from ludic.web import LudicApp
from your_app.components import Body, Header
from your_app.components.pages import Page


app = LudicApp(debug=True)


Expand Down
85 changes: 82 additions & 3 deletions docs/styles.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
There are two primary ways to apply CSS properties to components within your application:

1. The `style` HTML Attribute
2. The `styles` Class Property
3. The `styles` Class Property

## The `style` HTML Attribute

Expand Down Expand Up @@ -48,6 +48,23 @@ class Button(ComponentStrict[str, ButtonAttrs]):
return button(self.children[0], **self.attrs_for(button))
```

In this case, you need to make sure you collect and render the styles. See [Collecting The Styles](#collecting-the-styles) and [Integration In a Page Component](#integration-in-a-page-component).

It is also possible to nest styles similar to how you would nest them in SCSS. The only problem is that you might get typing errors if you are using `mypy` or `pyright`:

```python
class Button(ComponentStrict[str, ButtonAttrs]):
styles = {
"p": {
"color": "#eee", # type: ignore[dict-item]
".big": {
"font-size": "16px",
}
}
}
...
```

### Collecting The Styles

1. **Load Styles**: Use the `style.load()` method to gather styles from all components in your project. This generates a `<style>` element:
Expand All @@ -66,6 +83,8 @@ class Button(ComponentStrict[str, ButtonAttrs]):
</style>
```

You can also pass `styles.load(cache=True)` in order to cache the styles.

2. **Targeted Loading**: For more control, use `style.from_components(...)` to load styles from specific components:

```python
Expand All @@ -81,7 +100,9 @@ class Button(ComponentStrict[str, ButtonAttrs]):
Create a `Page` component responsible for rendering collected styles in the HTML `<head>`:

```python
from ludic.html import html, head, body, style, title, main, script
from typing import override

from ludic.html import html, head, body, style
from ludic.types import AnyChildren, Component, NoAttrs


Expand All @@ -90,10 +111,68 @@ class Page(Component[AnyChildren, NoAttrs]):
def render(self) -> html:
return html(
head(
style.load()
style.load(cache=True)
),
body(
...
),
)
```

### Caching The Styles

Note the `style.load(cache=True)` from the previous section in the `Page` component. As already mentioned, this caches loaded element during the first render. The problem is that the first request to your application renders the styles without cache so the response is slower. If you want to cache the styles before your `Page` component even renders, you can use the `lifespan` argument for the `LudicApp` class:

```python
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

from ludic.web import LudicApp


@asynccontextmanager
async def lifespan(_: LudicApp) -> AsyncIterator[None]:
style.load(cache=True) # cache styles before accepting requests
yield


app = LudicApp(lifespan=lifespan)
```

You can read more about `Lifespan` in [Starlette's documentation](https://www.starlette.io/lifespan/).

## The `style` HTML Element

You can also directly embed styles within a `Page` component using the `style` element. Here's an example:

```python
from ludic.html import style

style(
{
"a": {
"text-decoration": "none",
"color": "red",
},
"a:hover": {
"color": "blue",
"text-decoration": "underline",
},
}
)
```

It is also possible to pass raw CSS styles as a string:

```python
from ludic.html import style

style("""
.button {
padding: 3px 10px;
font-size: 12px;
border-radius: 3px;
border: 1px solid #e1e4e8;
}
""")
```
85 changes: 47 additions & 38 deletions examples/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from dataclasses import asdict, dataclass
from typing import Any, override

Expand Down Expand Up @@ -54,42 +56,43 @@ class DB:
people: dict[str, PersonData]


db = DB(
contacts={
"1": ContactData(
id="1",
first_name="John",
last_name="Doe",
email="qN6Z8@example.com",
)
},
people={
"1": PersonData(
id="1",
name="Joe Smith",
email="joe@smith.org",
active=True,
),
"2": PersonData(
id="2",
name="Angie MacDowell",
email="angie@macdowell.org",
active=True,
),
"3": PersonData(
id="3",
name="Fuqua Tarkenton",
email="fuqua@tarkenton.org",
active=True,
),
"4": PersonData(
id="4",
name="Kim Yee",
email="kim@yee.org",
active=False,
),
},
)
def init_db() -> DB:
return DB(
contacts={
"1": ContactData(
id="1",
first_name="John",
last_name="Doe",
email="qN6Z8@example.com",
)
},
people={
"1": PersonData(
id="1",
name="Joe Smith",
email="joe@smith.org",
active=True,
),
"2": PersonData(
id="2",
name="Angie MacDowell",
email="angie@macdowell.org",
active=True,
),
"3": PersonData(
id="3",
name="Fuqua Tarkenton",
email="fuqua@tarkenton.org",
active=True,
),
"4": PersonData(
id="4",
name="Kim Yee",
email="kim@yee.org",
active=False,
),
},
)


class Page(Component[AnyChildren, NoAttrs]):
Expand All @@ -98,7 +101,7 @@ def render(self) -> BaseElement:
return html(
head(
title("Ludic Example"),
style.load(),
style.load(cache=True),
meta(charset="utf-8"),
meta(name="viewport", content="width=device-width, initial-scale=1.0"),
),
Expand All @@ -123,7 +126,13 @@ def render(self) -> div:
return div(*self.children)


app = LudicApp(debug=True)
@asynccontextmanager
async def lifespan(_: LudicApp) -> AsyncIterator[None]:
style.load(cache=True)
yield


app = LudicApp(debug=True, lifespan=lifespan)


@app.exception_handler(404)
Expand Down
4 changes: 3 additions & 1 deletion examples/bulk_update.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Annotated, Self, override

from examples import Body, Header, Page, app, db
from examples import Body, Header, Page, app, init_db
from ludic.catalog.buttons import ButtonPrimary
from ludic.catalog.forms import FieldMeta, Form
from ludic.catalog.tables import ColumnMeta, Table, create_rows
Expand All @@ -9,6 +9,8 @@
from ludic.web.endpoints import Endpoint
from ludic.web.parsers import ListParser

db = init_db()


class PersonAttrs(Attrs, total=False):
id: Annotated[str, ColumnMeta(identifier=True)]
Expand Down
4 changes: 3 additions & 1 deletion examples/click_to_edit.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Annotated, NotRequired, Self, override

from examples import Body, Header, Page, app, db
from examples import Body, Header, Page, app, init_db
from ludic.catalog.buttons import ButtonDanger, ButtonPrimary
from ludic.catalog.forms import FieldMeta, Form, create_fields
from ludic.catalog.lists import Pairs
Expand All @@ -10,6 +10,8 @@
from ludic.web.exceptions import NotFoundError
from ludic.web.parsers import Parser, ValidationError

db = init_db()


def email_validator(email: str) -> str:
if len(email.split("@")) != 2:
Expand Down
4 changes: 3 additions & 1 deletion examples/delete_row.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from typing import Self, override

from examples import Body, Header, Page, app, db
from examples import Body, Header, Page, app, init_db
from ludic.catalog.buttons import ButtonDanger
from ludic.catalog.tables import TableHead, TableRow
from ludic.html import table, tbody, thead
from ludic.types import Attrs
from ludic.web.endpoints import Endpoint
from ludic.web.exceptions import NotFoundError

db = init_db()


class PersonAttrs(Attrs):
id: str
Expand Down
4 changes: 3 additions & 1 deletion examples/edit_row.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Annotated, NotRequired, Self, override

from examples import Body, Header, Page, app, db
from examples import Body, Header, Page, app, init_db
from ludic.catalog.buttons import ButtonPrimary, ButtonSecondary
from ludic.catalog.tables import ColumnMeta, TableHead, TableRow
from ludic.html import div, input, table, tbody, thead
Expand All @@ -9,6 +9,8 @@
from ludic.web.exceptions import NotFoundError
from ludic.web.parsers import Parser

db = init_db()


class PersonAttrs(Attrs):
id: NotRequired[str]
Expand Down
5 changes: 4 additions & 1 deletion ludic/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@

ELEMENT_REGISTRY: MutableMapping[str, list[type["BaseElement"]]] = {}

GlobalStyles = Mapping[str, CSSProperties]
# FIXME: Currently, it is impossible to properly type nested CSS properties
# defined similar as in SCSS, it will be possible when the following PEP is
# implemented and supported by type checkers: https://peps.python.org/pep-0728/
GlobalStyles = Mapping[str, "CSSProperties | GlobalStyles"]
"""CSS styles for elements or components which are defined by setting the ``styles``
class property.

Expand Down
4 changes: 2 additions & 2 deletions ludic/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,8 @@ def from_components(cls, *components: type[BaseElement]) -> Self:
return cls(collect_from_components(*components))

@classmethod
def load(cls) -> Self:
return cls(collect_from_loaded())
def load(cls, cache: bool = False) -> Self:
return cls(collect_from_loaded(cache=cache))

def to_html(self) -> str:
dom: BaseElement = self
Expand Down
Loading