bedrock-ddui-forms 0.0.0-stage → 1.0.0

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.
package/README.es.md ADDED
@@ -0,0 +1,262 @@
1
+ # bedrock-ddui-forms
2
+
3
+ > [🇬🇧 English](./README.md) · 🇪🇸 Español
4
+
5
+ Una capa declarativa para crear formularios **DDUI** (`CustomForm`) de `@minecraft/server-ui` a partir de objetos.
6
+
7
+ En lugar de encadenar llamadas sobre `CustomForm`, describes la vista como datos y un renderizador la traduce:
8
+
9
+ ```js
10
+ {
11
+ title: "Ajustes",
12
+ content: [
13
+ { type: "header", text: "General" },
14
+ { type: "toggle", label: "Activar función", toggled: new ObservableBoolean(true, { clientWritable: true }) },
15
+ { type: "close-button" }
16
+ ]
17
+ }
18
+ ```
19
+
20
+ > No afiliado a Mojang ni a Microsoft. "Minecraft" y "Bedrock" se usan solo para describir la plataforma a la que apunta esta librería.
21
+
22
+ ## Características
23
+
24
+ - **Vistas declarativas**: un formulario es un objeto con `title` y un arreglo `content`.
25
+ - **Tipado con la API oficial**: los modelos son typedefs de JSDoc que reutilizan los tipos de `@minecraft/server-ui` (`ButtonOptions`, `SliderOptions`, …), así que se mantienen sincronizados con el módulo.
26
+ - **Navegación sencilla**: un botón puede abrir otra vista con `to`, combinable con `onClick`.
27
+ - **Fácil de extender**: añadir un tipo de elemento nuevo es una entrada en una tabla y una función.
28
+ - **Fallback seguro**: un `type` no soportado muestra un label visible y registra una advertencia, en vez de romper todo el formulario.
29
+
30
+ ## Requisitos
31
+
32
+ - `@minecraft/server-ui` **2.2.0** (estable)
33
+
34
+ ## Instalación
35
+
36
+ El motor de scripts de Minecraft **no** resuelve paquetes de npm (dentro del juego no existe `node_modules`). Solo los módulos `@minecraft/*` declarados en el `manifest.json` los aporta el juego, así que la librería tiene que terminar como archivos dentro de la carpeta `scripts/` de tu addon. Hay dos formas de hacerlo.
37
+
38
+ ### Opción 1: copiar los archivos
39
+
40
+ Copia la librería dentro de tu behavior pack e impórtala con rutas relativas:
41
+
42
+ ```
43
+ BP/
44
+ manifest.json
45
+ scripts/
46
+ main.js
47
+ libs/bedrock-ddui-forms/
48
+ index.js
49
+ core/
50
+ models/
51
+ views/
52
+ ```
53
+
54
+ ```js
55
+ import { customFormRender } from "../libs/bedrock-ddui-forms/index";
56
+ ```
57
+
58
+ No necesita herramientas, pero para actualizar tienes que copiar los archivos de nuevo.
59
+
60
+ ### Opción 2: usar un bundler (recomendada si instalas desde npm)
61
+
62
+ ```bash
63
+ npm install bedrock-ddui-forms
64
+ ```
65
+
66
+ Después empaqueta tu código y la librería en un solo archivo dentro de `scripts/`. Por ejemplo, con [esbuild](https://esbuild.github.io/):
67
+
68
+ ```bash
69
+ npx esbuild src/index.js --bundle --format=esm --outfile=scripts/libs/bedrock-ddui-forms/index.js --external:@minecraft/server --external:@minecraft/server-ui
70
+ ```
71
+
72
+ Los `--external` son importantes: los módulos `@minecraft/*` los aporta el juego y no deben incluirse en el bundle. Con este flujo, `npm update` basta para actualizar la librería.
73
+
74
+ ### Siempre necesario
75
+
76
+ - Declara `@minecraft/server-ui` en tu `manifest.json` (esta librería apunta a la versión `2.2.0`). Nunca se copia ni se empaqueta:
77
+
78
+ ```json
79
+ {
80
+ "module_name": "@minecraft/server-ui",
81
+ "version": "2.2.0"
82
+ }
83
+ ```
84
+
85
+ - La propia librería no debe usar módulos de Node.js (`node:fs`, etc.), porque no existen en el juego.
86
+ - Los imports internos de la librería están escritos sin extensión (`./propertiesFactory`), y los bundlers los resuelven. Si copias los archivos y tu entorno exige extensiones explícitas, añade `.js` a los imports relativos.
87
+
88
+ ## Inicio rápido
89
+
90
+ ```js
91
+ import { ObservableBoolean, ObservableString } from "@minecraft/server-ui";
92
+ import { customFormRender } from "../libs/bedrock-ddui-forms/index";
93
+
94
+ const createSettingsPage = (player) => ({
95
+ title: "Ajustes",
96
+ content: [
97
+ { type: "header", text: "General" },
98
+ { type: "spacer" },
99
+ {
100
+ type: "text-field",
101
+ label: "Nombre",
102
+ text: new ObservableString("Steve", { clientWritable: true })
103
+ },
104
+ {
105
+ type: "toggle",
106
+ label: "Activar función",
107
+ toggled: new ObservableBoolean(true, { clientWritable: true })
108
+ },
109
+ { type: "divider" },
110
+ { type: "close-button" }
111
+ ]
112
+ });
113
+
114
+ export const openSettings = (player) => customFormRender(player, createSettingsPage(player));
115
+ ```
116
+
117
+ > **Consejo:** define cada vista como una función (`createXPage(player)`) para que los `Observable` se creen de nuevo cada vez que se abre la vista y no se arrastre estado anterior.
118
+
119
+ ## Elementos soportados
120
+
121
+ | `type` | Propiedades principales | Método de `CustomForm` |
122
+ | --- | --- | --- |
123
+ | `button` | `label`, `onClick?`, `to?`, `options?` | `button()` |
124
+ | `close-button` | – | `closeButton()` |
125
+ | `divider` | `options?` | `divider()` |
126
+ | `dropdown` | `label`, `value` (`ObservableNumber`), `items`, `options?` | `dropdown()` |
127
+ | `header` | `text`, `options?` | `header()` |
128
+ | `image` | `src`, `pack`, `options?` (`onClick`, `tooltip`, `visible`, `width`) | `image()` |
129
+ | `label` | `text`, `options?` | `label()` |
130
+ | `slider` | `label`, `value` (`ObservableNumber`), `min`, `max`, `options?` (`step`, …) | `slider()` |
131
+ | `spacer` | `options?` | `spacer()` |
132
+ | `text-field` | `label`, `text` (`ObservableString`), `options?` | `textField()` |
133
+ | `toggle` | `label`, `toggled` (`ObservableBoolean`), `options?` | `toggle()` |
134
+
135
+ `options` siempre coincide con el tipo de opciones correspondiente de `@minecraft/server-ui` (por ejemplo `SliderOptions`). `image.options.width` es una fracción del ancho del formulario (`0.5` es la mitad).
136
+
137
+ ## Navegación
138
+
139
+ Un botón puede abrir otra vista con `to`. Se ejecuta **después** de `onClick`, cuando el formulario actual ya está cerrado, así que se pueden combinar:
140
+
141
+ ```js
142
+ {
143
+ type: "button",
144
+ label: "Guardar y volver",
145
+ onClick: function () { save(player); }, // se ejecuta primero
146
+ to: () => openMainMenu(player) // luego cierra este formulario y abre el siguiente
147
+ }
148
+ ```
149
+
150
+ - Si el botón solo navega, usa `to`.
151
+ - Si hace algo más, usa `onClick`, y añade `to` si además debe navegar.
152
+ - Dentro de `onClick` y de `image.options.onClick`, `this` es el `CustomForm`, así que `this.close()` funciona si necesitas cerrar sin abrir nada.
153
+
154
+ ## Leer valores
155
+
156
+ Los valores son los `Observable` que tú creas, así que guarda una referencia y léelos cuando los necesites:
157
+
158
+ ```js
159
+ const createPage = (player) => {
160
+ const name = new ObservableString("", { clientWritable: true });
161
+
162
+ return {
163
+ title: "Renombrar",
164
+ content: [
165
+ { type: "text-field", label: "Nuevo nombre", text: name },
166
+ {
167
+ type: "button",
168
+ label: "Guardar",
169
+ onClick: function () { rename(player, name.getData()); },
170
+ to: () => openMainMenu(player)
171
+ }
172
+ ]
173
+ };
174
+ };
175
+ ```
176
+
177
+ ## Cerrar una vista: `onExit`
178
+
179
+ `formData.onExit` se ejecuta cuando el formulario se cierra, por cualquier motivo excepto `UserBusy` (cuando el formulario no pudo mostrarse). `this` es el `CustomForm` y el primer argumento es el `DataDrivenScreenClosedReason`:
180
+
181
+ ```js
182
+ {
183
+ title: "Ajustes",
184
+ content: [ /* ... */ ],
185
+ onExit(reason) {
186
+ // reason: ClientClosed | ServerClosed
187
+ }
188
+ }
189
+ ```
190
+
191
+ Cuando un botón navega con `to`, el servidor cierra el formulario actual, así que su `onExit` también se ejecuta, con `ServerClosed`, **después** de que `to` ya abrió la siguiente vista. Revisa `reason` si solo quieres reaccionar cuando el jugador cierra el formulario (`ClientClosed`).
192
+
193
+ ## Validación y mensajes de error
194
+
195
+ Antes de abrir una vista, `customFormRender` revisa cada elemento y reporta **todos** los problemas que encuentra en un solo mensaje en la consola, con el lugar exacto de cada uno:
196
+
197
+ ```
198
+ [bedrock-ddui-forms] View "Stored Inventory": 3 errors, 2 warnings
199
+ ERROR content[0] slider "Protection duration" > label: is required. Expected string | ObservableString | ObservableUIRawMessage | UIRawMessage.
200
+ WARNING content[0] slider "Protection duration" > text: unknown property "text". Valid properties: label, value, min, max, options.
201
+ WARNING content[0] slider "Protection duration" > options.steps: unknown option "steps". Did you mean "step"?
202
+ ERROR content[1] textfield "Owner" > type: unknown type "textfield". Did you mean "text-field"?
203
+ ERROR content[2] toggle "Keep armor" > toggled: expected ObservableBoolean, received boolean true. Create it with new ObservableBoolean(<initial value>, { clientWritable: true }).
204
+ ```
205
+
206
+ Los mensajes están en inglés, igual que el resto de la salida de la librería.
207
+
208
+ - Los **errores** dejan el elemento inutilizable. Las **advertencias** señalan equivocaciones probables (propiedades u opciones desconocidas, un valor de dropdown que ningún item tiene, `min` mayor que `max`, un botón sin acción...) y no detienen nada.
209
+ - Los errores de escritura reciben una sugerencia ("Did you mean ...?"), y si pasas un valor simple donde se necesita un `Observable`, el mensaje explica cómo crearlo.
210
+ - También se detecta un error muy común: `to: openMenu(player)` llama a la función en vez de pasarla (`to: () => openMenu(player)`).
211
+ - Los errores lanzados dentro de `onClick`, `to`, `image.options.onClick` y `onExit` se registran con el elemento al que pertenecen, en vez de fallar en silencio.
212
+
213
+ Qué pasa con un elemento con errores lo decides tú, con el tercer argumento de `customFormRender`:
214
+
215
+ ```js
216
+ customFormRender(player, formData, { onInvalid: "label" }); // por defecto
217
+ ```
218
+
219
+ | `onInvalid` | Comportamiento |
220
+ | --- | --- |
221
+ | `"label"` | Muestra un marcador visible en su lugar (útil mientras desarrollas). |
222
+ | `"skip"` | Deja el elemento fuera (mejor en producción o con contenido que viene de fuera). |
223
+ | `"throw"` | No abre el formulario y lanza un `FormValidationError` (su propiedad `issues` lista todos los problemas). |
224
+
225
+ Si el problema está en la vista misma (falta `title`, `content` no es un arreglo, no hay `player`), el formulario no se puede construir: se registra el reporte y `customFormRender` devuelve `undefined`.
226
+
227
+ También puedes validar sin renderizar, por ejemplo en una prueba o con vistas que vienen de un servidor:
228
+
229
+ ```js
230
+ import { validateForm } from "bedrock-ddui-forms";
231
+
232
+ const report = validateForm(formData, player);
233
+ report.errors; // problemas que rompen un elemento
234
+ report.warnings; // equivocaciones probables
235
+ report.message; // el texto mostrado arriba
236
+ ```
237
+
238
+ ## Valor de retorno
239
+
240
+ `customFormRender(player, formData)` devuelve una promesa que se resuelve con el `DataDrivenScreenClosedReason` (`ClientClosed`, `ServerClosed` o `UserBusy`), o `undefined` si la vista no se pudo construir o hubo un error al mostrarla. Un resultado `UserBusy` además registra una advertencia.
241
+
242
+ ## Estructura del proyecto
243
+
244
+ ```
245
+ index.js punto de entrada público
246
+ core/
247
+ customFormRender.js renderiza una vista y la muestra al jugador
248
+ propertiesFactory.js asocia cada `type` con su llamada a CustomForm
249
+ validation.js revisa las vistas y construye los mensajes de error
250
+ models/properties/
251
+ *.js typedefs de JSDoc de cada elemento
252
+ ```
253
+
254
+ ## Añadir un tipo de elemento nuevo
255
+
256
+ 1. Crea su typedef en `models/properties/` y añádelo a la unión `Property`.
257
+ 2. Escribe su función en `propertiesFactory.js`.
258
+ 3. Regístrala en la tabla `properties`.
259
+
260
+ ## Aviso legal
261
+
262
+ Este proyecto no está afiliado, respaldado ni patrocinado por Mojang Studios ni Microsoft.
package/README.md CHANGED
@@ -1,3 +1,260 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # bedrock-ddui-forms
2
+
3
+ > 🇬🇧 English · [🇪🇸 Español](./README.es.md)
4
+
5
+ A declarative layer for building `@minecraft/server-ui` **DDUI** forms (`CustomForm`) from plain objects.
6
+
7
+ Instead of chaining calls on `CustomForm`, you describe a view as data and let a renderer translate it:
8
+
9
+ ```js
10
+ {
11
+ title: "Settings",
12
+ content: [
13
+ { type: "header", text: "General" },
14
+ { type: "toggle", label: "Enable feature", toggled: new ObservableBoolean(true, { clientWritable: true }) },
15
+ { type: "close-button" }
16
+ ]
17
+ }
18
+ ```
19
+
20
+ > Not affiliated with Mojang or Microsoft. "Minecraft" and "Bedrock" are used only to describe the platform this library targets.
21
+
22
+ ## Features
23
+
24
+ - **Declarative views**: a form is an object with a `title` and a `content` array.
25
+ - **Typed with the official API**: models are JSDoc typedefs that reuse the types of `@minecraft/server-ui` (`ButtonOptions`, `SliderOptions`, …), so they stay in sync with the module.
26
+ - **Simple navigation**: a button can open another view with `to`, composed with `onClick`.
27
+ - **Easy to extend**: adding a new element type is one entry in a lookup table and one function.
28
+ - **Safe fallback**: an unsupported `type` renders a visible label and logs a warning instead of breaking the whole form.
29
+
30
+ ## Requirements
31
+
32
+ - `@minecraft/server-ui` **2.2.0** (stable)
33
+
34
+ ## Installation
35
+
36
+ Minecraft's script engine does **not** resolve npm packages (there is no `node_modules` inside the game). Only the `@minecraft/*` modules declared in the `manifest.json` are provided by the game, so the library has to end up as files inside your addon's `scripts/` folder. There are two ways to do it.
37
+
38
+ ### Option 1: copy the files
39
+
40
+ Copy the library into your behavior pack and import it with relative paths:
41
+
42
+ ```
43
+ BP/
44
+ manifest.json
45
+ scripts/
46
+ main.js
47
+ lib/bedrock-ddui-forms/
48
+ index.js
49
+ core/
50
+ models/
51
+ views/
52
+ ```
53
+
54
+ ```js
55
+ import { customFormRender } from "../libs/bedrock-ddui-forms/index";
56
+ ```
57
+
58
+ No tooling needed, but you have to copy the files again to update.
59
+
60
+ ### Option 2: use a bundler (recommended if you install from npm)
61
+
62
+ ```bash
63
+ npm install bedrock-ddui-forms
64
+ ```
65
+
66
+ Then bundle your code and the library into a single file inside `scripts/`. For example, with [esbuild](https://esbuild.github.io/):
67
+
68
+ ```bash
69
+ npx esbuild src/index.js --bundle --format=esm --outfile=scripts/libs/bedrock-ddui-forms/index.js --external:@minecraft/server --external:@minecraft/server-ui
70
+ ```
71
+
72
+ The `--external` flags are important: the `@minecraft/*` modules are provided by the game and must not be bundled. With this flow, `npm update` is enough to update the library.
73
+
74
+ ### Always required
75
+
76
+ - Declare `@minecraft/server-ui` in your `manifest.json` (this library targets version `2.2.0`). It is never copied or bundled:
77
+
78
+ ```json
79
+ {
80
+ "module_name": "@minecraft/server-ui",
81
+ "version": "2.2.0"
82
+ }
83
+ ```
84
+
85
+ - The library itself must not use Node.js modules (`node:fs`, etc.), because they don't exist in the game.
86
+ - The library's internal imports are written without file extensions (`./propertiesFactory`), which bundlers resolve. If you copy the files and your setup requires explicit extensions, add `.js` to the relative imports.
87
+
88
+ ## Quick start
89
+
90
+ ```js
91
+ import { ObservableBoolean, ObservableString } from "@minecraft/server-ui";
92
+ import { customFormRender } from "../libs/bedrock-ddui-forms/index";
93
+
94
+ const createSettingsPage = (player) => ({
95
+ title: "Settings",
96
+ content: [
97
+ { type: "header", text: "General" },
98
+ { type: "spacer" },
99
+ {
100
+ type: "text-field",
101
+ label: "Name",
102
+ text: new ObservableString("Steve", { clientWritable: true })
103
+ },
104
+ {
105
+ type: "toggle",
106
+ label: "Enable feature",
107
+ toggled: new ObservableBoolean(true, { clientWritable: true })
108
+ },
109
+ { type: "divider" },
110
+ { type: "close-button" }
111
+ ]
112
+ });
113
+
114
+ export const openSettings = (player) => customFormRender(player, createSettingsPage(player));
115
+ ```
116
+
117
+ > **Tip:** define each view as a function (`createXPage(player)`) so the `Observable` values are created again every time the view is opened and no old state is carried over.
118
+
119
+ ## Supported elements
120
+
121
+ | `type` | Main properties | `CustomForm` method |
122
+ | --- | --- | --- |
123
+ | `button` | `label`, `onClick?`, `to?`, `options?` | `button()` |
124
+ | `close-button` | – | `closeButton()` |
125
+ | `divider` | `options?` | `divider()` |
126
+ | `dropdown` | `label`, `value` (`ObservableNumber`), `items`, `options?` | `dropdown()` |
127
+ | `header` | `text`, `options?` | `header()` |
128
+ | `image` | `src`, `pack`, `options?` (`onClick`, `tooltip`, `visible`, `width`) | `image()` |
129
+ | `label` | `text`, `options?` | `label()` |
130
+ | `slider` | `label`, `value` (`ObservableNumber`), `min`, `max`, `options?` (`step`, …) | `slider()` |
131
+ | `spacer` | `options?` | `spacer()` |
132
+ | `text-field` | `label`, `text` (`ObservableString`), `options?` | `textField()` |
133
+ | `toggle` | `label`, `toggled` (`ObservableBoolean`), `options?` | `toggle()` |
134
+
135
+ `options` always matches the corresponding options type of `@minecraft/server-ui` (for example `SliderOptions`). `image.options.width` is a fraction of the form width (`0.5` is half).
136
+
137
+ ## Navigation
138
+
139
+ A button can open another view with `to`. It runs **after** `onClick`, once the current form is closed, so both can be combined:
140
+
141
+ ```js
142
+ {
143
+ type: "button",
144
+ label: "Save and go back",
145
+ onClick: function () { save(player); }, // runs first
146
+ to: () => openMainMenu(player) // then closes this form and opens the next one
147
+ }
148
+ ```
149
+
150
+ - If you only navigate, use `to`.
151
+ - If the button does something else, use `onClick`, and add `to` if it should also navigate.
152
+ - Inside `onClick` and `image.options.onClick`, `this` is the `CustomForm`, so `this.close()` works if you need to close without opening anything.
153
+
154
+ ## Reading values
155
+
156
+ The values are the `Observable` objects you create, so keep a reference and read them when you need them:
157
+
158
+ ```js
159
+ const createPage = (player) => {
160
+ const name = new ObservableString("", { clientWritable: true });
161
+
162
+ return {
163
+ title: "Rename",
164
+ content: [
165
+ { type: "text-field", label: "New name", text: name },
166
+ {
167
+ type: "button",
168
+ label: "Save",
169
+ onClick: function () { rename(player, name.getData()); },
170
+ to: () => openMainMenu(player)
171
+ }
172
+ ]
173
+ };
174
+ };
175
+ ```
176
+
177
+ ## Closing a view: `onExit`
178
+
179
+ `formData.onExit` runs after the form closes, for any reason except `UserBusy` (when the form could not be shown at all). `this` is the `CustomForm` and the first argument is the `DataDrivenScreenClosedReason`:
180
+
181
+ ```js
182
+ {
183
+ title: "Settings",
184
+ content: [ /* ... */ ],
185
+ onExit(reason) {
186
+ // reason: ClientClosed | ServerClosed
187
+ }
188
+ }
189
+ ```
190
+
191
+ When a button navigates with `to`, the current form is closed by the server, so its `onExit` also runs, with `ServerClosed`, **after** `to` has already opened the next view. Check `reason` if you only want to react when the player closes the form (`ClientClosed`).
192
+
193
+ ## Validation and error messages
194
+
195
+ Before opening a view, `customFormRender` checks every element and reports **all** the problems it finds in one message in the console, with the exact place of each one:
196
+
197
+ ```
198
+ [bedrock-ddui-forms] View "Stored Inventory": 3 errors, 2 warnings
199
+ ERROR content[0] slider "Protection duration" > label: is required. Expected string | ObservableString | ObservableUIRawMessage | UIRawMessage.
200
+ WARNING content[0] slider "Protection duration" > text: unknown property "text". Valid properties: label, value, min, max, options.
201
+ WARNING content[0] slider "Protection duration" > options.steps: unknown option "steps". Did you mean "step"?
202
+ ERROR content[1] textfield "Owner" > type: unknown type "textfield". Did you mean "text-field"?
203
+ ERROR content[2] toggle "Keep armor" > toggled: expected ObservableBoolean, received boolean true. Create it with new ObservableBoolean(<initial value>, { clientWritable: true }).
204
+ ```
205
+
206
+ - **Errors** make the element unusable. **Warnings** point at likely mistakes (unknown properties or options, a dropdown value that no item has, `min` greater than `max`, a button with no action...) and do not stop anything.
207
+ - Typos get a "Did you mean ...?" suggestion, and passing a plain value where an `Observable` is needed explains how to create it.
208
+ - A common mistake is also caught: `to: openMenu(player)` calls the function instead of passing it (`to: () => openMenu(player)`).
209
+ - Errors thrown inside `onClick`, `to`, `image.options.onClick` and `onExit` are logged with the element they belong to instead of failing silently.
210
+
211
+ What happens to an element with errors is up to you, with the third argument of `customFormRender`:
212
+
213
+ ```js
214
+ customFormRender(player, formData, { onInvalid: "label" }); // default
215
+ ```
216
+
217
+ | `onInvalid` | Behaviour |
218
+ | --- | --- |
219
+ | `"label"` | Shows a visible placeholder in its place (handy while developing). |
220
+ | `"skip"` | Leaves the element out (better for production or for content you load from outside). |
221
+ | `"throw"` | Does not open the form and throws a `FormValidationError` (its `issues` property lists every problem). |
222
+
223
+ If the problem is in the view itself (`title` missing, `content` not an array, no `player`), the form cannot be built: the report is logged and `customFormRender` returns `undefined`.
224
+
225
+ You can also validate without rendering, for example in a test or for views that come from a server:
226
+
227
+ ```js
228
+ import { validateForm } from "bedrock-ddui-forms";
229
+
230
+ const report = validateForm(formData, player);
231
+ report.errors; // problems that break an element
232
+ report.warnings; // likely mistakes
233
+ report.message; // the text shown above
234
+ ```
235
+
236
+ ## Return value
237
+
238
+ `customFormRender(player, formData)` returns a promise that resolves with the `DataDrivenScreenClosedReason` (`ClientClosed`, `ServerClosed` or `UserBusy`), or `undefined` if the view could not be built or an error occurred while showing it. A `UserBusy` result also logs a warning.
239
+
240
+ ## Project layout
241
+
242
+ ```
243
+ index.js public entry point
244
+ core/
245
+ customFormRender.js renders a view and shows it to the player
246
+ propertiesFactory.js maps each `type` to its CustomForm call
247
+ validation.js checks views and builds the error messages
248
+ models/properties/
249
+ *.js JSDoc typedefs for each element
250
+ ```
251
+
252
+ ## Adding a new element type
253
+
254
+ 1. Create its typedef in `models/properties/` and add it to the `Property` union.
255
+ 2. Write its function in `propertiesFactory.js`.
256
+ 3. Register it in the `properties` table.
257
+
258
+ ## Disclaimer
259
+
260
+ This project is not affiliated with, endorsed by, or sponsored by Mojang Studios or Microsoft.
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
- {
2
- "name": "bedrock-ddui-forms",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
1
+ {
2
+ "name": "bedrock-ddui-forms",
3
+ "version": "1.0.0",
4
+ "description": "A declarative layer for creating DDUI forms (CustomForm) from @minecraft/server-ui based on objects.",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "README.md",
13
+ "README.es.md"
14
+ ],
15
+ "scripts": {
16
+ "test": "node --import ./test/register-stubs.mjs --test \"test/*.test.js\""
17
+ },
18
+ "author": "victorsigma",
19
+ "license": "ISC",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/victorsigma/Bedrock-DDUI-Forms.git"
23
+ },
24
+ "keywords": [
25
+ "minecraft",
26
+ "bedrock",
27
+ "ddui",
28
+ "server-ui",
29
+ "forms",
30
+ "script-api"
31
+ ],
32
+ "peerDependencies": {
33
+ "@minecraft/server-ui": "2.2.0"
34
+ }
6
35
  }
@@ -0,0 +1,88 @@
1
+ import { CustomForm, DataDrivenScreenClosedReason, ObservableString, ObservableUIRawMessage } from "@minecraft/server-ui";
2
+ import { propertiesFactory } from "./propertiesFactory";
3
+ import { FormValidationError, logCallbackError, validateForm } from "./validation";
4
+
5
+ /**
6
+ * @typedef {Object} FormData
7
+ * @property {ObservableString | ObservableUIRawMessage | string} title
8
+ * @property {import("../models/properties/Property").Property[]} content
9
+ * @property {(this: CustomForm, reason: DataDrivenScreenClosedReason) => void} [onExit] - Runs after the form closes, for any reason except `UserBusy`.
10
+ */
11
+
12
+ /**
13
+ * @typedef {Object} RenderOptions
14
+ * @property {"label" | "skip" | "throw"} [onInvalid] - What to do with elements that have errors.
15
+ * - `"label"` (default): show a visible placeholder in their place.
16
+ * - `"skip"`: leave them out of the form.
17
+ * - `"throw"`: do not open the form and throw a `FormValidationError`.
18
+ */
19
+
20
+ /**
21
+ * @param {import("@minecraft/server").Player} player
22
+ * @param {FormData} formData
23
+ * @param {RenderOptions} [options]
24
+ * @returns {Promise<DataDrivenScreenClosedReason | undefined>} Motivo de cierre, o undefined si la vista no pudo mostrarse.
25
+ */
26
+ export const customFormRender = async (player, formData, options = {}) => {
27
+ const { onInvalid = "label" } = options;
28
+
29
+ const report = validateForm(formData, player);
30
+
31
+ if (report.errors.length) {
32
+ console.error(report.message);
33
+ } else if (report.warnings.length) {
34
+ console.warn(report.message);
35
+ }
36
+
37
+ if (report.errors.length && onInvalid === "throw") {
38
+ throw new FormValidationError(report);
39
+ }
40
+ if (report.fatal) return undefined;
41
+
42
+ let view = new CustomForm(player, formData.title);
43
+
44
+ formData.content.forEach((property, index) => {
45
+ const path = `content[${index}] ${property?.type ?? "element"}`;
46
+
47
+ if (report.invalid.has(index)) {
48
+ if (onInvalid !== "skip") view = placeholder(view, path);
49
+ return;
50
+ }
51
+
52
+ try {
53
+ view = propertiesFactory(view, property);
54
+ } catch (e) {
55
+ console.error(`[bedrock-ddui-forms] ${path}: the API rejected this element: ${e?.message ?? e}`);
56
+ if (onInvalid === "skip") return;
57
+ view = placeholder(view, path);
58
+ }
59
+ });
60
+
61
+ try {
62
+ const reason = await view.show();
63
+
64
+ if (reason === DataDrivenScreenClosedReason.UserBusy) {
65
+ console.warn("[bedrock-ddui-forms]: The player is busy with another UI.");
66
+ } else {
67
+ runOnExit(view, formData.onExit, reason);
68
+ }
69
+
70
+ return reason;
71
+ } catch (e) {
72
+ console.warn(`[bedrock-ddui-forms]: ${e}`);
73
+ }
74
+ }
75
+
76
+ /** Runs `onExit` on its own, so an error inside it is reported as such and does not look like a failure of show(). */
77
+ const runOnExit = (view, onExit, reason) => {
78
+ if (typeof onExit !== "function") return;
79
+
80
+ try {
81
+ onExit.call(view, reason);
82
+ } catch (e) {
83
+ logCallbackError("onExit", e);
84
+ }
85
+ };
86
+
87
+ /** Visible stand-in for an element that could not be built. */
88
+ const placeholder = (view, path) => view.label(`[invalid element] ${path} (see the console)`);