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 +262 -0
- package/README.md +260 -3
- package/package.json +34 -5
- package/src/core/customFormRender.js +88 -0
- package/src/core/propertiesFactory.js +190 -0
- package/src/core/validation.js +442 -0
- package/src/index.js +2 -0
- package/src/models/properties/Button.js +34 -0
- package/src/models/properties/Divider.js +11 -0
- package/src/models/properties/Dropdown.js +22 -0
- package/src/models/properties/Header.js +16 -0
- package/src/models/properties/Image.js +25 -0
- package/src/models/properties/Label.js +16 -0
- package/src/models/properties/Property.js +13 -0
- package/src/models/properties/Slider.js +25 -0
- package/src/models/properties/Spacer.js +12 -0
- package/src/models/properties/TextField.js +20 -0
- package/src/models/properties/Toggle.js +20 -0
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
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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)`);
|