Skip to content

Commit 4878fb4

Browse files
taeficromotecarusselljtdyer
authored
feat: add ListSignal section to the Signals documentation (#3844)
* initial version * edit contents for better organization * fix version badge * Apply suggestions from code review Co-authored-by: Luciano Vernaschi <luciano@cromoteca.com> * use API in singular form * add description for Operation return type * Edited add text. * More edits. * Minor edit. * Apply suggestions from code review Co-authored-by: Luciano Vernaschi <luciano@cromoteca.com> --------- Co-authored-by: Luciano Vernaschi <luciano@cromoteca.com> Co-authored-by: Russell J.T. Dyer <6652767+russelljtdyer@users.noreply.github.com> Co-authored-by: Russell JT Dyer <russelljtdyer@users.noreply.github.com>
1 parent e2f2084 commit 4878fb4

File tree

1 file changed

+219
-10
lines changed

1 file changed

+219
-10
lines changed

articles/hilla/guides/full-stack-signals.adoc

Lines changed: 219 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ order: 130
77

88
= [since:com.vaadin:vaadin@V24.5]#Full Stack Signals#
99

10-
When building a modern web application, you may often need to synchronize the state between the server and clients. You might want to notify all connected clients in a chat application when a new message is posted, or maybe visualize multiple users interacting while editing the same record in a form, or perhaps update clients when the status of an order changes in an e-commerce application. This becomes even trickier when you need these updates to be propagated to clients in real time. Fortunately, this is when full-stack signals can help.
10+
When building a modern web application, you may often need to synchronize the state between the server and clients. You might want to notify all connected clients in a chat application when a new message is posted, or maybe visualize multiple users interacting while editing the same record in a form, or perhaps update clients when the status of an order changes in an e-commerce application. It becomes trickier when you need these updates to be propagated to clients in real time. Fortunately, full-stack signals can help.
1111

1212
A full-stack signal can be seen as a special data type that holds the shared state. It enables clients to subscribe to it for receiving real-time updates when the state changes. The state can be updated by any client, and the changes are automatically propagated to all other clients which are subscribed to the signal.
1313

@@ -53,6 +53,7 @@ public class VoteService {
5353
<1> Create an instance of a [classname]`ValueSignal` with an initial value of false.
5454
<2> Return the instance of the [classname]`ValueSignal` from the [methodname]`startedSignal` method.
5555

56+
5657
[CAUTION]
5758
The server-side instance of a full-stack signal is meant to be created once and shared across all clients. Therefore, create the instance as a field in a service class, and return it from the methods. This way, all clients are able to subscribe to the same signal instance and receive real-time updates when the state changes.
5859

@@ -97,17 +98,19 @@ export default function VoteView() {
9798

9899

99100
[[available-full-stack-signal-types]]
100-
== Available Full-stack Signal Types
101+
== Available Full-Stack Signal Types
101102

102103
The full-stack signals are designed to be used in various scenarios. Based on the requirements, different types of full-stack signals are used. The server-side signal types are available in the `com.vaadin.hilla.signals` package. Their client-side counterparts are available in `@vaadin/hilla-react-signals`.
103104

104-
As this is currently under active development, more signal types are added with each new release. Currently available ones are: `ValueSignal` and `NumberSignal`. These are described in the following sections.
105+
As this is currently under active development, more signal types are added with each new release. The currently available ones are [classname]`ValueSignal`, [classname]`NumberSignal`, and [classname]`ListSignal`. These are described in the following sub-sections.
105106

106107

107108
[[value-signal]]
108109
=== ValueSignal
109110

110-
The `ValueSignal<T>` is a full-stack signal that holds a single value of an arbitrary type. The type should necessarily be a JSON-serializable one that is supported by the Hilla framework. The following example demonstrates how to create and use a [classname]`ValueSignal` in a server-side service:
111+
The `ValueSignal<T>` is a full-stack signal that holds a single value of an arbitrary type. It has to be a JSON-serializable type that's supported by the Hilla framework.
112+
113+
The following example demonstrates how to create and use a [classname]`ValueSignal` in a server-side service:
111114

112115
[source,java]
113116
.`SomeService.java`
@@ -179,7 +182,7 @@ public class PersonService {
179182
<3> The signal instance that holds the shared state of the person.
180183
<4> The service method that returns the signal instance. The [classname]`@Nonnull` annotations are used to indicate that both the returned signal and its value may never be null. However, if the signal instance or its value might be null, you can remove the `@Nonnull` annotations.
181184

182-
Although the above example shows the usage of a record, you can also use classes with mutable properties. There aren't any technical limitations on this, as the wrapped value of the signal is always replaced with a new instance whenever an update is applied to the signals. However, the usage of immutable types is always preferred when dealing with share values. It helps to reduce the confusion and potential bugs that might arise from the shared mutable state.
185+
Although the above example shows the usage of a record, you can also use classes with mutable properties. There aren't any technical limitations on this, as the wrapped value of the signal is always replaced with a new instance whenever an update is applied to the signals. However, the usage of immutable types is always preferred when dealing with share values. It helps to reduce confusion and potential bugs that might arise from the shared mutable state.
183186

184187
Having a [classname]`@BrowserCallable`-annotated service with a method that returns a [classname]`ValueSignal` instance similar to the above example, enables the client-side code to subscribe to it by calling the service method:
185188

@@ -226,7 +229,7 @@ All signals have a `value` property that can be used to both set and read the va
226229

227230
A call to `cancel()` is not guaranteed always to be effective, as a succeeding operation might already be on its way to the server.
228231

229-
Operations such as `replace` and `update` are performing a "compare and set" on the server using the [methodname]`equals` method of the value type to compare the values. Thus, it's important to make sure the value type has a proper implementation of the [methodname]`equals` method.
232+
Operations such as `replace` and `update` perform a "compare and set" on the server using the [methodname]`equals` method of the value type to compare the values. Thus, it's important to make sure the value type has a proper implementation of the [methodname]`equals` method.
230233

231234

232235
[[number-signal]]
@@ -286,11 +289,215 @@ export default function() {
286289
<4> Decrease the value of the signal using the atomic [methodname]`incrementBy` operation and providing a negative value.
287290
<5> Reset the value of the signal to `0` by assigning a new value to it.
288291

289-
The `incrementBy` operation is _incrementally atomic_, meaning it guarantees success by reading the current value and applying the increment on the value, atomically. Each operation builds on the previously accepted one, ensuring that `n` increments or decrements are always applied correctly -- even if there are multiple clients trying to update the value, concurrently.
292+
The [methodname]`incrementBy` operation is _incrementally atomic_, meaning it guarantees success by reading the current value and applying the increment on the value, atomically. Each operation builds on the previously accepted one, ensuring that `n` increments or decrements are always applied correctly -- even if there are multiple clients trying to update the value, concurrently.
290293

291294
Since [classname]`NumberSignal` is a [classname]`ValueSignal` with the additional atomic operation of [methodname]`incrementBy`, it inherits all methods, such as [methodname]`replace` and [methodname]`update`, making those operations available when using a [classname]`NumberSignal`.
292295

293296

297+
[[list-signal]]
298+
[role="since:com.vaadin:vaadin@V24.6"]
299+
=== ListSignal
300+
301+
The [classname]`ListSignal<T>` is a full-stack signal that holds a list of values of an arbitrary type. It has to be a JSON-serializable type that's supported by the Hilla framework.
302+
303+
The following example demonstrates how to create and use a [classname]`ListSignal` in a server-side service:
304+
305+
[source,java]
306+
.`TodoService.java`
307+
----
308+
package com.example.application;
309+
310+
import com.vaadin.flow.server.auth.AnonymousAllowed;
311+
import com.vaadin.hilla.BrowserCallable;
312+
import com.vaadin.hilla.signals.ListSignal;
313+
314+
@AnonymousAllowed
315+
@BrowserCallable
316+
public class TodoService {
317+
record TodoItem(String text, boolean done) {}
318+
319+
private final ListSignal<TodoItem> todoItems =
320+
new ListSignal<>(TodoItem.class); // <1>
321+
322+
@Nonnull
323+
public ListSignal<@Nonnull TodoItem> todoItems() { // <2>
324+
return todoItems;
325+
}
326+
}
327+
----
328+
<1> Create an instance of a [classname]`ListSignal`. The initial state of a [classname]`ListSignal` is an empty list.
329+
<2> Return the instance of the [classname]`ListSignal` from the [methodname]`todoItems` method.
330+
331+
On the client-side code, subscribing to the shared list signal instance is done in a similar way as with the [classname]`ValueSignal`.
332+
333+
The following example demonstrates how to create a to-do list view that enables concurrent users to add tasks to a shared list:
334+
335+
[source,tsx]
336+
.todo.tsx
337+
----
338+
import { TodoService } from "Frontend/generated/endpoints.js";
339+
import {
340+
Button,
341+
TextField,
342+
HorizontalLayout,
343+
VerticalLayout
344+
} from "@vaadin/react-components";
345+
import { effect, useSignal } from "@vaadin/hilla-react-signals";
346+
347+
const todoItems = TodoService.todoItems(); // <1>
348+
349+
export default function TodoView(){
350+
const newTodoValue = useSignal<string>('');
351+
return (
352+
<>
353+
<VerticalLayout theme="padding">
354+
<span style={{padding: '10px'}}><h2>Tasks</h2></span>
355+
{todoItems.value.length === 0 // <2>
356+
? <span style={{padding: '10px'}}>No tasks yet...</span>
357+
: todoItems.value.map((item, index) => // <3>
358+
<li key={index}>{item.value.text}</li>
359+
)
360+
}
361+
<HorizontalLayout theme='padding spacing'>
362+
<TextField placeholder="What's on your mind?"
363+
value={newTodoValue.value}
364+
onValueChanged={(e) => newTodoValue.value = e.detail.value}/>
365+
<Button onClick={() => {
366+
todoItems.insertLast({text: newTodoValue.value, done: false}); // <4>
367+
newTodoValue.value = '';
368+
}}>Add task</Button>
369+
</HorizontalLayout>
370+
</VerticalLayout>
371+
</>
372+
);
373+
}
374+
----
375+
376+
<1> Subscribe to the `todoItems` list signal.
377+
<2> The `value` property of the [classname]`ListSignal` holds the list of tasks. The length of the list is checked to display a message when there are no tasks.
378+
<3> The `map` function is used to render the list of tasks.
379+
<4> Add a new task to the list by calling the [methodname]`insertLast` method of the [classname]`ListSignal`.
380+
381+
Since the `todoItems` signal holds the shared list of tasks, any subscribed client to this signal receives real-time updates when the list changes. As a result, when a client adds a new task to the list, all other clients receive the update and the list is re-rendered to reflect the changes. The above example, however, doesn't demonstrate how to remove or update tasks in the list. This is covered in the next section.
382+
383+
384+
[[list-signal-api]]
385+
==== ListSignal API
386+
387+
The client-side API of the [classname]`ListSignal` provides methods to insert and remove items. The [classname]`ListSignal` is a sequence of [classname]`ValueSignal` entries. Therefore, its API is about how the entries are added to the list or removed, and how the concurrent operations regarding the structure of the entries is handled.
388+
389+
As this is currently under active development, more methods and functionalities are added with each new release. The currently available ones are [methodname]`inserLast` and [methodname]`remove`. These are described below:
390+
391+
`insertLast(value: T): Operation`:: Inserts a new value at the end of the list. The returned `Operation` object can be used to chain further operations via the `result` property, which is a `Promise`. The chained operations are resolved after the current operation is completed and confirmed by the server.
392+
`remove(item: ValueSignal<T>): Operation`:: Removes the given item from the list. The returned `Operation` object can be used to chain further operations via the `result` property, which is a `Promise`. The chained operations are resolved after the current operation is completed and confirmed by the server.
393+
394+
The following example demonstrates how to create a to-do list view that enables concurrent users to add, remove, and update tasks in a shared list, with no changes needed on the server-side:
395+
396+
[source,tsx]
397+
.todo.tsx
398+
----
399+
import { TodoService } from "Frontend/generated/endpoints.js";
400+
import {
401+
Button,
402+
Checkbox,
403+
Icon,
404+
TextField,
405+
TextArea,
406+
HorizontalLayout,
407+
VerticalLayout
408+
} from "@vaadin/react-components";
409+
import { effect, useSignal, type ValueSignal} from "@vaadin/hilla-react-signals";
410+
411+
const todoItems = TodoService.todoItems();
412+
413+
function TodoComponent({todoItem, onRemove}: {
414+
todoItem: ValueSignal<{text: string, done: boolean}>,
415+
onRemove: (signal: ValueSignal<{text: string, done: boolean}>) => void,
416+
}) {
417+
const editing = useSignal(false);
418+
const todoText = useSignal('');
419+
return (
420+
<HorizontalLayout theme='spacing'
421+
style={{ alignItems: 'BASELINE', paddingLeft: '10px' }} >
422+
{editing.value
423+
? <TextArea value={todoText.value}
424+
onValueChanged={(e) => todoText.value = e.detail.value}/>
425+
: <Checkbox label={todoItem.value.text}
426+
checked={todoItem.value.done}
427+
onCheckedChanged={(e) => {
428+
todoItem.value = {
429+
text: todoItem.value.text,
430+
done: e.detail.value
431+
};
432+
}}/>
433+
}
434+
<Button theme="icon"
435+
hidden={editing.value}
436+
onClick={() => {
437+
editing.value = true;
438+
todoText.value = todoItem.value.text;
439+
}}>
440+
<Icon icon="vaadin:pencil" />
441+
</Button>
442+
<Button theme="icon error"
443+
hidden={editing.value}
444+
onClick={() => onRemove(todoItem)}>
445+
<Icon icon="vaadin:trash" />
446+
</Button>
447+
<Button theme="icon"
448+
hidden={!editing.value}
449+
onClick={() => {
450+
todoItem.value = {
451+
text: todoText.value,
452+
done: todoItem.value.done
453+
};
454+
editing.value = false;
455+
}}>
456+
<Icon icon="vaadin:check" />
457+
</Button>
458+
<Button theme="icon error"
459+
hidden={!editing.value}
460+
onClick={() => {
461+
todoText.value = '';
462+
editing.value = false;
463+
}}>
464+
<Icon icon="vaadin:close-small" />
465+
</Button>
466+
</HorizontalLayout>
467+
);
468+
}
469+
470+
export default function TodoView(){
471+
const newTodoValue = useSignal<string>('');
472+
return (
473+
<>
474+
<VerticalLayout theme="padding">
475+
<span style={{padding: '10px'}}><h2>Tasks</h2></span>
476+
{todoItems.value.length === 0
477+
? <span style={{padding: '10px'}}>No tasks yet...</span>
478+
: todoItems.value.map((item, index) =>
479+
<TodoComponent todoItem={item}
480+
key={index}
481+
onRemove={() => todoItems.remove(item)}/>)
482+
}
483+
<HorizontalLayout theme='padding spacing'>
484+
<TextField placeholder="What's on your mind?"
485+
value={newTodoValue.value}
486+
onValueChanged={(e) => newTodoValue.value = e.detail.value}/>
487+
<Button onClick={() => {
488+
todoItems.insertLast({text: newTodoValue.value, done: false});
489+
newTodoValue.value = '';
490+
}}>Add task</Button>
491+
</HorizontalLayout>
492+
</VerticalLayout>
493+
</>
494+
);
495+
}
496+
----
497+
498+
As demonstrated in the above example, each entry in the [classname]`ListSignal<T>` is a [classname]`ValueSignal<T>` itself. Each value can be updated individually using the available API of the `ValueSignal`. The changes to each individual entry are automatically propagated to all other clients that are subscribed to each entry of the [classname]`ListSignal`. This enables the React rendering process to render only the updated entry, instead of re-rendering the whole list.
499+
500+
294501
[[method-parameters]]
295502
== Service Method Parameters
296503

@@ -340,7 +547,7 @@ public class VoteService {
340547
The above example demonstrates a simple voting service that returns different [classname]`NumberSignal` instances based on the name of the voting option. The client-side code can first ask for the available options, and then subscribe to each individual signal instance to send updates and to receive real-time updates when voting happens.
341548

342549
[IMPORTANT]
343-
It's vitally important to make sure that the behaviour of the service method returning a signal instance should be deterministic, meaning that the same input parameters should always produce the same output. This is necessary to ensure that the state is consistently shared across all of the clients.
550+
It's vitally important to make sure that the behaviour of the service method returning a signal instance is deterministic. The same input parameters should always produce the same output. This is necessary to ensure that the state is consistently shared across all of the clients.
344551

345552

346553
[[security]]
@@ -351,9 +558,11 @@ Security with full-Stack signals has a few nuances of which you should be aware.
351558

352559
=== Controlling Browser-Callable Service Access
353560

354-
Full-stack signals are exposed by the services that are annotated with [classname]`@BrowserCallable` -- or the synonym, [classname]`@Endpoint`. This means the services that expose the signals are secured by the same security rules as any other service using the [classname]`@AnonymousAllowed`, [classname]`@PermitAll`, [classname]`@RolesAllowed`, or [classname]`@DenyAll` on the class or the individual methods. For more information on how to secure the services, see the <<./security/intro#, security documentation>>.
561+
Full-stack signals are exposed by the services that are annotated with [classname]`@BrowserCallable` -- or the synonym, [classname]`@Endpoint`. This means the services that expose the signals are secured by the same security rules as any other service using the [classname]`@AnonymousAllowed`, [classname]`@PermitAll`, [classname]`@RolesAllowed`, or [classname]`@DenyAll` on the class or the individual methods.
562+
563+
For more information on how to secure the services, see the <<./security/intro#, security documentation>>.
355564

356565

357566
=== Fine-Grained Signal Access Control
358567

359-
Browser-callable access control can be considered as basic security for signals, since it only allows limited control over the access to the signals. However, there are situations that require finer control over the signals. For example, you might want to allow anyone to subscribe to a signal, but only certain logged-in users with a specific role that allows them to update the value of that signal. This level of control is not yet implemented, but it's expected to be added in future releases.
568+
Browser-callable access control can be considered as basic security for signals, since it only allows limited control over the access to the signals. However, there are situations that require finer control over the signals. For example, you might want to allow anyone to subscribe to a signal, but only certain logged-in users with a specific role to update the value of that signal. This level of control is not yet implemented, but it's expected to be added in future releases.

0 commit comments

Comments
 (0)