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}{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}\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'''
{template_content}
@@ -228,15 +231,26 @@ module.exports = configure(function (ctx) {