Skip to content

Commit 1f4a6cf

Browse files
Merge pull request #6 from webexpress-framework/develop
0.0.10-alpha
2 parents 4595755 + b736368 commit 1f4a6cf

346 files changed

Lines changed: 25387 additions & 2520 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/js/form.md

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# RestFormCtrl
2+
3+
`RestFormCtrl` is a controller that modernizes a traditional HTML form by replacing standard submit behavior with an asynchronous JSON request. It offers client-side validation, inline validation messages, and integrates with the `webexpress` event system. Additionally, the form can be initially populated with data from an API.
4+
5+
Decoupling form presentation from data processing provides significant advantages over classic server-side form submits:
6+
7+
- **Single-Page Application (SPA) Feeling:** The page does not need to reload upon submission. The user retains context, scroll position, and focus.
8+
- **Structured Data:** Data is sent as a typed JSON object. This facilitates backend processing compared to `x-www-form-urlencoded` and enables complex, nested structures.
9+
- **Precise Feedback:** Validation errors from the server can be mapped exactly to the affected fields without re-rendering the form.
10+
- **Status Feedback:** Events allow easy visualization of loading states (e.g., "Saving...") while the form is locked to prevent duplicate entries.
11+
12+
## Data Population Strategies
13+
14+
The controller supports two ways to load data into the form. These can also be combined (hybrid approach).
15+
16+
1. **Server-Side Rendering (Recommended for Initial Load):** The most performant method is to deliver the HTML form already populated from the server. Here, the template engine sets the `value` attributes of inputs and the `checked` status of checkboxes directly in the HTML.
17+
2. **Population via REST API (Client-Side):** Using the `data-api` attribute, the controller can be instructed to execute a GET request after initialization to retrieve the data.
18+
19+
## Declarative Configuration
20+
21+
The behavior of REST-based forms is defined entirely through declarative configuration using `data-*` attributes on the `<form>` element. By attaching the class `wx-webapp-restform`, the controller automatically interprets these attributes to determine how the form should load, validate, submit, and handle errors. This approach minimizes the need for custom JavaScript and ensures a consistent, predictable workflow across different forms. The following table outlines the available attributes and their specific roles in controlling form functionality.
22+
23+
| Attribute | Description
24+
|---------------------------|--------------------------------------------------------------------------------------
25+
| `data-api` | URL of the endpoint to which data is sent. If not set, `action` is used.
26+
| `data-method` | HTTP method for submission. Overrides the form's `method` attribute.
27+
| `data-json` | Determines whether data is sent as JSON (`true`) or `x-www-form-urlencoded` (`false`).
28+
| `data-validate-on-submit` | Enables client-side validation before sending.
29+
| `data-show-inline-errors` | Displays error messages directly below the affected fields.
30+
| `data-mode` | Force `new`, `edit` or `delete`
31+
| `data-id` | Primary key for Edit/Delete; appended as query parameter.
32+
33+
## Functionality and Behavior
34+
35+
The form controller manages the complete lifecycle of user interactions, from initialization to error handling. Its behavior ensures that forms are consistently prepared, validated, and submitted in a reliable way, while providing meaningful feedback to users. The following points outline the core aspects of how the controller operates, including setup, validation, submission flow, and error management.
36+
37+
- **Initialization & Hydration**
38+
- Upon loading, a container for global error messages (`.restform-error-container`) is created if it does not yet exist.
39+
- The controller retrieves the data and automatically populates the form fields.
40+
41+
- **Validation**
42+
- The controller uses the native HTML5 validation API (`required`, `pattern`, `min`/`max`, `type="email"`, etc.).
43+
- In case of errors, submission is prevented, the first invalid field is focused, and an error message is displayed.
44+
- Specific validations for email patterns are additionally checked to compensate for browser inconsistencies.
45+
46+
- **Submission**
47+
- The browser's standard submit is prevented.
48+
- All form fields (except `type="file"`) are serialized into a JSON object.
49+
- Checkbox groups and multi-selects are correctly processed as arrays or booleans.
50+
- During the submission process, the form is locked (`disabled`) and receives the CSS class `.restform-submitting`.
51+
52+
- **Error Handling**
53+
- **Client-side:** Inline error messages are displayed directly at the field (`aria-invalid="true"`).
54+
- **Server-side:** If the server responds with status `400` and a JSON object `{ errors: { fieldName: "Message" } }`, these errors are assigned to the corresponding fields.
55+
- **Global:** Other errors (network, Server 500) are displayed in a global alert box above the form.
56+
57+
## Programmatic Control
58+
59+
The instance can be retrieved to manually control the form or register external hooks.
60+
61+
### Accessing an Automatically Created Instance
62+
63+
For forms declared in HTML, the associated RestFormCtrl instance is retrieved via the `getInstanceByElement(element)` method of the central `webexpress.webui.Controller`.
64+
65+
```javascript
66+
// find the host form element in the DOM
67+
const formEl = document.getElementById('my-form');
68+
69+
// retrieve the RestFormCtrl instance associated with the element
70+
const restForm = webexpress.webui.Controller.getInstanceByElement(formEl);
71+
72+
// change options and register hooks programmatically
73+
if (restForm) {
74+
// set onSuccess hook to run after a successful submit
75+
// note: hooks run inside try/catch in the controller; avoid throwing here
76+
restForm.options.onSuccess = function(json, response) {
77+
// navigate or show a notification
78+
window.location.href = '/success';
79+
};
80+
81+
// add a beforeSend hook to mutate or cancel the payload
82+
restForm.options.beforeSend = function(payload, element) {
83+
// attach a timestamp and return the modified payload
84+
payload.ts = Date.now();
85+
return payload; // return false to cancel submit
86+
};
87+
88+
// trigger validation manually
89+
const valid = restForm.validate();
90+
if (valid) {
91+
// submit programmatically
92+
restForm.submit();
93+
}
94+
95+
// clear visible errors
96+
restForm.clear();
97+
}
98+
```
99+
100+
### Manual Instantiation
101+
102+
A RestFormCtrl can also be created entirely programmatically and attached to a host element. This is useful for dynamic UI scenarios.
103+
104+
```javascript
105+
// find or create a container element for the form
106+
const container = document.getElementById('dynamic-form-container');
107+
108+
// create a form element and append it to the container
109+
const form = document.createElement('form');
110+
form.className = 'wx-webapp-restform';
111+
form.setAttribute('data-api', '/api/items');
112+
container.appendChild(form);
113+
114+
// instantiate RestFormCtrl manually for the newly created form
115+
const dynamicRestForm = new webexpress.webapp.RestFormCtrl(form);
116+
117+
// set hooks and call methods as needed
118+
if (dynamicRestForm) {
119+
dynamicRestForm.options.onSuccess = function(json, response) {
120+
// show a confirmation or update UI
121+
console.log('saved', json);
122+
};
123+
124+
// optionally prefill and then submit
125+
dynamicRestForm.load().then(function() {
126+
// submit after load completes if desired
127+
dynamicRestForm.submit();
128+
}).catch(function(e) {
129+
// handle load errors
130+
console.error(e);
131+
});
132+
}
133+
```
134+
135+
## Accessibility
136+
137+
To ensure that forms are usable and inclusive for all users, accessibility features are integrated directly into the interaction flow. These measures help screen readers, assistive technologies, and keyboard navigation provide clear feedback and maintain a consistent user experience. The following points outline how error handling, status management, and focus control are implemented to meet accessibility standards.
138+
139+
- **Error Messages:**
140+
- Invalid fields receive the attribute `aria-invalid="true"`.
141+
- The error message is linked to the input field via `aria-describedby`.
142+
- **Status:**
143+
- During loading or submission, all interactive elements are disabled to prevent inconsistent states.
144+
- **Focus Management:**
145+
- In the event of validation errors, focus is automatically set to the first affected field.
146+
147+
## Events
148+
149+
The form controller communicates its internal lifecycle through a series of dispatched webexpress.webui.Event types. These events provide hooks for developers to monitor and react to key stages such as data requests, asynchronous operations, successful submissions, or error handling. Each event constant is triggered at a specific point in the process and carries a detailed payload that can be used for logging, UI updates, or custom logic. The following table summarizes the available events, their triggers, and the associated payload structure.
150+
151+
| Event Constant | Trigger | Detail Payload
152+
|------------------------|-----------------------------------|------------------------------------
153+
| `DATA_REQUESTED_EVENT` | Start of load/submit. | `{ type: "load"|"submit", url }`
154+
| `TASK_START_EVENT` | Async start. | `{ name: "loading"|"submitting" }`
155+
| `TASK_FINISH_EVENT` | Async end. | `{ name: "loading"|"submitting" }`
156+
| `DATA_ARRIVED_EVENT` | Data received. | `{ data, status, type: "load"|"submit" }`
157+
| `CHANGE_VALUE_EVENT` | Form populated (load). | `{ source: "load", data }`
158+
| `UPLOAD_SUCCESS_EVENT` | Submit succeeded (2xx). | `{ response, status, form }`
159+
| `UPLOAD_ERROR_EVENT` | Error (network, validation, 5xx). | `{ error|response, type: "validation"|undefined }`
160+
161+
## Examples
162+
163+
The following examples illustrate how to use REST-based forms within a web application. They show different scenarios, such as automatically loading and updating user profile data with the PUT method, or handling deletion requests with a static confirmation message.
164+
165+
**Form with Automatic Loading and PUT Method:**
166+
167+
This example demonstrates a form that automatically loads the current profile data and submits updates to the API using the PUT method. It is ideal for edit functions such as updating user information. In addition to validating the username, the user can also manage newsletter subscription preferences. Errors are displayed in a dedicated container, and changes are saved via a clearly marked button.
168+
169+
```html
170+
<form id="profile-edit"
171+
class="wx-webapp-restform"
172+
data-api="/api/profile"
173+
data-method="PUT">
174+
<div class="restform-error-container"></div>
175+
<div class="mb-3">
176+
<label for="username">Username</label>
177+
<input type="text" name="username" class="form-control" required minlength="3">
178+
</div>
179+
<div class="form-check">
180+
<input class="form-check-input" type="checkbox" name="newsletter" id="newsletter">
181+
<label class="form-check-label" for="newsletter">Subscribe to newsletter</label>
182+
</div>
183+
<button type="submit" class="btn btn-success">Save</button>
184+
</form>
185+
```
186+
187+
**Delete form without data load (static notice only, with `<confirm>`):**
188+
189+
The second example illustrates a delete form that does not require loading data beforehand. Instead, a static confirmation message is shown through the <confirm> element once the action has been successfully completed. Before deletion, the user is prompted with a clear confirmation question and can proceed deliberately using the red Delete button.
190+
191+
```html
192+
<form id="user-delete"
193+
class="wx-webapp-restform"
194+
data-api="/api/users"
195+
data-mode="delete"
196+
data-id="42">
197+
<confirm>
198+
<div class="alert alert-success">User was successfully deleted.</div>
199+
</confirm>
200+
<p>Do you really want to delete this user?</p>
201+
<button type="submit" class="btn btn-danger">Delete</button>
202+
</form>
203+
```

