Skip to content

Commit

Permalink
Merge pull request #99 from hydrostack/use-dispatch-in-hydro-on-tag-h…
Browse files Browse the repository at this point in the history
…elper

Support dispatching events using hydro-on tag helper
  • Loading branch information
kjeske authored Oct 12, 2024
2 parents ab0f1b2 + f956de0 commit fb7bca7
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 29 deletions.
4 changes: 2 additions & 2 deletions docs/content/features/actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Examples:
<div hydro-on:click.outside="@(() => Model.Close())">
<input type="text" hydro-on:keyup.shift.enter="@(() => Model.Save())">
<input type="text" hydro-on:keyup.shift.enter="@(() => Model.Save())" />
```

## Parameters
Expand Down Expand Up @@ -116,7 +116,7 @@ public class Content : HydroComponent
@model Content
<div>
<input type="text" id="myInput">
<input type="text" id="myInput" />
<button hydro-on:click="@(() => Model.Update(Param.JS("window.myInput.value")))">
Update content
</button>
Expand Down
9 changes: 3 additions & 6 deletions docs/content/features/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,11 @@ public class ProductList : HydroComponent
}
```

In this case using a Hydro action might be an overkill, since it will cause an unnecessary additional request and rendering of the component.

To avoid that, you can dispatch actions straight from your client code by using a `hydro-dispatch` tag helper:
In this case using a Hydro action might be an overkill, since it will cause an unnecessary additional request and rendering of the component. To avoid that, you can dispatch actions straight from your client code by using a `hydro-dispatch` tag helper:

