@ -19,6 +19,7 @@ class Component:
self . id = f " { component_type } _ { id ( self ) } "
self . _classes = props . get ( ' classes ' , [ ] )
self . _style = props . get ( ' style ' , { } )
self . _vue_ref = None
def add_child ( self , child : ' Component ' ) :
""" Add a child component. """
@ -46,10 +47,18 @@ class Component:
""" Convert component to Vue template string. """
spaces = " " * indent
# Special handling for script components
if self . component_type == ' script ' :
return self . _generate_script_template ( indent )
# Build attributes
attrs = [ ]
# Add ID
# Add ID (use manual id if provided, otherwise automatic)
manual_id = self . _props . get ( ' id ' )
if manual_id :
attrs . append ( f ' id= " { manual_id } " ' )
else :
attrs . append ( f ' id= " { self . id } " ' )
# Add classes
@ -71,7 +80,7 @@ class Component:
# Add other props
for key , value in self . _props . items ( ) :
if key not in [ ' classes ' , ' style ' ] :
if key not in [ ' classes ' , ' style ' , ' id ' ] : # Skip id since we handle it above
if isinstance ( value , bool ) :
if value :
attrs . append ( key )
@ -85,7 +94,19 @@ class Component:
attrs_str = " " + " " . join ( attrs ) if attrs else " "
# Generate template
if self . children :
if self . component_type == ' card ' and self . _props . get ( ' _card_title ' ) :
# Simplified card with automatic title section
title = self . _props . get ( ' _card_title ' )
template = f " { spaces } < { self . get_vue_tag ( ) } { attrs_str } > \n "
template + = f " { spaces } <q-card-section> \n "
template + = f " { spaces } <div class= \" text-h6 \" > { title } </div> \n "
template + = f " { spaces } </q-card-section> \n "
template + = f " { spaces } <q-card-section> \n "
for child in self . children :
template + = child . to_vue_template ( indent + 2 )
template + = f " { spaces } </q-card-section> \n "
template + = f " { spaces } </ { self . get_vue_tag ( ) } > \n "
elif self . children :
template = f " { spaces } < { self . get_vue_tag ( ) } { attrs_str } > \n "
for child in self . children :
template + = child . to_vue_template ( indent + 1 )
@ -118,6 +139,8 @@ class Component:
' footer ' : ' q-footer ' ,
' image ' : ' q-img ' ,
' icon ' : ' q-icon ' ,
' dialog ' : ' q-dialog ' ,
' script ' : ' script ' ,
' div ' : ' div ' ,
' router-link ' : ' router-link '
}
@ -127,6 +150,36 @@ class Component:
""" Get the text content of the component. """
return self . _props . get ( ' text ' , self . _props . get ( ' content ' , ' ' ) )
def _generate_script_template ( self , indent : int = 0 ) - > str :
""" Generate script tag template for script components. """
spaces = " " * indent
# Script components are usually not rendered in the main template
# They are handled separately during build
if self . _props . get ( ' _is_inline ' ) :
# Inline script - return as HTML comment for template
content = self . _props . get ( ' _script_content ' , ' ' )
return f " { spaces } <!-- Inline script: { len ( content ) } chars --> \n "
elif self . _props . get ( ' _external_file ' ) :
# External file - return as HTML comment for template
filename = self . _props . get ( ' _external_file ' )
return f " { spaces } <!-- External script: { filename } --> \n "
elif self . _props . get ( ' src ' ) :
# External URL - might be rendered in final build
src = self . _props . get ( ' src ' )
attrs = [ ]
for key , value in self . _props . items ( ) :
if key not in [ ' _script_content ' , ' _is_inline ' , ' _external_file ' , ' _file_content ' ] :
if isinstance ( value , bool ) and value :
attrs . append ( key )
elif not isinstance ( value , bool ) :
attrs . append ( f ' { key } = " { value } " ' )
attrs_str = " " + " " . join ( attrs ) if attrs else " "
return f " { spaces } <script { attrs_str } ></script> \n "
return f " { spaces } <!-- Script component --> \n "
def classes ( self , add : str = None , * , remove : str = None , replace : str = None ) :
""" Add, remove, or replace CSS classes (Tailwind, Quasar, custom). """
# Get current classes list
@ -200,6 +253,62 @@ class Component:
self . _props [ key ] = value
return self
def vue_ref ( self , ref_name : str ) :
""" Set a Vue ref name for this component to access it from JavaScript. """
self . _vue_ref = ref_name
self . _props [ ' ref ' ] = ref_name
return self
def get_vue_ref ( self ) - > Optional [ str ] :
""" Get the Vue ref name for this component. """
return self . _vue_ref
def bind_text ( self , reactive_var : str ) :
""" Bind component text content to a reactive variable using v-model or text interpolation. """
if self . component_type == ' label ' :
# For labels, use text interpolation (Vue templates don't need .value)
self . _props [ ' text ' ] = f " {{ {{ { reactive_var } }} }} "
else :
# For other components, try v-model
self . _props [ ' v-model ' ] = reactive_var
return self
def bind_value ( self , reactive_var : str ) :
""" Bind component value to a reactive variable using v-model. """
self . _props [ ' v-model ' ] = reactive_var
return self
def on ( self , event_type : str , handler : Union [ str , callable ] ) :
"""
Add an event handler to the component ( NiceGUI - style ) .
Args :
event_type : Event type ( e . g . , ' click ' , ' input ' , ' change ' )
handler : JavaScript function name / expression or Python function ( for future server support )
Usage :
button . on ( ' click ' , ' myJsFunction ' ) # JavaScript function name
button . on ( ' click ' , ' () => alert( " Hi " ) ' ) # JavaScript expression
button . on ( ' click ' , ' handleClick ' ) # JavaScript function name
input . on ( ' input ' , ' handleInput ' ) # JavaScript function name
"""
if callable ( handler ) :
# Python handler - for future server-side support
# For now, convert to JavaScript function name
handler_name = handler . __name__ if hasattr ( handler , ' __name__ ' ) else str ( handler )
self . _props [ f ' @ { event_type } ' ] = handler_name
elif isinstance ( handler , str ) :
# JavaScript handler (function name or expression)
self . _props [ f ' @ { event_type } ' ] = handler
else :
raise ValueError ( " Handler must be a string (JavaScript) or callable (Python function) " )
return self
def on_click ( self , handler : Union [ str , callable ] ) :
""" Convenience method for click events. """
return self . on ( ' click ' , handler )
def style ( self , * args , * * kwargs ) :
""" Set or update inline CSS styles. """
if not isinstance ( self . _style , dict ) :
@ -284,11 +393,15 @@ class App:
component = Component ( ' label ' , text = text , * * kwargs )
return self . _add_component ( component )
def button ( self , text : str , on_click : str = None , * * kwargs ) - > Component :
def button ( self , text : str , icon : str = None , color : str = ' primary ' , * * kwargs ) - > Component :
""" Create a button component. """
props = { ' label ' : text , * * kwargs }
if on_click :
props [ ' @click ' ] = on_click
if icon :
props [ ' icon ' ] = icon
if color :
props [ ' color ' ] = color
component = Component ( ' button ' , * * props )
return self . _add_component ( component )
@ -300,6 +413,23 @@ class App:
component = Component ( ' input ' , * * props )
return self . _add_component ( component )
@contextmanager
def div ( self , * * kwargs ) :
""" Create a div container component. """
component = Component ( ' div ' , * * kwargs )
component = self . _add_component ( component )
# Enter container context
page = self . _get_current_page ( )
page . _container_stack . append ( page . current_container )
page . current_container = component
try :
yield component
finally :
# Exit container context
page . current_container = page . _container_stack . pop ( )
def page ( self , path : str , name : str = None , title : str = None ) :
""" Create or switch to a page. """
if name is None :
@ -325,15 +455,30 @@ class App:
return self . _add_component ( component )
@contextmanager
def card ( self , * * kwargs ) :
""" Create a card container with shadow and padding. """
classes = kwargs . pop ( ' classes ' , [ ] ) # Remove classes from kwargs to avoid conflict
def card ( self , title : str = None , flat : bool = False , * * kwargs ) :
"""
Create a simplified card container .
Args :
title : Optional title for the card
flat : If True , removes the shadow for a flat appearance
* * kwargs : Additional properties
"""
classes = kwargs . pop ( ' classes ' , [ ] )
if isinstance ( classes , str ) :
classes = [ classes ]
elif not isinstance ( classes , list ) :
classes = [ ]
component = Component ( ' card ' , classes = classes , * * kwargs )
# Add default card styling
if flat :
classes . append ( ' q-card--flat ' )
props = { ' classes ' : classes , * * kwargs }
if title :
props [ ' _card_title ' ] = title
component = Component ( ' card ' , * * props )
component = self . _add_component ( component )
# Enter container context
@ -602,6 +747,218 @@ class App:
component = Component ( ' icon ' , * * props )
return self . _add_component ( component )
# Event Handling and Interactive Components
def button_with_handler ( self , text : str , on_click = None , js_handler : str = None , * * kwargs ) - > Component :
""" Create a button with enhanced event handling. """
props = { ' label ' : text , * * kwargs }
# Handle different types of click handlers
if on_click :
if callable ( on_click ) :
# Python function - we'll need to generate a unique handler ID
handler_id = f " handler_ { id ( on_click ) } "
props [ ' @click ' ] = f " handleEvent( ' { handler_id } ' , $event) "
# TODO: Register the Python handler for server communication
elif isinstance ( on_click , str ) :
if on_click . startswith ( ' js: ' ) :
# JavaScript function
props [ ' @click ' ] = on_click [ 3 : ] # Remove 'js:' prefix
else :
# Assume it's a method name or Vue expression
props [ ' @click ' ] = on_click
if js_handler :
# Custom JavaScript handler
props [ ' @click ' ] = js_handler
component = Component ( ' button ' , * * props )
return self . _add_component ( component )
@contextmanager
def dialog ( self , opened : bool = False , persistent : bool = False , * * kwargs ) :
""" Create a dialog component (NiceGUI-style). """
classes = kwargs . pop ( ' classes ' , [ ] )
if isinstance ( classes , str ) :
classes = [ classes ]
elif not isinstance ( classes , list ) :
classes = [ ]
props = { ' v-model ' : ' dialogOpen ' , * * kwargs }
if persistent :
props [ ' persistent ' ] = True
component = Component ( ' dialog ' , classes = classes , * * props )
component = self . _add_component ( component )
# Add dialog methods
component . opened = opened
component . open = lambda : self . _dialog_open ( component )
component . close = lambda : self . _dialog_close ( component )
component . submit = lambda result : self . _dialog_submit ( component , result )
# Enter container context
page = self . _get_current_page ( )
page . _container_stack . append ( page . current_container )
page . current_container = component
try :
yield component
finally :
# Exit container context
page . current_container = page . _container_stack . pop ( )
def _dialog_open ( self , dialog_component ) :
""" Open a dialog - this would be implemented via JS calls in real app. """
# For static generation, we'll add the JS methods to the component
dialog_component . _props [ ' ref ' ] = f " dialog_ { dialog_component . id } "
return f " this.$refs.dialog_ { dialog_component . id } .show() "
def _dialog_close ( self , dialog_component ) :
""" Close a dialog. """
return f " this.$refs.dialog_ { dialog_component . id } .hide() "
def _dialog_submit ( self , dialog_component , result ) :
""" Submit a dialog with a result. """
return f " this.$refs.dialog_ { dialog_component . id } .hide(); this.dialogResult = ' { result } ' "
def notify ( self , message : str , type_ : str = None , position : str = " bottom " ,
close_button : str = None , color : str = None , multiline : bool = False ,
js_call : bool = True , * * kwargs ) :
""" Create a notification (NiceGUI-style). """
# Build notification options
notify_options = {
' message ' : message ,
' position ' : position ,
* * kwargs
}
if type_ :
notify_options [ ' type ' ] = type_
if color :
notify_options [ ' color ' ] = color
if close_button :
notify_options [ ' actions ' ] = [ { ' label ' : close_button , ' color ' : ' white ' , ' handler ' : ' dismiss ' } ]
if multiline :
notify_options [ ' multiLine ' ] = True
# For static generation, create a notification call
if js_call :
# Generate JavaScript call for Vue component
options_str = json . dumps ( notify_options )
js_code = f " this.$q.notify( { options_str } ) "
# Add to current page's notification calls (we'll collect these during build)
page = self . _get_current_page ( )
if not hasattr ( page , ' _notifications ' ) :
page . _notifications = [ ]
page . _notifications . append ( js_code )
return js_code
else :
# Return options for manual handling
return notify_options
def script ( self , content : Union [ str , os . PathLike ] = None , src : Union [ str , os . PathLike ] = None ,
import_mode : bool = False , * * kwargs ) - > Component :
"""
Add custom JavaScript to the page .
Args :
content : JavaScript code as a string
src : Path to external JavaScript file
import_mode : If True , external files are imported as ES6 modules in the component
* * kwargs : Additional attributes for the script tag
Usage :
# Inline script
bg . script ( " console.log( ' Hello from BadGUI! ' ); " )
# External file (traditional script tag)
bg . script ( src = " path/to/script.js " )
# External file (ES6 module import)
bg . script ( src = " path/to/utils.js " , import_mode = True )
# With attributes
bg . script ( content = " ... " , type = " module " , defer = True )
"""
props = { * * kwargs }
if content is not None and src is not None :
raise ValueError ( " Cannot specify both ' content ' and ' src ' parameters " )
if content is None and src is None :
raise ValueError ( " Must specify either ' content ' or ' src ' parameter " )
if content is not None :
# Inline script content
if os . path . isfile ( content ) :
# If content looks like a file path, read it
try :
with open ( content , ' r ' , encoding = ' utf-8 ' ) as f :
props [ ' _script_content ' ] = f . read ( )
props [ ' _is_inline ' ] = True
except ( OSError , IOError ) :
# If file reading fails, treat as literal content
props [ ' _script_content ' ] = str ( content )
props [ ' _is_inline ' ] = True
else :
# Literal JavaScript content
props [ ' _script_content ' ] = str ( content )
props [ ' _is_inline ' ] = True
elif src is not None :
# External file reference
if os . path . isfile ( src ) :
# File exists, we'll copy it to the build output
with open ( src , ' r ' , encoding = ' utf-8 ' ) as f :
file_content = f . read ( )
# Store file content and path for build process
filename = os . path . basename ( src )
props [ ' _external_file ' ] = filename
props [ ' _file_content ' ] = file_content
props [ ' _import_mode ' ] = import_mode
if import_mode :
# For import mode, we'll copy to src/utils/ and import
props [ ' _import_path ' ] = f " ../utils/ { filename } "
else :
# Traditional script tag in public/js/
props [ ' src ' ] = f " /js/ { filename } "
else :
# Assume it's a URL or will be provided later
props [ ' src ' ] = str ( src )
props [ ' _import_mode ' ] = False
# Add to current page's script collection
page = self . _get_current_page ( )
if not hasattr ( page , ' _scripts ' ) :
page . _scripts = [ ]
component = Component ( ' script ' , * * props )
page . _scripts . append ( component )
# Also add as regular component for template generation
return self . _add_component ( component )
def on_event ( self , component : Component , event_type : str , handler = None , js_handler : str = None , * * kwargs ) :
""" Add event handler to a component (NiceGUI-style). """
if handler and callable ( handler ) :
# Python handler - generate unique ID and register
handler_id = f " handler_ { id ( handler ) } _ { event_type } "
component . _props [ f ' @ { event_type } ' ] = f " handleEvent( ' { handler_id } ' , $event) "
# TODO: Store handler for server communication
elif js_handler :
# JavaScript handler
component . _props [ f ' @ { event_type } ' ] = js_handler
elif isinstance ( handler , str ) :
# String handler (Vue expression or method name)
component . _props [ f ' @ { event_type } ' ] = handler
return component
def build ( self , output_dir : str = " ./badgui-output " , project_name : str = " badgui-app " , * * kwargs ) :
""" Build the Vue.js/Quasar project. """
from . generator import VueGenerator