Skip to content

Commit 8e4b4fe

Browse files
authored
Merge pull request #1926 from riganti/postbackscriptoptions-default-values
Fix default values of PostbackScriptOptions
2 parents 90e087c + 0245d4f commit 8e4b4fe

File tree

5 files changed

+116
-26
lines changed

5 files changed

+116
-26
lines changed

src/Framework/Framework/Compilation/Javascript/ParametrizedCode.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ public void Add(CodeParameterInfo parameter)
303303

304304
public void Add(ParametrizedCode code, byte operatorPrecedence = 20)
305305
{
306+
ThrowHelpers.ArgumentNull(code);
306307
var needsParens = code.OperatorPrecedence.NeedsParens(operatorPrecedence);
307308
if (needsParens) Add("(");
308309
code.CopyTo(this);

src/Framework/Framework/Controls/KnockoutHelper.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public static void AddKnockoutForeachDataBind(this IHtmlWriter writer, string ex
116116
/// <summary> Generates a function expression that invokes the command with specified commandArguments. Creates code like `(...commandArguments) => dotvvm.postBack(...)` </summary>
117117
public static string GenerateClientPostbackLambda(string propertyName, ICommandBinding command, DotvvmBindableObject control, PostbackScriptOptions? options = null)
118118
{
119-
options ??= PostbackScriptOptions.KnockoutBinding;
119+
options = PostbackScriptOptions.KnockoutBinding.Override(options);
120120

121121
var addArguments = options.CommandArgs is null && (command is IStaticCommandBinding || command.CommandJavascript.EnumerateAllParameters().Any(p => p == CommandBindingExpression.CommandArgumentsParameter));
122122
if (addArguments)
@@ -183,7 +183,7 @@ string getContextPath(DotvvmBindableObject? current)
183183

184184
string getHandlerScript()
185185
{
186-
if (!options.AllowPostbackHandlers) return "[]";
186+
if (options.AllowPostbackHandlers == false) return "[]";
187187
// turn validation off for static commands
188188
var validationPathExpr = expression is IStaticCommandBinding ? null : GetValidationTargetExpression(control);
189189
return GetPostBackHandlersScript(control, propertyName,
@@ -194,8 +194,8 @@ string getHandlerScript()
194194
$"[\"validate\", {{fn:{validationPathExpr.Value.javascriptExpression}, path:{MakeStringLiteral(validationPathExpr.Value.identificationExpression)}}}]",
195195

196196
// use window.setTimeout
197-
options.UseWindowSetTimeout ? "\"timeout\"" : null,
198-
options.IsOnChange ? "\"suppressOnUpdating\"" : null,
197+
options.UseWindowSetTimeout == true ? "\"timeout\"" : null,
198+
options.IsOnChange == true ? "\"suppressOnUpdating\"" : null,
199199
GenerateConcurrencyModeHandler(propertyName, control)
200200
);
201201
}
@@ -212,13 +212,14 @@ string getHandlerScript()
212212
adjustedExpression = adjustedExpression.AssignParameters(options.ParameterAssignment);
213213
}
214214
// when the expression changes the dataContext, we need to override the default knockout context fo the command binding.
215+
var elementAccessor = options.ElementAccessor ?? CodeParameterAssignment.FromIdentifier("this");
215216
CodeParameterAssignment knockoutContext;
216217
CodeParameterAssignment viewModel = default;
217218
if (!isStaticCommand)
218219
{
219220
knockoutContext = options.KoContext ?? (
220221
// adjustedExpression != expression.CommandJavascript ?
221-
new CodeParameterAssignment(new ParametrizedCode.Builder { "ko.contextFor(", options.ElementAccessor.Code!, ")" }.Build(OperatorPrecedence.Max))
222+
new CodeParameterAssignment(new ParametrizedCode.Builder { "ko.contextFor(", elementAccessor.Code!, ")" }.Build(OperatorPrecedence.Max))
222223
);
223224
viewModel = JavascriptTranslator.KnockoutViewModelParameter.DefaultAssignment.Code;
224225
}
@@ -240,7 +241,7 @@ options.KoContext is object ?
240241
{
241242
var commandArgsString = (options.CommandArgs?.Code != null) ? SubstituteArguments(options.CommandArgs!.Value.Code!) : "[]";
242243
var args = new List<string> {
243-
SubstituteArguments(options.ElementAccessor.Code!),
244+
SubstituteArguments(elementAccessor.Code!),
244245
getHandlerScript(),
245246
commandArgsString,
246247
optionalKnockoutContext.Code?.Apply(SubstituteArguments) ?? "undefined",
@@ -270,7 +271,7 @@ options.KoContext is object ?
270271
string SubstituteArguments(ParametrizedCode parametrizedCode)
271272
{
272273
return parametrizedCode.ToString(p =>
273-
p == JavascriptTranslator.CurrentElementParameter ? options.ElementAccessor :
274+
p == JavascriptTranslator.CurrentElementParameter ? elementAccessor :
274275
p == CommandBindingExpression.CurrentPathParameter ? CodeParameterAssignment.FromIdentifier(getContextPath(control)) :
275276
p == CommandBindingExpression.ControlUniqueIdParameter ? uniqueControlId?.GetParametrizedJsExpression(control) ?? CodeParameterAssignment.FromLiteral("") :
276277
p == JavascriptTranslator.KnockoutContextParameter ? knockoutContext :

src/Framework/Framework/Controls/PostbackScriptOptions.cs

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,47 @@ namespace DotVVM.Framework.Controls
88
/// <summary> Options for the <see cref="KnockoutHelper.GenerateClientPostBackExpression(string, Binding.Expressions.ICommandBinding, DotvvmBindableObject, PostbackScriptOptions)" /> method. </summary>
99
public sealed record PostbackScriptOptions
1010
{
11-
/// <summary>If true, the command invocation will be wrapped in window.setTimeout with timeout 0. This is necessary for some event handlers, when the handler is invoked before the change is actually applied.</summary>
12-
public bool UseWindowSetTimeout { get; init; }
13-
/// <summary>Return value of the event handler. If set to false, the script will also include event.stopPropagation()</summary>
11+
/// <summary>If true, the command invocation will be wrapped in window.setTimeout with timeout 0. This is necessary for some event handlers, when the handler is invoked before the change is actually applied. Default is false</summary>
12+
public bool? UseWindowSetTimeout { get; init; }
13+
/// <summary>Return value of the event handler. If set to false, the script will also include event.stopPropagation(). Null means that `null` will be returned.</summary>
1414
public bool? ReturnValue { get; init; }
15-
public bool IsOnChange { get; init; }
15+
/// <summary> When true, the invocation is suppressed while viewmodel is being updated. Default is false. </summary>
16+
public bool? IsOnChange { get; init; }
1617
/// <summary>Javascript variable where the sender element can be found. Set to $element when in knockout binding.</summary>
17-
public CodeParameterAssignment ElementAccessor { get; init; }
18+
public CodeParameterAssignment? ElementAccessor { get; init; }
1819
/// <summary>Javascript variable current knockout binding context can be found. By default, `ko.contextFor({elementAccessor})` is used</summary>
1920
public CodeParameterAssignment? KoContext { get; init; }
2021
/// <summary>Javascript expression returning an array of command arguments.</summary>
2122
public CodeParameterAssignment? CommandArgs { get; init; }
22-
/// <summary>When set to false, postback handlers will not be invoked for this command.</summary>
23-
public bool AllowPostbackHandlers { get; init; }
23+
/// <summary>When set to false, postback handlers will not be invoked for this command. Default is true.</summary>
24+
public bool? AllowPostbackHandlers { get; init; }
2425
/// <summary>Javascript expression returning <see href="https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal">AbortSignal</see> which can be used to cancel the postback (it's a JS variant of CancellationToken). </summary>
2526
public CodeParameterAssignment? AbortSignal { get; init; }
2627
public Func<CodeSymbolicParameter, CodeParameterAssignment>? ParameterAssignment { get; init; }
2728

2829
/// <param name="useWindowSetTimeout">If true, the command invocation will be wrapped in window.setTimeout with timeout 0. This is necessary for some event handlers, when the handler is invoked before the change is actually applied.</param>
2930
/// <param name="returnValue">Return value of the event handler. If set to false, the script will also include event.stopPropagation()</param>
3031
/// <param name="isOnChange">If set to true, the command will be suppressed during updating of view model. This is necessary for certain onChange events, if we don't want to trigger the command when the view model changes.</param>
31-
/// <param name="elementAccessor">Javascript variable where the sender element can be found. Set to $element when in knockout binding.</param>
32+
/// <param name="elementAccessor">Javascript variable where the sender element can be found. Set to $element when in knockout binding, and this when in JS event.</param>
3233
/// <param name="koContext">Javascript variable current knockout binding context can be found. By default, `ko.contextFor({elementAccessor})` is used</param>
3334
/// <param name="commandArgs">Javascript expression returning an array of command arguments.</param>
3435
/// <param name="allowPostbackHandlers">When set to false, postback handlers will not be invoked for this command.</param>
3536
/// <param name="abortSignal">Javascript expression returning <see href="https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal">AbortSignal</see> which can be used to cancel the postback (it's a JS variant of CancellationToken). </param>
36-
public PostbackScriptOptions(bool useWindowSetTimeout = false,
37+
public PostbackScriptOptions(
38+
bool? useWindowSetTimeout = null,
3739
bool? returnValue = false,
38-
bool isOnChange = false,
39-
string elementAccessor = "this",
40+
bool? isOnChange = null,
41+
string? elementAccessor = null,
4042
CodeParameterAssignment? koContext = null,
4143
CodeParameterAssignment? commandArgs = null,
42-
bool allowPostbackHandlers = true,
44+
bool? allowPostbackHandlers = null,
4345
CodeParameterAssignment? abortSignal = null,
4446
Func<CodeSymbolicParameter, CodeParameterAssignment>? parameterAssignment = null)
4547
{
4648
this.UseWindowSetTimeout = useWindowSetTimeout;
4749
this.ReturnValue = returnValue;
4850
this.IsOnChange = isOnChange;
49-
this.ElementAccessor = new CodeParameterAssignment(elementAccessor, OperatorPrecedence.Max);
51+
this.ElementAccessor = elementAccessor is null ? (CodeParameterAssignment?)null : new CodeParameterAssignment(elementAccessor, OperatorPrecedence.Max);
5052
this.KoContext = koContext;
5153
this.CommandArgs = commandArgs;
5254
this.AllowPostbackHandlers = allowPostbackHandlers;
@@ -55,19 +57,44 @@ public PostbackScriptOptions(bool useWindowSetTimeout = false,
5557
}
5658

5759
/// <summary> Default postback options, optimal for placing the script into a `onxxx` event attribute. </summary>
58-
public static readonly PostbackScriptOptions JsEvent = new PostbackScriptOptions();
59-
public static readonly PostbackScriptOptions KnockoutBinding = new PostbackScriptOptions(elementAccessor: "$element", koContext: new CodeParameterAssignment("$context", OperatorPrecedence.Max, isGlobalContext: true));
60+
public static readonly PostbackScriptOptions JsEvent = new PostbackScriptOptions(
61+
elementAccessor: "this",
62+
koContext: new CodeParameterAssignment(new ParametrizedCode(["ko.contextFor(", ")"], [JavascriptTranslator.CurrentElementParameter.ToInfo()], OperatorPrecedence.Max))
63+
);
64+
public static readonly PostbackScriptOptions KnockoutBinding = new PostbackScriptOptions(
65+
elementAccessor: "$element",
66+
koContext: new CodeParameterAssignment("$context", OperatorPrecedence.Max, isGlobalContext: true)
67+
);
68+
69+
public PostbackScriptOptions WithDefaults(PostbackScriptOptions? defaults)
70+
{
71+
if (defaults is null) return this;
72+
return this with {
73+
UseWindowSetTimeout = UseWindowSetTimeout ?? defaults.UseWindowSetTimeout,
74+
// ReturnValue is ignored on purpose, because it is set to false in the constructor
75+
IsOnChange = IsOnChange ?? defaults.IsOnChange,
76+
ElementAccessor = ElementAccessor ?? defaults.ElementAccessor,
77+
KoContext = KoContext ?? defaults.KoContext,
78+
CommandArgs = CommandArgs ?? defaults.CommandArgs,
79+
AllowPostbackHandlers = AllowPostbackHandlers ?? defaults.AllowPostbackHandlers,
80+
AbortSignal = AbortSignal ?? defaults.AbortSignal,
81+
ParameterAssignment = ParameterAssignment ?? defaults.ParameterAssignment,
82+
};
83+
}
84+
85+
public PostbackScriptOptions Override(PostbackScriptOptions? overrides) =>
86+
overrides is null ? this : overrides.WithDefaults(this);
6087

6188
public override string ToString()
6289
{
6390
var fields = new List<string>();
64-
if (UseWindowSetTimeout) fields.Add("useWindowSetTimeout: true");
91+
if (UseWindowSetTimeout is {}) fields.Add($"useWindowSetTimeout: {UseWindowSetTimeout}");
6592
if (ReturnValue != false) fields.Add($"returnValue: {(ReturnValue == true ? "true" : "null")}");
66-
if (IsOnChange) fields.Add("isOnChange: true");
93+
if (IsOnChange is {}) fields.Add($"isOnChange: {IsOnChange}");
6794
if (ElementAccessor.ToString() != "this") fields.Add($"elementAccessor: \"{ElementAccessor}\"");
6895
if (KoContext != null) fields.Add($"koContext: \"{KoContext}\"");
6996
if (CommandArgs != null) fields.Add($"commandArgs: \"{CommandArgs}\"");
70-
if (!AllowPostbackHandlers) fields.Add("allowPostbackHandlers: false");
97+
if (AllowPostbackHandlers is {}) fields.Add($"allowPostbackHandlers: {AllowPostbackHandlers}");
7198
if (AbortSignal != null) fields.Add($"abortSignal: \"{AbortSignal}\"");
7299
if (ParameterAssignment != null) fields.Add($"parameterAssignment: \"{ParameterAssignment}\"");
73100
return new StringBuilder("new PostbackScriptOptions(").Append(string.Join(", ", fields.ToArray())).Append(")").ToString();

src/Tests/Binding/JavascriptCompilationTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1637,5 +1637,4 @@ public enum TestEnum
16371637
public TestEnum Enum { get; set; }
16381638
public string String { get; set; }
16391639
}
1640-
16411640
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using DotVVM.Framework.Binding;
2+
using DotVVM.Framework.Binding.Expressions;
3+
using DotVVM.Framework.Compilation.ControlTree;
4+
using DotVVM.Framework.Compilation.Javascript;
5+
using DotVVM.Framework.Configuration;
6+
using DotVVM.Framework.Controls;
7+
using DotVVM.Framework.Testing;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.VisualStudio.TestTools.UnitTesting;
10+
11+
namespace DotVVM.Framework.Tests.Binding;
12+
[TestClass]
13+
public class KnockoutHelperTests
14+
{
15+
static readonly DotvvmConfiguration config = DotvvmTestHelper.DebugConfig;
16+
static readonly BindingCompilationService bindingService = config.ServiceProvider.GetRequiredService<BindingCompilationService>();
17+
static readonly ICommandBinding command = bindingService.Cache.CreateCommand("_this.BoolMethod()", DataContextStack.Create(typeof(TestViewModel)));
18+
static readonly IStaticCommandBinding clientOnlyStaticCommand = bindingService.Cache.CreateStaticCommand("_this.BoolProp = true", DataContextStack.Create(typeof(TestViewModel)));
19+
20+
21+
[TestMethod]
22+
public void NormalEvent_Command()
23+
{
24+
var result = KnockoutHelper.GenerateClientPostBackExpression("Click", command, new PlaceHolder(), new PostbackScriptOptions(abortSignal: CodeParameterAssignment.FromIdentifier("signal")));
25+
Assert.AreEqual("dotvvm.postBack(this,[],\"aS4YcJnv6U6PpCmC\",\"\",null,[\"validate-root\"],[],signal)", result);
26+
}
27+
28+
[TestMethod]
29+
public void NormalEvent_StaticCommand()
30+
{
31+
var result = KnockoutHelper.GenerateClientPostBackExpression("Click", clientOnlyStaticCommand, new PlaceHolder(), new PostbackScriptOptions(abortSignal: CodeParameterAssignment.FromIdentifier("signal")));
32+
Assert.AreEqual("dotvvm.applyPostbackHandlers((options) => options.viewModel.BoolProp(true).BoolProp(),this,[],[],undefined,signal)", result);
33+
}
34+
35+
[TestMethod]
36+
public void KnockoutExpression_Command()
37+
{
38+
var result = KnockoutHelper.GenerateClientPostBackExpression("Click", command, new PlaceHolder(), PostbackScriptOptions.KnockoutBinding with { AbortSignal = CodeParameterAssignment.FromIdentifier("signal") });
39+
Assert.AreEqual("dotvvm.postBack($element,[],\"aS4YcJnv6U6PpCmC\",\"\",$context,[\"validate-root\"],[],signal)", result);
40+
}
41+
42+
[TestMethod]
43+
public void KnockoutExpression_StaticCommand()
44+
{
45+
var result = KnockoutHelper.GenerateClientPostBackExpression("Click", clientOnlyStaticCommand, new PlaceHolder(), new PostbackScriptOptions(abortSignal: CodeParameterAssignment.FromIdentifier("signal")).WithDefaults(PostbackScriptOptions.KnockoutBinding));
46+
Assert.AreEqual("dotvvm.applyPostbackHandlers((options) => options.viewModel.BoolProp(true).BoolProp(),$element,[],[],$context,signal)", result);
47+
}
48+
49+
[TestMethod]
50+
public void Lambda_Command()
51+
{
52+
var result = KnockoutHelper.GenerateClientPostbackLambda("Click", command, new PlaceHolder(), new PostbackScriptOptions(abortSignal: CodeParameterAssignment.FromIdentifier("signal")));
53+
Assert.AreEqual("()=>(dotvvm.postBack($element,[],\"aS4YcJnv6U6PpCmC\",\"\",$context,[\"validate-root\"],[],signal))", result);
54+
}
55+
56+
[TestMethod]
57+
public void Lambda_StaticCommand()
58+
{
59+
var result = KnockoutHelper.GenerateClientPostbackLambda("Click", clientOnlyStaticCommand, new PlaceHolder(), new PostbackScriptOptions(abortSignal: CodeParameterAssignment.FromIdentifier("signal")));
60+
Assert.AreEqual("(...args)=>(dotvvm.applyPostbackHandlers((options) => options.viewModel.BoolProp(true).BoolProp(),$element,[],args,$context,signal))", result);
61+
}
62+
}

0 commit comments

Comments
 (0)