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 CSS class support for box component #995

Merged
merged 4 commits into from
Sep 30, 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
78 changes: 78 additions & 0 deletions demo/bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import mesop as me


@me.page(
security_policy=me.SecurityPolicy(
allowed_iframe_parents=["https://google.github.io"]
),
stylesheets=[
"https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css",
],
path="/bootstrap",
)
def page():
with me.box(classes="container"):
with me.box(
classes="d-flex flex-wrap justify-content-between py-3 mb-4 border-bottom",
):
with me.box(
classes="d-flex align-items-center mb-3 mb-md-0 me-md-auto link-body-emphasis text-decoration-none fs-4",
):
me.text("Mesop")

with me.box(classes="nav nav-pills"):
with me.box(classes="nav-item"):
with me.box(classes="nav-link active"):
me.text("Features")
with me.box(classes="nav-item"):
with me.box(classes="nav-link"):
me.text("About")

with me.box(classes="container px-4 py-5"):
with me.box(classes="pb-2 border-bottom"):
me.text("Columns", type="headline-5")

with me.box(classes="row g-4 py-5 row-cols-1 row-cols-lg-3"):
with me.box(classes="feature col"):
with me.box(classes="fs-2 text-body-emphasis"):
me.text("Featured title")
me.text(
"Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words."
)
me.link(text="Call to action", url="/#")

with me.box(classes="feature col"):
with me.box(classes="fs-2 text-body-emphasis"):
me.text("Featured title")
me.text(
"Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words."
)
me.link(text="Call to action", url="/#")

with me.box(classes="feature col"):
with me.box(classes="fs-2 text-body-emphasis"):
me.text("Featured title")
me.text(
"Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words."
)
me.link(text="Call to action", url="/#")

with me.box(
classes="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top"
):
with me.box(classes="col-md-4 mb-0 text-body-secondary"):
me.text("Copyright 2024 Mesop")

with me.box(classes="nav col-md-4 justify-content-end"):
with me.box(classes="nav-item"):
with me.box(classes="nav-link px-2 text-body-secondary"):
me.text("Home")
with me.box(classes="nav-item"):
with me.box(classes="nav-link px-2 text-body-secondary"):
me.text("Features")
with me.box(classes="nav-item"):
with me.box(classes="nav-link px-2 text-body-secondary"):
me.text("FAQs")
with me.box(classes="nav-item"):
with me.box(classes="nav-link px-2 text-body-secondary"):
me.text("About")
7 changes: 7 additions & 0 deletions demo/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import autocomplete as autocomplete
import badge as badge
import basic_animation as basic_animation
import bootstrap as bootstrap
import box as box
import button as button
import chat as chat
Expand Down Expand Up @@ -125,6 +126,12 @@ class Section:
Example(name="feedback"),
],
),
Section(
name="Integrations",
examples=[
Example(name="bootstrap"),
],
),
]

