mirror of
https://github.com/rio-labs/rio.git
synced 2026-02-09 07:09:00 -06:00
add CRUD tutorial
This commit is contained in:
@@ -0,0 +1 @@
|
||||
TODO
|
||||
@@ -0,0 +1,2 @@
|
||||
from .item_editor import ItemEditor
|
||||
from .item_list import ItemList
|
||||
@@ -0,0 +1,144 @@
|
||||
import rio
|
||||
|
||||
|
||||
# <additional-imports>
|
||||
from .. import models
|
||||
|
||||
# </additional-imports>
|
||||
|
||||
|
||||
# <component>
|
||||
class ItemEditor(rio.Component):
|
||||
"""
|
||||
A component for editing a menu item.
|
||||
|
||||
Returns a card component containing the menu item editor. The editor contains fields
|
||||
for name, description, price, and category of the menu item. The component also contains
|
||||
buttons for saving or canceling the changes.
|
||||
|
||||
Attributes:
|
||||
currently_selected_menu_item: The currently selected menu item.
|
||||
new_entry: A boolean flag indicating if the menu item is a new entry.
|
||||
on_cancel_event: An event handler for the cancel button.
|
||||
on_save_event: An event handler for the save button.
|
||||
"""
|
||||
|
||||
currently_selected_menu_item: models.MenuItems
|
||||
new_entry: bool = False
|
||||
on_cancel_event: rio.EventHandler[[]] = None
|
||||
on_save_event: rio.EventHandler[[]] = None
|
||||
|
||||
async def on_press_save_event(self) -> None:
|
||||
"""
|
||||
Asynchronously triggers the 'on_save_event' when the save button is pressed.
|
||||
"""
|
||||
await self.call_event_handler(self.on_save_event)
|
||||
|
||||
async def on_press_cancel_event(self) -> None:
|
||||
"""
|
||||
Asynchronously triggers the 'on_cancel_event' when the cancel button is pressed.
|
||||
"""
|
||||
await self.call_event_handler(self.on_cancel_event)
|
||||
|
||||
def on_change_name(self, ev: rio.TextInputChangeEvent) -> None:
|
||||
"""
|
||||
Changes the name of the currently selected menu item.
|
||||
|
||||
Args:
|
||||
ev: The event object that contains the new text.
|
||||
"""
|
||||
self.currently_selected_menu_item.name = ev.text
|
||||
|
||||
def on_change_description(self, ev: rio.TextInputChangeEvent) -> None:
|
||||
"""
|
||||
Changes the description of the currently selected menu item.
|
||||
|
||||
Args:
|
||||
ev: The event object that contains the new text.
|
||||
"""
|
||||
self.currently_selected_menu_item.description = ev.text
|
||||
|
||||
def on_change_price(self, ev: rio.NumberInputChangeEvent) -> None:
|
||||
"""
|
||||
Changes the price of the currently selected menu item.
|
||||
|
||||
Args:
|
||||
ev: The event object that contains the new price.
|
||||
"""
|
||||
self.currently_selected_menu_item.price = ev.value
|
||||
|
||||
def on_change_category(self, ev: rio.DropdownChangeEvent) -> None:
|
||||
"""
|
||||
Changes the category of the currently selected menu item.
|
||||
|
||||
Args:
|
||||
ev: The event object that contains the new category.
|
||||
"""
|
||||
self.currently_selected_menu_item.category = ev.value
|
||||
|
||||
def build(self) -> rio.Component:
|
||||
"""
|
||||
Builds the menu item editor component.
|
||||
|
||||
Returns:
|
||||
A card component containing the menu item editor.
|
||||
See the approx. layout below:
|
||||
|
||||
################ Card #################
|
||||
# Text #
|
||||
# TextInput (Name) #
|
||||
# TextInput (Description) #
|
||||
# NumberInput (Price) #
|
||||
# Dropdown (Category) #
|
||||
# Button (Save) | Button (Cancel) #
|
||||
#######################################
|
||||
"""
|
||||
|
||||
if self.new_entry is False:
|
||||
text = "Edit Menu Item"
|
||||
else:
|
||||
text = "Add New Menu Item"
|
||||
|
||||
return rio.Card(
|
||||
rio.Column(
|
||||
rio.Text(
|
||||
text=text,
|
||||
style="heading2",
|
||||
margin_bottom=1,
|
||||
),
|
||||
rio.TextInput(
|
||||
self.currently_selected_menu_item.name,
|
||||
label="Name",
|
||||
on_change=self.on_change_name,
|
||||
),
|
||||
rio.TextInput(
|
||||
self.currently_selected_menu_item.description,
|
||||
label="Description",
|
||||
on_change=self.on_change_description,
|
||||
),
|
||||
rio.NumberInput(
|
||||
self.currently_selected_menu_item.price,
|
||||
label="Price",
|
||||
suffix_text="$",
|
||||
on_change=self.on_change_price,
|
||||
),
|
||||
rio.Dropdown(
|
||||
options=["Burgers", "Desserts", "Drinks", "Salads", "Sides"],
|
||||
label="Category",
|
||||
selected_value=self.currently_selected_menu_item.category,
|
||||
on_change=self.on_change_category,
|
||||
),
|
||||
rio.Row(
|
||||
rio.Button("Save", on_press=self.on_press_save_event),
|
||||
rio.Button("Cancel", on_press=self.on_press_cancel_event),
|
||||
spacing=1,
|
||||
align_x=1,
|
||||
),
|
||||
spacing=1,
|
||||
align_y=0,
|
||||
margin=2,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# </component>
|
||||
@@ -0,0 +1,116 @@
|
||||
import rio
|
||||
|
||||
# <additional-imports>
|
||||
import functools
|
||||
from .. import models
|
||||
|
||||
# </additional-imports>
|
||||
|
||||
|
||||
# <component>
|
||||
class ItemList(rio.Component):
|
||||
"""
|
||||
A component for displaying a list of menu items.
|
||||
|
||||
Returns a list of menu items in a ListView component. Each item in the list contains the name,
|
||||
description, and a delete button. The delete button triggers an event to delete the item.
|
||||
|
||||
Attributes:
|
||||
menu_item_set: The list of menu items to be displayed.
|
||||
on_add_new_item_event: An event handler for adding a new item.
|
||||
on_delete_item_event: An event handler for deleting an item.
|
||||
on_select_item_event: An event handler for selecting an item.
|
||||
"""
|
||||
|
||||
menu_item_set: list[models.MenuItems]
|
||||
on_add_new_item_event: rio.EventHandler[[]] = None
|
||||
on_delete_item_event: rio.EventHandler[int] = None
|
||||
on_select_item_event: rio.EventHandler[models.MenuItems] = None
|
||||
|
||||
async def on_press_add_new_item_event(self) -> None:
|
||||
"""
|
||||
Asynchronously triggers the 'add new item' when the list item is pressed.
|
||||
"""
|
||||
await self.call_event_handler(self.on_add_new_item_event)
|
||||
|
||||
async def on_press_delete_item_event(self, idx: int) -> None:
|
||||
"""
|
||||
Asynchronously triggers the 'delete item' when the delete button is pressed.
|
||||
The event handler is passed the index of the item to be deleted.
|
||||
|
||||
Args:
|
||||
idx: The index of the item to be deleted.
|
||||
"""
|
||||
await self.call_event_handler(self.on_delete_item_event, idx)
|
||||
# update the list
|
||||
await self.force_refresh()
|
||||
|
||||
async def on_press_select_item_event(self, item: models.MenuItems) -> None:
|
||||
"""
|
||||
Asynchronously triggers the 'select item' when an item is selected.
|
||||
The event handler is passed the selected item.
|
||||
|
||||
Args:
|
||||
item: The selected item.
|
||||
"""
|
||||
await self.call_event_handler(self.on_select_item_event, item)
|
||||
|
||||
def build(self) -> rio.Component:
|
||||
"""
|
||||
Builds the component by returning a ListView component containing the menu items.
|
||||
|
||||
Returns:
|
||||
A ListView component containing the menu items.
|
||||
See the approx. layout below:
|
||||
|
||||
############### ListView ###############
|
||||
# + Add new #
|
||||
# Hamburger Button(Delete) #
|
||||
# Cheese Burger Button(Delete) #
|
||||
# Fries Button(Delete) #
|
||||
# ... #
|
||||
########################################
|
||||
"""
|
||||
|
||||
# Store all children in an intermediate list
|
||||
list_items = []
|
||||
|
||||
list_items.append(
|
||||
rio.SimpleListItem(
|
||||
text="Add new",
|
||||
secondary_text="Description",
|
||||
key="add_new",
|
||||
left_child=rio.Icon("material/add"),
|
||||
on_press=self.on_press_add_new_item_event,
|
||||
)
|
||||
)
|
||||
|
||||
for i, item in enumerate(self.menu_item_set):
|
||||
list_items.append(
|
||||
rio.SimpleListItem(
|
||||
text=item.name,
|
||||
secondary_text=item.description,
|
||||
right_child=rio.Button(
|
||||
rio.Icon("material/delete"),
|
||||
color=self.session.theme.danger_color,
|
||||
# Note the use of functools.partial to pass the
|
||||
# index to the event handler.
|
||||
on_press=functools.partial(self.on_press_delete_item_event, i),
|
||||
),
|
||||
# Use the name as the key to ensure that the list item
|
||||
# is unique.
|
||||
key=item.name,
|
||||
# Note the use of functools.partial to pass the
|
||||
# item to the event handler.
|
||||
on_press=functools.partial(self.on_press_select_item_event, item),
|
||||
)
|
||||
)
|
||||
|
||||
# Then unpack the list to pass the children to the ListView
|
||||
return rio.ListView(
|
||||
*list_items,
|
||||
align_y=0,
|
||||
)
|
||||
|
||||
|
||||
# </component>
|
||||
@@ -0,0 +1,76 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import * # type:ignore
|
||||
|
||||
|
||||
@dataclass
|
||||
class MenuItems:
|
||||
"""
|
||||
MenuItems data model.
|
||||
|
||||
Attributes:
|
||||
name: The name of the menu item.
|
||||
description: The description of the menu item.
|
||||
price: The price of the menu item.
|
||||
category: The category of the menu item.
|
||||
"""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
price: float
|
||||
category: str
|
||||
|
||||
@staticmethod
|
||||
def new_empty() -> "MenuItems":
|
||||
"""
|
||||
Creates a new empty MenuItems object.
|
||||
|
||||
Returns:
|
||||
MenuItems: A new empty MenuItems object.
|
||||
"""
|
||||
return MenuItems(
|
||||
name="",
|
||||
description="",
|
||||
price=0.0,
|
||||
category="",
|
||||
)
|
||||
|
||||
|
||||
# initial data
|
||||
MENUITEMSET: list[MenuItems] = [
|
||||
MenuItems(
|
||||
name="Hamburger",
|
||||
description="A classic hamburger with lettuce, tomato, and onions",
|
||||
price=4.99,
|
||||
category="Burgers",
|
||||
),
|
||||
MenuItems(
|
||||
name="Cheeseburger",
|
||||
description="A classic cheeseburger with lettuce, tomato, onions and cheese",
|
||||
price=5.99,
|
||||
category="Burgers",
|
||||
),
|
||||
MenuItems(
|
||||
name="Fries",
|
||||
description="A side of crispy fries",
|
||||
price=2.99,
|
||||
category="Sides",
|
||||
),
|
||||
MenuItems(
|
||||
name="Soda",
|
||||
description="A refreshing soda",
|
||||
price=1.99,
|
||||
category="Drinks",
|
||||
),
|
||||
MenuItems(
|
||||
name="Salad",
|
||||
description="A fresh salad",
|
||||
price=4.99,
|
||||
category="Salads",
|
||||
),
|
||||
MenuItems(
|
||||
name="Ice Cream",
|
||||
description="A sweet treat",
|
||||
price=3.99,
|
||||
category="Desserts",
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
from .crud_page import CrudPage
|
||||
@@ -0,0 +1,178 @@
|
||||
import rio
|
||||
|
||||
|
||||
# <additional-imports>
|
||||
from .. import components as comps
|
||||
from .. import models
|
||||
|
||||
# </additional-imports>
|
||||
|
||||
|
||||
# <component>
|
||||
class CrudPage(rio.Component):
|
||||
"""
|
||||
A CRUD page that allows users to create, read, update, and delete menu items.
|
||||
|
||||
This component is composed of a Banner component, an ItemList component, and an ItemEditor component.
|
||||
|
||||
The @rio.event.on_populate decorator is used to fetch data from a predefined data model and assign it to the
|
||||
menu_item_set attribute of the current instance. The on_press_delete_item, on_press_cancel_event, and
|
||||
on_press_save_event methods are used to handle delete, cancel, and save events, respectively. The on_press_add_new_item
|
||||
method is used to handle the add new item event. The on_press_select_menu_item method is used to handle the selection
|
||||
of a menu item.
|
||||
|
||||
Attributes:
|
||||
menu_item_set: A list of menu items.
|
||||
currently_selected_menu_item: The currently selected menu item.
|
||||
banner_text: The text to be displayed in the banner.
|
||||
is_new_entry: A flag to indicate if the currently selected menu item is a new entry.
|
||||
"""
|
||||
|
||||
menu_item_set: list[models.MenuItems] = []
|
||||
currently_selected_menu_item: models.MenuItems | None = None
|
||||
banner_text: str = ""
|
||||
is_new_entry: bool = False
|
||||
|
||||
@rio.event.on_populate
|
||||
def on_populate(self) -> None:
|
||||
"""
|
||||
Event handler that is called when the component is populated.
|
||||
|
||||
Fetches data from a predefined data model and assigns it to the menu_item_set
|
||||
attribute of the current instance.
|
||||
"""
|
||||
self.menu_item_set = models.MENUITEMSET
|
||||
|
||||
async def on_press_delete_item(self, idx: int) -> None:
|
||||
"""
|
||||
Perform actions when the "Delete" button is pressed.
|
||||
|
||||
Args:
|
||||
idx: The index of the item to be deleted.
|
||||
"""
|
||||
self.menu_item_set.pop(idx)
|
||||
self.banner_text = "Item was deleted"
|
||||
self.currently_selected_menu_item = None
|
||||
|
||||
async def on_press_cancel_event(self) -> None:
|
||||
"""
|
||||
Perform actions when the "Cancel" button is pressed.
|
||||
"""
|
||||
self.currently_selected_menu_item = None
|
||||
self.banner_text = ""
|
||||
|
||||
def on_press_save_event(self) -> None:
|
||||
"""
|
||||
Performs actions when the "Save" button is pressed.
|
||||
|
||||
This method appends the currently selected menu item to the menu item set if it is a new entry,
|
||||
or updates the menu item set if it is an existing entry. It also updates the banner text and sets
|
||||
the is_new_entry flag to False.
|
||||
"""
|
||||
assert self.currently_selected_menu_item is not None
|
||||
if self.is_new_entry:
|
||||
self.menu_item_set.append(self.currently_selected_menu_item)
|
||||
self.banner_text = "Item was added"
|
||||
self.is_new_entry = False
|
||||
self.currently_selected_menu_item = None
|
||||
else:
|
||||
self.banner_text = "Item was updated"
|
||||
|
||||
async def on_press_add_new_item(self) -> None:
|
||||
"""
|
||||
Perform actions when the "Add New" ListItem is pressed.
|
||||
|
||||
This method sets the currently selected menu item to a new empty instance of models.MenuItems,
|
||||
clears the banner text, and sets the is_new_entry flag to True.
|
||||
"""
|
||||
self.currently_selected_menu_item = models.MenuItems.new_empty()
|
||||
self.banner_text = ""
|
||||
self.is_new_entry = True
|
||||
|
||||
async def on_press_select_menu_item(
|
||||
self, selected_menu_item: models.MenuItems
|
||||
) -> None:
|
||||
"""
|
||||
Perform actions when a menu item is selected.
|
||||
|
||||
This method sets the currently selected menu item to the selected menu item,
|
||||
which is passed as an argument.
|
||||
|
||||
Args:
|
||||
selected_menu_item: The selected menu item.
|
||||
"""
|
||||
self.currently_selected_menu_item = selected_menu_item
|
||||
|
||||
def build(self) -> rio.Component:
|
||||
"""
|
||||
Builds the component to be rendered.
|
||||
|
||||
If there is no currently selected menu item, only the Banner and ItemList component is returned.
|
||||
|
||||
Otherwise, if there is a currently selected menu item, both the Banner and ItemList component
|
||||
and the ItemEditor component are returned.
|
||||
|
||||
Returns:
|
||||
rio.Component: The components to be rendered.
|
||||
See the approx. layout below:
|
||||
|
||||
############### Column ###############
|
||||
# Banner #
|
||||
# +------------------------------- #
|
||||
# | ItemList | #
|
||||
# | | #
|
||||
# +------------------------------- #
|
||||
######################################
|
||||
|
||||
or
|
||||
|
||||
############### Column ###############
|
||||
# Banner #
|
||||
# +------------------------------- #
|
||||
# | ItemList | ItemEditor | #
|
||||
# | | | #
|
||||
# +------------------------------- #
|
||||
######################################
|
||||
|
||||
"""
|
||||
|
||||
if self.currently_selected_menu_item is None:
|
||||
return rio.Column(
|
||||
rio.Banner(self.banner_text, style="danger"),
|
||||
comps.ItemList(
|
||||
menu_item_set=self.menu_item_set,
|
||||
on_add_new_item_event=self.on_press_add_new_item,
|
||||
on_delete_item_event=self.on_press_delete_item,
|
||||
on_select_item_event=self.on_press_select_menu_item,
|
||||
align_y=0,
|
||||
),
|
||||
align_y=0,
|
||||
margin=3,
|
||||
)
|
||||
else:
|
||||
return rio.Column(
|
||||
rio.Banner(self.banner_text, style="danger"),
|
||||
rio.Row(
|
||||
comps.ItemList(
|
||||
menu_item_set=self.menu_item_set,
|
||||
on_add_new_item_event=self.on_press_add_new_item,
|
||||
on_delete_item_event=self.on_press_delete_item,
|
||||
on_select_item_event=self.on_press_select_menu_item,
|
||||
align_y=0,
|
||||
),
|
||||
comps.ItemEditor(
|
||||
self.currently_selected_menu_item,
|
||||
new_entry=self.is_new_entry,
|
||||
on_cancel_event=self.on_press_cancel_event,
|
||||
on_save_event=self.on_press_save_event,
|
||||
),
|
||||
spacing=1,
|
||||
proportions=(2, 1),
|
||||
),
|
||||
spacing=1,
|
||||
align_y=0,
|
||||
margin=3,
|
||||
)
|
||||
|
||||
|
||||
# </component>
|
||||
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1000"
|
||||
height="600"
|
||||
viewBox="0 0 264.58333 158.75"
|
||||
version="1.1"
|
||||
id="svg896"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
||||
sodipodi:docname="thumbnail.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs890">
|
||||
<rect
|
||||
x="24.318259"
|
||||
y="28.991958"
|
||||
width="878.03067"
|
||||
height="524.25413"
|
||||
id="rect1" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#535353"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.1767979"
|
||||
inkscape:cx="758.83888"
|
||||
inkscape:cy="505.60933"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:document-rotation="0"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:pagecheckerboard="true"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2078"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="45"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata893">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="scale(0.26458333)"
|
||||
id="text1"
|
||||
style="fill:#002c32;-inkscape-font-specification:'Roboto Medium';font-family:Roboto;font-size:21.33333333px;line-height:30.23622047px;paint-order:stroke fill markers;stroke-width:185;font-weight:500;text-align:center;white-space:pre;shape-inside:url(#rect1)" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-weight:500;font-size:21.1667px;line-height:30px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';text-align:center;text-anchor:middle;fill:#ff0000;fill-opacity:1;stroke-width:48.9479;paint-order:stroke fill markers"
|
||||
x="132.78259"
|
||||
y="25.720444"
|
||||
id="text2"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan2"
|
||||
style="font-size:21.1667px;line-height:30px;fill:#ff0000;fill-opacity:1;stroke-width:48.9479"
|
||||
x="132.78259"
|
||||
y="25.720444">TODO: Thumbnail</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-weight:500;font-size:50.1539px;line-height:71.0843px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';text-align:center;text-anchor:middle;fill:#ff0000;fill-opacity:1;stroke-width:48.9479;paint-order:stroke fill markers"
|
||||
x="132.05902"
|
||||
y="77.584312"
|
||||
id="text3"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3"
|
||||
style="font-size:50.1539px;line-height:71.0843px;fill:#ff0000;fill-opacity:1;stroke-width:48.9479"
|
||||
x="132.05902"
|
||||
y="77.584312">Simple</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
style="font-size:50.1539px;line-height:71.0843px;fill:#ff0000;fill-opacity:1;stroke-width:48.9479"
|
||||
x="132.05902"
|
||||
y="148.66861"
|
||||
id="tspan1">Dashboard</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
Reference in New Issue
Block a user