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.
This commit is contained in:
2025-09-28 17:29:16 +02:00
parent 7d5b6a6c99
commit 2e7cff5eee
9 changed files with 1037 additions and 19 deletions
+203
View File
@@ -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.
+28 -2
View File
@@ -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)
+368 -11
View File
@@ -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} <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"
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}<!-- 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):
"""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
+118 -6
View File
@@ -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'''<template>
<q-page class="row items-center justify-evenly">
{template_content}
@@ -228,15 +231,26 @@ module.exports = configure(function (ctx) {
</template>
<script setup>
import {{ ref }} from 'vue'
import {{ ref, onMounted, watch }} from 'vue'
import {{ useQuasar }} from 'quasar'
{script_imports}
// Component state
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
const handleClick = (event) => {{
console.log('Button clicked:', event)
}}
{script_content}
</script>
<style scoped>
@@ -263,13 +277,105 @@ const handleClick = (event) => {{
"""Generate Vue template from page components."""
if not page.components:
return " <div>No components defined</div>"
template = ""
for component in page.components:
template += component.to_vue_template(2) # 2 spaces indent for page content
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):
"""Generate Vue Router configuration."""
router_content = '''import { route } from 'quasar/wrappers'
@@ -416,8 +522,14 @@ $warning : #F2C037;
def _copy_static_files(self, output_dir: str):
"""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
index_html = '''<!DOCTYPE html>
index_html = f'''<!DOCTYPE html>
<html>
<head>
<title><%= productName %></title>
@@ -425,14 +537,14 @@ $warning : #F2C037;
<meta name="description" content="<%= productDescription %>">
<meta name="format-detection" content="telephone=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="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="16x16" href="icons/favicon-16x16.png">
<link rel="icon" type="image/ico" href="favicon.ico">
</head>
{script_tags} </head>
<body>
<!-- quasar:entry-point -->
</body>
+81
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)