COMPONENTS_SECTIONS = [
Expand Down
1 change: 1 addition & 0 deletions mesop/components/box/box.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ package mesop.components.box;

message BoxType {
optional string on_click_handler_id = 2;
repeated string classes = 3;
}
3 changes: 3 additions & 0 deletions mesop/components/box/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def box(
*,
style: Style | None = None,
on_click: Callable[[ClickEvent], Any] | None = None,
classes: list[str] | str = "",
key: str | None = None,
) -> Any:
"""Creates a box component.
Expand All @@ -23,6 +24,7 @@ def box(
style: Style to apply to component. Follows [HTML Element inline style API](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style).
on_click: The callback function that is called when the box is clicked.
It receives a ClickEvent as its only argument.
classes: CSS classes.
key: The component [key](../components/index.md#component-key).
Returns:
Expand All @@ -35,6 +37,7 @@ def box(
on_click_handler_id=register_event_handler(on_click, event=ClickEvent)
if on_click
else "",
classes=classes if isinstance(classes, list) else classes.split(" "),
),
style=style,
)
18 changes: 17 additions & 1 deletion mesop/editor/component_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
import inspect
from dataclasses import is_dataclass
from types import NoneType
from typing import Any, Callable, ItemsView, Literal, Sequence
from typing import (
Any,
Callable,
ItemsView,
Literal,
Sequence,
get_args,
get_origin,
)

import mesop.protos.ui_pb2 as pb
from mesop.exceptions import MesopInternalException
Expand Down Expand Up @@ -80,6 +88,14 @@ def get_fields(
and args[1] is str
and args[2] is NoneType
)
or (
# special case, for list[str]|str (used for box classes), use str
args
and len(args) == 2
and get_origin(args[0]) is list
and get_args(args[0]) == (str,)
and args[1] is str
)
):
field_type = pb.FieldType(string_type=pb.StringType())
elif getattr(param_type, "__origin__", None) is Literal:
Expand Down
1 change: 1 addition & 0 deletions mesop/examples/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
)
from mesop.examples import starter_kit as starter_kit
from mesop.examples import sxs as sxs
from mesop.examples import tailwind as tailwind
from mesop.examples import testing as testing
from mesop.examples import viewport_size as viewport_size
from mesop.examples import web_component as web_component
Expand Down
131 changes: 131 additions & 0 deletions mesop/examples/tailwind.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""
Example Tailwind command:

```
npx tailwindcss -i ./tailwind_input.css -o ./tailwind.css
```

Example Tailwind config:

```
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["main.py"],
theme: {
extend: {},
},
plugins: [],
safelist: [],
};
```

Original HTML mark up:

```
<html>
<body class="min-h-screen flex flex-col">

<!-- Header -->
<header class="bg-gray-800 text-white py-4">
<div class="container mx-auto">
<h1 class="text-2xl font-bold">Header</h1>
</div>
</header>

<!-- Main Content with Sidebar -->
<div class="flex flex-1">
<!-- Sidebar -->
<aside class="w-64 bg-gray-200 p-4">
<h2 class="text-lg font-semibold mb-4">Sidebar</h2>
<ul>
<li><a href="#" class="text-gray-700 block py-2">Link 1</a></li>
<li><a href="#" class="text-gray-700 block py-2">Link 2</a></li>
<li><a href="#" class="text-gray-700 block py-2">Link 3</a></li>
</ul>
</aside>

<!-- Main Content -->
<main class="flex-1 p-6 bg-gray-100">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="bg-white p-6 rounded shadow">
<h2 class="text-xl font-bold mb-2">Box 1</h2>
<p>This is the content for box 1.</p>
</div>
<div class="bg-white p-6 rounded shadow">
<h2 class="text-xl font-bold mb-2">Box 2</h2>
<p>This is the content for box 2.</p>
</div>
<div class="bg-white p-6 rounded shadow">
<h2 class="text-xl font-bold mb-2">Box 3</h2>
<p>This is the content for box 3.</p>
</div>
</div>
</main>
</div>

<!-- Footer -->
<footer class="bg-gray-800 text-white py-4">
<div class="container mx-auto">
<p>&copy; 2024 Your Company</p>
</div>
</footer>

</body>
</html>
```
"""

import mesop as me


@me.page(
security_policy=me.SecurityPolicy(
allowed_iframe_parents=["https://google.github.io"]
),
stylesheets=[
# Specify your Tailwind CSS URL here.
#
# For local testing, you can just launch a basic Python HTTP server:
# python -m http.server 8000
"http://localhost:8000/assets/tailwind.css",
],
path="/tailwind",
)
def app():
with me.box(classes="min-h-screen flex flex-col"):
with me.box(classes="bg-gray-800 text-white py-4"):
with me.box(classes="container mx-auto"):
with me.box(classes="text-2xl font-bold"):
me.text("Header")

with me.box(classes="flex flex-1"):
with me.box(classes="w-64 bg-gray-200 p-4"):
with me.box(classes="text-lg font-semibold mb-4"):
me.text("Sidebar")
with me.box(classes="text-gray-700 block py-2"):
me.text("Link 1")
with me.box(classes="text-gray-700 block py-2"):
me.text("Link 2")
with me.box(classes="text-gray-700 block py-2"):
me.text("Link 3")

with me.box(classes="flex-1 p-6 bg-gray-100"):
with me.box(classes="grid grid-cols-1 md:grid-cols-3 gap-4"):
with me.box(classes="bg-white p-6 rounded shadow"):
with me.box(classes="text-xl font-bold mb-2"):
me.text("Box 1")
me.text("This is the content for box 1.")

with me.box(classes="bg-white p-6 rounded shadow"):
with me.box(classes="text-xl font-bold mb-2"):
me.text("Box 2")
me.text("This is the content for box 2")

with me.box(classes="bg-white p-6 rounded shadow"):
with me.box(classes="text-xl font-bold mb-2"):
me.text("Box 3")
me.text("This is the content for box 3")

with me.box(classes="bg-gray-800 text-white py-4"):
with me.box(classes="container mx-auto"):
me.text("2024 Mesop")
17 changes: 17 additions & 0 deletions mesop/web/src/app/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,23 @@ body {
}
}

