diff --git a/Documentation/extensions/contract.md b/Documentation/extensions/contract.md index 2fbe28c9..52174412 100644 --- a/Documentation/extensions/contract.md +++ b/Documentation/extensions/contract.md @@ -136,3 +136,9 @@ The subject supplies the definition, the properties it mounts with, different pr - [Screenplay screens](https://github.com/Cratis/Screenplay/blob/main/Documentation/screenplay/screens.md): placing a component on a screen. - [Stage custom packages](https://github.com/Cratis/Stage/blob/main/Documentation/reference/custom-packages.md): how Stage loads packages at runtime. Hosting extension components is tracked in [Cratis/Stage#1070](https://github.com/Cratis/Stage/issues/1070) and the language support in [Cratis/Screenplay#1255](https://github.com/Cratis/Screenplay/issues/1255). + +## Binding a raised signal to a screen interaction + +A screen handles a signal with `on `. Scene models that binding as a `SignalInteractionTrigger`, which carries only the `signalName` as the component's contract declares it (`@cratis/scene.model`, `Cratis.Scene.Model.Interactions`). It serializes as `{ "signalName": "succeeded" }` in an interaction binding's `trigger`, next to `kind`, `eventName`, `seconds` and `triggerName` triggers; the other trigger kinds are unchanged. + +When a component raises a signal, the host collects the behaviors attached to that component and resolves them with `resolveSignalBehaviors(attachments, signalName)` from `@cratis/scene.engine`. The result is in the same deterministic order as other triggers. A signal trigger never matches an application trigger of the same name, and the reverse. diff --git a/Documentation/glossary.md b/Documentation/glossary.md index c11eb0ac..cc2452bc 100644 --- a/Documentation/glossary.md +++ b/Documentation/glossary.md @@ -36,6 +36,7 @@ is what makes an interaction that names nothing real a diagnostic instead of a d | --- | --- | | **Interaction trigger** | What starts an interaction — a click, a submit, a screen becoming live, an observed domain event, an elapsed interval, or a declared application trigger firing. Anonymous: it exists only inside a behavior. | | **Application trigger** | Screenplay's declared top-level signal consumed by backend reactions. A different concept with a similar name — an interaction may `raise` one, which is how the two connect. | +| **Signal trigger** | An interaction trigger started by an extension component raising a signal its contract declares (`on `). It is named by the signal alone, belongs to the component the behavior is attached to, and is resolved with `resolveSignalBehaviors`. Not an application trigger, even when the names match. | | **Action** | One declarative effect: execute a command, navigate, open or close a dialog, refresh a query, set state, notify, confirm, raise. | | **Continuation** | The `on success` / `on failure` / `on result` actions that run after an action that can fail. | | **Behavior** | A bundle of interaction-trigger-to-action bindings, attachable to an element, form, screen, template, layout, module or feature. Anonymous when written inline; named when reusable. Attachments are additive — a template's behavior and an element's both run. | diff --git a/Source/DotNET/Model/Interactions/InteractionTrigger.cs b/Source/DotNET/Model/Interactions/InteractionTrigger.cs index 2bab652b..d70a096c 100644 --- a/Source/DotNET/Model/Interactions/InteractionTrigger.cs +++ b/Source/DotNET/Model/Interactions/InteractionTrigger.cs @@ -18,4 +18,5 @@ namespace Cratis.Scene.Model.Interactions; [JsonDerivedType(typeof(EventInteractionTrigger))] [JsonDerivedType(typeof(IntervalInteractionTrigger))] [JsonDerivedType(typeof(ApplicationTriggerInteractionTrigger))] +[JsonDerivedType(typeof(SignalInteractionTrigger))] public abstract record InteractionTrigger; diff --git a/Source/DotNET/Model/Interactions/SignalInteractionTrigger.cs b/Source/DotNET/Model/Interactions/SignalInteractionTrigger.cs new file mode 100644 index 00000000..e07f96ca --- /dev/null +++ b/Source/DotNET/Model/Interactions/SignalInteractionTrigger.cs @@ -0,0 +1,10 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Scene.Model.Interactions; + +/// +/// Represents an interaction started by an extension component raising a signal its contract declares. +/// +/// The signal the component raised, as its contract names it. +public record SignalInteractionTrigger(string SignalName) : InteractionTrigger; diff --git a/Source/JavaScript/engine/for_resolveBehaviors/when_resolving_additive_attachments.ts b/Source/JavaScript/engine/for_resolveBehaviors/when_resolving_additive_attachments.ts index 8a6ce36e..e5dd2315 100644 --- a/Source/JavaScript/engine/for_resolveBehaviors/when_resolving_additive_attachments.ts +++ b/Source/JavaScript/engine/for_resolveBehaviors/when_resolving_additive_attachments.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { Behavior, InteractionActionKind, InteractionTriggerKind } from '@cratis/scene.model'; -import { BehaviorAttachment, resolveApplicationTriggerBehaviors, resolveBehaviors } from '../resolveBehaviors'; +import { BehaviorAttachment, resolveApplicationTriggerBehaviors, resolveBehaviors, resolveSignalBehaviors } from '../resolveBehaviors'; const behavior = (name: string | undefined, kind: InteractionTriggerKind, order?: number): Behavior => ({ name, @@ -94,6 +94,26 @@ describe('when resolving additive attachments', () => { resolved.should.have.lengthOf(1); }); + it('should resolve a raised signal separately from an application trigger of the same name', () => { + const attachments: BehaviorAttachment[] = [ + { + level: 'component', + behaviors: [ + { + bindings: [ + { trigger: { signalName: 'succeeded' } as never, actions: [] }, + { trigger: { signalName: 'failed' } as never, actions: [] }, + { trigger: { triggerName: 'succeeded' } as never, actions: [] }, + ], + }, + ], + }, + ]; + + resolveSignalBehaviors(attachments, 'succeeded').should.have.lengthOf(1); + resolveApplicationTriggerBehaviors(attachments, 'succeeded').should.have.lengthOf(1); + }); + it('should tolerate an attachment with no behaviors at all', () => { resolveBehaviors([{ level: 'element', behaviors: [] }], InteractionTriggerKind.Click).should.be.empty; }); diff --git a/Source/JavaScript/engine/resolveBehaviors.ts b/Source/JavaScript/engine/resolveBehaviors.ts index b6e40509..87424e0a 100644 --- a/Source/JavaScript/engine/resolveBehaviors.ts +++ b/Source/JavaScript/engine/resolveBehaviors.ts @@ -100,6 +100,20 @@ export function resolveApplicationTriggerBehaviors(attachments: BehaviorAttachme return collect(attachments, trigger => (trigger as { triggerName?: string }).triggerName === triggerName); } +/** + * Collects the bindings that respond to an extension component raising a signal (`on `). + * + * Pass the attachments of the component that raised it: a signal belongs to its component, so the name is not + * qualified and an application trigger of the same name is never matched. + * + * @param attachments The attachment levels, outermost first. + * @param signalName The signal the component raised, as its contract names it. + * @returns The bindings to run, in order. + */ +export function resolveSignalBehaviors(attachments: BehaviorAttachment[], signalName: string): ResolvedBinding[] { + return collect(attachments, trigger => (trigger as { signalName?: string }).signalName === signalName); +} + /** * Collects the bindings that respond to a modeled domain event being observed. * diff --git a/Source/JavaScript/model/interactions/SignalInteractionTrigger.ts b/Source/JavaScript/model/interactions/SignalInteractionTrigger.ts new file mode 100644 index 00000000..df65b5c5 --- /dev/null +++ b/Source/JavaScript/model/interactions/SignalInteractionTrigger.ts @@ -0,0 +1,16 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { InteractionTrigger } from './InteractionTrigger'; + +/** + * An interaction started by an extension component raising a signal its contract declares (`on `). + * + * The signal belongs to the component the behavior is attached to, so the name is not qualified. It is a different + * concept from an application trigger, which a declared `trigger ` names and a backend reaction can consume. + */ +export interface SignalInteractionTrigger extends InteractionTrigger { + signalName: string; +} + +export const SignalInteractionTriggerPropertyNames: (keyof SignalInteractionTrigger)[] = ['signalName']; diff --git a/Source/JavaScript/model/interactions/index.ts b/Source/JavaScript/model/interactions/index.ts index f9fb0504..3a7a5f64 100644 --- a/Source/JavaScript/model/interactions/index.ts +++ b/Source/JavaScript/model/interactions/index.ts @@ -8,6 +8,7 @@ export * from './BuiltInInteractionTrigger'; export * from './EventInteractionTrigger'; export * from './IntervalInteractionTrigger'; export * from './ApplicationTriggerInteractionTrigger'; +export * from './SignalInteractionTrigger'; export * from './InteractionMessage'; export * from './InteractionArgument'; export * from './InteractionActionKind'; diff --git a/scene-model-shape.json b/scene-model-shape.json index 8b0b1948..2f5b828a 100644 --- a/scene-model-shape.json +++ b/scene-model-shape.json @@ -409,6 +409,9 @@ "ApplicationTriggerInteractionTrigger": [ "triggerName" ], + "SignalInteractionTrigger": [ + "signalName" + ], "InteractionAction": [ "kind", "arguments",