Browse Source

Refactor and enhance BadGUI framework

- Removed duplicate event handler definition in generator.py
- Added complete feature summary documentation for BadGUI in BADGUI_COMPLETE_GUIDE.md
- Introduced detailed workflow analysis in BADGUI_WORKFLOW_ANALYSIS.md
- Created tests for debugging button click events in debug_button_test.py
- Developed extended generic component tests in extended_generic_test.py
- Implemented generic component tests showcasing bg.component() method in generic_component_test.py
- Refactored components to utilize generic component() method in refactored_test.py
master
Matteo Benedetto 6 months ago
parent
commit
793cf618aa
  1. 329
      BADGUI_COMPLETE_GUIDE.md
  2. 678
      BADGUI_WORKFLOW_ANALYSIS.md
  3. 4
      badgui/__init__.py
  4. 179
      badgui/core.py
  5. 5
      badgui/generator.py
  6. 26
      debug_button_test.py
  7. 191
      extended_generic_test.py
  8. 144
      generic_component_test.py
  9. 94
      refactored_test.py

329
BADGUI_COMPLETE_GUIDE.md

@ -0,0 +1,329 @@
# BadGUI Framework v0.2.0 - Complete Feature Summary
## Overview
BadGUI is a Python web UI framework that generates Vue.js applications with Quasar components. It provides a simple, declarative API similar to NiceGUI but generates static Vue.js projects that can be deployed anywhere.
## Core Framework Features
### 1. Component System
BadGUI provides both predefined components and a generic component method:
#### Predefined Components
```python
import badgui as bg
# Basic components
bg.label("Hello World")
bg.button("Click Me").on_click('handleClick')
bg.input_("Enter text").bind_value('userText')
# Layout components
with bg.row():
with bg.column():
bg.label("Column content")
# Card components (simplified syntax)
with bg.card():
bg.label("Card content")
bg.button("Action")
```
#### Generic Component Method (NEW!)
```python
# Create ANY Quasar component
rating = bg.component('q-rating', max=5, size='2em', color='amber')
rating.bind_value('userRating')
select = bg.component('q-select',
filled=True,
options=['Option 1', 'Option 2', 'Option 3'],
label='Choose one')
select.bind_value('selectedOption')
slider = bg.component('q-slider', min=0, max=100, step=10, markers=True)
slider.bind_value('volumeLevel')
# Container components
with bg.component('q-expansion-item',
label='Advanced Settings',
container=True) as expansion:
bg.label("Nested content")
bg.component('q-separator')
```
### 2. JavaScript Integration
BadGUI provides comprehensive JavaScript integration with Vue.js:
#### Script Component
```python
# Inline JavaScript
bg.script("""
const userRating = ref(3);
const selectedOption = ref(null);
const updateDisplay = () => {
console.log('Rating:', userRating.value);
};
onMounted(() => {
console.log('Component mounted!');
});
""")
# External JavaScript files
bg.script("./my-script.js")
# ES6 Module imports (automatically copied to Vue app)
bg.script("./utils/helpers.js", import_mode=True)
```
#### Vue Ref Integration
```python
# Create components with Vue template refs
button = bg.button("Click Me")
button.vue_ref('myButton')
# Access in JavaScript
bg.script("""
const { myButton } = getCurrentInstance().refs;
const focusButton = () => {
myButton.value.$el.focus();
};
""")
```
### 3. Data Binding
BadGUI supports comprehensive Vue.js data binding:
```python
# Two-way binding
input_field = bg.input_("Enter text")
input_field.bind_value('userInput')
# Text binding
display = bg.label("")
display.bind_text('computedText')
# Event handling with .on() method pattern
button = bg.button("Submit")
button.on_click('submitForm')
button.on('mouseenter', 'handleHover')
```
### 4. Context Management
BadGUI uses Python context managers for clean layout structure:
```python
with bg.page("/", "HomePage", "My App"):
with bg.column().classes("q-pa-lg"):
bg.label("Page Title").classes("text-h3")
with bg.row().classes("q-gutter-md"):
with bg.card():
bg.label("Card 1")
with bg.card():
bg.label("Card 2")
# Generic container components
with bg.component('q-timeline', container=True):
bg.component('q-timeline-entry',
title='Event 1',
subtitle='Description')
```
## Complete API Reference
### App Class Methods
```python
# Page management
bg.page(route, name, title)
# Layout components
bg.row(**kwargs)
bg.column(**kwargs)
bg.div(**kwargs)
# Content components
bg.label(text, **kwargs)
bg.button(text, **kwargs)
bg.input_(placeholder, **kwargs)
bg.card(**kwargs)
# Generic component creation
bg.component(quasar_component, container=False, **kwargs)
# JavaScript integration
bg.script(code_or_file, import_mode=False)
# Development server
bg.dev(port=3000, host='localhost')
```
### Component Methods
```python
# Styling
component.classes("q-pa-md text-h5")
component.style("color: red; font-size: 16px")
# Data binding
component.bind_value('variable_name')
component.bind_text('text_variable')
# Vue refs
component.vue_ref('ref_name')
# Event handling (.on() pattern)
component.on_click('function_name')
component.on('event_name', 'handler_function')
# Properties
component.props(disabled=True, color='primary')
```
## Example Applications
### 1. Simple Form with Generic Components
```python
import badgui as bg
with bg.page("/", "Form", "Advanced Form"):
with bg.column().classes("q-pa-lg q-gutter-md"):
bg.label("User Preferences").classes("text-h4")
# Rating
bg.label("Rate our service:")
rating = bg.component('q-rating', max=5, size='2em')
rating.bind_value('serviceRating')
# Select
category = bg.component('q-select',
options=['General', 'Technical', 'Billing'],
label='Category')
category.bind_value('selectedCategory')
# Slider
bg.label("Priority Level:")
priority = bg.component('q-slider', min=1, max=10, step=1)
priority.bind_value('priorityLevel')
# Toggle
notifications = bg.component('q-toggle',
label='Email notifications')
notifications.bind_value('emailEnabled')
# Submit button
bg.button("Submit Feedback").on_click('submitFeedback')
# JavaScript handling
bg.script("""
const serviceRating = ref(5);
const selectedCategory = ref('General');
const priorityLevel = ref(5);
const emailEnabled = ref(true);
const submitFeedback = () => {
const feedback = {
rating: serviceRating.value,
category: selectedCategory.value,
priority: priorityLevel.value,
notifications: emailEnabled.value
};
console.log('Feedback submitted:', feedback);
// Send to API...
};
""")
bg.dev(port=3030)
```
### 2. Dashboard with Container Components
```python
import badgui as bg
with bg.page("/", "Dashboard", "Analytics Dashboard"):
with bg.column().classes("full-width"):
bg.label("Analytics Dashboard").classes("text-h3 q-pa-lg")
with bg.row().classes("q-pa-md q-gutter-md"):
# Stats cards
with bg.card().classes("col"):
bg.label("Total Users").classes("text-h6")
user_count = bg.label("0")
user_count.bind_text('totalUsers')
with bg.card().classes("col"):
bg.label("Revenue").classes("text-h6")
revenue = bg.label("$0")
revenue.bind_text('totalRevenue')
# Expandable sections
with bg.component('q-expansion-item',
label='Detailed Analytics',
icon='analytics',
container=True):
# Progress indicators
bg.label("Loading Progress:")
bg.component('q-linear-progress',
value=0.8,
size='15px',
color='primary')
# Data table (could be implemented as generic component)
with bg.component('q-table',
title='Recent Activity',
columns=[{'name': 'date', 'label': 'Date'}],
container=True):
pass # Table rows would be added here
bg.script("""
const totalUsers = ref('1,234');
const totalRevenue = ref('$56,789');
onMounted(() => {
// Simulate data loading
console.log('Dashboard loaded');
});
""")
bg.dev(port=3031)
```
## Framework Architecture
### Generated Vue.js Structure
```
vue-app/
├── src/
│ ├── components/
│ │ └── BadGUIComponents.vue # Generated components
│ ├── pages/
│ │ └── IndexPage.vue # Generated pages
│ ├── utils/ # ES6 modules (if using import_mode)
│ │ └── helpers.js
│ ├── App.vue
│ └── main.js
├── package.json
└── quasar.config.js
```
### Key Benefits
1. **Extensibility**: Generic `component()` method allows access to entire Quasar library
2. **Vue.js Integration**: Full Vue 3 Composition API support with refs, reactive data, and lifecycle hooks
3. **JavaScript Flexibility**: Inline scripts, external files, and ES6 module imports
4. **Clean Syntax**: Context managers and method chaining for readable code
5. **Static Generation**: Produces deployable Vue.js applications
6. **Type Safety**: Python-based development with IDE support
## Development Workflow
1. **Write BadGUI Code**: Use Python to define UI structure and behavior
2. **Test with Dev Server**: `bg.dev()` automatically generates and serves Vue app
3. **Iterate Quickly**: Changes to Python code regenerate the Vue app
4. **Deploy**: Generated Vue app can be built and deployed anywhere
BadGUI v0.2.0 combines the simplicity of Python-based UI frameworks with the power and flexibility of Vue.js and Quasar, making it perfect for rapid prototyping and production applications alike.

