Compare commits
17 Commits
5cb628099a
...
WorkingOnB
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b86194c7e | |||
| 408c8332dc | |||
| dc2f6fd04a | |||
| 42e8566bcf | |||
| 255f145aca | |||
| fdc58942eb | |||
| c9f6be105f | |||
| ad2823042c | |||
| 6a05a84f0c | |||
| e8ecf72205 | |||
| cc11e4edaa | |||
| 9696e67910 | |||
| 7553c28f8e | |||
| c3d6958c1a | |||
| aaba6a5468 | |||
| 991a6f07ff | |||
| 3721bb7ad7 |
@@ -932,4 +932,3 @@ user.find_element("textarea[name='message']")
|
|||||||
|
|
||||||
* 0.1.0 : First release
|
* 0.1.0 : First release
|
||||||
* 0.2.0 : Updated to myauth 0.2.0
|
* 0.2.0 : Updated to myauth 0.2.0
|
||||||
* 0.3.0 : Added Bindings support
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "myfasthtml"
|
name = "myfasthtml"
|
||||||
version = "0.3.0"
|
version = "0.2.0"
|
||||||
description = "Set of tools to quickly create HTML pages using FastHTML."
|
description = "Set of tools to quickly create HTML pages using FastHTML."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [
|
authors = [
|
||||||
|
|||||||
23
src/app.py
23
src/app.py
@@ -1,23 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from fasthtml import serve
|
|
||||||
|
|
||||||
from myfasthtml.controls.Layout import Layout
|
|
||||||
from myfasthtml.myfastapp import create_app
|
|
||||||
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.DEBUG, # Set logging level to DEBUG
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', # Log format
|
|
||||||
datefmt='%Y-%m-%d %H:%M:%S', # Timestamp format
|
|
||||||
)
|
|
||||||
|
|
||||||
app, rt = create_app(protect_routes=False, mount_auth_app=True, pico=False, title="MyFastHtml" )
|
|
||||||
|
|
||||||
@rt("/")
|
|
||||||
def index(session):
|
|
||||||
layout = Layout(session, "Testing Layout", right_drawer=False)
|
|
||||||
layout.set_footer("Goodbye World")
|
|
||||||
return layout
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
serve(port=5003)
|
|
||||||
@@ -12,160 +12,4 @@
|
|||||||
height: 16px;
|
height: 16px;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* MF Layout Component - CSS Grid Layout
|
|
||||||
* Provides fixed header/footer, collapsible drawers, and scrollable main content
|
|
||||||
* Compatible with DaisyUI 5
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* Main layout container using CSS Grid */
|
|
||||||
.mf-layout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-areas:
|
|
||||||
"header header header"
|
|
||||||
"left-drawer main right-drawer"
|
|
||||||
"footer footer footer";
|
|
||||||
grid-template-rows: 32px 1fr 32px;
|
|
||||||
grid-template-columns: auto 1fr auto;
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header - fixed at top */
|
|
||||||
.mf-layout-header {
|
|
||||||
grid-area: header;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 0 1rem;
|
|
||||||
background-color: var(--color-base-300);
|
|
||||||
border-bottom: 1px solid color-mix(in oklab, var(--color-base-content) 10%, #0000);
|
|
||||||
z-index: 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footer - fixed at bottom */
|
|
||||||
.mf-layout-footer {
|
|
||||||
grid-area: footer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 1rem;
|
|
||||||
background-color: var(--color-neutral);
|
|
||||||
color: var(--color-neutral-content);
|
|
||||||
border-top: 1px solid color-mix(in oklab, var(--color-base-content) 10%, #0000);
|
|
||||||
z-index: 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main content area - scrollable */
|
|
||||||
.mf-layout-main {
|
|
||||||
grid-area: main;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: auto;
|
|
||||||
padding: 1rem;
|
|
||||||
background-color: var(--color-base-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Drawer base styles */
|
|
||||||
.mf-layout-drawer {
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
background-color: var(--color-base-100);
|
|
||||||
transition: width 0.3s ease-in-out, margin 0.3s ease-in-out;
|
|
||||||
width: 250px;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Left drawer */
|
|
||||||
.mf-layout-left-drawer {
|
|
||||||
grid-area: left-drawer;
|
|
||||||
border-right: 1px solid color-mix(in oklab, var(--color-base-content) 10%, #0000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Right drawer */
|
|
||||||
.mf-layout-right-drawer {
|
|
||||||
grid-area: right-drawer;
|
|
||||||
border-left: 1px solid color-mix(in oklab, var(--color-base-content) 10%, #0000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Collapsed drawer states */
|
|
||||||
.mf-layout-drawer.collapsed {
|
|
||||||
width: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toggle buttons positioning */
|
|
||||||
.mf-layout-toggle-left {
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mf-layout-toggle-right {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Smooth scrollbar styling for webkit browsers */
|
|
||||||
.mf-layout-main::-webkit-scrollbar,
|
|
||||||
.mf-layout-drawer::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mf-layout-main::-webkit-scrollbar-track,
|
|
||||||
.mf-layout-drawer::-webkit-scrollbar-track {
|
|
||||||
background: var(--color-base-200);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mf-layout-main::-webkit-scrollbar-thumb,
|
|
||||||
.mf-layout-drawer::-webkit-scrollbar-thumb {
|
|
||||||
background: color-mix(in oklab, var(--color-base-content) 20%, #0000);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mf-layout-main::-webkit-scrollbar-thumb:hover,
|
|
||||||
.mf-layout-drawer::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: color-mix(in oklab, var(--color-base-content) 30%, #0000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments for smaller screens */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.mf-layout-drawer {
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mf-layout-header,
|
|
||||||
.mf-layout-footer {
|
|
||||||
padding: 0 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mf-layout-main {
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Handle layouts with no drawers */
|
|
||||||
.mf-layout[data-left-drawer="false"] {
|
|
||||||
grid-template-areas:
|
|
||||||
"header header"
|
|
||||||
"main right-drawer"
|
|
||||||
"footer footer";
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mf-layout[data-right-drawer="false"] {
|
|
||||||
grid-template-areas:
|
|
||||||
"header header"
|
|
||||||
"left-drawer main"
|
|
||||||
"footer footer";
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mf-layout[data-left-drawer="false"][data-right-drawer="false"] {
|
|
||||||
grid-template-areas:
|
|
||||||
"header"
|
|
||||||
"main"
|
|
||||||
"footer";
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
}
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
/**
|
|
||||||
* MF Layout Component - JavaScript Controller
|
|
||||||
* Manages drawer state and provides programmatic control
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Global registry for layout instances
|
|
||||||
if (typeof window.mfLayoutInstances === 'undefined') {
|
|
||||||
window.mfLayoutInstances = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize a layout instance with drawer controls
|
|
||||||
* @param {string} layoutId - The unique ID of the layout (mf-layout-xxx)
|
|
||||||
*/
|
|
||||||
function initLayout(layoutId) {
|
|
||||||
const layoutElement = document.getElementById(layoutId);
|
|
||||||
|
|
||||||
if (!layoutElement) {
|
|
||||||
console.error(`Layout with id "${layoutId}" not found`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create layout controller object
|
|
||||||
const layoutController = {
|
|
||||||
layoutId: layoutId,
|
|
||||||
element: layoutElement,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get drawer element by side
|
|
||||||
* @param {string} side - 'left' or 'right'
|
|
||||||
* @returns {HTMLElement|null} The drawer element
|
|
||||||
*/
|
|
||||||
getDrawer: function (side) {
|
|
||||||
if (side !== 'left' && side !== 'right') {
|
|
||||||
console.error(`Invalid drawer side: "${side}". Must be "left" or "right".`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const drawerClass = side === 'left' ? '.mf-layout-left-drawer' : '.mf-layout-right-drawer';
|
|
||||||
return this.element.querySelector(drawerClass);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a drawer is currently open
|
|
||||||
* @param {string} side - 'left' or 'right'
|
|
||||||
* @returns {boolean} True if drawer is open
|
|
||||||
*/
|
|
||||||
isDrawerOpen: function (side) {
|
|
||||||
const drawer = this.getDrawer(side);
|
|
||||||
return drawer ? !drawer.classList.contains('collapsed') : false;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open a drawer
|
|
||||||
* @param {string} side - 'left' or 'right'
|
|
||||||
*/
|
|
||||||
openDrawer: function (side) {
|
|
||||||
const drawer = this.getDrawer(side);
|
|
||||||
if (drawer) {
|
|
||||||
drawer.classList.remove('collapsed');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close a drawer
|
|
||||||
* @param {string} side - 'left' or 'right'
|
|
||||||
*/
|
|
||||||
closeDrawer: function (side) {
|
|
||||||
const drawer = this.getDrawer(side);
|
|
||||||
if (drawer) {
|
|
||||||
drawer.classList.add('collapsed');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle a drawer between open and closed
|
|
||||||
* @param {string} side - 'left' or 'right'
|
|
||||||
*/
|
|
||||||
toggleDrawer: function (side) {
|
|
||||||
if (this.isDrawerOpen(side)) {
|
|
||||||
this.closeDrawer(side);
|
|
||||||
} else {
|
|
||||||
this.openDrawer(side);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize event listeners for toggle buttons
|
|
||||||
*/
|
|
||||||
initEventListeners: function () {
|
|
||||||
// Get all toggle buttons within this layout
|
|
||||||
const toggleButtons = this.element.querySelectorAll('[class*="mf-layout-toggle"]');
|
|
||||||
|
|
||||||
toggleButtons.forEach(button => {
|
|
||||||
button.addEventListener('click', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
const side = button.getAttribute('data-side');
|
|
||||||
if (side) {
|
|
||||||
this.toggleDrawer(side);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize event listeners
|
|
||||||
layoutController.initEventListeners();
|
|
||||||
|
|
||||||
// Store instance in global registry for programmatic access
|
|
||||||
window.mfLayoutInstances[layoutId] = layoutController;
|
|
||||||
|
|
||||||
// Log successful initialization
|
|
||||||
console.log(`Layout "${layoutId}" initialized successfully`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export for module environments if needed
|
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
|
||||||
module.exports = {initLayout};
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
class BaseCommands:
|
|
||||||
def __init__(self, owner):
|
|
||||||
self._owner = owner
|
|
||||||
self._id = owner.get_id()
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
class BaseControl:
|
|
||||||
def __init__(self, session, _id):
|
|
||||||
self.session = session
|
|
||||||
self._id = _id
|
|
||||||
|
|
||||||
def get_id(self):
|
|
||||||
return self._id
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
"""
|
|
||||||
Layout component for FastHTML applications.
|
|
||||||
|
|
||||||
This component provides a responsive layout with fixed header/footer,
|
|
||||||
optional collapsible left/right drawers, and a scrollable main content area.
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
import uuid
|
|
||||||
from typing import Literal
|
|
||||||
|
|
||||||
from fasthtml.common import *
|
|
||||||
|
|
||||||
from myfasthtml.controls.BaseCommands import BaseCommands
|
|
||||||
from myfasthtml.controls.BaseControl import BaseControl
|
|
||||||
from myfasthtml.controls.helpers import mk
|
|
||||||
from myfasthtml.core.commands import Command
|
|
||||||
from myfasthtml.icons.fluent import icon_panel_left_expand20_regular as left_drawer_icon
|
|
||||||
from myfasthtml.icons.fluent import icon_panel_right_expand20_regular as right_drawer_icon
|
|
||||||
|
|
||||||
logger = logging.getLogger("LayoutControl")
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LayoutState:
|
|
||||||
left_drawer_open: bool = True
|
|
||||||
|
|
||||||
|
|
||||||
class Commands(BaseCommands):
|
|
||||||
def toggle_left_drawer(self):
|
|
||||||
return Command("ToggleDrawer", "Toggle main layout drawer", self._owner.toggle_drawer, "left")
|
|
||||||
|
|
||||||
|
|
||||||
class Layout(BaseControl):
|
|
||||||
"""
|
|
||||||
A responsive layout component with header, footer, main content area,
|
|
||||||
and optional collapsible side drawers.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
app_name (str): Name of the application
|
|
||||||
left_drawer (bool): Whether to include a left drawer
|
|
||||||
right_drawer (bool): Whether to include a right drawer
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, session, app_name, left_drawer=True, right_drawer=True):
|
|
||||||
"""
|
|
||||||
Initialize the Layout component.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
app_name (str): Name of the application
|
|
||||||
left_drawer (bool): Enable left drawer. Default is True.
|
|
||||||
right_drawer (bool): Enable right drawer. Default is True.
|
|
||||||
"""
|
|
||||||
super().__init__(session, f"mf-layout-{str(uuid.uuid4())}")
|
|
||||||
self.app_name = app_name
|
|
||||||
self.left_drawer = left_drawer
|
|
||||||
self.right_drawer = right_drawer
|
|
||||||
|
|
||||||
# Content storage
|
|
||||||
self._header_content = None
|
|
||||||
self._footer_content = None
|
|
||||||
self._main_content = None
|
|
||||||
self._left_drawer_content = None
|
|
||||||
self._right_drawer_content = None
|
|
||||||
self._state = LayoutState()
|
|
||||||
self.commands = Commands(self)
|
|
||||||
|
|
||||||
# def set_header(self, content):
|
|
||||||
# """
|
|
||||||
# Set the header content.
|
|
||||||
#
|
|
||||||
# Args:
|
|
||||||
# content: FastHTML component(s) or content for the header
|
|
||||||
# """
|
|
||||||
# self._header_content = content
|
|
||||||
|
|
||||||
def set_footer(self, content):
|
|
||||||
"""
|
|
||||||
Set the footer content.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
content: FastHTML component(s) or content for the footer
|
|
||||||
"""
|
|
||||||
self._footer_content = content
|
|
||||||
|
|
||||||
def set_main(self, content):
|
|
||||||
"""
|
|
||||||
Set the main content area.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
content: FastHTML component(s) or content for the main area
|
|
||||||
"""
|
|
||||||
self._main_content = content
|
|
||||||
|
|
||||||
def set_left_drawer(self, content):
|
|
||||||
"""
|
|
||||||
Set the left drawer content.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
content: FastHTML component(s) or content for the left drawer
|
|
||||||
"""
|
|
||||||
if self.left_drawer:
|
|
||||||
self._left_drawer_content = content
|
|
||||||
|
|
||||||
def set_right_drawer(self, content):
|
|
||||||
"""
|
|
||||||
Set the right drawer content.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
content: FastHTML component(s) or content for the right drawer
|
|
||||||
"""
|
|
||||||
if self.right_drawer:
|
|
||||||
self._right_drawer_content = content
|
|
||||||
|
|
||||||
def toggle_drawer(self, side: Literal["left", "right"]):
|
|
||||||
logger.debug(f"Toggle drawer: {side=}, {self._state.left_drawer_open=}")
|
|
||||||
if side == "left":
|
|
||||||
self._state.left_drawer_open = not self._state.left_drawer_open
|
|
||||||
return self._mk_left_drawer_icon(), self._mk_left_drawer()
|
|
||||||
elif side == "right":
|
|
||||||
self._state.right_drawer_open = not self._state.right_drawer_open
|
|
||||||
return Div(), self._mk_right_drawer()
|
|
||||||
else:
|
|
||||||
raise ValueError("Invalid drawer side")
|
|
||||||
|
|
||||||
def _mk_header(self):
|
|
||||||
"""
|
|
||||||
Render the header component.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Header: FastHTML Header component
|
|
||||||
"""
|
|
||||||
return Header(
|
|
||||||
self._mk_left_drawer_icon(),
|
|
||||||
cls="mf-layout-header"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _mk_footer(self):
|
|
||||||
"""
|
|
||||||
Render the footer component.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Footer: FastHTML Footer component
|
|
||||||
"""
|
|
||||||
footer_content = self._footer_content if self._footer_content else ""
|
|
||||||
return Footer(
|
|
||||||
footer_content,
|
|
||||||
cls="mf-layout-footer footer sm:footer-horizontal"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _mk_main(self):
|
|
||||||
"""
|
|
||||||
Render the main content area.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Main: FastHTML Main component
|
|
||||||
"""
|
|
||||||
main_content = self._main_content if self._main_content else ""
|
|
||||||
return Main(
|
|
||||||
main_content,
|
|
||||||
cls="mf-layout-main"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _mk_left_drawer(self):
|
|
||||||
"""
|
|
||||||
Render the left drawer if enabled.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Div or None: FastHTML Div component for left drawer, or None if disabled
|
|
||||||
"""
|
|
||||||
if not self.left_drawer:
|
|
||||||
return None
|
|
||||||
|
|
||||||
print(f"{self._state.left_drawer_open=}")
|
|
||||||
|
|
||||||
drawer_content = self._left_drawer_content if self._left_drawer_content else ""
|
|
||||||
return Div(
|
|
||||||
drawer_content,
|
|
||||||
id=f"{self._id}_ld",
|
|
||||||
cls=f"mf-layout-drawer mf-layout-left-drawer {'collapsed' if not self._state.left_drawer_open else ''}",
|
|
||||||
**{"data-side": "left"}
|
|
||||||
)
|
|
||||||
|
|
||||||
def _mk_right_drawer(self):
|
|
||||||
"""
|
|
||||||
Render the right drawer if enabled.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Div or None: FastHTML Div component for right drawer, or None if disabled
|
|
||||||
"""
|
|
||||||
if not self.right_drawer:
|
|
||||||
return None
|
|
||||||
|
|
||||||
drawer_content = self._right_drawer_content if self._right_drawer_content else ""
|
|
||||||
return Div(
|
|
||||||
drawer_content,
|
|
||||||
cls="mf-layout-drawer mf-layout-right-drawer",
|
|
||||||
id=f"{self._id}_rd",
|
|
||||||
**{"data-side": "right"}
|
|
||||||
)
|
|
||||||
|
|
||||||
def _mk_left_drawer_icon(self):
|
|
||||||
return mk.icon(right_drawer_icon if self._state.left_drawer_open else left_drawer_icon,
|
|
||||||
id=f"{self._id}_ldi",
|
|
||||||
command=self.commands.toggle_left_drawer())
|
|
||||||
|
|
||||||
def render(self):
|
|
||||||
"""
|
|
||||||
Render the complete layout.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Div: Complete layout as FastHTML Div component
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Wrap everything in a container div
|
|
||||||
return Div(
|
|
||||||
self._mk_header(),
|
|
||||||
self._mk_left_drawer(),
|
|
||||||
self._mk_main(),
|
|
||||||
self._mk_right_drawer(),
|
|
||||||
self._mk_footer(),
|
|
||||||
Script(f"initLayout('{self._id}');"),
|
|
||||||
id=self._id,
|
|
||||||
cls="mf-layout",
|
|
||||||
**{
|
|
||||||
"data-left-drawer": str(self.left_drawer).lower(),
|
|
||||||
"data-right-drawer": str(self.right_drawer).lower()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def __ft__(self):
|
|
||||||
"""
|
|
||||||
FastHTML magic method for rendering.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Div: The rendered layout
|
|
||||||
"""
|
|
||||||
return self.render()
|
|
||||||
@@ -36,7 +36,6 @@ class BaseCommand:
|
|||||||
def get_htmx_params(self):
|
def get_htmx_params(self):
|
||||||
return self._htmx_extra | {
|
return self._htmx_extra | {
|
||||||
"hx-post": f"{ROUTE_ROOT}{Routes.Commands}",
|
"hx-post": f"{ROUTE_ROOT}{Routes.Commands}",
|
||||||
"hx-swap": "outerHTML",
|
|
||||||
"hx-vals": f'{{"c_id": "{self.id}"}}',
|
"hx-vals": f'{{"c_id": "{self.id}"}}',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,17 +124,11 @@ class Command(BaseCommand):
|
|||||||
for data in self._bindings:
|
for data in self._bindings:
|
||||||
remove_event_listener(ObservableEvent.AFTER_PROPERTY_CHANGE, data, "", binding_result_callback)
|
remove_event_listener(ObservableEvent.AFTER_PROPERTY_CHANGE, data, "", binding_result_callback)
|
||||||
|
|
||||||
# Set the hx-swap-oob attribute on all elements returned by the callback
|
|
||||||
if isinstance(ret, (list, tuple)):
|
|
||||||
for r in ret[1:]:
|
|
||||||
if hasattr(r, 'attrs'):
|
|
||||||
r.attrs["hx-swap-oob"] = "true"
|
|
||||||
|
|
||||||
if not ret_from_bindings:
|
if not ret_from_bindings:
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
if isinstance(ret, (list, tuple)):
|
if isinstance(ret, list):
|
||||||
return list(ret) + ret_from_bindings
|
return ret + ret_from_bindings
|
||||||
else:
|
else:
|
||||||
return [ret] + ret_from_bindings
|
return [ret] + ret_from_bindings
|
||||||
|
|
||||||
|
|||||||
@@ -54,10 +54,7 @@ def create_app(daisyui: Optional[bool] = True,
|
|||||||
:return: A tuple containing the FastHtml application instance and the associated router.
|
:return: A tuple containing the FastHtml application instance and the associated router.
|
||||||
:rtype: Any
|
:rtype: Any
|
||||||
"""
|
"""
|
||||||
hdrs = [
|
hdrs = [Link(href="/myfasthtml/myfasthtml.css", rel="stylesheet", type="text/css")]
|
||||||
Link(href="/myfasthtml/myfasthtml.css", rel="stylesheet", type="text/css"),
|
|
||||||
Script(src="/myfasthtml/myfasthtml.js"),
|
|
||||||
]
|
|
||||||
|
|
||||||
if daisyui:
|
if daisyui:
|
||||||
hdrs += [
|
hdrs += [
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from dataclasses import dataclass
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fasthtml.components import Button, Div
|
from fasthtml.components import Button
|
||||||
from myutils.observable import make_observable, bind
|
from myutils.observable import make_observable, bind
|
||||||
|
|
||||||
from myfasthtml.core.commands import Command, CommandsManager
|
from myfasthtml.core.commands import Command, CommandsManager
|
||||||
@@ -93,32 +93,3 @@ def test_i_can_bind_a_command_to_an_observable_2():
|
|||||||
res = command.execute()
|
res = command.execute()
|
||||||
|
|
||||||
assert res == ["another 1", "another 2", ("hello", "new value")]
|
assert res == ["another 1", "another 2", ("hello", "new value")]
|
||||||
|
|
||||||
|
|
||||||
def test_by_default_swap_is_set_to_outer_html():
|
|
||||||
command = Command('test', 'Command description', callback)
|
|
||||||
elt = Button()
|
|
||||||
updated = command.bind_ft(elt)
|
|
||||||
|
|
||||||
expected = Button(hx_post=f"{ROUTE_ROOT}{Routes.Commands}", hx_swap="outerHTML")
|
|
||||||
|
|
||||||
assert matches(updated, expected)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("return_values", [
|
|
||||||
[Div(), Div(), "hello", Div()], # list
|
|
||||||
(Div(), Div(), "hello", Div()) # tuple
|
|
||||||
])
|
|
||||||
def test_swap_oob_is_automatically_set_when_multiple_elements_are_returned(return_values):
|
|
||||||
"""Test that hx-swap-oob is automatically set, but not for the first."""
|
|
||||||
|
|
||||||
def another_callback():
|
|
||||||
return return_values
|
|
||||||
|
|
||||||
command = Command('test', 'Command description', another_callback)
|
|
||||||
|
|
||||||
res = command.execute()
|
|
||||||
|
|
||||||
assert "hx_swap_oob" not in res[0].attrs
|
|
||||||
assert res[1].attrs["hx-swap-oob"] == "true"
|
|
||||||
assert res[3].attrs["hx-swap-oob"] == "true"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user