Skip to content
Merged
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
6 changes: 6 additions & 0 deletions Documentation/extensions/contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <signal>`. 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.
1 change: 1 addition & 0 deletions Documentation/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <signal>`). 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. |
Expand Down
1 change: 1 addition & 0 deletions Source/DotNET/Model/Interactions/InteractionTrigger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
10 changes: 10 additions & 0 deletions Source/DotNET/Model/Interactions/SignalInteractionTrigger.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents an interaction started by an extension component raising a signal its contract declares.
/// </summary>
/// <param name="SignalName">The signal the component raised, as its contract names it.</param>
public record SignalInteractionTrigger(string SignalName) : InteractionTrigger;
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
});
Expand Down
14 changes: 14 additions & 0 deletions Source/JavaScript/engine/resolveBehaviors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <signal>`).
*
* 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.
*
Expand Down
16 changes: 16 additions & 0 deletions Source/JavaScript/model/interactions/SignalInteractionTrigger.ts
Original file line number Diff line number Diff line change
@@ -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 <signal>`).
*
* 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 <Name>` names and a backend reaction can consume.
*/
export interface SignalInteractionTrigger extends InteractionTrigger {
signalName: string;
}

export const SignalInteractionTriggerPropertyNames: (keyof SignalInteractionTrigger)[] = ['signalName'];
1 change: 1 addition & 0 deletions Source/JavaScript/model/interactions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 3 additions & 0 deletions scene-model-shape.json
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,9 @@
"ApplicationTriggerInteractionTrigger": [
"triggerName"
],
"SignalInteractionTrigger": [
"signalName"
],
"InteractionAction": [
"kind",
"arguments",
Expand Down
Loading