Components
Components are the building blocks of your application. They are reusable pieces of code that can be composed to create complex UIs. Each component has its own logic and template, making them easy to manage and maintain.
File Structure
Each component is a subfolder inside the components/ directory. The name of the folder is the name of the component. You can also have other components inside a subfolder for another component.
For a component to be correctly defined, you need two files:
- A template file with the suffix
_template.html. This is a Jinja template that receives data from the rendering function. - A Python file with the suffix
_logic.py. This file defines the business logic for the component and returns data to be rendered in the template. This is similar to the controller in MVC frameworks.
Component File Structure
Each component folder in /components could one of each of these files:
[component_name]_template.html: Jinja template for rendering (required)[component_name]_logic.py: Server-side logic and data preparation[component_name]_models.py: Optional SQLAlchemy models specific to this component
This strict naming convention ensures components are self-contained and easy to identify.
Subcomponents
You can create subcomponents by organizing components in subfolders. For example:
components/
maincomponent/
maincomponent_template.html
maincomponent_logic.py
subcomponent/
subcomponent_template.html
subcomponent_logic.py
Subcomponents are rendered using dot notation:
{{ component("maincomponent.subcomponent", param="value") }}
This allows for hierarchical component organization and reuse.
Component Models
When a component needs its own database models, create a [component_name]_models.py file. Use SQLAlchemy's DeclarativeBase for model definitions:
# mycomponent_models.py
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class MyModel(Base):
__tablename__ = "my_models"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column()
Models in component folders are automatically detected by Alembic for migrations.
Rendering Components
Whenever you are in an HTML template, you can render a component by including it like this:
{{ component("mycomponent", param="hello") }}
This will render the component named mycomponent. You can optionally pass parameters as key-value pairs of strings. At this moment, passing other objects as parameters is not allowed.
Passing Data with Props
The parameters you pass to a component in the template are received by the component's logic handler in the **props dictionary. This allows you to pass data from a parent template down to a child component.
# In your_component_logic.py
def load_template_context(request, session, db, **props):
# Access the "param" value passed from the template
my_param = props.get("param") # This will be "hello"
# You can now use this value in your logic
return {"processed_param": my_param.upper()}
The props dictionary is a key-value object where the keys are the parameter names you defined in the template, and the values are the strings you assigned to them. Currently, only string values are accepted as props.
Data Flow and Context Isolation
Components maintain strict isolation - each component's template can only access data returned by its own load_template_context function. Data is not shared between components automatically.
The data flow follows this pattern:
1. Template renders {{ component("mycomponent", param="value") }}
2. Framework calls mycomponent_logic.load_template_context(request, session, db, param="value")
3. Logic function returns a dictionary
4. Template receives and renders the dictionary data
Component Logic and Rendering Order
The _logic.py file handles the business logic of a component. It's important to understand how and when this logic is executed.
Important Note on Rendering Order
On GET requests, components are rendered hierarchically, meaning the components at the deepest level of nesting are rendered first. However, on POST requests (actions), the component that receives and processes the POST request is handled first. After its action handler finishes, the other components on the page are rendered by calling their GET handlers (load_template_context). This ensures that any state changes made during the action can be immediately reflected on the page.
Handling GET Requests
Each component that is part of a page (route) will have its load_template_context function called on GET requests. This handler must be present in every component's logic file.
def load_template_context(request, session, db, **props):
# Business logic goes here
return {"my_data": "Hello from the component"}
If a component is purely presentational and has no business logic to execute, you can omit creating the _logic.py file.
Handling POST Requests (Actions)
When you need components to handle actions, like submitting a form or saving data, you need to implement action functions. Action functions handle POST requests and their names must start with action_.
In Noventa, we rely on standard HTML form submissions. To link a form submission to an action_ handler, the form must contain a hidden input field named "action":
<form method="POST">
<input type="hidden" name="action" value="submit_website">
<!-- other form fields -->
<button type="submit">Submit</button>
</form>
This hidden input tells Noventa that the form submission should be handled by the action_submit_website handler in the corresponding _logic.py file.
def action_submit_website(request, session, db, **props):
# Handle form data from request.form
website_url = request.form.get("website_url")
# ... save to database or perform other actions
return {"message": "Website submitted successfully!"}
Template Restrictions
Noventa templates have important restrictions on templates because we are using Minijinja Rust crate, which does not fully implement all the functionality from Python's Jinja2:
- No functions or filters: Do not use Jinja functions, filters, or variables beyond basic evaluations
- No direct access: Cannot access
request,session, or global variables directly - Data only: Templates only render the dictionary returned from logic functions
<!-- Good: Simple conditional rendering -->
{% if user %}
<p>Hello {{ user.name }}!</p>
{% endif %}
<!-- Bad: No filters or functions -->
{{ user.name|upper }} <!-- Not allowed -->
{{ request.args.get('id') }} <!-- Not allowed -->