Browse Source

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.
master
Matteo Benedetto 3 months ago
parent
commit
2e7cff5eee
  1. 203
      BG_SCRIPT_IMPLEMENTATION_SUMMARY.md
  2. 30
      badgui/__init__.py
  3. 379
      badgui/core.py
  4. 120
      badgui/generator.py
  5. 81
      button_comparison_test.py
  6. 78
      simple_button_test.py
  7. 74
      simplified_card_test.py
  8. 37
      vmodel.js
  9. 50
      vmodel_vue_ref_test.py

203
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 `<script src="">` tags
- **Vue Integration**: Inline scripts included in component `<script setup>` section
### Vue.js Template Integration
- **Global Access**: `window` object made available via `const window = globalThis.window`
- **Quasar Integration**: `useQuasar()` provides `$q` for notifications/dialogs
- **Event Handlers**: Button handlers can call global functions with `() => window.myFunction()`
## Generated Output Structure
### Vue Component Template
```vue
<template>
<q-page class="row items-center justify-evenly">
<!-- Component templates -->
<!-- Script components show as HTML comments -->
</q-page>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useQuasar } from 'quasar'
// Component state
const state = ref({})
const $q = useQuasar()
// Make window object available to template
const window = globalThis.window
// Inline script content inserted here
// Vue component reactive data and methods
</script>
```
### Index.html Integration
```html
<!DOCTYPE html>
<html>
<head>
<!-- Meta tags -->
<script src="/js/external-script.js"></script>
</head>
<body>
<!-- quasar:entry-point -->
</body>
</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.

30
badgui/__init__.py

@ -20,14 +20,18 @@ def label(text: str, **kwargs):
"""Create a label component.""" """Create a label component."""
return app.label(text, **kwargs) 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.""" """Create a button component."""
return app.button(text, **kwargs) return app.button(text, icon, color, **kwargs)
def input(placeholder: str = "", **kwargs): def input(placeholder: str = "", **kwargs):
"""Create an input component.""" """Create an input component."""
return app.input(placeholder, **kwargs) 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): def build(output_dir: str = "./badgui-output", **kwargs):
"""Build the Vue.js/Quasar project.""" """Build the Vue.js/Quasar project."""
return app.build(output_dir, **kwargs) 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.""" """Create an icon component."""
return app.icon(name, size, color, **kwargs) 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): 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.""" """Create a temporary build and run it in development mode."""
return app.dev(port, host, auto_reload, project_name, **kwargs) return app.dev(port, host, auto_reload, project_name, **kwargs)

379
badgui/core.py

@ -19,6 +19,7 @@ class Component:
self.id = f"{component_type}_{id(self)}" self.id = f"{component_type}_{id(self)}"
self._classes = props.get('classes', []) self._classes = props.get('classes', [])
self._style = props.get('style', {}) self._style = props.get('style', {})
self._vue_ref = None
def add_child(self, child: 'Component'): def add_child(self, child: 'Component'):
"""Add a child component.""" """Add a child component."""
@ -46,11 +47,19 @@ class Component:
"""Convert component to Vue template string.""" """Convert component to Vue template string."""
spaces = " " * indent spaces = " " * indent
# Special handling for script components
if self.component_type == 'script':
return self._generate_script_template(indent)
# Build attributes # Build attributes
attrs = [] attrs = []
# Add ID # Add ID (use manual id if provided, otherwise automatic)
attrs.append(f'id="{self.id}"') manual_id = self._props.get('id')
if manual_id:
attrs.append(f'id="{manual_id}"')
else:
attrs.append(f'id="{self.id}"')
# Add classes # Add classes
if self._classes: if self._classes:
@ -71,7 +80,7 @@ class Component:
# Add other props # Add other props
for key, value in self._props.items(): 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 isinstance(value, bool):
if value: if value:
attrs.append(key) attrs.append(key)
@ -85,7 +94,19 @@ class Component:
attrs_str = " " + " ".join(attrs) if attrs else "" attrs_str = " " + " ".join(attrs) if attrs else ""
# Generate template # 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} <q-card-section>\n"
template += f"{spaces} <div class=\"text-h6\">{title}</div>\n"
template += f"{spaces} </q-card-section>\n"
template += f"{spaces} <q-card-section>\n"
for child in self.children:
template += child.to_vue_template(indent + 2)
template += f"{spaces} </q-card-section>\n"
template += f"{spaces}</{self.get_vue_tag()}>\n"
elif self.children:
template = f"{spaces}<{self.get_vue_tag()}{attrs_str}>\n" template = f"{spaces}<{self.get_vue_tag()}{attrs_str}>\n"
for child in self.children: for child in self.children:
template += child.to_vue_template(indent + 1) template += child.to_vue_template(indent + 1)
@ -118,6 +139,8 @@ class Component:
'footer': 'q-footer', 'footer': 'q-footer',
'image': 'q-img', 'image': 'q-img',
'icon': 'q-icon', 'icon': 'q-icon',
'dialog': 'q-dialog',
'script': 'script',
'div': 'div', 'div': 'div',
'router-link': 'router-link' 'router-link': 'router-link'
} }
@ -127,6 +150,36 @@ class Component:
"""Get the text content of the component.""" """Get the text content of the component."""
return self._props.get('text', self._props.get('content', '')) 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}<!-- Inline script: {len(content)} chars -->\n"
elif self._props.get('_external_file'):
# External file - return as HTML comment for template
filename = self._props.get('_external_file')
return f"{spaces}<!-- External script: {filename} -->\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}<script{attrs_str}></script>\n"
return f"{spaces}<!-- Script component -->\n"
def classes(self, add: str = None, *, remove: str = None, replace: str = None): def classes(self, add: str = None, *, remove: str = None, replace: str = None):
"""Add, remove, or replace CSS classes (Tailwind, Quasar, custom).""" """Add, remove, or replace CSS classes (Tailwind, Quasar, custom)."""
# Get current classes list # Get current classes list
@ -200,6 +253,62 @@ class Component:
self._props[key] = value self._props[key] = value
return self 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): def style(self, *args, **kwargs):
"""Set or update inline CSS styles.""" """Set or update inline CSS styles."""
if not isinstance(self._style, dict): if not isinstance(self._style, dict):
@ -284,11 +393,15 @@ class App:
component = Component('label', text=text, **kwargs) component = Component('label', text=text, **kwargs)
return self._add_component(component) 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.""" """Create a button component."""
props = {'label': text, **kwargs} 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) component = Component('button', **props)
return self._add_component(component) return self._add_component(component)
@ -300,6 +413,23 @@ class App:
component = Component('input', **props) component = Component('input', **props)
return self._add_component(component) 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): def page(self, path: str, name: str = None, title: str = None):
"""Create or switch to a page.""" """Create or switch to a page."""
if name is None: if name is None:
@ -325,15 +455,30 @@ class App:
return self._add_component(component) return self._add_component(component)
@contextmanager @contextmanager
def card(self, **kwargs): def card(self, title: str = None, flat: bool = False, **kwargs):
"""Create a card container with shadow and padding.""" """
classes = kwargs.pop('classes', []) # Remove classes from kwargs to avoid conflict 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): if isinstance(classes, str):
classes = [classes] classes = [classes]
elif not isinstance(classes, list): elif not isinstance(classes, list):
classes = [] 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) component = self._add_component(component)
# Enter container context # Enter container context
@ -602,6 +747,218 @@ class App:
component = Component('icon', **props) component = Component('icon', **props)
return self._add_component(component) 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): def build(self, output_dir: str = "./badgui-output", project_name: str = "badgui-app", **kwargs):
"""Build the Vue.js/Quasar project.""" """Build the Vue.js/Quasar project."""
from .generator import VueGenerator from .generator import VueGenerator

120
badgui/generator.py

@ -221,6 +221,9 @@ module.exports = configure(function (ctx) {
# Generate the template from page components # Generate the template from page components
template_content = self._generate_vue_template_for_page(page) 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'''<template> page_content = f'''<template>
<q-page class="row items-center justify-evenly"> <q-page class="row items-center justify-evenly">
{template_content} {template_content}
@ -228,15 +231,26 @@ module.exports = configure(function (ctx) {
</template> </template>
<script setup> <script setup>
import {{ ref }} from 'vue' import {{ ref, onMounted, watch }} from 'vue'
import {{ useQuasar }} from 'quasar'
{script_imports}
// Component state // Component state
const state = ref({{}}) const state = ref({{}})
const $q = useQuasar()
// Dialog state for Quasar dialogs
const dialogOpen = ref(false)
// Make window object available to template
const window = globalThis.window
// Event handlers // Event handlers
const handleClick = (event) => {{ const handleClick = (event) => {{
console.log('Button clicked:', event) console.log('Button clicked:', event)
}} }}
{script_content}
</script> </script>
<style scoped> <style scoped>
@ -270,6 +284,98 @@ const handleClick = (event) => {{
return template return template
def _process_page_scripts(self, page, output_dir: str) -> tuple:
"""Process script components for a page and return imports and content."""
script_imports = ""
script_content = ""
# Collect Vue refs from all components
vue_refs = self._collect_vue_refs(page)
if vue_refs:
# Create template refs using Vue 3 Composition API
for ref_name in vue_refs:
script_content += f"\n// Vue ref for component\nconst {ref_name} = ref(null);\n"
# Check if page has scripts
if hasattr(page, '_scripts'):
for script_component in page._scripts:
props = script_component._props
if props.get('_is_inline'):
# Add inline script content
content = props.get('_script_content', '')
script_content += f"\n// Inline script\n{content}\n"
elif props.get('_external_file'):
filename = props.get('_external_file')
file_content = props.get('_file_content', '')
import_mode = props.get('_import_mode', False)
if import_mode:
# Copy to src/utils directory and add import
utils_dir = os.path.join(output_dir, 'src', 'utils')
os.makedirs(utils_dir, exist_ok=True)
# Write the external file to utils directory
js_file_path = os.path.join(utils_dir, filename)
with open(js_file_path, 'w') as f:
f.write(file_content)
# Generate import statement
import_path = props.get('_import_path', f"../utils/{filename}")
module_name = os.path.splitext(filename)[0] # Remove extension
script_imports += f"import * as {module_name} from '{import_path}';\n"
script_content += f"\n// Imported module: {filename}\n// Available as: {module_name}\n"
else:
# Copy external file to public/js directory (traditional way)
js_dir = os.path.join(output_dir, 'public', 'js')
os.makedirs(js_dir, exist_ok=True)
# Write the external file
js_file_path = os.path.join(js_dir, filename)
with open(js_file_path, 'w') as f:
f.write(file_content)
script_content += f"\n// External script: {filename} (loaded via <script> tag)\n"
return script_imports, script_content
def _collect_vue_refs(self, page) -> list:
"""Collect all Vue ref names from page components."""
refs = []
def collect_refs_recursive(component):
if hasattr(component, '_vue_ref') and component._vue_ref:
refs.append(component._vue_ref)
for child in component.children:
collect_refs_recursive(child)
# Collect from all page components
for component in page.components:
collect_refs_recursive(component)
return refs
def _collect_external_scripts(self) -> list:
"""Collect all external script files from all pages."""
external_scripts = []
for path, page in self.app.pages.items():
if hasattr(page, '_scripts') and page._scripts:
for script_comp in page._scripts:
if script_comp._props.get('_external_file'):
filename = script_comp._props.get('_external_file')
script_src = f"/js/{filename}"
if script_src not in external_scripts:
external_scripts.append(script_src)
elif script_comp._props.get('src') and not script_comp._props.get('_is_inline'):
# External URL
src = script_comp._props.get('src')
if src not in external_scripts:
external_scripts.append(src)
return external_scripts
def _generate_router(self, output_dir: str): def _generate_router(self, output_dir: str):
"""Generate Vue Router configuration.""" """Generate Vue Router configuration."""
router_content = '''import { route } from 'quasar/wrappers' router_content = '''import { route } from 'quasar/wrappers'
@ -416,8 +522,14 @@ $warning : #F2C037;
def _copy_static_files(self, output_dir: str): def _copy_static_files(self, output_dir: str):
"""Copy static files and create additional necessary files.""" """Copy static files and create additional necessary files."""
# Collect all external script files from all pages
external_scripts = self._collect_external_scripts()
script_tags = ""
for script_src in external_scripts:
script_tags += f' <script src="{script_src}"></script>\n'
# Create index.html in root directory # Create index.html in root directory
index_html = '''<!DOCTYPE html> index_html = f'''<!DOCTYPE html>
<html> <html>
<head> <head>
<title><%= productName %></title> <title><%= productName %></title>
@ -425,14 +537,14 @@ $warning : #F2C037;
<meta name="description" content="<%= productDescription %>"> <meta name="description" content="<%= productDescription %>">
<meta name="format-detection" content="telephone=no"> <meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no"> <meta name="msapplication-tap-highlight" content="no">
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.theme && ctx.theme.capacitor || ctx.mode && ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>"> <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.theme && ctx.theme.capacitor || ctx.mode && ctx.mode.capacitor) {{ %>, viewport-fit=cover<% }} %>">
<link rel="icon" type="image/png" sizes="128x128" href="icons/favicon-128x128.png"> <link rel="icon" type="image/png" sizes="128x128" href="icons/favicon-128x128.png">
<link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png"> <link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
<link rel="icon" type="image/ico" href="favicon.ico"> <link rel="icon" type="image/ico" href="favicon.ico">
</head> {script_tags} </head>
<body> <body>
<!-- quasar:entry-point --> <!-- quasar:entry-point -->
</body> </body>

81
button_comparison_test.py

@ -0,0 +1,81 @@
"""
Button Handler Comparison
Shows the evolution from confusing parameters to clean NiceGUI-style .on() method.
"""
import badgui as bg
with bg.page("/", "ButtonComparison", "Button Handler Comparison"):
with bg.column() as main_col:
main_col.classes("q-pa-lg q-gutter-md")
bg.label("Button Handler Evolution").classes("text-h3 q-mb-lg")
with bg.card():
bg.label("❌ Old Confusing Way (multiple parameters)").classes("text-h5 text-red q-mb-md")
bg.label("Don't use these - confusing and redundant:").classes("text-body2 q-mb-sm")
# These would be the old confusing ways (DON'T USE):
# bg.button("Old Way 1", js_handler="myFunction").classes("q-btn-red q-mb-sm")
# bg.button("Old Way 2", on_click="myFunction").classes("q-btn-red q-mb-sm")
# bg.button("Old Way 3", js_handler="() => alert('hi')").classes("q-btn-red q-mb-sm")
bg.label("✅ New Clean Way (.on() method)").classes("text-h5 text-green q-mb-md q-mt-lg")
bg.label("Much cleaner and follows NiceGUI patterns:").classes("text-body2 q-mb-sm")
# ✅ NEW CLEAN WAY:
bg.button("Function Name").on('click', 'myFunction').classes("q-btn-primary q-mb-sm")
bg.button("Inline Expression").on('click', '() => alert("Hello!")').classes("q-btn-secondary q-mb-sm")
bg.button("With Notification").on('click', '() => $q.notify("Clicked!")').classes("q-btn-accent q-mb-sm")
# ✅ CONVENIENCE METHODS:
bg.button("On Click").on_click('testClick').classes("q-btn-positive q-mb-sm")
bg.button("On Input").on('input', 'handleInput').classes("q-btn-info q-mb-sm")
# ✅ CHAINING WITH STYLING:
bg.button("Save", icon="save", color="green").on_click('saveData').classes("q-mb-sm")
bg.button("Delete", icon="delete", color="red").on_click('deleteData').classes("q-mb-sm")
# JavaScript handlers
bg.script("""
const myFunction = () => {
console.log('myFunction called!');
$q.notify('Function name handler works!');
};
const testClick = () => {
console.log('testClick called!');
$q.notify('on_click convenience method works!');
};
const handleInput = () => {
console.log('handleInput called!');
$q.notify('Input handler works!');
};
const saveData = () => {
$q.notify({
message: 'Data saved!',
color: 'green',
icon: 'check'
});
};
const deleteData = () => {
$q.notify({
message: 'Data deleted!',
color: 'red',
icon: 'warning'
});
};
onMounted(() => {
console.log('Clean .on() method pattern loaded!');
});
""")
if __name__ == "__main__":
print("🧪 Button Handler Comparison")
print("Shows the clean .on() method vs old confusing parameters")
# Run dev server
bg.dev(port=3027)

78
simple_button_test.py

@ -0,0 +1,78 @@
"""
Simple Button Test
Testing the new .on() method pattern (like NiceGUI).
"""
import badgui as bg
with bg.page("/", "ButtonTest", "Button Test"):
with bg.column() as main_col:
main_col.classes("q-pa-lg q-gutter-md")
bg.label("Button .on() Method Test").classes("text-h3 q-mb-lg")
with bg.card():
bg.label("Test the new .on() method pattern").classes("text-h5 q-mb-md")
# Using .on('click') method (NiceGUI style) - much cleaner!
bg.button("Click Handler").on('click', 'testClick').classes("q-btn-primary q-mb-sm")
bg.button("Alert Button").on('click', '() => window.alert("Hello!")').classes("q-btn-secondary q-mb-sm")
bg.button("Notify Button").on('click', '() => $q.notify("Hello from Quasar!")').classes("q-btn-accent q-mb-sm")
# Using .on_click() convenience method
bg.button("On Click").on_click('testOnClick').classes("q-btn-positive q-mb-sm")
# Buttons with icons and colors
bg.button("Save", icon="save", color="green").on_click('saveData').classes("q-mb-sm")
bg.button("Delete", icon="delete", color="red").on_click('deleteData').classes("q-mb-sm")
# JavaScript handlers
bg.script("""
const testClick = () => {
console.log('Click handler called!');
$q.notify({
message: 'Click handler executed!',
color: 'positive',
position: 'top'
});
};
const testOnClick = () => {
console.log('On Click handler called!');
$q.notify({
message: 'On Click method executed!',
color: 'info',
position: 'top-right'
});
};
const saveData = () => {
console.log('Save data called!');
$q.notify({
message: 'Data saved successfully!',
color: 'green',
icon: 'check',
position: 'center'
});
};
const deleteData = () => {
console.log('Delete data called!');
$q.notify({
message: 'Data deleted!',
color: 'red',
icon: 'warning',
position: 'bottom'
});
};
onMounted(() => {
console.log('Button test with .on() method mounted!');
});
""")
if __name__ == "__main__":
print("🧪 Simple Button Test")
print("Testing unified button() method")
# Run dev server
bg.dev(port=3025)

74
simplified_card_test.py

@ -0,0 +1,74 @@
"""
Simplified Card Test
Demonstrates the new simplified card component usage.
"""
import badgui as bg
with bg.page("/", "SimplifiedCards", "Simplified Card Test"):
with bg.column() as main_col:
main_col.classes("q-pa-lg q-gutter-md")
bg.label("Simplified Card Examples").classes("text-h3 q-mb-lg")
# 1. Simple card with title (automatic sections)
with bg.card(title="User Information"):
bg.label("This card automatically gets a title section and content section!")
name_input = bg.input("Enter your name")
name_input.classes("q-mb-md")
bg.button_with_handler("Submit", js_handler="handleSubmit").classes("q-btn-primary")
# 2. Simple card without title (just a container)
with bg.card():
bg.label("Simple Card Content").classes("text-h5 q-mb-md")
bg.label("This card works like a simple container with card styling.")
bg.label("No need for card_section or card_actions!")
with bg.row():
bg.button_with_handler("Action 1", js_handler="action1").classes("q-btn-primary q-mr-sm")
bg.button_with_handler("Action 2", js_handler="action2").classes("q-btn-secondary")
# 3. Flat card (no shadow)
with bg.card(title="Settings", flat=True):
bg.label("This is a flat card with no shadow").classes("q-mb-md")
with bg.row():
bg.label("Dark Mode:")
# You could add a switch here
bg.button_with_handler("Toggle", js_handler="toggleDarkMode").classes("q-btn-outline")
# 4. Card with custom classes
with bg.card(title="Custom Styled Card") as custom_card:
custom_card.classes("bg-primary text-white")
bg.label("This card has custom background and text color")
bg.button_with_handler("White Button", js_handler="whiteButton").classes("q-btn text-primary bg-white")
# Simple JavaScript handlers
bg.script("""
const handleSubmit = () => {
console.log('Submit clicked');
};
const action1 = () => {
console.log('Action 1 clicked');
};
const action2 = () => {
console.log('Action 2 clicked');
};
const toggleDarkMode = () => {
console.log('Dark mode toggled');
};
const whiteButton = () => {
console.log('White button clicked');
};
""")
if __name__ == "__main__":
print("🧪 Simplified Card Test")
print("Testing the new simplified card component")
# Build and run
bg.dev(port=3022)

37
vmodel.js

@ -0,0 +1,37 @@
// Reactive data - these automatically sync with v-model bound components
const statusMessage = ref('Dialog is closed');
const greetingMessage = ref('Enter your name below');
const userName = ref('');
const openDialog = () => {
console.log('Opening dialog...');
dialogOpen.value = true;
statusMessage.value = 'Dialog is open';
};
const closeDialog = () => {
console.log('Closing dialog...');
dialogOpen.value = false;
statusMessage.value = 'Dialog is closed';
greetingMessage.value = 'Enter your name below'; // Reset greeting
userName.value = ''; // Clear input
};
const sayHello = () => {
const name = userName.value || 'Anonymous';
console.log('User name from v-model:', name);
greetingMessage.value = `Hello, ${name}! Nice to meet you!`;
};
onMounted(() => {
console.log('Component mounted!');
console.log('Reactive variables initialized:');
console.log('- statusMessage:', statusMessage.value);
console.log('- greetingMessage:', greetingMessage.value);
console.log('- userName:', userName.value);
// Watch for changes to demonstrate reactivity
watch(userName, (newValue) => {
console.log('userName changed to:', newValue);
});
});

50
vmodel_vue_ref_test.py

@ -0,0 +1,50 @@
"""
V-Model Vue Ref Dialog Test
Uses v-model for reactive two-way data binding instead of DOM manipulation.
"""
import badgui as bg
with bg.page("/", "VModelDialog", "V-Model Dialog Test"):
with bg.column() as main_col:
main_col.classes("q-pa-lg q-gutter-md")
bg.label("V-Model Vue Ref Dialog Test").classes("text-h3 q-mb-lg")
# Simple dialog with simplified card
with bg.dialog() as my_dialog:
with bg.card(title="Hello from Dialog!"):
# Use v-model for reactive binding instead of Vue ref
greeting_label = bg.label("Loading...")
greeting_label.bind_text("greetingMessage") # Bind to reactive variable
# Input with v-model binding
name_input = bg.input("Your name")
name_input.bind_value("userName") # Bind to reactive variable
name_input.classes("q-mb-md")
# Actions can go directly in the card
with bg.row():
bg.button("Say Hello").on_click("sayHello").classes("q-btn-primary q-mr-sm")
bg.button("Close").on_click("closeDialog").classes("q-btn-secondary")
my_dialog.vue_ref("myDialog")
# Main content - use reactive binding for status
status_label = bg.label("Loading...")
status_label.bind_text("statusMessage") # Bind to reactive variable
status_label.classes("text-h6 q-mb-md")
bg.button("Open Dialog").on_click("openDialog").classes("q-btn-primary")
# JavaScript functions - V-Model version with reactive variables
bg.script("vmodel.js")
if __name__ == "__main__":
print("🧪 V-Model Vue Ref Dialog Test")
print("Testing v-model reactive data binding instead of DOM manipulation")
# Build to examine output
#bg.app.build("./vmodel-vue-ref-output", "vmodel-vue-ref")
# Run dev server
bg.dev(port=3019)
Loading…
Cancel
Save