icon.png

6.99 KB
Loading

src/WebExpress.WebApp.Test/Fixture/UnitTestControlFixture.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,19 @@ public static ComponentHub CreateAndRegisterComponentHubMock()
9191
return componentHub;
9292
}
9393

94+
/// <summary>
95+
/// Create a fake request with no content and an empty URI.
96+
/// </summary>
97+
/// <returns>A fake request for testing.</returns>
98+
public static IRequest CreateRequestMock() => CreateRequestMock("", "");
99+
94100
/// <summary>
95101
/// Create a fake request.
96102
/// </summary>
97103
/// <param name="content">The content of the request.</param>
98104
/// <param name="uri">The URI of the request.</param>
99105
/// <returns>A fake request for testing.</returns>
100-
public static Request CreateRequestMock(string content = "", string uri = "")
106+
public static IRequest CreateRequestMock(string content, string uri)
101107
{
102108
var context = CreateHttpContextMock(content);
103109

@@ -112,6 +118,24 @@ public static Request CreateRequestMock(string content = "", string uri = "")
112118
return request;
113119
}
114120

121+
/// <summary>
122+
/// Create a fake request.
123+
/// </summary>
124+
/// <param name="uri">The URI of the request.</param>
125+
/// <returns>A fake request for testing.</returns>
126+
public static IRequest CreateRequestMock(string uri)
127+
{
128+
var content = $@"GET {uri} HTTP/1.1
129+
Host: localhost
130+
Content-Type: text/html";
131+
132+
var context = CreateHttpContextMock(content);
133+
134+
var request = context.Request;
135+
136+
return request;
137+
}
138+
115139
/// <summary>
116140
/// Create a fake http context.
117141
/// </summary>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using WebExpress.WebCore.WebIcon;
2+
using WebExpress.WebCore.WebUri;
3+
using WebExpress.WebIndex;
4+
5+
namespace WebExpress.WebApp.Test.Model
6+
{
7+
/// <summary>
8+
/// Represents an item in a test index, providing key information, state,
9+
/// and associated metadata for display and identification purposes.
10+
/// </summary>
11+
public class TestIndexItem : IIndexItem
12+
{
13+
/// <summary>
14+
/// Returns or sets the unique identifier of the current entity.
15+
/// </summary>
16+
public Guid Id { get; set; }
17+
18+
/// <summary>
19+
/// Retuens or sets the unique key associated with the current entity.
20+
/// </summary>
21+
public string Key { get; set; }
22+
23+
/// <summary>
24+
/// Returns or sets the collection of names associated with the current entity.
25+
/// </summary>
26+
public IEnumerable<string> Names { get; set; }
27+
28+
/// <summary>
29+
/// Returns or sets the state of the current entity.
30+
/// </summary>
31+
public string State { get; set; }
32+
33+
/// <summary>
34+
/// Returns or sets the description of the current entity.
35+
/// </summary>
36+
public string Description { get; set; }
37+
38+
/// <summary>
39+
/// Returns or sets the icon associated with the table row.
40+
/// </summary>
41+
public IIcon Icon { get; set; }
42+
43+
/// <summary>
44+
/// Returns or sets the URI associated with the table row.
45+
/// </summary>
46+
public IUri Uri { get; set; }
47+
}
48+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using WebExpress.WebIndex;
2+
3+
namespace WebExpress.WebApp.Test.Model
4+
{
5+
/// <summary>
6+
/// Represents an index item that contains a unique identifier and a collection
7+
/// of associated names for testing purposes.
8+
/// </summary>
9+
public class TestIndexItemTemplateTag : IIndexItem
10+
{
11+
/// <summary>
12+
/// Returns or sets the unique identifier of the current entity.
13+
/// </summary>
14+
public Guid Id { get; set; }
15+
16+
/// <summary>
17+
/// Returns or sets the collection of tags associated with the current entity.
18+
/// </summary>
19+
public IEnumerable<string> Tags1 { get; set; }
20+
21+
/// <summary>
22+
/// Returns or sets the collection of tags associated with the current entity.
23+
/// </summary>
24+
public IEnumerable<string> Tags2 { get; set; }
25+
26+
/// <summary>
27+
/// Returns or sets the collection of tags associated with the current entity.
28+
/// </summary>
29+
public IEnumerable<string> Tags3 { get; set; }
30+
}
31+
}

src/WebExpress.WebApp.Test/TestApplication.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public sealed class TestApplication : IApplication
2121
private TestApplication(IApplicationContext applicationContext)
2222
{
2323
// test the injection
24-
if (applicationContext == null)
24+
if (applicationContext is null)
2525
{
2626
throw new ArgumentNullException(nameof(applicationContext), "Parameter cannot be null or empty.");
2727
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using WebExpress.WebApp.WebFragment;
2+
using WebExpress.WebApp.WebSection;
3+
using WebExpress.WebCore.WebAttribute;
4+
using WebExpress.WebCore.WebFragment;
5+
6+
namespace WebExpress.WebApp.Test
7+
{
8+
/// <summary>
9+
/// A dummy fragment for testing purposes.
10+
/// </summary>
11+
[Section<SectionBodySecondary>()]
12+
[Scope<TestPageA>]
13+
public sealed class TestFragmentControlModalRemoteForm : FragmentControlModalRemoteForm
14+
{
15+
/// <summary>
16+
/// Initializes a new instance of the class.
17+
/// </summary>
18+
public TestFragmentControlModalRemoteForm(IFragmentContext fragmentContext)
19+
: base(fragmentContext)
20+
{
21+
}
22+
}
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using WebExpress.WebApp.WebFragment;
2+
using WebExpress.WebApp.WebSection;
3+
using WebExpress.WebCore.WebAttribute;
4+
using WebExpress.WebCore.WebFragment;
5+
6+
namespace WebExpress.WebApp.Test
7+
{
8+
/// <summary>
9+
/// A dummy fragment for testing purposes.
10+
/// </summary>
11+
[Section<SectionContentSecondary>()]
12+
[Scope<TestPageA>]
13+
public sealed class TestFragmentControlRestDashboard : FragmentControlRestDashboard
14+
{
15+
/// <summary>
16+
/// Initializes a new instance of the class.
17+
/// </summary>
18+
public TestFragmentControlRestDashboard(IFragmentContext fragmentContext)
19+
: base(fragmentContext)
20+
{
21+
}
22+
}
23+
}

0 commit comments

Comments
 (0)