Skip to content

Commit 445054f

Browse files
authored
Merge pull request #25 from hydrostack/16-support-subjects-in-the-events
Support event subjects
2 parents 5bd28ec + 2e0734d commit 445054f

File tree

8 files changed

+158
-32
lines changed

8 files changed

+158
-32
lines changed

docs/content/features/events.md

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ public void Add()
145145
`Add` triggers the event asynchronously, so the button that triggers this action will be disabled until the action is done. The event execution won't be connected with the action's pipeline and will be run on its own.
146146

147147

148-
## Events scope
148+
## Event scope
149149

150150
By default, the events are dispatched only to their parent component. To publish a global event use the following
151151
method:
@@ -154,6 +154,12 @@ method:
154154
Dispatch(new ShowMessage(Content), Scope.Global);
155155
```
156156

157+
or
158+
159+
```c#
160+
DispatchGlobal(new ShowMessage(Content));
161+
```
162+
157163
Any component that subscribes for `ShowMessage` will be notified, no matter the component's location.
158164

159165
## Inlined subscription
@@ -193,4 +199,63 @@ public class ProductList : HydroComponent
193199
// When ProductAddedEvent occurs, component will be rerendered
194200
}
195201
}
196-
```
202+
```
203+
204+
## Event subject
205+
206+
There might be a situation where you want to filter the events you receive in your subscription handler. It means that
207+
your component subscribes to an event, but handles it only when it contains a certain flag. That flag can be any string
208+
and is called a `subject`.
209+
210+
You can imagine a page with multiple lists of todos. Each list is a Hydro component that listens to events like `TodoAdded`,
211+
`TodoRemoved` or `TodoEdited`. When a todo is removed on one list, you don't want all the other lists to receive and react to that event, but only
212+
the list that contained that todo item. This is solved in Hydro by using `subject` parameter, which in this case will be the list's id.
213+
When `TodoAdded`, `TodoRemoved` or `TodoEdited` are dispatched, `subject` is set to their list's id. The list component subscribes to those
214+
events with `subject` set to the their list's id.
215+
216+
Example:
217+
218+
```c#
219+
// Todo.cshtml.cs
220+
221+
public class Todo : HydroComponent
222+
{
223+
public string TodoId { get; set; }
224+
public string ListId { get; set; }
225+
public string Text { get; set; }
226+
public bool IsDone { get; set; }
227+
228+
public void Remove(string id)
229+
{
230+
DispatchGlobal(new TodoRemoved { TodoId }, subject: ListId);
231+
}
232+
}
233+
```
234+
235+
```c#
236+
// TodoList.cshtml.cs
237+
238+
public class TodoList : HydroComponent
239+
{
240+
public string ListId { get; set; }
241+
public List<Todo> Todos { get; set; }
242+
243+
public TodoList()
244+
{
245+
Subscribe<TodoRemoved>(subject: () => ListId, Handle);
246+
}
247+
248+
public void Handle(TodoRemoved data)
249+
{
250+
// will be called only when subject is ListId
251+
Todos.RemoveAll(todo => todo.TodoId == data.TodoId);
252+
}
253+
}
254+
```
255+
256+
In `Subscribe` method call `subject` parameter is a `Func<string>` instead of `string`,
257+
because its value could be taken from component's properties that are not set yet, since it's
258+
a constructor.
259+
260+
> NOTE: If you subscribe for an event without specifying the subject, it will catch all the events
261+
of that type, no matter if they were dispatched with subject or not.

src/HydroComponent.cs

Lines changed: 77 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,11 @@ public async Task<IHtmlContent> InvokeAsync(object parameters = null, string key
143143
/// Subscribes to a Hydro event
144144
/// </summary>
145145
/// <typeparam name="TEvent">Event type</typeparam>
146-
public void Subscribe<TEvent>() =>
146+
public void Subscribe<TEvent>(Func<string> subject = null) =>
147147
_subscriptions.Add(new HydroEventSubscription
148148
{
149149
EventName = GetFullTypeName(typeof(TEvent)),
150+
SubjectRetriever = subject,
150151
Action = (TEvent _) => { }
151152
});
152153

@@ -162,6 +163,20 @@ public void Subscribe<TEvent>(Action<TEvent> action) =>
162163
Action = action
163164
});
164165

166+
/// <summary>
167+
/// Subscribes to a Hydro event
168+
/// </summary>
169+
/// <param name="subject">Subject</param>
170+
/// <param name="action">Action to execute when event occurs</param>
171+
/// <typeparam name="TEvent">Event type</typeparam>
172+
public void Subscribe<TEvent>(Func<string> subject, Action<TEvent> action) =>
173+
_subscriptions.Add(new HydroEventSubscription
174+
{
175+
EventName = GetFullTypeName(typeof(TEvent)),
176+
SubjectRetriever = subject,
177+
Action = action
178+
});
179+
165180
private static string GetFullTypeName(Type type) =>
166181
type.DeclaringType != null
167182
? type.DeclaringType.Name + "+" + type.Name
@@ -179,6 +194,20 @@ public void Subscribe<TEvent>(Func<TEvent, Task> action) =>
179194
Action = action
180195
});
181196

197+
/// <summary>
198+
/// Subscribes to a Hydro event
199+
/// </summary>
200+
/// <param name="subject">Subject</param>
201+
/// <param name="action">Action to execute when event occurs</param>
202+
/// <typeparam name="TEvent">Event type</typeparam>
203+
public void Subscribe<TEvent>(Func<string> subject, Func<TEvent, Task> action) =>
204+
_subscriptions.Add(new HydroEventSubscription
205+
{
206+
EventName = GetFullTypeName(typeof(TEvent)),
207+
Action = action,
208+
SubjectRetriever = subject
209+
});
210+
182211
/// <summary>
183212
/// Triggers a Hydro event
184213
/// </summary>
@@ -195,9 +224,10 @@ public void Dispatch<TEvent>(TEvent data, Scope scope = Scope.Parent, bool async
195224
/// <param name="name">Name of the event</param>
196225
/// <param name="data">Data to pass</param>
197226
/// <param name="scope">Scope of the event</param>
227+
/// <param name="subject">Subject</param>
198228
/// <param name="asynchronous">Do not chain the execution of handlers and run them separately</param>
199229
/// <typeparam name="TEvent">Event type</typeparam>
200-
public void Dispatch<TEvent>(string name, TEvent data, Scope scope = Scope.Parent, bool asynchronous = false)
230+
public void Dispatch<TEvent>(string name, TEvent data, Scope scope = Scope.Parent, bool asynchronous = false, string subject = null)
201231
{
202232
var operationId = !asynchronous && HttpContext.Request.Headers.TryGetValue(HydroConsts.RequestHeaders.OperationId, out var incomingOperationId)
203233
? incomingOperationId.First()
@@ -208,10 +238,21 @@ public void Dispatch<TEvent>(string name, TEvent data, Scope scope = Scope.Paren
208238
Name = name,
209239
Data = data,
210240
Scope = scope.ToString().ToLower(),
241+
Subject = subject,
211242
OperationId = operationId
212243
});
213244
}
214245

246+
/// <summary>
247+
/// Triggers a Hydro event in a global scope
248+
/// </summary>
249+
/// <param name="data">Data to pass</param>
250+
/// <param name="subject">Subject</param>
251+
/// <param name="asynchronous">Do not chain the execution of handlers and run them separately</param>
252+
/// <typeparam name="TEvent">Event type</typeparam>
253+
public void DispatchGlobal<TEvent>(TEvent data, string subject = null, bool asynchronous = false) =>
254+
Dispatch(GetFullTypeName(typeof(TEvent)), data, Scope.Global, asynchronous, subject);
255+
215256
/// <summary>
216257
/// Provides actions that can be executed on client side
217258
/// </summary>
@@ -232,7 +273,7 @@ public virtual Task MountAsync()
232273
public virtual void Mount()
233274
{
234275
}
235-
276+
236277
/// <summary>
237278
/// Triggered before each render
238279
/// </summary>
@@ -538,6 +579,7 @@ private HtmlNode GetEventSubscriptionScript(HtmlDocument document, HydroEventSub
538579
var eventData = new
539580
{
540581
name = subscription.EventName,
582+
subject = subscription.SubjectRetriever?.Invoke(),
541583
path = $"/hydro/{GetType().Name}/event".ToLower()
542584
};
543585

@@ -566,7 +608,14 @@ private void PopulateDispatchers()
566608
}
567609

568610
var data = _dispatchEvents
569-
.Select(e => new { name = e.Name, data = Base64.Serialize(e.Data), scope = e.Scope, operationId = e.OperationId })
611+
.Select(e => new
612+
{
613+
name = e.Name,
614+
data = Base64.Serialize(e.Data),
615+
scope = e.Scope,
616+
subject = e.Subject,
617+
operationId = e.OperationId
618+
})
570619
.ToList();
571620

572621
HttpContext.Response.Headers.TryAdd(HydroConsts.ResponseHeaders.Trigger, JsonConvert.SerializeObject(data));
@@ -676,35 +725,36 @@ private async Task TriggerEvent()
676725
return;
677726
}
678727

679-
var subscription = _subscriptions.FirstOrDefault(s => s.EventName == eventNameValue);
728+
var eventSubject = (string)HttpContext.Items[HydroConsts.ContextItems.EventSubject];
729+
var subscriptions = _subscriptions
730+
.Where(s => s.EventName == eventNameValue && (s.SubjectRetriever == null || s.SubjectRetriever() == eventSubject))
731+
.ToList();
680732

681-
if (subscription == null)
733+
foreach (var subscription in subscriptions)
682734
{
683-
return;
684-
}
735+
var methodInfo = subscription.Action.Method;
736+
var parameters = methodInfo.GetParameters();
737+
var parameterType = parameters.First().ParameterType;
738+
var model = Base64.Deserialize((string)HttpContext.Items[HydroConsts.ContextItems.EventData], parameterType);
685739

686-
var methodInfo = subscription.Action.Method;
687-
var parameters = methodInfo.GetParameters();
688-
var parameterType = parameters.First().ParameterType;
689-
var model = Base64.Deserialize((string)HttpContext.Items[HydroConsts.ContextItems.EventData], parameterType);
740+
var operationId = HttpContext.Request.Headers.TryGetValue(HydroConsts.RequestHeaders.OperationId, out var incomingOperationId)
741+
? incomingOperationId.First()
742+
: Guid.NewGuid().ToString("N");
690743

691-
var operationId = HttpContext.Request.Headers.TryGetValue(HydroConsts.RequestHeaders.OperationId, out var incomingOperationId)
692-
? incomingOperationId.First()
693-
: Guid.NewGuid().ToString("N");
744+
HttpContext.Response.Headers.TryAdd(HydroConsts.ResponseHeaders.OperationId, operationId);
694745

695-
HttpContext.Response.Headers.TryAdd(HydroConsts.ResponseHeaders.OperationId, operationId);
696-
697-
var isAsync = typeof(Task).IsAssignableFrom(methodInfo.ReturnType);
746+
var isAsync = typeof(Task).IsAssignableFrom(methodInfo.ReturnType);
698747

699-
if (isAsync)
700-
{
701-
var method = InvokeActionAsyncMethod.MakeGenericMethod(parameterType);
702-
await (Task)method.Invoke(null, new[] { subscription.Action, model })!;
703-
}
704-
else
705-
{
706-
var method = InvokeActionMethod.MakeGenericMethod(parameterType);
707-
method.Invoke(null, new[] { subscription.Action, model });
748+
if (isAsync)
749+
{
750+
var method = InvokeActionAsyncMethod.MakeGenericMethod(parameterType);
751+
await (Task)method.Invoke(null, new[] { subscription.Action, model })!;
752+
}
753+
else
754+
{
755+
var method = InvokeActionMethod.MakeGenericMethod(parameterType);
756+
method.Invoke(null, new[] { subscription.Action, model });
757+
}
708758
}
709759
}
710760

src/HydroComponentEvent.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
internal class HydroComponentEvent
44
{
55
public string Name { get; init; }
6+
public string Subject { get; init; }
67
public object Data { get; init; }
78
public string Scope { get; set; }
89
public string OperationId { get; set; }

src/HydroComponentsExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ private static async Task ExecuteRequestOperations(HttpContext context, string m
9898
{
9999
context.Items.Add(HydroConsts.ContextItems.EventName, eventData.Name);
100100
context.Items.Add(HydroConsts.ContextItems.EventData, eventData.Data);
101+
context.Items.Add(HydroConsts.ContextItems.EventSubject, eventData.Subject);
101102
}
102103

103104
if (!string.IsNullOrWhiteSpace(method) && type != "event")

src/HydroConsts.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public static class ContextItems
2727
public const string RequestForm = "hydro-request-form";
2828
public const string Parameters = "hydro-parameters";
2929
public const string EventData = "hydro-event-model";
30+
public const string EventSubject = "hydro-event-subject";
3031
public const string IsRootRendered = "hydro-root-rendered";
3132
}
3233

src/HydroEventPayload.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ namespace Hydro;
33
internal class HydroEventPayload
44
{
55
public string Name { get; set; }
6+
public string Subject { get; set; }
67
public object Data { get; set; }
78
public Scope Scope { get; set; }
89
public string OperationId { get; set; }

src/HydroEventSubscription.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
internal class HydroEventSubscription
44
{
55
public string EventName { get; set; }
6+
public Func<string> SubjectRetriever { get; set; }
67
public Delegate Action { get; set; }
78
}

src/Scripts/hydro.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
const operationId = eventData.operationId;
111111
const hydroEvent = el.getAttribute("x-on-hydro-event");
112112
const wireEventData = JSON.parse(hydroEvent);
113-
await hydroRequest(el, url, { eventData: { name: wireEventData.name, data: eventData.data } }, 'event', wireEventData, operationId);
113+
await hydroRequest(el, url, { eventData: { name: wireEventData.name, data: eventData.data, subject: eventData.subject } }, 'event', wireEventData, operationId);
114114
}
115115

116116
async function hydroBind(el) {
@@ -393,11 +393,17 @@
393393
const eventData = {
394394
detail: {
395395
data: trigger.data,
396+
subject: trigger.subject,
396397
operationId: trigger.operationId
397398
}
398399
};
399400

400401
document.dispatchEvent(new CustomEvent(eventName, eventData));
402+
403+
if (trigger.subject) {
404+
const subjectEventName = `${eventScope}:${trigger.name}:${trigger.subject}`;
405+
document.dispatchEvent(new CustomEvent(subjectEventName, eventData));
406+
}
401407
});
402408
}
403409
});
@@ -598,8 +604,8 @@ document.addEventListener('alpine:init', () => {
598604
}
599605

600606
const data = JSON.parse(expression);
601-
const globalEventName = `global:${data.name}`;
602-
const localEventName = `${component.id}:${data.name}`;
607+
const globalEventName = data.subject ? `global:${data.name}:${data.subject}` : `global:${data.name}`;
608+
const localEventName = data.subject ? `${component.id}:${data.name}:${data.subject}` : `${component.id}:${data.name}`;
603609
const eventPath = data.path;
604610

605611
const eventHandler = async (event) => {

0 commit comments

Comments
 (0)