1. Introducción
Para crear un menú desplegable usando CSSS y FastHTML, primeramente se crea un menú con html y luego se convierte a FastHtml.
Hay que tener en cuent que el código FastHTML requiere incluior los ficheros estáticos (CSS) tal como se vió en el post anterior.
Para ver como se hacía he tomado como referencia estas dos urls:
phppot de Vincy
La información de los menús a mostrar está en un fichero con formato YAML (static/conf/menu.yml) y será esta:
menus: - { id: 1 , description: 'Menu 1' , action: null } - { id: 11 , description: 'Menu 11' , action: 'action11' } - { id: 2 , description: 'Menu 2' , action: 'action2' } - { id: 3 , description: 'Menu 3' , action: null } - { id: 31 , description: 'Menu 31' , action: 'action31' } - { id: 32 , description: 'Menu 32' , action: 'action32' } - { id: 321 , description: 'Menu 321' , action: 'action321' } - { id: 322 , description: 'Menu 322' , action: null } - { id: 323 , description: 'Menu 323' , action: 'action323'} - { id: 324 , description: 'Menu 324' , action: 'action324'} - { id: 3221 , description: 'Menu 3221' , action: 'action3221'} - { id: 3222 , description: 'Menu 3222' , action: 'action3222'} - { id: 3223 , description: 'Menu 3223' , action: 'action3223'} - { id: 3224 , description: 'Menu 3224' , action: 'action3224'} - { id: 3225 , description: 'Menu 3225' , action: 'action3225'}
En este fichero se han tomado estos criterios:
Los menús de la cabecera tienen un id con un solo dígito: Por ejemplo los ids: 1,2,3
Los hijos del menú de cabecera tendrán 2 dígitos en el id, el dígito de la decena corresponde al id del menú padre. Por ejemplo el el id 11 es hijo del id 1, los ids 31 y 32 son hijos del menú con id 3
Análogamente si dividimos por 10 un id, el cociente nos dará el id del menú padre.
Al final nos queda este árbol de menus donde se muestra solamente los ids:
11
2
3
31
32
321
322
3221
3222
3223
3224
3225
323
324
El funcionamioento es el siguiente:
Definimos la clase Menu, en el header defiimos las rutas de los css y librerías , defoinimos los menus después de leer el fichero yaml, creamos el menú y cuando seejecuta una cción en el post demomento modificamos el contenido del final de la página diciendo que acción se ha ejecutado
2. Código python (menu.py)
#1. Imports from fastapi.staticfiles import StaticFiles from fasthtml import common as fh import yaml #2. Defining Menu class class Menu(): # Constructor def __init__(self, id, description, action): self.id = id self.description = description self.action = action self.parentId=self.parentId=None if self.id is None or self.id<10 else int(str(id)[0:-1]) self.childrenIds=[] # Alternative constructor from a dioctionary using @classmethod @classmethod def fromDict(cls, d:dict): return cls(d['id'], d['description'], d['action']) # Setter for parentId def setParendId(self): self.parentId=self.parentId=None if self.id is None or self.id<10 else int(str(id)[0:-1]) #3. Load menus from conf/menus.yml as a list of dict def getMenus(menuFilePath: str='static/conf/menus.yml')->dict: with open(menuFilePath) as f: mnuDict=yaml.safe_load(f) mnuLst=[Menu.fromDict(m) for m in mnuDict['menus']] return mnuLst #4. Convert the list of menus into a dict whose key is the id # and get the children menus def getMenuDict(menus:list[Menu]): menuDict={} for i, m in enumerate(menus): m.childrenIds=[elem.id for elem in filter(lambda x:x.parentId==m.id,menus)] # Set childrenIds menuDict[m.id] = m return menuDict #5. Get the code of a sub menu and its nested (children) menus def getSubmenuCodeFH(id:int, menuDict:dict[int, Menu]): menu=menuDict.get(id) paramDict={} if len(menu.childrenIds)==0: paramDict['hx_post']=f"/action/{menu.action}" paramDict['hx_target']="#message" paramDict['hx_swap']="innerHTML" SpanContent='' else: SpanContent=fh.I(cls='triang fa fa-caret-right') href="#" tttClas='tooltiptext' if menu.id<10 else 'tooltiptextchild' return fh.Li( fh.A( menu.description, fh.Span( SpanContent, cls='expand' ), fh.Span( menu.description + '-tooltiptext', cls=tttClas ), **paramDict, cls='tooltip' ), fh.Ul(*[getSubmenuCodeFH(k, menuDict) for k in sorted(menu.childrenIds)], cls='child'), cls='parent' ) # Get the codde of the main menu def getGeneralMenuCodeFH(menuDict:dict[int, Menu]): return fh.Body( fh.Div(id='navbar', cls='navbar'), fh.Ul( *[getSubmenuCodeFH(k, menuDict) for k in filter(lambda x: x<10,list(menuDict.keys())) ], id='menu' ), fh.Div('qqqqq',id='message', cls='message') ) menus=getMenus('static/conf/menus.yml') menuDict=getMenuDict(menus) #6. Defining Headers (Styles, Scripts, etc) myHeaders = [ fh.Meta(name='viewport', content='width=device-width, initial-scale=1'), fh.Link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'), fh.Link(rel='stylesheet', href='../static/css/menu.css') ] #7. Creating FastAPI App app = fh.FastHTML(hdrs=myHeaders) #8. Mounting the static folder for accessing the menu.css app.mount("/static", StaticFiles(directory="static"), name="static") #9. Defining the routes @app.get("/") def geto1(): return getGeneralMenuCodeFH(menuDict=menuDict) #10. Menu Actions executed in a @app.post("/action/{actEdu}") def postAction(actEdu:str): return f"Action received: {actEdu}"
3. Código CSS (static/css/menu.css)
body {font-family: Arial, Helvetica, sans-serif;} .navbar {overflow: hidden;background-color: black;position:fixed;top: 0;height:46px; width: 100%; z-index:-1} #menu {padding-left:10px; } .parent {display: block;position: relative;float: left;line-height: 30px;background-color: black;border-right:#CCC 1px solid;} .parent a{margin: 10px;color: #FFFFFF;text-decoration: none;} .parent:hover > ul {display:block;position:absolute;} .parent li:hover {background-color: #F0F0F0;} .child {display: none;padding-top: 10px;box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);} .child li {background-color: #f9f9f9;line-height: 30px;border-right:#CCC 1px solid; width:100%;} .child li a{color: #000000;} .child .child{padding-top: 0px;} ul{list-style: none;margin: 0;padding: 0px; min-width:10em;} ul ul ul{left: 100%;top: 0;margin-left:1px;} li:hover {background-color: red;} .expand{float:right;margin-right:5px;} .triang{margin-top:6px;} .message {position: fixed; bottom:50px;color: green;background-color: white;border-right:#CCC 1px solid;} .tooltip {} .tooltip .tooltiptext { visibility: hidden; width: 150px; font-size:85%; color: black; background-color:gold; text-align: center; border-radius: 6px; padding: 5px 0; position: absolute; z-index: 1; top: -5px; left: 105%; } .tooltip:hover .tooltiptext {visibility: visible;} .tooltip .tooltiptextchild { visibility: hidden; width: 150px; font-size:85%; color: black; background-color:gold; text-align: center; border-radius: 6px; padding: 5px 0; position: absolute; z-index: 1; top: -5px; left: 105%; } .tooltip:hover .tooltiptextchild {visibility: visible;}
4. Ejecución (uvicorn menu:app)
En el terminal ejecutmos uvicorn menu:app