Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codenameone.developerguide.backend;

public final class HtmlProduct {
private final int id;
private final String name;

public HtmlProduct(int id, String name) {
this.id = id;
this.name = name;
}

public int getId() { return id; }
public String getName() { return name; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codenameone.developerguide.backend;

import com.codename1.backend.annotations.Controller;
import com.codename1.backend.annotations.GetMapping;
import com.codename1.backend.annotations.ModelAttribute;
import com.codename1.backend.annotations.PostMapping;
import com.codename1.backend.mvc.BindingResult;
import com.codename1.backend.mvc.Model;
import java.util.ArrayList;
import java.util.List;

@Controller
public class HtmlProducts {
private final List<HtmlProduct> products = new ArrayList<HtmlProduct>();

// tag::backend-views-controller[]
@GetMapping("/products")
public synchronized String list(Model model) {
model.addAttribute("products", new ArrayList<HtmlProduct>(products));
return "products";
}
// end::backend-views-controller[]

// tag::backend-views-save[]
@PostMapping("/products")
public synchronized String save(@ModelAttribute("form") ProductForm form,
BindingResult errors, Model model) {
if (form.name == null || form.name.trim().isEmpty()) {
errors.rejectValue("name", "Enter a name.");
}
if (errors.hasErrors()) {
return "edit";
}
products.add(new HtmlProduct(products.size() + 1, form.name));
return "redirect:/products";
}
// end::backend-views-save[]

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codenameone.developerguide.backend;

/// Dedicated editable fields for the compiled-view guide example.
public class ProductForm {
public String name;
}
9 changes: 9 additions & 0 deletions docs/demos/backend/src/main/snippets/edit.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- tag::backend-views-form[] -->
<!-- cn1:model form com.codenameone.developerguide.backend.ProductForm -->
<form method="post" action="/products" th:object="${form}">
<label for="name">Name</label>
<input th:field="*{name}">
<span th:errors="*{name}"></span>
<button type="submit">Save</button>
</form>
<!-- end::backend-views-form[] -->
9 changes: 9 additions & 0 deletions docs/demos/backend/src/main/snippets/products.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- tag::backend-views-list[] -->
<!-- cn1:model products java.util.List<com.codenameone.developerguide.backend.HtmlProduct> -->
<ul th:fragment="rows">
<li th:each="product : ${products}">
<a th:href="@{/products/{id}(id=${product.id})}"
th:text="${product.name}">Example product</a>
</li>
</ul>
<!-- end::backend-views-list[] -->
180 changes: 180 additions & 0 deletions docs/developer-guide/Backend-Views.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
[[backend-views]]
== Compiled HTML views

The backend can serve HTML applications without a Codename One client. Write HTML
with a supported subset of Thymeleaf attributes, return view names from Java
controllers, and use htmx when an interaction should update part of the page.
The build parses the templates and produces Java renderers. Native packaging
translates those renderers through ParparVM to C along with the rest of the server.
There is no template engine, expression interpreter or compiler in the request path.

The runnable example is `scripts/backend-mvc`. It includes an in-memory product
catalog, form errors, CSRF protection, shared page fragments, and a pinned local
htmx script. Its forms work with JavaScript disabled too.

=== Controllers and models

Use `com.codename1.backend.annotations.Controller` for HTML controllers and
`com.codename1.backend.mvc.Model` for their model. Existing REST controllers retain
their response semantics. An HTML controller uses the same mapping annotations,
constructor injection, sessions and security chains as a REST controller.

[source,java]
----
include::../demos/backend/src/main/java/com/codenameone/developerguide/backend/HtmlProducts.java[tag=backend-views-controller,indent=0]
----

A string return value selects a compiled view. `products` selects
`src/main/resources/templates/products.html`; `products :: rows` selects that
file's named fragment. `redirect:/products` redirects to a local absolute path:
303 for normal requests, or 200 with `HX-Redirect` for requests from htmx. A null
return is 404. Unknown view names fail the request; they never resolve file paths.

`ModelAndView` carries a view name and attributes added with `addObject`.
`@ResponseBody`, on the method or controller, retains the REST response rules.
An explicit `HttpServer.Response` is returned unchanged. A view's default status
is 200; `@ResponseStatus` can change it.

=== Typed HTML

Declare model types with comments. Names are explicit and types are fully qualified:

[source,html]
----
include::../demos/backend/src/main/snippets/products.html[tag=backend-views-list,indent=0]
----

The build resolves public JavaBean getters or public fields and emits direct
access. Generic type arguments are preserved through getters, fields, and inherited
classes or interfaces. A missing property or undeclared model name fails the build with the
file and source position. At runtime, a referenced model must be present and have
the declared type. A present null renders as empty text; property navigation
propagates null. Numeric comparisons require non-null operands. Fragments only
require model names they read. Collection loops check element types before access.

Supported directives:

* `th:text`, `th:if`, `th:unless`, and `th:each="item, status : ${items}"`.
The optional status exposes `index`, `count`, `size`, `first`, `last`, `even`, and `odd`.
* `th:href`, `src`, `action`, `value`, `id`, `name`, `class`, `title`, `alt`,
`placeholder`, `method`, and `for`, plus `th:classappend`.
* Boolean attributes `checked`, `selected`, `disabled`, `readonly`, `multiple`,
`required`, `autofocus`, and `hidden`.
* `th:attr="attribute=expression, other=expression"`, including htmx URLs.
* `th:fragment="name"`, `th:insert="~{layout :: name}"`, and
`th:replace="~{layout :: name}"`. References are static, fragments have no
parameters, and recursive inclusion is a build error. `th:block` adds no wrapper.
* `th:object`, `th:field`, and `th:errors` for forms.

Expressions support `${model.property}`, typed list/array/map indexing, string,
number, boolean and null literals, boolean operators, equality, numeric comparisons,
addition/subtraction, and `condition ? yes : no`. Selection expressions `*{field}`
use the enclosing `th:object`. URL expressions use local paths with encoded path
and query arguments, such as `@{/search(q=${query})}`.

Dynamic text and attributes are HTML-escaped. URL arguments use URL encoding
before attribute escaping. Dynamic URL attributes reject executable URL schemes.
Dynamic htmx attributes that evaluate expressions (`hx-on*`, `hx-vars`, `hx-vals`,
`hx-headers`, `hx-request`, and `hx-trigger`) are rejected, including `data-hx-*`
aliases. Define those attributes as static template content.
Script sources, link-resource URLs (including stylesheets), and base URLs must be
static template attributes. Meta `http-equiv` directives, including refresh content,
must also be static. Named metadata such as a description can use dynamic content.
Dynamic attributes are evaluated once per rendered
element, including each iteration of a loop.
Raw HTML, dynamic script/style content, arbitrary method calls, expression
preprocessing, message expressions, inline expressions, custom dialects, and
Spring EL are outside this subset. Unsupported `th:` attributes fail the build.
This is a syntax-compatible subset, not the Thymeleaf library.

=== Forms and validation

A mapped method can receive a dedicated form DTO:

[source,java]
----
include::../demos/backend/src/main/java/com/codenameone/developerguide/backend/HtmlProducts.java[tag=backend-views-save,indent=0]
----

Form DTOs need a public no-argument constructor and writable scalar fields or
setters. Supported values are strings, numeric primitives/boxes, booleans and
characters. Nested objects, collections, file uploads through the form DTO, and
ORM entities are refused. Bind identifiers through route parameters.
Unknown submitted fields are ignored; no property path from a request is executed.

`BindingResult` must immediately follow its form parameter. Conversion errors are
recorded before the controller runs. Without a `BindingResult`, conversion failure
returns 400. `reject(message)` adds a global error; `rejectValue(field, message)`
adds a field error. Validation is application code; Bean Validation annotations
aren't implemented.

[source,html]
----
include::../demos/backend/src/main/snippets/edit.html[tag=backend-views-form,indent=0]
----

`th:field` renders the name, a default id, and the submitted value if binding failed.
It supports inputs, textareas, scalar selects, checkboxes and radio buttons.
Boolean checkboxes use field presence, including with custom values. They emit a
hidden marker so an unchecked field binds to false. The marker shares the
checkbox's disabled state and form association. Without a marker, Boolean fields
use strict value conversion.
`th:errors="*{*}"` renders all errors for the selected form. Use distinct explicit
ids for radio buttons in a group. Multiple-selection collection binding is deferred.

Each `@ModelAttribute` parameter on a route must have a distinct name; duplicate
names fail the build instead of overwriting form objects and validation results.

=== htmx, CSRF and assets

Ordinary `hx-*` attributes pass through. Put `hx-post` on a form and target a
named fragment's wrapper. `Htmx.isRequest(request)` distinguishes fragment requests
from normal navigation and history restoration. A controller chooses the view;
request headers never select a template themselves. Return form-error fragments
with status 200 so htmx swaps them normally. `Htmx.redirect`, `refresh`, and `trigger`
provide response-header helpers.

The existing security chain remains responsible for CSRF enforcement. The model
contains `_csrf`, whose type is declared automatically. Unsafe forms include a
hidden CSRF token when one is available; htmx serializes it with the other fields.
Form `action`, submit-control `formaction`, mutating htmx URLs (`hx-post`,
`hx-put`, `hx-patch`, `hx-delete`), and static base URLs must be local
absolute paths such as `/products/save`, or empty to use the current document.
This applies to static and dynamic form destinations, including controls in fragments,
to prevent submissions from disclosing the generated token to another origin.
For `hx-get`, generated `hx-params` filtering excludes the CSRF parameter while
preserving the token for a native POST fallback. Explicit parameter allow lists
and exclusion lists are retained; included fragments receive their parent's
original parameter filter, so nested POST requests don't inherit the GET-only
CSRF exclusion. The configured CSRF parameter name is used.
When a CSRF token is available, submit controls can't override the native method
with GET or an invalid value that defaults to GET. This restriction also applies
to controls in fragments or controls linked to a form by its ID. Use a separate
GET form for searches and previews. POST and dialog overrides remain supported.
Form `enctype` and submit-control `formenctype` support
`application/x-www-form-urlencoded` and `multipart/form-data`. Unsupported static
encodings fail the build; unsupported dynamic values fail rendering. In particular,
`text/plain` isn't supported by form binding.
For requests outside forms, supply the existing token header explicitly. Defining
HTML controllers doesn't enable a security chain automatically.

Files in `src/main/resources/static` are embedded at build time and served under
`/static/` after controller routes. Only enumerated assets are accessible; templates
are private. Embedded assets use `Cache-Control: no-cache` and `nosniff`. Use URL-safe
ASCII filenames. Each embedded asset is limited to 2 MiB; use the existing
`cn1.static.root` facility for larger files.

=== Building and porting

Maven compiles views during `process-annotations` in `process-classes`; native
packaging also runs this compiler. Gradle uses the same engine and tracks templates
and static assets as compilation inputs. Rebuild after editing HTML. Generated
view and asset registries are replaced on each build, including after file deletion.
The first request only executes generated code.

To port a small Spring MVC application, change the controller/model imports, add
template model declarations, use dedicated scalar form DTOs, and replace unsupported
expressions or dialect features. Keep existing HTML and htmx attributes within the
supported subset. Controller advice, method-level `@ModelAttribute`, flash attributes,
`forward:` views, custom converters, dynamic fragment selectors, and internationalized
message bundles aren't part of this first version.
1 change: 1 addition & 0 deletions docs/developer-guide/Backend.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ a server and go into depth:

* <<backend-web>>: mapping HTTP requests to methods, sharing a typed contract
with the app, and holding WebSocket connections open.
* <<backend-views>>: compiled HTML pages, typed templates, forms and htmx fragments.
* <<backend-beans>>: splitting a server into services the build wires together,
with scopes, conditions and configuration binding.
* <<backend-data>>: the connection pool, the object mapping, and
Expand Down
2 changes: 2 additions & 0 deletions docs/developer-guide/developer-guide.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ include::Backend.asciidoc[]

include::Backend-Web.asciidoc[]

include::Backend-Views.asciidoc[]

include::Backend-Beans.asciidoc[]

include::Backend-Testing.asciidoc[]
Expand Down
4 changes: 4 additions & 0 deletions docs/developer-guide/languagetool-accept.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# LanguageTool accept list for the developer guide.
# Compiled HTML views: library names and the IEC mebibyte unit symbol.
htmx
Thymeleaf
MiB
#
# Each non-blank, non-comment line is a Python regex matched against the
# exact text LanguageTool flagged (m.context[offsetInContext:
Expand Down
Loading
Loading