// The box component has div-like semantics, so make it `display: block` by default.
// We use a high-level selector first since this will allow custom CSS selectors
// to override this style more easily, specifically for flex and grid displays.
component-renderer {
display: block;
}

// Custom elements like Angular component tags are treated as inline by default.
//
// Since component-renderer encompasses many different types of a components, we
// need to add a more specific selector to make these inline by default.
component-renderer:not([mesop-box]),
// The first component-renderer object is a box, but it needs to be inline.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does it need to be inline?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great question. I wasn't quite able to figure that out. But when I looked at the generated markup, that first component-renderer box element does not have display block by default on main branch (which is definitely strange since in component renderer ts code, in the main branch, it should set the style to display block, at least that's what I would have thought).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I just remembered, I think it's because the first element isn't actually a box component, the root component is a special synthetic component that IIRC doesn't actually have a component type (the purpose of it is that sometimes you may have multiple top-level components under a page fn, so we want to treat it as a tree with the synthetic node). Maybe we should have it be a box component just to be more consistent? (I'm not sure if you noticed anything broke when switching from block to inline), but that can be a future change. this PR LGTM

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I noticed this because some things definitely broke when I changed it to display block. I think the primary issue occurred in the chat apps where the full height stuff wasn't working. So seems like something that could be adjusted.

mat-sidenav-content > component-renderer:first-child {
display: inline;
}

mesop-markdown {
h1,
h2,
Expand Down
17 changes: 14 additions & 3 deletions mesop/web/src/component_renderer/component_renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ export class ComponentRenderer {
this._boxType = BoxType.deserializeBinary(
this.component.getType()!.getValue() as unknown as Uint8Array,
);
// Used for determinine which component-renderer elements are not boxes.
this.elementRef.nativeElement.setAttribute('mesop-box', 'true');
}

this.computeStyles();
Expand Down Expand Up @@ -288,6 +290,10 @@ export class ComponentRenderer {

computeStyles() {
this.elementRef.nativeElement.style = this.getStyle();
const classes = this.getClasses();
if (classes) {
this.elementRef.nativeElement.classList = classes;
}
}

createComponentRef() {
Expand Down Expand Up @@ -395,9 +401,7 @@ Make sure the web component name is spelled the same between Python and JavaScri
return '';
}

// `display: block` because box should have "div"-like semantics.
// Custom elements like Angular component tags are treated as inline by default.
let style = 'display: block;';
let style = '';

if (this.component.getStyle()) {
style += formatStyle(this.component.getStyle()!);
Expand All @@ -407,6 +411,13 @@ Make sure the web component name is spelled the same between Python and JavaScri
);
}

getClasses(): string {
if (this._boxType) {
return this._boxType.getClassesList().join(' ');
}
return '';
}

@HostListener('click', ['$event'])
onClick(event: Event) {
if (!this._boxType) {
Expand Down
2 changes: 1 addition & 1 deletion mesop/web/src/dev_tools/editor_panel/editor_panel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
--default-bottom-panel-height: 400px;
}

.container {
.me-container {
height: 100%;
}

Expand Down
4 changes: 2 additions & 2 deletions mesop/web/src/editor/editor.ng.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<mat-sidenav-container class="container">
<mat-sidenav-content #sidenavContent class="content">
<mat-sidenav-container class="me-container">
<mat-sidenav-content #sidenavContent class="me-content">
<mesop-shell></mesop-shell>
@defer (when showEditorToolbar()) {
<mesop-editor-toolbar></mesop-editor-toolbar>
Expand Down
4 changes: 2 additions & 2 deletions mesop/web/src/editor/editor.scss
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
.container {
.me-container {
height: 100%;
}

.content {
.me-content {
overflow: hidden;
}

Expand Down
Loading