678
BADGUI_WORKFLOW_ANALYSIS.md

@ -0,0 +1,678 @@
# BadGUI Workflow Analysis: From Python to Vue.js
## Overview
BadGUI is a Python framework that generates complete Vue.js/Quasar projects using a NiceGUI-like syntax. This document traces the complete workflow from Python code to a running Vue.js application.
## Table of Contents
1. [Python Code Structure](#1-python-code-structure)
2. [Component Creation and Storage](#2-component-creation-and-storage)
3. [Page Management](#3-page-management)
4. [Build Process](#4-build-process)
5. [Vue.js Generation](#5-vuejs-generation)
6. [Project Structure Creation](#6-project-structure-creation)
7. [Development Server](#7-development-server)
8. [Areas for Improvement](#8-areas-for-improvement)
---
## 1. Python Code Structure
### Example Input Code
```python
import badgui as bg
with bg.page("/", "HomePage", "Home"):
with bg.column() as main_col:
main_col.classes("q-pa-lg q-gutter-md")
bg.label("Hello BadGUI!").classes("text-h3")
name_input = bg.input("Enter your name")
name_input.bind_value("userName")
bg.button("Greet").on_click("greetUser").classes("q-btn-primary")
bg.script("""
const userName = ref('');
const greetUser = () => {
$q.notify(`Hello, ${userName.value}!`);
};
""")
bg.dev(port=3000)
```
### Key Framework Entry Points
**File: `badgui/__init__.py`**
```python
from .core import App
# Main app instance
app = App()
# Convenience functions that delegate to the main app
def label(text: str, **kwargs):
return app.label(text, **kwargs)
def button(text: str, icon: str = None, color: str = 'primary', **kwargs):
return app.button(text, icon, color, **kwargs)
```
**Analysis:** All component functions are convenience wrappers around a singleton `App` instance. This centralizes state management but could limit flexibility for multiple app instances.
---
## 2. Component Creation and Storage
### Component Class Structure
**File: `badgui/core.py`**
```python
class Component:
def __init__(self, component_type: str, **props):
self.component_type = component_type
self.id = f"{component_type}_{id(self)}"
self._props = props
self._classes = []
self._style = {}
self.children = []
self._vue_ref = None
def classes(self, class_names: Union[str, List[str]]):
if isinstance(class_names, str):
self._classes = class_names.split()
else:
self._classes = class_names
return self
def vue_ref(self, ref_name: str):
self._vue_ref = ref_name
self._props['ref'] = ref_name
return self
```
**Analysis:** Components use method chaining for fluent API. Each component gets a unique ID based on memory address. Props, classes, and styles are stored separately but could be unified.
### Component Addition Process
**File: `badgui/core.py`**
```python
class App:
def _add_component(self, component: Component) -> Component:
page = self._get_current_page()
# Add to current container (if in context) or page
if page.current_container:
page.current_container.children.append(component)
else:
page.components.append(component)
return component
def label(self, text: str, **kwargs) -> Component:
component = Component('label', text=text, **kwargs)
return self._add_component(component)
```
**Analysis:** Components are added to either the current page or the current container (for nested structures). The container stack manages nesting levels.
---
## 3. Page Management
### Page Class and Context Management
**File: `badgui/core.py`**
```python
class Page:
def __init__(self, path: str, name: str, title: str = None):
self.path = path
self.name = name
self.title = title or name
self.components = []
self.current_container = None
self._container_stack = []
self._scripts = []
class App:
def __init__(self):
self.pages = {}
self.current_page = None
@contextmanager
def page(self, path: str, name: str = None, title: str = None):
# Create or get existing page
page = Page(path, name or self._generate_page_name(path), title)
self.pages[path] = page
# Set as current page
old_page = self.current_page
self.current_page = page
try:
yield page
finally:
self.current_page = old_page
```
**Analysis:** Pages use context managers to maintain state. The current page is tracked globally, which could cause issues with concurrent page creation.
### Container Context Management
```python
@contextmanager
def column(self, **kwargs):
component = Component('column', classes=['column'], **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()
```
**Analysis:** Container nesting uses a stack-based approach. This works well but could be error-prone if exceptions occur during context management.
---
## 4. Build Process
### Build Initiation
**File: `badgui/core.py`**
```python
def build(self, output_dir: str = "./badgui-output", project_name: str = "badgui-app", **kwargs):
from .generator import VueGenerator
print(f"🔧 Building BadGUI project...")
print(f"📁 Output directory: {output_dir}")
generator = VueGenerator(self)
generator.generate_project(output_dir, project_name, **kwargs)
print("✅ BadGUI project generated successfully!")
```
**Analysis:** Build process is straightforward but lacks error handling and progress reporting for large projects.
### Generator Initialization
**File: `badgui/generator.py`**
```python
class VueGenerator:
def __init__(self, app: App):
self.app = app
def generate_project(self, output_dir: str, project_name: str = "badgui-app", **kwargs):
os.makedirs(output_dir, exist_ok=True)
self._create_project_structure(output_dir, project_name)
self._generate_package_json(output_dir, project_name)
self._generate_quasar_config(output_dir)
self._generate_main_layout(output_dir)
self._generate_pages(output_dir)
self._generate_router(output_dir)
self._generate_app_vue(output_dir)
self._generate_boot_files(output_dir)
self._generate_css_files(output_dir)
self._copy_static_files(output_dir)
```
**Analysis:** Generation follows a fixed sequence. Each step is independent, which is good for maintainability but doesn't allow for conditional generation based on features used.
---
## 5. Vue.js Generation
### Component-to-Vue Template Conversion
**File: `badgui/core.py`**
```python
def to_vue_template(self, indent: int = 0) -> str:
spaces = " " * indent
# Build attributes
attrs = []
attrs.append(f'id="{self.id}"')
if self._classes:
attrs.append(f'class="{" ".join(self._classes)}"')
# Add other props
for key, value in self._props.items():
if key not in ['classes', 'style', 'id']:
if isinstance(value, bool) and value:
attrs.append(key)
elif isinstance(value, str):
attrs.append(f'{key}="{value}"')
attrs_str = " " + " ".join(attrs)
vue_tag = self.get_vue_tag()
# Generate template
if self.children:
template = f"{spaces}<{vue_tag}{attrs_str}>\n"
for child in self.children:
template += child.to_vue_template(indent + 1)
template += f"{spaces}</{vue_tag}>\n"
else:
content = self.get_content()
if content:
template = f"{spaces}<{vue_tag}{attrs_str}>{content}</{vue_tag}>\n"
else:
template = f"{spaces}<{vue_tag}{attrs_str} />\n"
return template
def get_vue_tag(self) -> str:
component_map = {
'label': 'q-item-label',
'button': 'q-btn',
'input': 'q-input',
'column': 'div',
'card': 'q-card',
'dialog': 'q-dialog',
'div': 'div'
}
return component_map.get(self.component_type, self.component_type)
```
**Analysis:** Template generation is recursive and handles nested components well. The component mapping is hardcoded, which makes adding new components require code changes.
### Page Component Generation
**File: `badgui/generator.py`**
```python
def _generate_page_component(self, output_dir: str, page):
template_content = self._generate_vue_template_for_page(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}
</q-page>
</template>
<script setup>
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
{script_content}
</script>'''
page_path = os.path.join(output_dir, 'src', 'pages', f'{page.name}.vue')
with open(page_path, 'w') as f:
f.write(page_content)
```
**Analysis:** Page generation is template-based with string formatting. This approach works but is fragile and hard to modify. A proper template engine would be more robust.
### Script Processing
```python
def _process_page_scripts(self, page, output_dir: str) -> tuple:
script_imports = ""
script_content = ""
# Collect Vue refs
vue_refs = self._collect_vue_refs(page)
if vue_refs:
for ref_name in vue_refs:
script_content += f"\n// Vue ref for component\nconst {ref_name} = ref(null);\n"
# Process script components
if hasattr(page, '_scripts'):
for script_component in page._scripts:
props = script_component._props
if props.get('_is_inline'):
content = props.get('_script_content', '')
script_content += f"\n// Inline script\n{content}\n"
elif props.get('_external_file'):
filename = props.get('_external_file')
import_mode = props.get('_import_mode', False)
if import_mode:
# ES6 module import
utils_dir = os.path.join(output_dir, 'src', 'utils')
os.makedirs(utils_dir, exist_ok=True)
module_name = os.path.splitext(filename)[0]
script_imports += f"import * as {module_name} from '../utils/{filename}';\n"
else:
# Traditional script tag
js_dir = os.path.join(output_dir, 'public', 'js')
os.makedirs(js_dir, exist_ok=True)
return script_imports, script_content
```
**Analysis:** Script processing handles both inline and external scripts with ES6 module support. The dual approach (traditional vs module imports) adds complexity but provides flexibility.
---
## 6. Project Structure Creation
### Package.json Generation
**File: `badgui/generator.py`**
```python
def _generate_package_json(self, output_dir: str, project_name: str):
package_json = {
"name": project_name,
"version": "0.0.1",
"scripts": {
"dev": "quasar dev",
"build": "quasar build"
},
"dependencies": {
"@quasar/extras": "^1.16.4",
"quasar": "^2.12.0",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
},
"devDependencies": {
"@quasar/app-vite": "^1.3.0",
"typescript": "^5.1.6"
}
}
with open(os.path.join(output_dir, 'package.json'), 'w') as f:
json.dump(package_json, f, indent=2)
```
**Analysis:** Dependencies are hardcoded with specific versions. This ensures consistency but makes updates difficult and doesn't allow for feature-based dependency inclusion.
### Router Generation
```python
def _generate_router(self, output_dir: str):
page_routes = []
for path, page in self.app.pages.items():
route = f" {{ path: '{path}', component: () => import('pages/{page.name}.vue') }}"
page_routes.append(route)
routes_content = f'''const routes = [
{{
path: '/',
component: () => import('layouts/MainLayout.vue'),
children: [
{",".join([chr(10) + route for route in page_routes])}
]
}}
]
export default routes'''
```
**Analysis:** Router generation is dynamic based on defined pages. This works well for basic routing but doesn't support advanced features like route guards or nested routing.
---
## 7. Development Server
### Dev Server Implementation
**File: `badgui/core.py`**
```python
def dev(self, port: int = 9000, host: str = "localhost", auto_reload: bool = False,
project_name: str = "badgui-dev", **kwargs):
import tempfile
import subprocess
# Create temporary build
temp_dir = tempfile.mkdtemp(prefix="badgui_dev_")
print(f"🔧 Creating temporary build...")
print(f"📁 Temp directory: {temp_dir}")
self.build(temp_dir, project_name, **kwargs)
# Install dependencies and run dev server
print("📦 Installing dependencies...")
subprocess.run(["npm", "install"], cwd=temp_dir, check=True, capture_output=True)
print(f"🚀 Starting dev server at http://{host}:{port}")
env = os.environ.copy()
env['PORT'] = str(port)
env['HOST'] = host
try:
subprocess.run(["npm", "run", "dev"], cwd=temp_dir, env=env, check=True)
except KeyboardInterrupt:
print("\n🛑 Dev server stopped")
finally:
# Cleanup temp directory
shutil.rmtree(temp_dir, ignore_errors=True)
```
**Analysis:** Dev server creates temporary builds, which is inefficient for development. A proper dev server would support hot reloading and incremental builds.
---
## 8. Areas for Improvement
### 8.1 Architecture Improvements
#### Issue: Singleton App Instance
**Current Code:**
```python
# badgui/__init__.py
app = App() # Global singleton
def label(text: str, **kwargs):
return app.label(text, **kwargs)
```
**Problem:** Single global instance limits flexibility and testing.
**Improvement:** Support multiple app instances:
```python
class BadGUI:
def __init__(self):
self.app = App()
def label(self, text: str, **kwargs):
return self.app.label(text, **kwargs)
# Default instance for convenience
bg = BadGUI()
label = bg.label
```
#### Issue: Component Mapping Hardcoded
**Current Code:**
```python
def get_vue_tag(self) -> str:
component_map = {
'label': 'q-item-label',
'button': 'q-btn',
# ... hardcoded mapping
}
return component_map.get(self.component_type, self.component_type)
```
**Improvement:** Plugin-based component system:
```python
class ComponentRegistry:
def __init__(self):
self._components = {}
def register(self, component_type: str, vue_tag: str, props_mapper=None):
self._components[component_type] = {
'vue_tag': vue_tag,
'props_mapper': props_mapper
}
def get_vue_tag(self, component_type: str) -> str:
return self._components.get(component_type, {}).get('vue_tag', component_type)
```
### 8.2 Performance Improvements
#### Issue: Inefficient Dev Server
**Current Code:**
```python
def dev(self, port: int = 9000, **kwargs):
temp_dir = tempfile.mkdtemp(prefix="badgui_dev_")
self.build(temp_dir, project_name, **kwargs) # Full rebuild every time
subprocess.run(["npm", "install"], cwd=temp_dir) # Reinstall dependencies
```
**Improvement:** Incremental builds and dependency caching:
```python
class DevServer:
def __init__(self, app: App):
self.app = app
self._build_cache = {}
self._dependency_cache = None
def start(self, port: int = 9000):
if not self._needs_full_rebuild():
self._incremental_build()
else:
self._full_build()
if not self._dependency_cache:
self._install_dependencies()
```
#### Issue: String-based Template Generation
**Current Code:**
```python
page_content = f'''<template>
<q-page class="row items-center justify-evenly">
{template_content}
</q-page>
</template>'''
```
**Improvement:** Template engine with caching:
```python
from jinja2 import Environment, FileSystemLoader
class TemplateGenerator:
def __init__(self):
self.env = Environment(loader=FileSystemLoader('templates'))
self._template_cache = {}
def render_page(self, page: Page) -> str:
template = self.env.get_template('page.vue.j2')
return template.render(page=page, components=page.components)
```
### 8.3 Error Handling and Validation
#### Issue: No Component Validation
**Current Code:**
```python
def button(self, text: str, icon: str = None, **kwargs) -> Component:
props = {'label': text, **kwargs}
component = Component('button', **props)
return self._add_component(component)
```
**Improvement:** Schema validation:
```python
from pydantic import BaseModel, validator
class ButtonProps(BaseModel):
label: str
icon: Optional[str] = None
color: str = 'primary'
@validator('color')
def validate_color(cls, v):
valid_colors = ['primary', 'secondary', 'accent', 'positive', 'negative']
if v not in valid_colors:
raise ValueError(f'Invalid color: {v}')
return v
def button(self, text: str, **kwargs) -> Component:
props = ButtonProps(label=text, **kwargs)
component = Component('button', **props.dict())
return self._add_component(component)
```
### 8.4 Type Safety and IDE Support
#### Issue: Limited Type Hints
**Current Code:**
```python
def classes(self, class_names): # No type hints
# Implementation
```
**Improvement:** Full type safety:
```python
from typing import Union, List, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from .core import Component
def classes(self, class_names: Union[str, List[str]]) -> 'Component':
"""Add CSS classes to the component."""
# Implementation
```
### 8.5 Testing and Debugging
#### Issue: No Built-in Testing Support
**Improvement:** Add testing utilities:
```python
class TestRenderer:
def __init__(self, app: App):
self.app = app
def render_component(self, component: Component) -> Dict:
"""Render component to testable structure."""
return {
'type': component.component_type,
'props': component._props,
'classes': component._classes,
'children': [self.render_component(child) for child in component.children]
}
def find_components(self, component_type: str) -> List[Component]:
"""Find all components of a specific type."""
# Implementation
```
---
## Conclusion
BadGUI provides a solid foundation for generating Vue.js applications from Python code. The workflow is straightforward and functional, but there are several areas where improvements could significantly enhance the framework:
1. **Architecture**: Move away from singleton pattern, implement plugin system
2. **Performance**: Add incremental builds, template caching, dependency management
3. **Developer Experience**: Improve error handling, add type safety, better IDE support
4. **Extensibility**: Plugin-based component system, customizable generators
5. **Testing**: Built-in testing utilities and debugging tools
The current implementation demonstrates the core concepts well but would benefit from a more modular, extensible architecture for production use.
---
*Document generated on: September 28, 2025*
*BadGUI Version: 0.2.0*

4
badgui/__init__.py

@ -32,6 +32,10 @@ def div(**kwargs):
"""Create a div container component."""
return app.div(**kwargs)
def component(quasar_component: str, container: bool = False, **kwargs):
"""Create any Quasar component by name."""
return app.component(quasar_component, container, **kwargs)
def build(output_dir: str = "./badgui-output", **kwargs):
"""Build the Vue.js/Quasar project."""
return app.build(output_dir, **kwargs)

179
badgui/core.py

@ -390,8 +390,7 @@ class App:
def label(self, text: str, **kwargs) -> Component:
"""Create a label component."""
component = Component('label', text=text, **kwargs)
return self._add_component(component)
return self.component('label', container=False, text=text, **kwargs)
def button(self, text: str, icon: str = None, color: str = 'primary', **kwargs) -> Component:
"""Create a button component."""
@ -402,16 +401,55 @@ class App:
if color:
props['color'] = color
component = Component('button', **props)
return self._add_component(component)
return self.component('q-btn', container=False, **props)
def input(self, placeholder: str = "", v_model: str = None, **kwargs) -> Component:
"""Create an input component."""
props = {'placeholder': placeholder, **kwargs}
if v_model:
props['v-model'] = v_model
component = Component('input', **props)
return self._add_component(component)
return self.component('q-input', container=False, **props)
def component(self, quasar_component: str, container: bool = False, **kwargs) -> Component:
"""
Create any Quasar component by name.
Args:
quasar_component: The Quasar component name (e.g., 'q-btn', 'q-card', 'q-select')
container: Whether this component can contain children (affects context management)
**kwargs: Properties to pass to the component
Usage:
# Create any Quasar component
bg.component('q-select', options=['Option 1', 'Option 2'])
bg.component('q-rating', v_model='rating', max=5)
bg.component('q-slider', v_model='volume', min=0, max=100)
# Container components (can have children)
with bg.component('q-expansion-item', label='Advanced Settings', container=True):
bg.label('Advanced options here')
"""
# Create component with the exact Quasar component name as type
component = Component(quasar_component, **kwargs)
component = self._add_component(component)
if container:
# Return a context manager for container components
return self._make_container_context(component)
else:
return component
@contextmanager
def _make_container_context(self, component: Component):
"""Helper to create container context for generic components."""
page = self._get_current_page()
page._container_stack.append(page.current_container)
page.current_container = component
try:
yield component
finally:
page.current_container = page._container_stack.pop()
@contextmanager
def div(self, **kwargs):
@ -454,7 +492,6 @@ class App:
component = Component('router-link', text=text, **props)
return self._add_component(component)
@contextmanager
def card(self, title: str = None, flat: bool = False, **kwargs):
"""
Create a simplified card container.
@ -464,7 +501,7 @@ class App:
flat: If True, removes the shadow for a flat appearance
**kwargs: Additional properties
"""
classes = kwargs.pop('classes', [])
classes = kwargs.get('classes', [])
if isinstance(classes, str):
classes = [classes]
elif not isinstance(classes, list):
@ -474,69 +511,11 @@ class App:
if flat:
classes.append('q-card--flat')
props = {'classes': classes, **kwargs}
kwargs['classes'] = classes
if title:
props['_card_title'] = title
component = Component('card', **props)
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()
@contextmanager
def card_section(self, **kwargs):
"""Create a card section container."""
classes = kwargs.pop('classes', []) # Remove classes from kwargs to avoid conflict
if isinstance(classes, str):
classes = [classes]
elif not isinstance(classes, list):
classes = []
component = Component('card_section', classes=classes, **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
kwargs['_card_title'] = title
try:
yield component
finally:
# Exit container context
page.current_container = page._container_stack.pop()
@contextmanager
def card_actions(self, **kwargs):
"""Create a card actions container for buttons and actions."""
classes = kwargs.pop('classes', []) # Remove classes from kwargs to avoid conflict
if isinstance(classes, str):
classes = [classes]
elif not isinstance(classes, list):
classes = []
component = Component('card_actions', classes=classes, **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()
return self.component('q-card', container=True, **kwargs)
@contextmanager
def tabs(self, **kwargs):
@ -624,29 +603,35 @@ class App:
# Exit container context
page.current_container = page._container_stack.pop()
@contextmanager
def row(self, **kwargs):
"""Create a row layout container."""
classes = kwargs.pop('classes', []) # Remove classes from kwargs to avoid conflict
# Ensure row class is added
classes = kwargs.get('classes', [])
if isinstance(classes, str):
classes = [classes]
elif not isinstance(classes, list):
classes = []
classes.append('row')
component = Component('row', classes=classes, **kwargs)
component = self._add_component(component)
if 'row' not in classes:
classes.append('row')
kwargs['classes'] = classes
# Enter container context
page = self._get_current_page()
page._container_stack.append(page.current_container)
page.current_container = component
return self.component('div', container=True, **kwargs)
def column(self, **kwargs):
"""Create a column layout container."""
# Ensure column class is added
classes = kwargs.get('classes', [])
if isinstance(classes, str):
classes = [classes]
elif not isinstance(classes, list):
classes = []
try:
yield component
finally:
# Exit container context
page.current_container = page._container_stack.pop()
if 'column' not in classes:
classes.append('column')
kwargs['classes'] = classes
return self.component('div', container=True, **kwargs)
@contextmanager
def column(self, **kwargs):
@ -747,33 +732,7 @@ 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):

5
badgui/generator.py

@ -245,11 +245,6 @@ 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>

26
debug_button_test.py

@ -0,0 +1,26 @@
"""
Simple test to debug the handleClick duplication issue
"""
import badgui as bg
with bg.page("/", "DebugTest", "Debug Test"):
bg.label("Debug Test").classes("text-h3")
# Just one button with one handler
button = bg.button("Test Button")
button.on_click('handleClick')
# Simple script with just one function
bg.script("""
const handleClick = () => {
console.log('Button clicked!');
};
onMounted(() => {
console.log('Debug test loaded');
});
""")
if __name__ == "__main__":
bg.dev(port=3034)

191
extended_generic_test.py

@ -0,0 +1,191 @@
"""
Extended Generic Component Test
Testing more advanced Quasar components with the bg.component() method.
"""
import badgui as bg
with bg.page("/", "ExtendedTest", "Extended Generic Component Test"):
with bg.column() as main_col:
main_col.classes("q-pa-lg q-gutter-lg")
bg.label("Extended Quasar Component Test").classes("text-h3 q-mb-lg")
# Date and Time components
with bg.card():
bg.label("Date & Time Components").classes("text-h5 q-mb-md")
bg.label("Date Picker:").classes("q-mb-sm")
date_picker = bg.component('q-date', minimal=True)
date_picker.bind_value('selectedDate')
date_picker.classes("q-mb-md")
bg.label("Time Picker:").classes("q-mb-sm")
time_picker = bg.component('q-time', format24h=True)
time_picker.bind_value('selectedTime')
time_picker.classes("q-mb-md")
# Advanced Input Components
with bg.card():
bg.label("Advanced Inputs").classes("text-h5 q-mb-md")
# Option Group (Radio buttons)
bg.label("Select Option:").classes("q-mb-sm")
option_group = bg.component('q-option-group',
options=[
{'label': 'Option 1', 'value': 'opt1'},
{'label': 'Option 2', 'value': 'opt2'},
{'label': 'Option 3', 'value': 'opt3'}
],
type='radio',
inline=True)
option_group.bind_value('selectedOption')
option_group.classes("q-mb-md")
# File input
bg.label("File Upload:").classes("q-mb-sm")
file_input = bg.component('q-file',
label='Choose file',
filled=True,
accept='.jpg,.png,.pdf')
file_input.bind_value('uploadedFile')
file_input.classes("q-mb-md")
# Color picker
bg.label("Color Picker:").classes("q-mb-sm")
color_picker = bg.component('q-color')
color_picker.bind_value('selectedColor')
color_picker.classes("q-mb-md")
# Layout Components
with bg.card():
bg.label("Layout Components").classes("text-h5 q-mb-md")
# Tabs
with bg.component('q-tabs',
v_model='activeTab',
dense=True,
class_='text-grey',
active_color='primary',
indicator_color='primary',
align='justify',
narrow_indicator=True,
container=True) as tabs:
bg.component('q-tab', name='tab1', label='Tab 1')
bg.component('q-tab', name='tab2', label='Tab 2')
bg.component('q-tab', name='tab3', label='Tab 3')
# Tab panels
with bg.component('q-tab-panels',
v_model='activeTab',
animated=True,
container=True):
with bg.component('q-tab-panel', name='tab1', container=True):
bg.label("Content for Tab 1").classes("text-h6")
bg.component('q-separator').classes("q-my-md")
bg.label("This is the first tab content")
with bg.component('q-tab-panel', name='tab2', container=True):
bg.label("Content for Tab 2").classes("text-h6")
bg.component('q-separator').classes("q-my-md")
bg.label("This is the second tab content")
with bg.component('q-tab-panel', name='tab3', container=True):
bg.label("Content for Tab 3").classes("text-h6")
bg.component('q-separator').classes("q-my-md")
bg.label("This is the third tab content")
# Display Components
with bg.card():
bg.label("Display Components").classes("text-h5 q-mb-md")
# Circular progress
bg.label("Circular Progress:").classes("q-mb-sm")
with bg.row() as progress_row:
progress_row.classes("q-gutter-md q-mb-md")
bg.component('q-circular-progress',
value=0.25,
size='50px',
color='red')
bg.component('q-circular-progress',
value=0.5,
size='50px',
color='orange')
bg.component('q-circular-progress',
value=0.75,
size='50px',
color='green')
# Chip components
bg.label("Chips:").classes("q-mb-sm")
with bg.row() as chip_row:
chip_row.classes("q-gutter-sm q-mb-md")
bg.component('q-chip', label='Tag 1', removable=True, color='primary')
bg.component('q-chip', label='Tag 2', removable=True, color='secondary')
bg.component('q-chip', label='Tag 3', removable=True, color='accent')
# Avatar
bg.label("Avatar:").classes("q-mb-sm")
bg.component('q-avatar', size='64px', color='teal', text_color='white', icon='person')
# Results display
with bg.card():
bg.label("Current Values").classes("text-h5 q-mb-md")
results = bg.label("Click 'Update Display' to see values")
results.bind_text('displayResults')
results.classes("text-body1 q-mb-md")
bg.button("Update Display").on_click('updateDisplay').classes("q-btn-primary")
# JavaScript for the extended components
bg.script("""
// Reactive data
const selectedDate = ref(null);
const selectedTime = ref(null);
const selectedOption = ref('opt1');
const uploadedFile = ref(null);
const selectedColor = ref('#1976D2');
const activeTab = ref('tab1');
const displayResults = ref('Click "Update Display" to see values');
// Update function
const updateDisplay = () => {
const results = [
`Date: ${selectedDate.value || 'Not selected'}`,
`Time: ${selectedTime.value || 'Not selected'}`,
`Option: ${selectedOption.value}`,
`File: ${uploadedFile.value ? uploadedFile.value.name : 'None'}`,
`Color: ${selectedColor.value}`,
`Active Tab: ${activeTab.value}`
];
displayResults.value = results.join('');
};
// Watch for changes
watch([selectedDate, selectedTime, selectedOption, uploadedFile, selectedColor, activeTab],
updateDisplay, { deep: true });
onMounted(() => {
console.log('Extended component test loaded!');
console.log('Testing advanced Quasar components:');
console.log('- q-date, q-time');
console.log('- q-option-group, q-file, q-color');
console.log('- q-tabs, q-tab-panels');
console.log('- q-circular-progress, q-chip, q-avatar');
updateDisplay();
});
""")
if __name__ == "__main__":
print("🧪 Extended Generic Component Test")
print("Testing advanced Quasar components with bg.component()")
print()
print("Components being tested:")
print("📅 Date & Time: q-date, q-time")
print("📝 Advanced Inputs: q-option-group, q-file, q-color")
print("📑 Layout: q-tabs, q-tab-panels")
print("📊 Display: q-circular-progress, q-chip, q-avatar")
# Run dev server
bg.dev(port=3032)

144
generic_component_test.py

@ -0,0 +1,144 @@
"""
Generic Component Test
Demonstrates the new bg.component() method for creating any Quasar component.
"""
import badgui as bg
with bg.page("/", "GenericComponentTest", "Generic Component Test"):
with bg.column() as main_col:
main_col.classes("q-pa-lg q-gutter-md")
bg.label("Generic Quasar Component Test").classes("text-h3 q-mb-lg")
with bg.card():
bg.label("Form Components").classes("text-h5 q-mb-md")
# Rating component
bg.label("Rating:").classes("q-mb-sm")
rating = bg.component('q-rating', max=5, size='2em', color='amber')
rating.bind_value('userRating')
rating.classes("q-mb-md")
# Select component
bg.label("Select an option:").classes("q-mb-sm")
select = bg.component('q-select',
filled=True,
options=['Option 1', 'Option 2', 'Option 3'],
label='Choose one')
select.bind_value('selectedOption')
select.classes("q-mb-md")
# Slider component
bg.label("Volume:").classes("q-mb-sm")
slider = bg.component('q-slider',
min=0,
max=100,
step=10,
markers=True,
color='primary')
slider.bind_value('volumeLevel')
slider.classes("q-mb-md")
# Toggle component
toggle = bg.component('q-toggle',
label='Enable notifications',
color='green')
toggle.bind_value('notificationsEnabled')
toggle.classes("q-mb-md")
with bg.card():
bg.label("Display Components").classes("text-h5 q-mb-md")
# Progress bar
bg.label("Progress:").classes("q-mb-sm")
progress = bg.component('q-linear-progress',
size='20px',
color='secondary',
value=0.6)
progress.classes("q-mb-md")
# Badge
btn = bg.component('q-btn', label='Messages', container=False)
btn.classes("q-mb-md")
# Note: q-badge with floating=True should be added as a separate component
# since it positions itself relative to the previous component
bg.component('q-badge', color='red', floating=True, label='5')
with bg.card():
bg.label("Container Components").classes("text-h5 q-mb-md")
# Expansion item (container component)
with bg.component('q-expansion-item',
label='Advanced Settings',
icon='settings',
container=True) as expansion:
bg.label("These are advanced settings").classes("q-pa-md")
# Nested components
bg.component('q-separator').classes("q-my-md")
with bg.row():
bg.component('q-checkbox', label='Option A', val='a').bind_value('optionA')
bg.component('q-checkbox', label='Option B', val='b').bind_value('optionB')
# Results display
with bg.card():
bg.label("Current Values").classes("text-h5 q-mb-md")
result_display = bg.label("Values will appear here")
result_display.bind_text('displayResults')
result_display.classes("text-body1")
bg.button("Update Display").on_click('updateDisplay').classes("q-btn-primary q-mt-md")
# JavaScript to handle the generic components
bg.script("""
// Reactive data for all the generic components
const userRating = ref(3);
const selectedOption = ref(null);
const volumeLevel = ref(50);
const notificationsEnabled = ref(false);
const optionA = ref(false);
const optionB = ref(false);
const displayResults = ref('Values will appear here');
// Update display function
const updateDisplay = () => {
const results = [
`Rating: ${userRating.value}/5 stars`,
`Selected: ${selectedOption.value || 'None'}`,
`Volume: ${volumeLevel.value}%`,
`Notifications: ${notificationsEnabled.value ? 'On' : 'Off'}`,
`Options: A=${optionA.value}, B=${optionB.value}`
];
displayResults.value = results.join('');
};
// Watch for changes and auto-update
watch([userRating, selectedOption, volumeLevel, notificationsEnabled, optionA, optionB],
updateDisplay, { immediate: true });
onMounted(() => {
console.log('Generic component test loaded!');
console.log('Available Quasar components can be created with bg.component()');
updateDisplay();
});
""")
if __name__ == "__main__":
print("🧪 Generic Component Test")
print("Testing bg.component() method for creating any Quasar component")
print()
print("Examples of components created:")
print("- q-rating (rating selector)")
print("- q-select (dropdown)")
print("- q-slider (range input)")
print("- q-toggle (switch)")
print("- q-linear-progress (progress bar)")
print("- q-badge (notification badge)")
print("- q-expansion-item (collapsible container)")
print("- q-separator (divider)")
print("- q-checkbox (checkbox)")
# Run dev server
bg.dev(port=3028)

94
refactored_test.py

@ -0,0 +1,94 @@
"""
Test refactored components using generic component() as base
"""
import badgui as bg
with bg.page("/", "RefactoredTest", "Refactored Components Test"):
with bg.column() as main_col:
main_col.classes("q-pa-lg q-gutter-md")
bg.label("Refactored Components Test").classes("text-h3 q-mb-lg")
# Test basic components
with bg.card(title="Basic Components") as card1:
card1.classes("q-mb-md")
bg.label("This is a label created with the refactored method")
input_field = bg.input("Enter some text here")
input_field.bind_value('userText')
input_field.classes("q-mb-md")
button = bg.button("Click Me", icon="thumb_up")
button.on_click('handleClick')
# Test layout components
with bg.card(title="Layout Components") as card2:
card2.classes("q-mb-md")
bg.label("Row layout:")
with bg.row() as test_row:
test_row.classes("q-gutter-md q-mb-md")
bg.label("Item 1").classes("col")
bg.label("Item 2").classes("col")
bg.label("Item 3").classes("col")
bg.label("Column layout:")
with bg.column() as test_col:
test_col.classes("q-gutter-sm")
bg.label("Column Item 1")
bg.label("Column Item 2")
bg.label("Column Item 3")
# Test mixed with generic components
with bg.card(title="Mixed Components") as card3:
card3.classes("q-mb-md")
bg.label("Regular button vs Generic button:")
with bg.row() as button_row:
button_row.classes("q-gutter-md")
# Regular button (refactored)
regular_btn = bg.button("Regular Button")
regular_btn.on_click('handleRegular')
# Generic button
generic_btn = bg.component('q-btn', label='Generic Button', color='secondary')
generic_btn.on_click('handleGeneric')
# JavaScript
bg.script("""
const userText = ref('');
const handleClick = () => {
console.log('Refactored button clicked!');
};
const handleRegular = () => {
console.log('Regular (refactored) button clicked!');
};
const handleGeneric = () => {
console.log('Generic button clicked!');
};
onMounted(() => {
console.log('Refactored components test loaded!');
console.log('All components now use the generic component() method as their base');
});
""")
if __name__ == "__main__":
print("🔧 Refactored Components Test")
print("Testing components that use generic component() as their base")
print()
print("This demonstrates:")
print("- Consistent behavior across all components")
print("- Reduced code duplication")
print("- Easier maintenance")
bg.dev(port=3033)
Loading…
Cancel
Save