From 2e7cff5eee8ffeae5a0b674023f127298a46a102 Mon Sep 17 00:00:00 2001 From: Matteo Benedetto Date: Sun, 28 Sep 2025 17:29:16 +0200 Subject: [PATCH] feat: Enhance button and dialog components with new event handling and styling options - Updated button function to accept icon and color parameters for better customization. - Introduced button_with_handler for enhanced event handling with JavaScript and Python functions. - Added dialog component with open/close functionality and persistent option. - Implemented notify function for displaying notifications with customizable options. - Added script function for including custom JavaScript in the application. - Improved component structure to support Vue refs and reactive data binding. - Updated generator to process scripts and manage external JavaScript files. - Created tests for button handling and simplified card usage demonstrating new features. --- BG_SCRIPT_IMPLEMENTATION_SUMMARY.md | 203 +++++++++++++++ badgui/__init__.py | 30 ++- badgui/core.py | 379 +++++++++++++++++++++++++++- badgui/generator.py | 124 ++++++++- button_comparison_test.py | 81 ++++++ simple_button_test.py | 78 ++++++ simplified_card_test.py | 74 ++++++ vmodel.js | 37 +++ vmodel_vue_ref_test.py | 50 ++++ 9 files changed, 1037 insertions(+), 19 deletions(-) create mode 100644 BG_SCRIPT_IMPLEMENTATION_SUMMARY.md create mode 100644 button_comparison_test.py create mode 100644 simple_button_test.py create mode 100644 simplified_card_test.py create mode 100644 vmodel.js create mode 100644 vmodel_vue_ref_test.py diff --git a/BG_SCRIPT_IMPLEMENTATION_SUMMARY.md b/BG_SCRIPT_IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..eef9041 --- /dev/null +++ b/BG_SCRIPT_IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,203 @@ +# BadGUI bg.script() Component Implementation Summary + +## Overview + +The `bg.script()` component has been successfully implemented in BadGUI v0.2.0, providing a clean and flexible way to include custom JavaScript in your applications. This component bridges Python declarative UI code with JavaScript runtime functionality. + +## Usage Patterns + +### 1. Inline JavaScript +```python +# Simple inline script +bg.script(""" + console.log('Hello from BadGUI!'); + + window.myFunction = function() { + alert('Custom function executed!'); + }; +""") +``` + +### 2. External JavaScript Files +```python +# From file path +bg.script(src="/path/to/script.js") + +# From URL +bg.script(src="https://cdn.example.com/library.js") +``` + +### 3. Script Attributes +```python +# Deferred execution +bg.script("console.log('Deferred script');", defer=True) + +# ES6 module +bg.script("export const myVar = 'test';", type="module") + +# Async loading (using **kwargs to avoid Python keyword conflict) +bg.script("console.log('Async script');", **{"async": True}) +``` + +### 4. Vue.js Integration +```python +# Vue 3 Composition API integration +bg.script(""" + // Reactive data + const counter = ref(0); + const message = ref('Hello BadGUI!'); + + // Computed properties + const doubled = computed(() => counter.value * 2); + + // Methods + const increment = () => counter.value++; + + // Lifecycle hooks + onMounted(() => { + console.log('Component mounted with BadGUI scripts!'); + }); +""") +``` + +## Implementation Details + +### Component Architecture +- **Component Type**: `script` +- **Props Handling**: Special `_script_content`, `_external_file`, `_is_inline` props +- **Template Generation**: Custom `_generate_script_template()` method +- **Build Integration**: Scripts processed during Vue.js project generation + +### Generator Integration +- **Script Collection**: `_process_page_scripts()` method collects all scripts per page +- **File Copying**: External files copied to `public/js/` directory +- **HTML Injection**: External scripts added to `index.html` as ` +``` + +### Index.html Integration +```html + + + + + + + + + + +``` + +### Project Structure +``` +project-output/ +├── src/ +│ ├── pages/ +│ │ └── PageName.vue (with integrated scripts) +│ └── ... +├── public/ +│ ├── js/ +│ │ └── external-script.js (copied external files) +│ └── ... +└── index.html (with script tags) +``` + +## Key Features + +### ✅ Implemented +- Inline JavaScript code inclusion +- External file inclusion with automatic copying +- Script attributes (defer, async, type, etc.) +- Vue.js 3 Composition API integration +- Global function definitions +- Automatic window object access in templates +- HTML injection for external scripts +- Build-time script processing + +### 🔧 Technical Highlights +- **No Template Rendering**: Script components render as HTML comments in templates +- **Build-Time Processing**: Scripts processed during project generation, not runtime +- **Vue Integration**: Seamless integration with Vue 3 reactive system +- **File Management**: Automatic copying and path resolution for external files +- **Conflict Resolution**: Proper handling of reserved Python keywords (`async`) + +### 🎯 Use Cases +- Custom utility functions +- External library integration +- Vue.js reactive data and methods +- DOM manipulation and event handling +- API integration patterns +- Complex business logic +- Third-party service integration + +## Example Integration + +```python +import badgui as bg + +with bg.page("/", "ScriptDemo", "Script Demo"): + with bg.column().classes("q-pa-lg"): + bg.label("BadGUI Script Integration").classes("text-h3") + + # Inline reactive data + bg.script(""" + const count = ref(0); + const increment = () => count.value++; + """) + + # External utilities + bg.script(src="./utils.js") + + # UI with JavaScript integration + bg.label("Count: {{ count }}").classes("text-h4") + bg.button_with_handler("Increment", js_handler="increment") + bg.button_with_handler("External Function", js_handler="() => window.myUtility()") + +if __name__ == "__main__": + bg.dev() +``` + +## Next Steps + +The `bg.script()` component provides a solid foundation for JavaScript integration. Future enhancements could include: + +- **TypeScript Support**: Type checking for inline scripts +- **Module System**: Better ES6 module integration +- **Server Communication**: Python callback patterns for backend integration +- **Hot Reload**: Script changes trigger browser updates +- **Validation**: JavaScript syntax checking during build +- **Minification**: Script optimization for production builds + +The implementation successfully bridges the gap between Python's declarative UI approach and JavaScript's runtime interactivity, enabling developers to build rich, interactive web applications with BadGUI. \ No newline at end of file diff --git a/badgui/__init__.py b/badgui/__init__.py index 870c4a4..6743a04 100644 --- a/badgui/__init__.py +++ b/badgui/__init__.py @@ -20,14 +20,18 @@ def label(text: str, **kwargs): """Create a label component.""" return app.label(text, **kwargs) -def button(text: str, **kwargs): +def button(text: str, icon: str = None, color: str = 'primary', **kwargs): """Create a button component.""" - return app.button(text, **kwargs) + return app.button(text, icon, color, **kwargs) def input(placeholder: str = "", **kwargs): """Create an input component.""" return app.input(placeholder, **kwargs) +def div(**kwargs): + """Create a div container component.""" + return app.div(**kwargs) + def build(output_dir: str = "./badgui-output", **kwargs): """Build the Vue.js/Quasar project.""" return app.build(output_dir, **kwargs) @@ -92,6 +96,28 @@ def icon(name: str, size: str = None, color: str = None, **kwargs): """Create an icon component.""" return app.icon(name, size, color, **kwargs) +# Event handling and interactive components +def button_with_handler(text: str, on_click=None, js_handler: str = None, **kwargs): + """Create a button with enhanced event handling.""" + return app.button_with_handler(text, on_click, js_handler, **kwargs) + +def dialog(opened: bool = False, persistent: bool = False, **kwargs): + """Create a dialog component.""" + return app.dialog(opened, persistent, **kwargs) + +def notify(message: str, type_: str = None, position: str = "bottom", + close_button: str = None, color: str = None, multiline: bool = False, **kwargs): + """Create a notification.""" + return app.notify(message, type_, position, close_button, color, multiline, **kwargs) + +def on_event(component, event_type: str, handler=None, js_handler: str = None, **kwargs): + """Add event handler to a component.""" + return app.on_event(component, event_type, handler, js_handler, **kwargs) + +def script(content=None, src=None, **kwargs): + """Add custom JavaScript to the page.""" + return app.script(content, src, **kwargs) + def dev(port: int = 9000, host: str = "localhost", auto_reload: bool = False, project_name: str = "badgui-dev", **kwargs): """Create a temporary build and run it in development mode.""" return app.dev(port, host, auto_reload, project_name, **kwargs) \ No newline at end of file diff --git a/badgui/core.py b/badgui/core.py index e418cca..7773f73 100644 --- a/badgui/core.py +++ b/badgui/core.py @@ -19,6 +19,7 @@ class Component: self.id = f"{component_type}_{id(self)}" self._classes = props.get('classes', []) self._style = props.get('style', {}) + self._vue_ref = None def add_child(self, child: 'Component'): """Add a child component.""" @@ -46,11 +47,19 @@ class Component: """Convert component to Vue template string.""" spaces = " " * indent + # Special handling for script components + if self.component_type == 'script': + return self._generate_script_template(indent) + # Build attributes attrs = [] - # Add ID - attrs.append(f'id="{self.id}"') + # Add ID (use manual id if provided, otherwise automatic) + manual_id = self._props.get('id') + if manual_id: + attrs.append(f'id="{manual_id}"') + else: + attrs.append(f'id="{self.id}"') # Add classes if self._classes: @@ -71,7 +80,7 @@ class Component: # Add other props for key, value in self._props.items(): - if key not in ['classes', 'style']: + if key not in ['classes', 'style', 'id']: # Skip id since we handle it above if isinstance(value, bool): if value: attrs.append(key) @@ -85,7 +94,19 @@ class Component: attrs_str = " " + " ".join(attrs) if attrs else "" # Generate template - if self.children: + if self.component_type == 'card' and self._props.get('_card_title'): + # Simplified card with automatic title section + title = self._props.get('_card_title') + template = f"{spaces}<{self.get_vue_tag()}{attrs_str}>\n" + template += f"{spaces} \n" + template += f"{spaces}
{title}
\n" + template += f"{spaces}
\n" + template += f"{spaces} \n" + for child in self.children: + template += child.to_vue_template(indent + 2) + template += f"{spaces} \n" + template += f"{spaces}\n" + elif self.children: template = f"{spaces}<{self.get_vue_tag()}{attrs_str}>\n" for child in self.children: template += child.to_vue_template(indent + 1) @@ -118,6 +139,8 @@ class Component: 'footer': 'q-footer', 'image': 'q-img', 'icon': 'q-icon', + 'dialog': 'q-dialog', + 'script': 'script', 'div': 'div', 'router-link': 'router-link' } @@ -127,6 +150,36 @@ class Component: """Get the text content of the component.""" return self._props.get('text', self._props.get('content', '')) + def _generate_script_template(self, indent: int = 0) -> str: + """Generate script tag template for script components.""" + spaces = " " * indent + + # Script components are usually not rendered in the main template + # They are handled separately during build + if self._props.get('_is_inline'): + # Inline script - return as HTML comment for template + content = self._props.get('_script_content', '') + return f"{spaces}\n" + elif self._props.get('_external_file'): + # External file - return as HTML comment for template + filename = self._props.get('_external_file') + return f"{spaces}\n" + elif self._props.get('src'): + # External URL - might be rendered in final build + src = self._props.get('src') + attrs = [] + for key, value in self._props.items(): + if key not in ['_script_content', '_is_inline', '_external_file', '_file_content']: + if isinstance(value, bool) and value: + attrs.append(key) + elif not isinstance(value, bool): + attrs.append(f'{key}="{value}"') + + attrs_str = " " + " ".join(attrs) if attrs else "" + return f"{spaces}\n" + + return f"{spaces}\n" + def classes(self, add: str = None, *, remove: str = None, replace: str = None): """Add, remove, or replace CSS classes (Tailwind, Quasar, custom).""" # Get current classes list @@ -200,6 +253,62 @@ class Component: self._props[key] = value return self + def vue_ref(self, ref_name: str): + """Set a Vue ref name for this component to access it from JavaScript.""" + self._vue_ref = ref_name + self._props['ref'] = ref_name + return self + + def get_vue_ref(self) -> Optional[str]: + """Get the Vue ref name for this component.""" + return self._vue_ref + + def bind_text(self, reactive_var: str): + """Bind component text content to a reactive variable using v-model or text interpolation.""" + if self.component_type == 'label': + # For labels, use text interpolation (Vue templates don't need .value) + self._props['text'] = f"{{{{{reactive_var}}}}}" + else: + # For other components, try v-model + self._props['v-model'] = reactive_var + return self + + def bind_value(self, reactive_var: str): + """Bind component value to a reactive variable using v-model.""" + self._props['v-model'] = reactive_var + return self + + def on(self, event_type: str, handler: Union[str, callable]): + """ + Add an event handler to the component (NiceGUI-style). + + Args: + event_type: Event type (e.g., 'click', 'input', 'change') + handler: JavaScript function name/expression or Python function (for future server support) + + Usage: + button.on('click', 'myJsFunction') # JavaScript function name + button.on('click', '() => alert("Hi")') # JavaScript expression + button.on('click', 'handleClick') # JavaScript function name + input.on('input', 'handleInput') # JavaScript function name + """ + if callable(handler): + # Python handler - for future server-side support + # For now, convert to JavaScript function name + handler_name = handler.__name__ if hasattr(handler, '__name__') else str(handler) + self._props[f'@{event_type}'] = handler_name + elif isinstance(handler, str): + # JavaScript handler (function name or expression) + self._props[f'@{event_type}'] = handler + else: + raise ValueError("Handler must be a string (JavaScript) or callable (Python function)") + + return self + + def on_click(self, handler: Union[str, callable]): + """Convenience method for click events.""" + return self.on('click', handler) + def style(self, *args, **kwargs): """Set or update inline CSS styles.""" if not isinstance(self._style, dict): @@ -284,11 +393,15 @@ class App: component = Component('label', text=text, **kwargs) return self._add_component(component) - def button(self, text: str, on_click: str = None, **kwargs) -> Component: + def button(self, text: str, icon: str = None, color: str = 'primary', **kwargs) -> Component: """Create a button component.""" props = {'label': text, **kwargs} - if on_click: - props['@click'] = on_click + + if icon: + props['icon'] = icon + if color: + props['color'] = color + component = Component('button', **props) return self._add_component(component) @@ -300,6 +413,23 @@ class App: component = Component('input', **props) return self._add_component(component) + @contextmanager + def div(self, **kwargs): + """Create a div container component.""" + component = Component('div', **kwargs) + component = self._add_component(component) + + # Enter container context + page = self._get_current_page() + page._container_stack.append(page.current_container) + page.current_container = component + + try: + yield component + finally: + # Exit container context + page.current_container = page._container_stack.pop() + def page(self, path: str, name: str = None, title: str = None): """Create or switch to a page.""" if name is None: @@ -325,15 +455,30 @@ class App: return self._add_component(component) @contextmanager - def card(self, **kwargs): - """Create a card container with shadow and padding.""" - classes = kwargs.pop('classes', []) # Remove classes from kwargs to avoid conflict + def card(self, title: str = None, flat: bool = False, **kwargs): + """ + Create a simplified card container. + + Args: + title: Optional title for the card + flat: If True, removes the shadow for a flat appearance + **kwargs: Additional properties + """ + classes = kwargs.pop('classes', []) if isinstance(classes, str): classes = [classes] elif not isinstance(classes, list): classes = [] - component = Component('card', classes=classes, **kwargs) + # Add default card styling + if flat: + classes.append('q-card--flat') + + props = {'classes': classes, **kwargs} + if title: + props['_card_title'] = title + + component = Component('card', **props) component = self._add_component(component) # Enter container context @@ -602,6 +747,218 @@ class App: component = Component('icon', **props) return self._add_component(component) + # Event Handling and Interactive Components + + def button_with_handler(self, text: str, on_click=None, js_handler: str = None, **kwargs) -> Component: + """Create a button with enhanced event handling.""" + props = {'label': text, **kwargs} + + # Handle different types of click handlers + if on_click: + if callable(on_click): + # Python function - we'll need to generate a unique handler ID + handler_id = f"handler_{id(on_click)}" + props['@click'] = f"handleEvent('{handler_id}', $event)" + # TODO: Register the Python handler for server communication + elif isinstance(on_click, str): + if on_click.startswith('js:'): + # JavaScript function + props['@click'] = on_click[3:] # Remove 'js:' prefix + else: + # Assume it's a method name or Vue expression + props['@click'] = on_click + + if js_handler: + # Custom JavaScript handler + props['@click'] = js_handler + + component = Component('button', **props) + return self._add_component(component) + + @contextmanager + def dialog(self, opened: bool = False, persistent: bool = False, **kwargs): + """Create a dialog component (NiceGUI-style).""" + classes = kwargs.pop('classes', []) + if isinstance(classes, str): + classes = [classes] + elif not isinstance(classes, list): + classes = [] + + props = {'v-model': 'dialogOpen', **kwargs} + if persistent: + props['persistent'] = True + + component = Component('dialog', classes=classes, **props) + component = self._add_component(component) + + # Add dialog methods + component.opened = opened + component.open = lambda: self._dialog_open(component) + component.close = lambda: self._dialog_close(component) + component.submit = lambda result: self._dialog_submit(component, result) + + # Enter container context + page = self._get_current_page() + page._container_stack.append(page.current_container) + page.current_container = component + + try: + yield component + finally: + # Exit container context + page.current_container = page._container_stack.pop() + + def _dialog_open(self, dialog_component): + """Open a dialog - this would be implemented via JS calls in real app.""" + # For static generation, we'll add the JS methods to the component + dialog_component._props['ref'] = f"dialog_{dialog_component.id}" + return f"this.$refs.dialog_{dialog_component.id}.show()" + + def _dialog_close(self, dialog_component): + """Close a dialog.""" + return f"this.$refs.dialog_{dialog_component.id}.hide()" + + def _dialog_submit(self, dialog_component, result): + """Submit a dialog with a result.""" + return f"this.$refs.dialog_{dialog_component.id}.hide(); this.dialogResult = '{result}'" + + def notify(self, message: str, type_: str = None, position: str = "bottom", + close_button: str = None, color: str = None, multiline: bool = False, + js_call: bool = True, **kwargs): + """Create a notification (NiceGUI-style).""" + # Build notification options + notify_options = { + 'message': message, + 'position': position, + **kwargs + } + + if type_: + notify_options['type'] = type_ + if color: + notify_options['color'] = color + if close_button: + notify_options['actions'] = [{'label': close_button, 'color': 'white', 'handler': 'dismiss'}] + if multiline: + notify_options['multiLine'] = True + + # For static generation, create a notification call + if js_call: + # Generate JavaScript call for Vue component + options_str = json.dumps(notify_options) + js_code = f"this.$q.notify({options_str})" + + # Add to current page's notification calls (we'll collect these during build) + page = self._get_current_page() + if not hasattr(page, '_notifications'): + page._notifications = [] + page._notifications.append(js_code) + + return js_code + else: + # Return options for manual handling + return notify_options + + def script(self, content: Union[str, os.PathLike] = None, src: Union[str, os.PathLike] = None, + import_mode: bool = False, **kwargs) -> Component: + """ + Add custom JavaScript to the page. + + Args: + content: JavaScript code as a string + src: Path to external JavaScript file + import_mode: If True, external files are imported as ES6 modules in the component + **kwargs: Additional attributes for the script tag + + Usage: + # Inline script + bg.script("console.log('Hello from BadGUI!');") + + # External file (traditional script tag) + bg.script(src="path/to/script.js") + + # External file (ES6 module import) + bg.script(src="path/to/utils.js", import_mode=True) + + # With attributes + bg.script(content="...", type="module", defer=True) + """ + props = {**kwargs} + + if content is not None and src is not None: + raise ValueError("Cannot specify both 'content' and 'src' parameters") + + if content is None and src is None: + raise ValueError("Must specify either 'content' or 'src' parameter") + + if content is not None: + # Inline script content + if os.path.isfile(content): + # If content looks like a file path, read it + try: + with open(content, 'r', encoding='utf-8') as f: + props['_script_content'] = f.read() + props['_is_inline'] = True + except (OSError, IOError): + # If file reading fails, treat as literal content + props['_script_content'] = str(content) + props['_is_inline'] = True + else: + # Literal JavaScript content + props['_script_content'] = str(content) + props['_is_inline'] = True + + elif src is not None: + # External file reference + if os.path.isfile(src): + # File exists, we'll copy it to the build output + with open(src, 'r', encoding='utf-8') as f: + file_content = f.read() + + # Store file content and path for build process + filename = os.path.basename(src) + props['_external_file'] = filename + props['_file_content'] = file_content + props['_import_mode'] = import_mode + + if import_mode: + # For import mode, we'll copy to src/utils/ and import + props['_import_path'] = f"../utils/{filename}" + else: + # Traditional script tag in public/js/ + props['src'] = f"/js/{filename}" + else: + # Assume it's a URL or will be provided later + props['src'] = str(src) + props['_import_mode'] = False + + # Add to current page's script collection + page = self._get_current_page() + if not hasattr(page, '_scripts'): + page._scripts = [] + + component = Component('script', **props) + page._scripts.append(component) + + # Also add as regular component for template generation + return self._add_component(component) + + def on_event(self, component: Component, event_type: str, handler=None, js_handler: str = None, **kwargs): + """Add event handler to a component (NiceGUI-style).""" + if handler and callable(handler): + # Python handler - generate unique ID and register + handler_id = f"handler_{id(handler)}_{event_type}" + component._props[f'@{event_type}'] = f"handleEvent('{handler_id}', $event)" + # TODO: Store handler for server communication + elif js_handler: + # JavaScript handler + component._props[f'@{event_type}'] = js_handler + elif isinstance(handler, str): + # String handler (Vue expression or method name) + component._props[f'@{event_type}'] = handler + + return component + def build(self, output_dir: str = "./badgui-output", project_name: str = "badgui-app", **kwargs): """Build the Vue.js/Quasar project.""" from .generator import VueGenerator diff --git a/badgui/generator.py b/badgui/generator.py index cabbbda..4132277 100644 --- a/badgui/generator.py +++ b/badgui/generator.py @@ -221,6 +221,9 @@ module.exports = configure(function (ctx) { # Generate the template from page components template_content = self._generate_vue_template_for_page(page) + # Process scripts for this page + script_imports, script_content = self._process_page_scripts(page, output_dir) + page_content = f'''