add CRUD tutorial

This commit is contained in:
Sn3llius
2024-04-10 18:18:38 +02:00
parent a0ad98531e
commit af02880887
9 changed files with 615 additions and 0 deletions

View File

@@ -0,0 +1 @@
TODO

View File

@@ -0,0 +1,2 @@
from .item_editor import ItemEditor
from .item_list import ItemList

View File

@@ -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>

View File

@@ -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>

View File

@@ -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",
),
]

View File

@@ -0,0 +1 @@
from .crud_page import CrudPage

View File

@@ -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>

View File

@@ -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