--------------------------------------------------------------------------------
/demosite/demo/components/examples/disable_button/__init__.py:
--------------------------------------------------------------------------------
1 | from tetra import Component, public
2 |
3 |
4 | class DisableButton(Component):
5 |
6 | # update=False, because in the demo, we don't want to refresh the component,
7 | # as the button would be re-enabled then.
8 | @public(update=False)
9 | def submit(self):
10 | pass
11 |
--------------------------------------------------------------------------------
/demosite/demo/components/examples/disable_button/disable_button.html:
--------------------------------------------------------------------------------
1 |
28 | """
--------------------------------------------------------------------------------
/demosite/demo/templates/counter_index.txt:
--------------------------------------------------------------------------------
1 | {% @ demo.Counter key="counter-1" %}
2 | {% @ demo.Counter key="counter-2" current_sum=sum %}
3 | {% @ demo.Counter key="counter-3" current_sum=sum / %}
4 | {% /@ demo.Counter %}
5 | {% /@ demo.Counter %}
--------------------------------------------------------------------------------
/demosite/demo/templates/examples/README.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | This directory contains Tetra examples. It follows a certain structure:
4 |
5 | ```
6 | name_of_example/
7 | demo.html
8 | text.md
9 | component.py
10 | other_example/
11 | demo.html
12 | text.md
13 | component.py
14 | ```
15 |
16 | * The `text.md` file contains the description of the example, with code sections. This is rendered as HTML. It must contain a `title` as front matter. You can include source files using `{% md_include_source 'path/to/file' 'optional_first_line_comment' %}`
17 | * The `demo.html` part, which is a django template, using the Tetra component, is rendered.
18 |
--------------------------------------------------------------------------------
/demosite/demo/templates/examples/click_to_edit/demo.html:
--------------------------------------------------------------------------------
1 | {% load tetra %}
2 |
3 |
Click the line below to start editing.
4 |
5 | {% @ demo.examples.ClickToEdit / %}
6 |
--------------------------------------------------------------------------------
/demosite/demo/templates/examples/click_to_edit/text.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Click to Edit
3 | ---
4 |
5 | # Click to Edit
6 |
7 | The *click-to-edit* pattern enables inline editing of a record without refreshing the page.
8 |
9 | This is a simple way to implement this as Tetra component, including save/cancel buttons:
10 | {% md_include_source "demo/components/examples/click_to_edit/__init__.py" %}
11 | {% md_include_source "demo/components/examples/click_to_edit/click_to_edit.html" %}
12 |
13 |
14 | If you click the text, it is replaced with an input form field.
15 |
16 | You could also imagine to do that in other ways:
17 |
18 | * by hiding the borders of the input field in display mode, and showing them again using Alpine when in edit_mode.
19 | * without buttons, just by using the `@blur` event for saving.
--------------------------------------------------------------------------------
/demosite/demo/templates/examples/counter/demo.html:
--------------------------------------------------------------------------------
1 | {% load tetra %}
2 |
3 | {% @ demo.examples.Counter / %}
4 |
--------------------------------------------------------------------------------
/demosite/demo/templates/examples/counter/text.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Counter
3 | ---
4 |
5 | # Counter demo
6 |
7 | The "counter" is basically the "Hello World" demo of components. It is a simple demo of how to use Tetra components.
8 |
9 | The component itself only provides a `count` attribute, and a public `increment()` method.
10 |
11 | 'nuff said, show me the code.
12 |
13 | {% md_include_component_source "demo.examples.Counter" %}
14 |
15 | Rendering is straightforward.
16 |
17 | {% md_include_component_template "demo.examples.Counter" %}
18 |
19 | Note below in the demo how fast Tetra rendering is. Component updates almost feel as fast as native Javascript.
20 |
--------------------------------------------------------------------------------
/demosite/demo/templates/examples/delete_row/demo.html:
--------------------------------------------------------------------------------
1 | {% load tetra %}
2 |
3 | {% @ demo.examples.DeleteRowTable / %}
4 |
--------------------------------------------------------------------------------
/demosite/demo/templates/examples/delete_row/text.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Delete Row
3 | ---
4 |
5 | # Delete Row
6 |
7 | Here's an example component demonstrating how to create a delete button that removes a table row when clicked.
8 |
9 | {% md_include_source "demo/components/examples/delete_row_table/__init__.py" %}
10 | {% md_include_source "demo/components/examples/delete_row_table/delete_row_table.html" %}
11 |
12 | So far for the table component. The rows are components themselves. Each row is responsible for its own deletion. So there is no `delete_item(some_id)` necessary, as the component already knows its id since it internally saves its state. `delete_item()` is sufficient within the component's template code.
13 |
14 | {% md_include_source "demo/components/examples/delete_row/__init__.py" %}
15 |
16 | {% md_include_source "demo/components/examples/delete_row/delete_row.html" %}
17 |
--------------------------------------------------------------------------------
/demosite/demo/templates/examples/disable_button/demo.html:
--------------------------------------------------------------------------------
1 | {% load tetra %}
2 |
3 | {% @ demo.examples.DisableButton / %}
4 |
--------------------------------------------------------------------------------
/demosite/demo/templates/examples/disable_button/text.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Disable submit button
3 | ---
4 |
5 | # Disable submit button
6 |
7 | When submitting a form, many users tend to double-click the `submit` button, leading to double entries in the databases, if the timing was right ;-)
8 |
9 | It is an easy pattern to just disabling the button right after clicking it. You can do two things in the `@click` listener: disable the button *and* call `submit()`.
10 |
11 | {% md_include_source "demo/components/examples/disable_button/disable_button.html" %}
12 |
13 |
14 | If you click the button, it is disabled, without altering the state. When the component is reloaded, the buttin is enabled again (for a create form), but mostly, you will redirect to another page using `self.client._redirect(...)`
15 |
16 |
--------------------------------------------------------------------------------
/demosite/demo/templates/examples/introduction/text.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Examples
3 | ---
4 |
5 | # Tetra examples
6 |
7 | Here you can see a few examples of common coding patterns, solved by the Tetra approach.
8 |
9 | Keep in mind that these are not the only way how to solve these problems, even not in Tetra.
10 |
11 | Choose an example on the left sidebar.
12 |
--------------------------------------------------------------------------------
/demosite/demo/templates/examples/spinner/demo.html:
--------------------------------------------------------------------------------
1 | {% load tetra %}
2 |
3 | {% @ demo.examples.LoadingIndicatorDemo / %}
4 |
--------------------------------------------------------------------------------
/demosite/demo/templates/examples/spinner/text.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Click to Edit
3 | ---
4 |
5 | # Loading indicator / spinner
6 |
7 | A common pattern is showing loading indicator (also called "spinner"), whenever a request duration is longer than the usual user is inclined to wait, without getting nervous...
8 |
9 |
10 | {% md_include_source "demo/components/examples/spinner/__init__.py" %}
11 | {% md_include_source "demo/components/examples/spinner/spinner.html" %}
12 |
13 | You'll need a bit of CSS to get this to work, as you have to hide the spinner per default:
14 |
15 | {% md_include_source "demo/components/examples/spinner/spinner.css" %}
16 |
17 | You can also accomplish the hiding with `opacity: 0` and `opacity:1` with a `transition` to make it smoother.
18 |
19 | You can click the button below, the spinner is shown for the period of the tetra request and hidden again afterwords.
--------------------------------------------------------------------------------
/demosite/demo/templates/reactive_components.txt:
--------------------------------------------------------------------------------
1 | import itertools
2 | from sourcetypes import django_html
3 | from tetra import Component, public, Library
4 | from .movies import movies
5 |
6 | class ReactiveSearch(Component):
7 | query = public("")
8 | results = []
9 |
10 | @public.watch("query").throttle(200, leading=False, trailing=True)
11 | def watch_query(self, value, old_value, attr):
12 | if self.query:
13 | self.results = itertools.islice(
14 | (movie for movie in movies if self.query.lower() in movie.lower()), 20
15 | )
16 | else:
17 | self.results = []
18 |
19 | template: django_html = """
20 |
", "")
11 |
12 |
13 | def doc(request, slug="introduction"):
14 | with open(settings.BASE_DIR.parent / "docs" / "structure.yaml") as f:
15 | raw_structure = yaml.load(f, Loader=yaml.CLoader)
16 | structure = []
17 | slugs = []
18 | for top_level in raw_structure:
19 | key, value = next(iter(top_level.items()))
20 | if isinstance(value, str):
21 | # Header with link
22 | slugs.append(key)
23 | structure.append(
24 | {
25 | "slug": key,
26 | "title": markdown_title(value),
27 | "items": [],
28 | }
29 | )
30 | else:
31 | # Header with sub items
32 | items = []
33 | for item in value:
34 | item_key, item_value = next(iter(item.items()))
35 | slugs.append(item_key)
36 | items.append(
37 | {
38 | "slug": item_key,
39 | "title": markdown_title(item_value),
40 | }
41 | )
42 | structure.append(
43 | {
44 | "slug": None,
45 | "title": markdown_title(key),
46 | "items": items,
47 | }
48 | )
49 |
50 | if slug not in slugs:
51 | raise Http404()
52 |
53 | with open(settings.BASE_DIR.parent / "docs" / f"{slug}.md") as f:
54 | md = markdown.Markdown(
55 | extensions=[
56 | "extra",
57 | "meta",
58 | TocExtension(permalink="#", toc_depth=3),
59 | ]
60 | )
61 | content = md.convert(f.read())
62 | print(md.Meta)
63 | return render(
64 | request,
65 | "base_docs.html",
66 | {
67 | "structure": structure,
68 | "content": content,
69 | "toc": md.toc,
70 | "active_slug": slug,
71 | "title": " ".join(md.Meta["title"]),
72 | },
73 | )
74 |
--------------------------------------------------------------------------------
/demosite/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 | from pathlib import Path
6 |
7 | # Add parent dir to PYTHONPATH so that 'tetra' is available
8 | sys.path.append(str(Path(__file__).resolve().parent.parent))
9 |
10 |
11 | def main() -> None:
12 | """Run administrative tasks."""
13 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demosite.settings")
14 | try:
15 | from django.core.management import execute_from_command_line
16 | except ImportError as exc:
17 | raise ImportError(
18 | "Couldn't import Django. Are you sure it's installed and "
19 | "available on your PYTHONPATH environment variable? Did you "
20 | "forget to activate a virtual environment?"
21 | ) from exc
22 | execute_from_command_line(sys.argv)
23 |
24 |
25 | if __name__ == "__main__":
26 | main()
27 |
--------------------------------------------------------------------------------
/demosite/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demosite",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "test": "echo \"Error: no test specified\" && exit 1"
7 | },
8 | "private": true,
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "esbuild": "~=0.25"
13 | },
14 | "directories": {
15 | "doc": "docs"
16 | },
17 | "description": ""
18 | }
19 |
--------------------------------------------------------------------------------
/docs/attribute-tag.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "The `...` Attribute"
3 | ---
4 |
5 | # `...` Attribute Tag
6 |
7 | HTML attributes regularly need to be set programmatically in a template. To aid in this, Tetra has the "attribute tag", available as `...` (three periods) as it "unpacks" the arguments provided to it as HTML attributes.
8 |
9 | The attributes tag is automaticity available in your **component templates**. In other templates be sure to `{% load tetra %}`.
10 |
11 | ``` django
12 | {% load tetra %}
13 |
15 | ```
16 |
17 | All Tetra components have an `attrs` context available, which is a `dict` of attributes that have been passed to the component when it is included in a template with the [`@` tag](component-tag.md). It can be unpacked as HTML attributes on your root node:
18 |
19 | ``` django
20 |
21 | ```
22 |
23 | The attribute tag can take the following arguments:
24 |
25 | - A (not keyword) variable resolving to a `dict` of attribute names mapped to values. This is what the `attrs` context variable is.
26 |
27 | - An attribute name and literal value such as `class="test"`.
28 |
29 | - An attribute name and context variable such as `class=list_of_classes`.
30 |
31 | The attributes are processed left to right, and if there is a duplicate attribute name the last occurrence is used (there is a special case for [`class`](#class-attribute) and [`style`](#style-attribute)). This allows you to provide both default values and overrides. In the example below the `id` has a default value of `"test"` but can be overridden when the component is used via the `attrs` variable. It also forces the `title` attribute to a specific value overriding any set in `attrs`.
32 |
33 | ``` django
34 |
35 | ```
36 |
37 | ## Boolean attributes
38 |
39 | Boolean values have a special case. If an attribute is set to `False` it is not included in the final HTML. If an attribute is set to `True` it is included in the HTML as just the attribute name, such as ``.
40 |
41 | ## Class attribute
42 |
43 | The `class` attribute treats each class name as an individual option concatenating all passed classes. In the example below all classes will appear on the final element:
44 |
45 | ``` django
46 | {# where the component is used #}
47 | {% @ Component attrs: class="class1" %}
48 |
49 | {# component template with a_list_of_classes=["classA", "classB"] #}
50 |
57 | ```
58 |
59 | ## Style attribute
60 |
61 | There is a special case for the `style` attribute, similar to the `class` attribute. All passed styes are split into individual property names with the final value for property name used in the final attribute.
62 |
63 | ``` django
64 |
65 | ```
66 |
67 | Would result in:
68 |
69 | ``` html
70 |
71 | ```
72 |
73 | !!! note
74 | Tetra currently does not understand that a style property can be applied in multiple ways. Therefore, if you pass both `margin-top: 1em` and `margin: 2em 0 0 0`, both will appear in the final HTML style tag, with the final property taking precedence in the browser.
75 |
76 | ## Conditional values
77 |
78 | The [`if` and `else` template filters](if-else-filters.md) are provided to enable conditional attribute values:
79 |
80 | ``` html
81 |
82 | ```
83 |
84 | See the documentation for the [`if` and `else` template filters](if-else-filters.md).
85 |
--------------------------------------------------------------------------------
/docs/basic-components.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Basic Components
3 | ---
4 |
5 | # Basic Components
6 |
7 | `BasicComponent` supports CSS, but not JS, Alpine.js, or any public attributes or methods. Basic Components should be used for encapsulating reusable components that have no direct client side interaction and are useful for composing within other components.
8 |
9 | As they don't save their state to be resumable, or initiate an Alpine.js component in the browser, they have lower overhead.
10 |
11 | They are registered exactly the same way as normal components and their CSS styles will be bundled with the rest of the library's styles.
12 |
13 | Supported features:
14 |
15 | - `load` method
16 | - `template`
17 | - `styles`
18 | - Private methods and attributes
19 |
20 | ``` python
21 | from tetra import BasicComponent
22 |
23 | class MyBasicComponent(BasicComponent):
24 | ...
25 | ```
--------------------------------------------------------------------------------
/docs/changelog.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Changelog
3 | ---
4 |
5 | # Changelog
6 |
7 | !!! note
8 | Tetra is still early in its development, and we can make no promises about
9 | API stability at this stage.
10 |
11 | The intention is to stabilise the API prior to a v1.0 release, as well as
12 | implementing some additional functionality.
13 | After v1.0 we will move to using [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
14 |
15 | ## [0.3.2] - unreleased
16 | ### Changed
17 | - rename `request.tetra.current_url_abs_path` to `current_url_full_path` to better adhere to naming standards. `current_url_abs_path` is deprecated.
18 |
19 | ### Added
20 | - `request.tetra.current_url_path` that holds the path without query params
21 | - a `tetra` template variable that holds the TetraDetails of a request, or possible equivalents of the current main request.
22 | - Client URL pushes are now anticipated and reflected on the server before rendering the component on updates
23 |
24 | ## [0.3.1] - 2025-04-19
25 | - **BREAKING CHANGE** rename all `tetra:*` events to kebab-case: `before-request`, `after-request`, `component-updated`, `component-before-remove` etc. This was necessary because camelCase Events cannot be used properly in `x-on:` attributes - HTMX attributes are forced to lowercase, which breaks the event capture.
26 |
27 | ## [0.3.0] - 2025-04-18
28 | ### Added
29 | - beforeRequest, afterRequest events
30 | - add support for loading indicators (=spinners)
31 | - add support for file downloads in component methods
32 |
33 | ### Changed
34 | - **BREAKING CHANGE** rename all `tetra:*` events to camelCase: `componentUpdated`, `componentBeforeRemove` etc.
35 |
36 | ### Fixed
37 | - fix file uploads for FormComponent (using multipart/form-data)
38 |
39 | ## [0.2.1] - 2025-03-29
40 | - fix a small bug that could early-delete temporary uploaded files
41 |
42 | ## [0.2.0] - 2025-03-27
43 | ### Added
44 | - DynamicFormMixin for dynamically updatable FormComponents
45 | - Improved demo site
46 | - Added debug logging handler
47 | - Improved component import error handling
48 | - Allow component names to be dynamic
49 | - `@v` shortcut templatetag for "live" rendering of frontend variables
50 | - Better support for Django models
51 | - Experimental FormComponent and ModelFormComponent support with form validation
52 | - Definable extra context per component class
53 | - `reset()` method for FormComponent
54 | - `push_url()` and `replace_url()` component methods for manipulating the URL in the address bar
55 | - `recalculate_attrs()` method for calculated updates to attributes before and after component methods
56 | - `request.tetra` helper for *current_url*, *current_abs_path* and *url_query_params*, like in HTMX
57 | - add life cycle Js events when updating/removing etc. components
58 | - add a T-Response header that only is available in Tetra responses.
59 | - Integration of Django messages into Tetra, using T-Messages response header
60 | - `` at begin of components are possible now
61 |
62 | ### Removed
63 | - **BREAKING CHANGE** `ready()` method is removed and functionally replaced with `recalculate_attrs()`
64 |
65 | ### Changed
66 | - **BREAKING CHANGE**: registering libraries is completely different. Libraries are directories and automatically found. The library automatically is the containing module name now. Explicit "default = Library()" ist disregarded.
67 | - Components should be referenced using PascalCase in templates now
68 | - Component tags: replace `**context` with `__all__` when passing all context in template tags
69 | - More verbose error when template is not enclosed in HTML tags
70 | - Improved component import error handling
71 | - Improved demo site
72 |
73 | ## [0.1.1] - 2024-04-10
74 | ### Changed
75 | - **New package name: tetra**
76 | - Add conditional block check within components
77 | - Update Alpine.js to v3.13.8
78 | - Switch to pyproject.toml based python package
79 | - Improve demo project: TodoList component,- add django-environ for keeping secrets, use whitenoise for staticfiles
80 | - Give users more hints when no components are found
81 | - MkDocs based documentation
82 | - Format codebase with Black
83 |
84 | ### Added
85 | - Basic testing using pytest
86 |
87 | ### Fixed
88 | - Correctly find components
89 |
90 | ## [0.0.5] - 2022-06-13
91 | ### Changed
92 | - **This is the last package with the name "tetraframework", transition to "tetra"**
93 | - Provisional Python 3.8 support
94 |
95 | ### Fixed
96 | - Windows support
97 |
98 |
99 | ## [0.0.4] - 2022-06-22
100 | - Cleanup
101 |
102 |
103 | ## [0.0.3] - 2022-05-29
104 | ### Added
105 | - `_parent` client attribute added, this can be used to access the parent component mounted in the client.
106 | - `_redirect` client method added, this can be used to redirect to another url from the public server methods. `self.client._redirect("/test")` would redirect to the `/test` url.
107 | - `_dispatch` client method added, this is a wrapper around the Alpine.js [`dispatch` magic](https://alpinejs.dev/magics/dispatch) allowing you to dispatch events from public server methods. These bubble up the DOM and be captured by listeners on (grand)parent components. Example: `self.client._dispatch("MyEvent", {'some_data': 123})`.
108 | - `_refresh` public method added, this simply renders the component on the server updating the dom in the browser. This can be used in combination with `_parent` to instruct a parent component to re-render from a child components public method such as: `self.client._parent._refresh()`
109 |
110 | ### Changed
111 | - Built in Tetra client methods renamed to be prefixed with an underscore so that they are separated from user implemented methods:
112 | - `updateHtml` is now `_updateHtml`
113 | - `updateData` is now `_updateData`
114 | - `removeComponent` is now `_removeComponent`
115 | - `replaceComponentAndState` is now `_replaceComponent`
116 |
--------------------------------------------------------------------------------
/docs/component-inheritance.md:
--------------------------------------------------------------------------------
1 | Title: Component Inheritance
2 |
3 | # Component inheritance - abstract components
4 |
5 | Components basically are inheritable, to create components that bundle common features, which can be reused and extended by more specialized ones. But: **You cannot inherit from already registered components.**
6 |
7 | As components are registered automatically by putting them into a library module, you can create *abstract components* to exclude them from registering.
8 |
9 | This works with both `BasicComponent` and `Component`.
10 |
11 | ```python
12 | # no registering here!
13 | class BaseCard(BasicComponent):
14 | __abstract__ = True
15 | template = ""
16 |
17 |
18 | # no registering here!
19 | class Card(BaseCard):
20 | __abstract__ = True
21 | template: django_html = """
22 |
23 | {% block default %}{% endblock %]}
24 |
25 | """
26 |
27 |
28 | # This component is registered:
29 | class GreenCard(Card):
30 | style: css = """
31 | .mycard {
32 | background-color: green
33 | }
34 | """
35 | ```
36 |
37 | You can even define more than one directory style components in one file, as long as only *one* of them is actually registered, the others must be abstract.
--------------------------------------------------------------------------------
/docs/component-libraries.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Libraries
3 | ---
4 |
5 | # Libraries
6 |
7 | Every Tetra component belongs to a component library. Basically, libraries are the modules within `.components` (or, alternatively, `.tetra_components`) where components are found automatically:
8 |
9 | ```
10 | myapp
11 | ├── components/
12 | │ ├──anotherlib
13 | │ ├──default *
14 | │ ├──ui
15 | ```
16 |
17 | With other words: The first module layer within `myapp.components` are libraries. It doesn't matter if you create libraries as file modules, or packages, both are equally used, with one difference: package modules allow [Directory style components](#directory-style-components), see below.
18 |
19 | When resolving a component, and you don't specify a library in your *component tag*, Tetra assumes you put the component in the `default` library. However, you can have infinite libraries. This is a good way to organise components into related sets. Each library's Javascript and CSS is packaged together. As long as components are registered to a library and that library instance is available in `.components` or `.tetra_components` they will be available to use from templates, and within other components.
20 |
21 | While it is not necessary, it is also possible to create libraries manually (e.g. in your testing files). You have to provide a `name` and an `app`, and if the same library was already registered, it is not recreated - the library with that name is reused, so name and app are unique together within libraries.
22 |
23 | ```python
24 | from tetra import Library, Component
25 | from django.apps import apps
26 |
27 | class FooComponent(Component):
28 | template = "
foo!
"
29 |
30 | # create a new library named "default" for the "main" app
31 | default = Library(name="default", app=apps.get_app_config("main"))
32 |
33 | # register the FooComponent to the default library
34 | default.register(FooComponent)
35 |
36 | # if you create a library twice, or you use a library that was already created automatically by
37 | # creating a "default" folder in your `.components` directory, that library is reused.
38 | default_double = Library("default", "main")
39 | assert default_double is default
40 | ```
41 |
42 | #### Directory style components
43 | A component is created as a subclass of `BasicComponent` or `Component` and registered to a library by placing it into the library package. Let's see how the directory structure would look like for a `MyCalendar` component:
44 |
45 | ```
46 | myapp
47 | ├── components/
48 | │ │ └──default
49 | │ │ └── my_calendar/
50 | │ │ ├──__init__.py* <-- here is the component class defined
51 | │ │ ├──script.js
52 | │ │ ├──style.css
53 | │ │ └──my_calendar.html*
54 | ```
55 |
56 | The `__init__.py` and `my_calendar.html` template are mandatory, css/js and other files are optional.
57 |
58 | #### Inline components
59 |
60 | There is another (shortcut) way of creating components, especially for simple building bricks (like `BasicComponents` without Js, CSS, and with small HTML templates).
61 | Create a component class and place it directly into a library module. You can create multiple components directly in the module. The simplest form is directly in the `default` library:
62 | ``` python
63 | #myapp/components/default.py
64 |
65 | class Link(BasicComponent):
66 | href: str = ""
67 | title: str = ""
68 | template: django_html = "{{title}}
69 | ```
70 |
71 | However, You can mix directory libraries and file libraries as you want: Put a few components into `default/__init__.py`, and another into `default/my_component/__init__.py`. Both are found:
72 |
73 | ```
74 | myapp
75 | ├── components/
76 | │ │ ├──default
77 | │ │ │ └──__init__.py <-- put all "default" component classes in here
78 | │ │ ├──otherlib.py <-- put all "otherlib" component classes in here
79 | │ │ ├──widgets
80 | │ │ │ ├──__init__.py <-- put all "widgets" component classes in here
81 | │ │ │ ├──link
82 | │ │ │ │ ├──__init__.py <-- Link component definition
83 | │ │ │ │ └──link.html
84 | ...
85 | ```
86 |
87 |
88 |
89 | !!! note
90 | If you use a **directory style component**, make sure you define only ONE component class per module (e.g. in `components/default/my_calendar.py`). If you use the library module directly to create components (`components/default.py`), you can certainly put multiple components in there.
91 |
92 | ## Manually declared libraries
93 |
94 | It is not necessary to follow the directory structure. You can also declare a Library anywhere in your code, and register components to it. The `Library` class takes the *name of the library* and the *AppConfig (or app label)* as parameters. You can declare the libraries more then once, everything with the same name will be merged together.
95 |
96 | !!! note
97 | Library names must be globally unique. Declaring Libraries with the same name in different apps is forbidden.
98 |
99 | ```python
100 | from tetra import Library
101 |
102 | widgets = Library("widgets", "ui")
103 | # this is the same library!
104 | widgets_too = Library("widgets", "ui")
105 | ```
106 |
107 | When a Library is declared, you can register Components to it by using the `@.register` decorator:
108 |
109 | ```python
110 | @widgets.register
111 | class Button(BasicComponent):
112 | ...
113 | ```
114 |
115 | As a decorator can be used as a function, you can even register components in code:
116 |
117 | ```python
118 | lib = Library("mylib", "myapp")
119 | lib.register(MyComponentClass)
120 | ```
--------------------------------------------------------------------------------
/docs/component-life-cycle.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Component life cycle
3 | ---
4 |
5 | # Attribute data life cycle
6 |
7 | The data attributes of a component exist within a specific lifecycle. The component, when constructed or resumed sets its atributes in a certain order (see below). In each step, the already existing attribute data are overridden.
8 |
9 | ## 1. Attribute assignment
10 |
11 | ```python
12 | class Person(Component):
13 | name:str = "John Doe"
14 | age:int = None
15 |
16 | ...
17 | ```
18 |
19 | When Attributes are set directly in the class, they are used as default values, as in any other python class too. Even if no `load()` method is present, the component can use this values.
20 |
21 | ## 2. Resumed data from encrypted state
22 |
23 | If the component is resumed from a previous state, the encrypted state data is decrypted, and all component attributes are set from the previous state.
24 | This is omitted if the component is initialized the first time, as there is no encrypted previous state yet.
25 |
26 | ## 3. The `load()` method
27 |
28 | Next, the component's `load()` method is called. Any data assignment to the attributes overrides the previous values.
29 |
30 | ```python
31 | class Person(Component):
32 | name:str = "John Doe"
33 | age:int = None
34 |
35 | def load(self, pk:int, *args, **kwargs):
36 | person = Person.objects.get(pk=pk)
37 | self.name = person.name
38 | self.age = person.age
39 | ```
40 |
41 | Attributes that set in the `load()` method are **not** saved with the state, as the values are overwritten in the subsequent step. This seems to be extraneous, but in fact makes sure that the component attributes gets a consistent initialization.
42 |
43 |
44 | ## 4. The client data
45 |
46 | The final step involves updating attributes using *data* passed from the client-side to the server via component methods. Note that this is not the same as the *state*:
47 |
48 | * The **state** represents the "frozen data" sent to the client during the last render, essentially what the client received initially.
49 | * The **data** refers to dynamic values, such as a component input tag's value, which may have changed during the last interaction cycle.
50 |
51 |
52 | # Events on the client side
53 |
54 | Have a look at the [events][events.md].
55 |
56 | # Sequence diagram
57 |
58 | What happens when a public method is called? This sequence diagram shows everything.
59 | ```mermaid
60 | sequenceDiagram
61 | box Client
62 | participant Client
63 | end
64 |
65 | box Server
66 | participant Server
67 | participant component_method
68 | participant Component
69 | participant TetraJSONEncoder
70 | participant TetraJSONDecoder
71 | participant decode_component
72 | participant StateUnpickler
73 | end
74 |
75 | Client->> Server: POST request to /component_method/
76 | Server ->> component_method: Call component_method() view
77 | component_method ->> component_method: Set PersistentTemporaryFileUploadHandler
78 | component_method ->> component_method: Validate request method (==POST?)
79 | component_method ->> component_method: Retrieve Component class from Library list
80 | component_method ->> component_method: Validate method name is public
81 | component_method ->> TetraJSONDecoder: Decode request.POST data (from_json)
82 | TetraJSONDecoder -->> component_method: Return decoded data
83 | component_method ->> component_method: Add Component class to set of used components in this request
84 | component_method ->> Component: Request new component instance (using Component.from_state)
85 | Component ->> Component: Validate ComponentState data structure
86 |
87 | Component ->> decode_component: decode_component(ComponentState["state"], request)
88 | decode_component ->> decode_component: get fernet for request
89 | decode_component ->> decode_component: decrypt encoded state_token using fernet
90 | decode_component ->> decode_component: decompress decrypted data with gzip
91 | decode_component ->> StateUnpickler: unpickle component data state
92 | StateUnpickler -->> decode_component: component
93 | decode_component -->> Component: component
94 |
95 | Component ->> Component: Set component request, key, attrs, context, blocks
96 | Component ->> Component: recall load() with initial params
97 | Component ->> Component: set component attributes from client data
98 | Component ->> Component: client data contains a Model PK? -> replace it with Model instance from DB
99 | Component ->> Component: hook: recalculate_attrs(component_method_finished=False)
100 | Component -->> component_method: Return initialized component instance
101 |
102 |
103 | component_method ->> component_method: Attach uploaded files (from request.FILES) to component
104 | component_method ->> Component: Call Component's _call_public_method
105 | Component ->> Component: Execute public method
106 |
107 | Component ->> TetraJSONEncoder: Encode result data to JSON
108 | TetraJSONEncoder -->> Component: Return JSON-encoded data
109 |
110 | Note over Component: JSON response
111 | Component -->> component_method: Return encoded result
112 | component_method -->> Server: Return JsonResponse
113 | Server -->>Client: Send response
114 |
115 |
116 | ```
117 |
--------------------------------------------------------------------------------
/docs/contribute.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Contributing
3 | ---
4 |
5 | # Contributing to the project
6 |
7 | You can help/contribute in many ways:
8 |
9 | * Bring in new ideas and [discussions](https://github.com/tetra-framework/tetra/discussions)
10 | * Report bugs in our [issue tracker](https://github.com/tetra-framework/tetra/issues)
11 | * Add documentation
12 | * Write code
13 |
14 |
15 | ## Writing code
16 |
17 | Fork the repository locally and install it as editable package:
18 |
19 | ```bash
20 | git clone git@github.com:tetra-framework/tetra.git
21 | cd tetra
22 | python -m pip install -e .
23 | ```
24 |
25 |
26 | ### Code style
27 |
28 | * Please only write [Black](https://github.com/psf/black) styled code. You can automate that by using your IDE's save
29 | trigger feature.
30 | * Document your code well, using [Napoleon style docstrings](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html#example-google).
31 | * Write appropriate tests for your code.
--------------------------------------------------------------------------------
/docs/events.md:
--------------------------------------------------------------------------------
1 | # Tetra events
2 |
3 | There are some events that occur during the tetra component life cycle. You can use them to hook into.
4 |
5 |
6 | On the client, there are certain javascript events fired when certain things happen. You can react on that using Alpine's `x-on` or by using custom Javascript code.
7 |
8 | If not stated otherwise, all events have the actual component as `component` payload attached in `event.detail`.
9 |
10 | ## Event list
11 |
12 | ### `tetra:before-request`
13 |
14 | This event fires after a component method has completed — whether the request was successful (even if the response includes an HTTP error like 404) or if a network error occurred. It can be used alongside `tetra:before-request` to implement custom behavior around the full request lifecycle, such as showing or hiding a loading indicator.
15 |
16 |
17 | ### `tetra:after-request`
18 |
19 | This event is triggered before a component method is called.
20 |
21 | ### `tetra:child-component-init`
22 |
23 | Whenever a child component is initialized, this event is fired. This is mainly used internally within Tetra.
24 |
25 | ### `tetra:child-component-destroy`
26 |
27 | Called before a child component is going to be destroyed. This is mainly used internally within Tetra.
28 |
29 | ### `tetra:component-updated`
30 | This event is fired after a component has called a public method and the new HTML is completely morphed into the DOM.
31 | It is also fired after a component has been replaced.
32 |
33 | ```html
34 |
35 | Original text
36 |
37 | ```
38 |
39 | ### `tetra:component-data-updated`
40 |
41 | The same goes for data updates: the event is fired after a data update without HTML changes was finished.
42 |
43 | ### `tetra:component-before-remove`
44 |
45 | Right before a component is removed using `self.client._removeComponent()` this event is triggered.
46 |
47 | ### `tetra:new-message`
48 |
49 | After a request returns a response, Tetra fires this event if there are new messages from the Django messaging system. You can react to these messages, e.g. display them in a component.
50 |
51 | #### Details
52 | * `messages`: a list of message objects, see [messages](messages.md)
53 |
--------------------------------------------------------------------------------
/docs/files.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: File handling in Tetra
3 | ---
4 |
5 | # File handling in Tetra
6 |
7 | ## File uploads
8 |
9 | When using HTML file input tags, Tetra's [FormComponent](form-components.md) takes care of the uploading process. While in normal HTML `