textual-wtf 0.8.0__tar.gz → 0.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- textual_wtf-0.8.1/.github/workflows/pypi.yaml +76 -0
- textual_wtf-0.8.1/CHANGELOG-v0.9-dev1.md +188 -0
- textual_wtf-0.8.1/CHANGELOG.md +139 -0
- textual_wtf-0.8.1/IMPLEMENTATION_SUMMARY.md +327 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/PKG-INFO +1 -1
- textual_wtf-0.8.1/examples/custom_layouts_demo.py +195 -0
- textual_wtf-0.8.1/examples/test_new_features.py +342 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/pyproject.toml +14 -1
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/src/textual_wtf/__init__.py +5 -1
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/src/textual_wtf/bound_fields.py +47 -2
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/src/textual_wtf/demo/advanced_form.py +2 -2
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/src/textual_wtf/demo/basic_form.py +1 -1
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/src/textual_wtf/demo/nested_once_form.py +2 -2
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/src/textual_wtf/demo/nested_twice_form.py +2 -2
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/src/textual_wtf/demo/user_registration.py +2 -2
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/src/textual_wtf/fields.py +7 -1
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/src/textual_wtf/forms.py +62 -169
- textual_wtf-0.8.1/src/textual_wtf/version.py +1 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/src/textual_wtf/widgets.py +28 -24
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/uv.lock +1 -1
- textual_wtf-0.8.0/docs/BOUNDFIELD.md +0 -44
- textual_wtf-0.8.0/images/rendered_user_form.png +0 -0
- textual_wtf-0.8.0/src/textual_wtf/version.py +0 -1
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/.coveragerc +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/.gitignore +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/BOUNDFIELD_REFACTORING_SUMMARY.md +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/INSTALLATION.md +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/LICENSE +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/Makefile +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/PACKAGE_SUMMARY.txt +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/QUICKSTART.md +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/README.md +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/REFACTORING_SUMMARY.md +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/docs/TESTING_GUIDE.md +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/src/textual_wtf/demo/__init__.py +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/src/textual_wtf/demo/basic_form.tcss +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/src/textual_wtf/demo/launcher.py +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/src/textual_wtf/demo/results_screen.py +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/src/textual_wtf/exceptions.py +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/src/textual_wtf/py.typed +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/src/textual_wtf/validators.py +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/tests/__init__.py +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/tests/conftest.py +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/tests/test_composition.py +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/tests/test_exceptions.py +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/tests/test_fields.py +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/tests/test_forms.py +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/tests/test_integration.py +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/tests/test_user_input.py +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/tests/test_validators.py +0 -0
- {textual_wtf-0.8.0 → textual_wtf-0.8.1}/tests/test_widgets.py +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
name: Publish to PyPI and TestPyPI
|
|
2
|
+
|
|
3
|
+
on: push
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
|
|
7
|
+
build:
|
|
8
|
+
name: Build distribution 📦
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v6
|
|
13
|
+
with:
|
|
14
|
+
persist-credentials: false
|
|
15
|
+
- name: Set up Python
|
|
16
|
+
uses: actions/setup-python@v6
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.x"
|
|
19
|
+
- name: Install pypa/build
|
|
20
|
+
run: >-
|
|
21
|
+
python3 -m
|
|
22
|
+
pip install
|
|
23
|
+
build
|
|
24
|
+
--user
|
|
25
|
+
- name: Build a binary wheel and a source tarball
|
|
26
|
+
run: python3 -m build
|
|
27
|
+
- name: Store the distribution packages
|
|
28
|
+
uses: actions/upload-artifact@v5
|
|
29
|
+
with:
|
|
30
|
+
name: python-package-distributions
|
|
31
|
+
path: dist/
|
|
32
|
+
|
|
33
|
+
publish-to-pypi:
|
|
34
|
+
name: >-
|
|
35
|
+
Publish Python 🐍 distribution 📦 to PyPI
|
|
36
|
+
if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
|
|
37
|
+
needs:
|
|
38
|
+
- build
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
environment:
|
|
41
|
+
name: pypi
|
|
42
|
+
url: https://pypi.org/p/textual-wtf # Replace <package-name> with your PyPI project name
|
|
43
|
+
permissions:
|
|
44
|
+
id-token: write # IMPORTANT: mandatory for trusted publishing
|
|
45
|
+
steps:
|
|
46
|
+
- name: Download all the dists
|
|
47
|
+
uses: actions/download-artifact@v6
|
|
48
|
+
with:
|
|
49
|
+
name: python-package-distributions
|
|
50
|
+
path: dist/
|
|
51
|
+
- name: Publish distribution 📦 to PyPI
|
|
52
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
53
|
+
|
|
54
|
+
publish-to-testpypi:
|
|
55
|
+
name: Publish Python 🐍 distribution 📦 to TestPyPI
|
|
56
|
+
needs:
|
|
57
|
+
- build
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
|
|
60
|
+
environment:
|
|
61
|
+
name: testpypi
|
|
62
|
+
url: https://test.pypi.org/p/<package-name>
|
|
63
|
+
|
|
64
|
+
permissions:
|
|
65
|
+
id-token: write # IMPORTANT: mandatory for trusted publishing
|
|
66
|
+
|
|
67
|
+
steps:
|
|
68
|
+
- name: Download all the dists
|
|
69
|
+
uses: actions/download-artifact@v6
|
|
70
|
+
with:
|
|
71
|
+
name: python-package-distributions
|
|
72
|
+
path: dist/
|
|
73
|
+
- name: Publish distribution 📦 to TestPyPI
|
|
74
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
75
|
+
with:
|
|
76
|
+
repository-url: https://test.pypi.org/legacy/
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# textual-wtf v0.9-dev1 - Changelog
|
|
2
|
+
|
|
3
|
+
## Major Architectural Changes
|
|
4
|
+
|
|
5
|
+
### 1. Callable BoundFields (WTForms-style API)
|
|
6
|
+
|
|
7
|
+
**File: `bound_fields.py`**
|
|
8
|
+
|
|
9
|
+
- Added `__call__()` method to BoundField class
|
|
10
|
+
- Returns widget configured with keyword arguments
|
|
11
|
+
- Tracks field rendering to prevent duplicates
|
|
12
|
+
- Example: `form.name(placeholder="Enter name", disabled=True)`
|
|
13
|
+
|
|
14
|
+
- Updated `create_widget()` method
|
|
15
|
+
- Now accepts `**kwargs` for runtime configuration
|
|
16
|
+
- Merges runtime kwargs with field's widget_kwargs
|
|
17
|
+
- Syncs initial value to widget on creation
|
|
18
|
+
|
|
19
|
+
**File: `fields.py`**
|
|
20
|
+
|
|
21
|
+
- Updated `Field.create_widget()` method
|
|
22
|
+
- Added optional `widget_kwargs` parameter
|
|
23
|
+
- Merges base configuration with runtime overrides
|
|
24
|
+
|
|
25
|
+
### 2. FormLayout Architecture
|
|
26
|
+
|
|
27
|
+
**File: `layouts.py` (NEW)**
|
|
28
|
+
|
|
29
|
+
Created new layout system with two classes:
|
|
30
|
+
|
|
31
|
+
**FormLayout (base class)**
|
|
32
|
+
- Inherits from `VerticalScroll`
|
|
33
|
+
- Tracks rendered fields via `_rendered_fields` set
|
|
34
|
+
- Provides `_track_field_render()` to prevent duplicate field rendering
|
|
35
|
+
- Abstract `compose_form()` method for subclasses to implement
|
|
36
|
+
- Delegates to form for data operations (get_data, set_data, validate)
|
|
37
|
+
|
|
38
|
+
**DefaultFormLayout**
|
|
39
|
+
- Implements default vertical form layout
|
|
40
|
+
- Replicates previous RenderedForm behavior
|
|
41
|
+
- Renders: title → subform titles → fields with labels → buttons
|
|
42
|
+
- Handles submit/cancel button events
|
|
43
|
+
- Posts Form.Submitted and Form.Cancelled messages
|
|
44
|
+
|
|
45
|
+
### 3. Form Class Updates
|
|
46
|
+
|
|
47
|
+
**File: `forms.py`**
|
|
48
|
+
|
|
49
|
+
**Removed:**
|
|
50
|
+
- `RenderedForm` class (replaced by FormLayout system)
|
|
51
|
+
- Unused imports (on, Vertical, Center, Horizontal, VerticalScroll, Button, Static, Label)
|
|
52
|
+
|
|
53
|
+
**Modified BaseForm:**
|
|
54
|
+
- Added `layout_class` class attribute (defaults to DefaultFormLayout)
|
|
55
|
+
- Updated `__init__()`:
|
|
56
|
+
- Added `layout_class` parameter (overrides class-level default)
|
|
57
|
+
- Removed `render_type` parameter
|
|
58
|
+
- Layout selection logic: instance param → class attribute → DefaultFormLayout
|
|
59
|
+
|
|
60
|
+
- Updated `render()` method:
|
|
61
|
+
- Creates layout instance using `self._layout_class`
|
|
62
|
+
- Sets `self._current_layout` for field rendering tracking
|
|
63
|
+
- Returns layout instance (not RenderedForm)
|
|
64
|
+
|
|
65
|
+
**Modified Form:**
|
|
66
|
+
- Updated `Submitted` and `Cancelled` message classes:
|
|
67
|
+
- Accept `layout` parameter instead of `r_form`
|
|
68
|
+
- Store as `self.layout`
|
|
69
|
+
- Provide `self.form` for backward compatibility
|
|
70
|
+
|
|
71
|
+
### 4. Public API Updates
|
|
72
|
+
|
|
73
|
+
**File: `__init__.py`**
|
|
74
|
+
|
|
75
|
+
Added exports:
|
|
76
|
+
- `FormLayout`
|
|
77
|
+
- `DefaultFormLayout`
|
|
78
|
+
|
|
79
|
+
Fixed typo in `__all__` (was `"__version__,"` now `"__version__"`)
|
|
80
|
+
|
|
81
|
+
## Benefits of These Changes
|
|
82
|
+
|
|
83
|
+
1. **Cleaner API**: Fields are now callable, making layout code more intuitive
|
|
84
|
+
```python
|
|
85
|
+
yield self.form.name(placeholder="Your name")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
2. **Flexible Layouts**: Easy to create custom form layouts by subclassing FormLayout
|
|
89
|
+
```python
|
|
90
|
+
class TwoColumnLayout(FormLayout):
|
|
91
|
+
def compose_form(self):
|
|
92
|
+
with Horizontal():
|
|
93
|
+
yield self.form.name()
|
|
94
|
+
yield self.form.email()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
3. **Safety**: Prevents duplicate field rendering (1:1 field-to-widget mapping)
|
|
98
|
+
- Raises `FormError` if same field rendered twice
|
|
99
|
+
|
|
100
|
+
4. **Backward Compatible**: Existing code continues to work with DefaultFormLayout
|
|
101
|
+
|
|
102
|
+
## Migration Guide
|
|
103
|
+
|
|
104
|
+
### Before (v0.8)
|
|
105
|
+
```python
|
|
106
|
+
class MyForm(Form):
|
|
107
|
+
name = StringField(label="Name")
|
|
108
|
+
|
|
109
|
+
form = MyForm()
|
|
110
|
+
rendered = form.render()
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### After (v0.9-dev1)
|
|
114
|
+
```python
|
|
115
|
+
# Still works exactly the same!
|
|
116
|
+
class MyForm(Form):
|
|
117
|
+
name = StringField(label="Name")
|
|
118
|
+
|
|
119
|
+
form = MyForm()
|
|
120
|
+
rendered = form.render()
|
|
121
|
+
|
|
122
|
+
# But now you can also do custom layouts:
|
|
123
|
+
class MyLayout(FormLayout):
|
|
124
|
+
def compose_form(self):
|
|
125
|
+
yield Label("Custom Layout")
|
|
126
|
+
yield self.form.name(placeholder="Enter name")
|
|
127
|
+
yield Button("Submit", id="submit")
|
|
128
|
+
|
|
129
|
+
form = MyForm(layout_class=MyLayout)
|
|
130
|
+
rendered = form.render()
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Breaking Changes
|
|
134
|
+
|
|
135
|
+
⚠️ **Potential Breaking Changes:**
|
|
136
|
+
|
|
137
|
+
1. **RenderedForm class removed**
|
|
138
|
+
- If code directly instantiated `RenderedForm`, use `DefaultFormLayout` instead
|
|
139
|
+
- Message handlers that checked `isinstance(x, RenderedForm)` need updating
|
|
140
|
+
|
|
141
|
+
2. **Message structure changed**
|
|
142
|
+
- `Form.Submitted` and `Form.Cancelled` now receive `layout` instead of `r_form`
|
|
143
|
+
- `message.form` now refers to the layout (backward compatible)
|
|
144
|
+
- To access actual form: `message.layout.form`
|
|
145
|
+
|
|
146
|
+
3. **render_type parameter removed**
|
|
147
|
+
- Use `layout_class` parameter instead
|
|
148
|
+
- Applies to both `BaseForm.__init__()` and subclasses
|
|
149
|
+
|
|
150
|
+
## Testing Requirements
|
|
151
|
+
|
|
152
|
+
The following areas need test updates:
|
|
153
|
+
|
|
154
|
+
1. **Test files referencing RenderedForm:**
|
|
155
|
+
- Replace `RenderedForm` imports with `DefaultFormLayout`
|
|
156
|
+
- Update assertions checking widget type
|
|
157
|
+
|
|
158
|
+
2. **Test message handling:**
|
|
159
|
+
- Update to use `message.layout` instead of `message.form` for layout
|
|
160
|
+
- Or use `message.form` (backward compatible alias)
|
|
161
|
+
|
|
162
|
+
3. **New tests needed:**
|
|
163
|
+
- BoundField `__call__()` with various kwargs
|
|
164
|
+
- Duplicate field rendering raises FormError
|
|
165
|
+
- Custom layout classes
|
|
166
|
+
- Field rendering tracking
|
|
167
|
+
|
|
168
|
+
## Files Modified
|
|
169
|
+
|
|
170
|
+
- `src/textual_wtf/bound_fields.py` - Added __call__ method
|
|
171
|
+
- `src/textual_wtf/fields.py` - Updated create_widget()
|
|
172
|
+
- `src/textual_wtf/forms.py` - Removed RenderedForm, updated BaseForm/Form
|
|
173
|
+
- `src/textual_wtf/layouts.py` - NEW FILE
|
|
174
|
+
- `src/textual_wtf/__init__.py` - Updated exports
|
|
175
|
+
|
|
176
|
+
## Files Needing Updates
|
|
177
|
+
|
|
178
|
+
- All test files that import or reference `RenderedForm`
|
|
179
|
+
- Demo applications in `src/textual_wtf/demo/` (likely need updates)
|
|
180
|
+
- Documentation files
|
|
181
|
+
|
|
182
|
+
## Next Steps
|
|
183
|
+
|
|
184
|
+
1. Update test suite to use new architecture
|
|
185
|
+
2. Update demo applications
|
|
186
|
+
3. Update documentation
|
|
187
|
+
4. Add new tests for layout system
|
|
188
|
+
5. Consider whether to add validation that all fields were rendered
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.9.0-dev1] - 2025-02-02
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
|
|
7
|
+
**Major architectural refactoring: Form/Layout separation**
|
|
8
|
+
|
|
9
|
+
This release introduces a clean separation between form logic (data and validation) and form presentation (layout and rendering). This is a **breaking change** but provides much better flexibility and follows Textual best practices.
|
|
10
|
+
|
|
11
|
+
#### What Changed
|
|
12
|
+
|
|
13
|
+
1. **RenderedForm removed** - Forms are no longer widgets themselves
|
|
14
|
+
2. **FormLayout introduced** - New base class for creating form layouts
|
|
15
|
+
3. **Form.render() now returns FormLayout** - Not a breaking API change, but returns a different type
|
|
16
|
+
4. **BoundField is now callable** - WTForms-style widget rendering: `field()`
|
|
17
|
+
|
|
18
|
+
#### Migration Guide
|
|
19
|
+
|
|
20
|
+
**Before (v0.8.x):**
|
|
21
|
+
```python
|
|
22
|
+
form = MyForm(data={'name': 'Alice'})
|
|
23
|
+
app.mount(form.render()) # Returns RenderedForm (a widget)
|
|
24
|
+
|
|
25
|
+
# Message handling
|
|
26
|
+
@on(Form.Submitted)
|
|
27
|
+
def handle_submit(self, event):
|
|
28
|
+
data = event.form.get_data() # event.form was RenderedForm
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**After (v0.9.x):**
|
|
32
|
+
```python
|
|
33
|
+
form = MyForm(data={'name': 'Alice'})
|
|
34
|
+
app.mount(form.render()) # Returns DefaultFormLayout (a widget)
|
|
35
|
+
|
|
36
|
+
# Message handling - slightly different
|
|
37
|
+
@on(Form.Submitted)
|
|
38
|
+
def handle_submit(self, event):
|
|
39
|
+
data = event.form.get_data() # event.form is now the Form instance
|
|
40
|
+
# event.layout is the FormLayout widget
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
#### New Capabilities
|
|
44
|
+
|
|
45
|
+
**1. Custom Layouts**
|
|
46
|
+
|
|
47
|
+
You can now create custom layouts by subclassing `FormLayout`:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from textual_wtf import FormLayout
|
|
51
|
+
from textual.containers import Horizontal, Vertical
|
|
52
|
+
|
|
53
|
+
class TwoColumnLayout(FormLayout):
|
|
54
|
+
def compose_form(self):
|
|
55
|
+
with Horizontal():
|
|
56
|
+
with Vertical():
|
|
57
|
+
yield self.render_field('first_name')
|
|
58
|
+
yield self.render_field('email')
|
|
59
|
+
with Vertical():
|
|
60
|
+
yield self.render_field('last_name')
|
|
61
|
+
yield self.render_field('phone')
|
|
62
|
+
|
|
63
|
+
# Render submit buttons
|
|
64
|
+
# ... (see DefaultFormLayout for button example)
|
|
65
|
+
|
|
66
|
+
# Use it
|
|
67
|
+
form = ContactForm()
|
|
68
|
+
app.mount(TwoColumnLayout(form))
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**2. Same Form, Different Layouts**
|
|
72
|
+
|
|
73
|
+
Since Form is just data, you can render the same form instance with different layouts:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
form = UserProfileForm()
|
|
77
|
+
|
|
78
|
+
# Desktop view
|
|
79
|
+
desktop_panel.mount(DefaultFormLayout(form))
|
|
80
|
+
|
|
81
|
+
# Mobile view (after unmounting desktop)
|
|
82
|
+
mobile_panel.mount(CompactFormLayout(form))
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**3. Callable BoundFields (WTForms-style)**
|
|
86
|
+
|
|
87
|
+
Fields can now be called to get their widgets:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
class CustomLayout(FormLayout):
|
|
91
|
+
def compose_form(self):
|
|
92
|
+
# Call field to get widget
|
|
93
|
+
yield self.form.fields['name']()
|
|
94
|
+
|
|
95
|
+
# Or with configuration
|
|
96
|
+
yield self.form.fields['email'](placeholder="Enter email")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**4. Duplicate Rendering Prevention**
|
|
100
|
+
|
|
101
|
+
The layout system prevents accidentally rendering the same field twice:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
class BadLayout(FormLayout):
|
|
105
|
+
def compose_form(self):
|
|
106
|
+
yield self.render_field('name')
|
|
107
|
+
yield self.render_field('name') # Raises DuplicateFieldRenderError
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Added
|
|
111
|
+
|
|
112
|
+
- `FormLayout` - Base class for custom form layouts
|
|
113
|
+
- `DefaultFormLayout` - Default vertical layout (replicates old RenderedForm behavior)
|
|
114
|
+
- `DuplicateFieldRenderError` - Exception raised when field rendered twice
|
|
115
|
+
- `BoundField.__call__()` - Make fields callable to get widgets
|
|
116
|
+
- `Field.create_widget(override_kwargs)` - Accept optional kwargs for widget configuration
|
|
117
|
+
|
|
118
|
+
### Changed
|
|
119
|
+
|
|
120
|
+
- `Form` is no longer a widget - use `form.render()` to get a FormLayout widget
|
|
121
|
+
- `Form.Submitted` message now has `.layout` (FormLayout) and `.form` (Form) attributes
|
|
122
|
+
- `Form.Cancelled` message now has `.layout` (FormLayout) and `.form` (Form) attributes
|
|
123
|
+
- Validation logic moved from Form to FormLayout
|
|
124
|
+
|
|
125
|
+
### Removed
|
|
126
|
+
|
|
127
|
+
- `RenderedForm` class - replaced by `DefaultFormLayout`
|
|
128
|
+
- `BaseForm.render_type` parameter - use `form.render(layout_class=...)` instead
|
|
129
|
+
- `BaseForm.validate()` method - now on FormLayout
|
|
130
|
+
|
|
131
|
+
### Internal
|
|
132
|
+
|
|
133
|
+
- Better separation of concerns: Form = business logic, Layout = presentation
|
|
134
|
+
- Layouts are composable Textual widgets
|
|
135
|
+
- Forms are pure Python objects (no widget inheritance)
|
|
136
|
+
|
|
137
|
+
## [0.8.0] - Previous Release
|
|
138
|
+
|
|
139
|
+
(See previous changelog entries...)
|