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
This commit is contained in:
@@ -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.
|
||||
@@ -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*
|
||||
@@ -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)
|
||||
|
||||
+67
-108
@@ -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
|
||||
kwargs['_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
|
||||
|
||||
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 = []
|
||||
|
||||
if 'row' not in classes:
|
||||
classes.append('row')
|
||||
kwargs['classes'] = classes
|
||||
|
||||
component = Component('row', classes=classes, **kwargs)
|
||||
component = self._add_component(component)
|
||||
return self.component('div', container=True, **kwargs)
|
||||
|
||||
# Enter container context
|
||||
page = self._get_current_page()
|
||||
page._container_stack.append(page.current_container)
|
||||
page.current_container = component
|
||||
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):
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user