```razor
<button type="button"
hydro-dispatch="@(new OpenAddModal())"
event-scope="@Scope.Global">
<button
hydro-on:click="@(() => Model.Client.Dispatch(new OpenAddModal())">
Add
</button>
```
Expand Down
4 changes: 2 additions & 2 deletions docs/content/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ features:
@model NameForm
<div>
<input asp-for="Name" hydro-bind:keydown>
<input asp-for="Name" hydro-bind:keydown />
<div>Hello @Model.Name</div>
</div>
```
Expand All @@ -65,7 +65,7 @@ public class NameForm : HydroComponent
@model NameForm
<form hydro-on:submit="@(() => Model.Save())">
<input asp-for="Name" hydro-bind>
<input asp-for="Name" hydro-bind />
<span asp-validation-for="Name"></span>
<button type="submit">Save</button>
Expand Down
2 changes: 1 addition & 1 deletion src/Hydro.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Authors>Krzysztof Jeske</Authors>
<Build>$([System.DateTime]::op_Subtraction($([System.DateTime]::get_Now().get_Date()),$([System.DateTime]::new(2000,1,1))).get_TotalDays())</Build>
Expand Down
30 changes: 30 additions & 0 deletions src/HydroClientActions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,36 @@ internal HydroClientActions(HydroComponent hydroComponent) =>
public void ExecuteJs([LanguageInjection(InjectedLanguage.JAVASCRIPT)] string jsExpression) =>
_hydroComponent.AddClientScript(jsExpression);

/// <summary>
/// Dispatch a Hydro event
/// </summary>
public void Dispatch<TEvent>(TEvent data, Scope scope, bool asynchronous) =>
_hydroComponent.Dispatch(data, scope, asynchronous);

/// <summary>
/// Dispatch a Hydro event
/// </summary>
public void Dispatch<TEvent>(TEvent data, Scope scope) =>
_hydroComponent.Dispatch(data, scope);

/// <summary>
/// Dispatch a Hydro event
/// </summary>
public void Dispatch<TEvent>(TEvent data) =>
_hydroComponent.Dispatch(data);

/// <summary>
/// Dispatch a Hydro event
/// </summary>
public void DispatchGlobal<TEvent>(TEvent data) =>
_hydroComponent.DispatchGlobal(data);

/// <summary>
/// Dispatch a Hydro event
/// </summary>
public void DispatchGlobal<TEvent>(TEvent data, string subject) =>
_hydroComponent.DispatchGlobal(data, subject: subject);

/// <summary>
/// Invoke JavaScript expression on client side
/// </summary>
Expand Down
34 changes: 34 additions & 0 deletions src/Scripts/hydro.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,33 @@
await hydroRequest(el, url, { eventData: { name: wireEventData.name, data: eventData.data, subject: eventData.subject } }, 'event', wireEventData, operationId);
}

function hydroDispatch(el, event, operationId) {
const component = findComponent(el);
const parentComponent = findComponent(component.element.parentElement);

if (event.scope === 'parent' && !parentComponent) {
return;
}

const scope = event.scope === 'parent' ? parentComponent.id : 'global';

const eventName = `${scope}:${event.name}`;
const eventData = {
detail: {
data: event.data,
subject: event.subject,
operationId
}
};

document.dispatchEvent(new CustomEvent(eventName, eventData));

if (event.subject) {
const subjectEventName = `${scope}:${event.name}:${event.subject}`;
document.dispatchEvent(new CustomEvent(subjectEventName, eventData));
}
}

async function hydroBind(el, debounce) {
if (!isElementDirty(el)) {
return;
Expand Down Expand Up @@ -592,6 +619,7 @@
enableHydroScripts,
loadPageContent,
findComponent,
hydroDispatch,
generateGuid,
waitFor,
executeComponentJs,
Expand Down Expand Up @@ -792,6 +820,12 @@ document.addEventListener('alpine:init', () => {
}
await window.Hydro.hydroAction(this.$el, this.$component, action);
},
async dispatch(e, event) {
const el = this.$el;
const operationId = window.Hydro.generateGuid();
el.setAttribute("data-operation-id", operationId);
window.Hydro.hydroDispatch(el, event, operationId);
},
async bind(debounce) {
let element = this.$el;

Expand Down
1 change: 1 addition & 0 deletions src/TagHelpers/HydroDispatchTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace Hydro.TagHelpers;
/// Provides a binding from the DOM element to the Hydro action
/// </summary>
[HtmlTargetElement("*", Attributes = DispatchAttribute)]
[Obsolete("Use hydro-on instead")]
public sealed class HydroDispatchTagHelper : TagHelper
{
private const string DispatchAttribute = "hydro-dispatch";
Expand Down
86 changes: 68 additions & 18 deletions src/TagHelpers/HydroOnTagHelper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Linq.Expressions;
using Hydro.Utils;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
Expand Down Expand Up @@ -50,13 +51,13 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
return;
}

foreach (var eventItem in _handlers)
foreach (var eventItem in _handlers.Where(h => h.Value != null))
{
if (eventItem.Value is not LambdaExpression actionExpression)
{
throw new InvalidOperationException($"Wrong event handler statement in component for {modelType.Namespace}");
}

var jsExpression = GetJsExpression(actionExpression);

if (jsExpression == null)
Expand All @@ -77,37 +78,86 @@ public override void Process(TagHelperContext context, TagHelperOutput output)

private static string GetJsExpression(LambdaExpression expression)
{
var clientAction = GetJsClientActionExpression(expression);
if (expression is not { Body: MethodCallExpression methodCall })
{
throw new InvalidOperationException("Hydro action should contain a method call.");
}

if (clientAction != null)
var methodDeclaringType = methodCall.Method.DeclaringType;

if (methodDeclaringType == typeof(HydroClientActions))
{
return clientAction;
return GetClientActionExpression(expression);
}

return GetJsInvokeExpression(expression);
return GetActionInvokeExpression(expression);
}

private static string GetJsClientActionExpression(LambdaExpression expression)
private static string GetClientActionExpression(LambdaExpression expression)
{
if (expression is not { Body: MethodCallExpression methodCall }
|| methodCall.Method.DeclaringType != typeof(HydroClientActions))
{
return null;
}
var methodCall = (MethodCallExpression)expression.Body;

var methodName = methodCall.Method.Name;

switch (methodCall.Method.Name)
switch (methodName)
{
case nameof(HydroClientActions.ExecuteJs):
case nameof(HydroClientActions.Invoke):
var expressionValue = EvaluateExpressionValue(methodCall.Arguments[0]);
return ReplaceJsQuotes(expressionValue?.ToString());

var jsExpressionValue = EvaluateExpressionValue(methodCall.Arguments[0]);
return ReplaceJsQuotes(jsExpressionValue?.ToString());

case nameof(HydroComponent.Dispatch):
case nameof(HydroComponent.DispatchGlobal):
return GetDispatchInvokeExpression(expression, methodName);

default:
return null;
}
}

private static string GetJsInvokeExpression(LambdaExpression expression)
private static string GetDispatchInvokeExpression(LambdaExpression expression, string methodName)
{
var dispatchData = expression.GetNameAndParameters();

if (dispatchData == null)
{
return null;
}

var parameters = dispatchData.Value.Parameters;

var data = parameters["data"];
var scope = methodName == nameof(HydroComponent.DispatchGlobal)
? Scope.Global
: GetParam<Scope>(parameters, "scope");
var subject = GetParam<string>(parameters, "subject");

var invokeData = new
{
name = GetFullTypeName(data.GetType()),
data = Base64.Serialize(data),
scope = scope.ToString().ToLower(),
subject = subject
};

var invokeJson = JsonConvert.SerializeObject(invokeData, JsonSettings.SerializerSettings);
var invokeJsObject = DecodeJsExpressionsInJson(invokeJson);

return $"dispatch($event, {invokeJsObject})";
}

private static T GetParam<T>(IDictionary<string, object> parameters, string name, T fallback = default) =>
(T)(parameters.TryGetValue(name, out var value)
? value is T ? value : default(T)
: default(T)
);

private static string GetFullTypeName(Type type) =>
type.DeclaringType != null
? type.DeclaringType.Name + "+" + type.Name
: type.Name;

private static string GetActionInvokeExpression(LambdaExpression expression)
{
var eventData = expression.GetNameAndParameters();

Expand Down

0 comments on commit fb7bca7

Please sign in to comment.