diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs index 43f41f370a..73b736bc60 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -817,12 +817,9 @@ private void AddDataSetOptionsTranslations() new JsIdentifierExpression("dotvvm").Member("dataSet").Member("loadDataSet").Invoke( args[1].WithAnnotation(ShouldBeObservableAnnotation.Instance), args[2], - dataSetHelper.Clone().Member("loadDataSet"), - dataSetHelper.Clone().Member("postProcessor") + GridViewDataSetBindingProvider.LoadDataDelegate.ToExpression(), + GridViewDataSetBindingProvider.PostProcessorDelegate.ToExpression() ).WithAnnotation(new ResultIsPromiseAnnotation(e => e)))); - AddMethodTranslator(() => GridViewDataSetBindingProvider.GetCurrentGridDataSet(), new GenericMethodCompiler(args => - dataSetHelper.Clone().Member("dataSet") - )); // _dataPager.Load() AddMethodTranslator(() => default(DataPagerApi)!.Load(), new GenericMethodCompiler(args => diff --git a/src/Framework/Framework/Compilation/Javascript/JsParensFixingVisitor.cs b/src/Framework/Framework/Compilation/Javascript/JsParensFixingVisitor.cs index 02a1f0a16b..44432ef220 100644 --- a/src/Framework/Framework/Compilation/Javascript/JsParensFixingVisitor.cs +++ b/src/Framework/Framework/Compilation/Javascript/JsParensFixingVisitor.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -56,7 +56,8 @@ public override string ToString() return Precedence + (IsPreferredSide ? "+" : "-") + " (" + name + ")"; } - public static readonly OperatorPrecedence Max = new OperatorPrecedence(20, true); + /// Assume that the expression is an atomic + public static OperatorPrecedence Max => new OperatorPrecedence(20, true); /// atomic expression, like `x`, `(x + y)`, `0`, `{"f": 123}`, `x[1]`, ... public const byte Atomic = 20; diff --git a/src/Framework/Framework/Controls/AppendableDataPager.cs b/src/Framework/Framework/Controls/AppendableDataPager.cs index 1433895eb0..f246d4f6ef 100644 --- a/src/Framework/Framework/Controls/AppendableDataPager.cs +++ b/src/Framework/Framework/Controls/AppendableDataPager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; using DotVVM.Framework.Binding; @@ -6,6 +6,7 @@ using DotVVM.Framework.Binding.HelperNamespace; using DotVVM.Framework.Compilation.Javascript; using DotVVM.Framework.Hosting; +using DotVVM.Framework.Utils; namespace DotVVM.Framework.Controls { @@ -45,6 +46,7 @@ public IPageableGridViewDataSet DataSet public static readonly DotvvmProperty DataSetProperty = DotvvmProperty.Register(c => c.DataSet, null); + [MarkupOptions(Required = true)] public ICommandBinding? LoadData { get => (ICommandBinding?)GetValue(LoadDataProperty); @@ -87,19 +89,32 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest { var dataSetBinding = GetDataSetBinding().GetKnockoutBindingExpression(this, unwrapped: true); - var helperBinding = new KnockoutBindingGroup(); - helperBinding.Add("dataSet", dataSetBinding); - if (this.LoadData is { } loadData) + var loadData = this.LoadData.NotNull("AppendableDataPager.LoadData is currently required."); + var loadDataCore = KnockoutHelper.GenerateClientPostbackLambda("LoadDataCore", loadData, this, PostbackScriptOptions.KnockoutBinding with { AllowPostbackHandlers = false }); + var loadNextPage = KnockoutHelper.GenerateClientPostbackLambda("LoadData", dataPagerCommands!.GoToNextPage!, this, PostbackScriptOptions.KnockoutBinding with { + ParameterAssignment = p => + p == GridViewDataSetBindingProvider.LoadDataDelegate ? new CodeParameterAssignment(loadDataCore, default) : + p == GridViewDataSetBindingProvider.PostProcessorDelegate ? new CodeParameterAssignment("dotvvm.dataSet.postProcessors.append", OperatorPrecedence.Max) : + default + }); + + if (LoadTemplate is null) + { + var binding = new KnockoutBindingGroup(); + binding.Add("dataSet", dataSetBinding); + binding.Add("loadNextPage", loadNextPage); + binding.Add("autoLoadWhenInViewport", "true"); + writer.AddKnockoutDataBind("dotvvm-appendable-data-pager", binding); + } + else { - helperBinding.Add("loadDataSet", KnockoutHelper.GenerateClientPostbackLambda("LoadDataCore", loadData, this, new PostbackScriptOptions(elementAccessor: "$element", koContext: CodeParameterAssignment.FromIdentifier("$context")))); - helperBinding.Add("loadNextPage", KnockoutHelper.GenerateClientPostbackLambda("LoadData", dataPagerCommands!.GoToNextPage!, this)); + var helperBinding = new KnockoutBindingGroup(); + helperBinding.Add("dataSet", dataSetBinding); + // helperBinding.Add("loadDataSet", KnockoutHelper.GenerateClientPostbackLambda("LoadDataCore", loadData, this, PostbackScriptOptions.KnockoutBinding); + helperBinding.Add("loadNextPage", loadNextPage); helperBinding.Add("postProcessor", "dotvvm.dataSet.postProcessors.append"); + writer.AddKnockoutDataBind("dotvvm-gridviewdataset", helperBinding.ToString()); } - writer.AddKnockoutDataBind("dotvvm-gridviewdataset", helperBinding.ToString()); - - var binding = new KnockoutBindingGroup(); - binding.Add("autoLoadWhenInViewport", LoadTemplate == null ? "true" : "false"); - writer.AddKnockoutDataBind("dotvvm-appendable-data-pager", binding); base.AddAttributesToRender(writer, context); } diff --git a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs index 80a8095224..00d6376ded 100644 --- a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs +++ b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs @@ -10,6 +10,8 @@ using DotVVM.Framework.Binding.Properties; using DotVVM.Framework.Compilation; using DotVVM.Framework.Compilation.ControlTree; +using DotVVM.Framework.Compilation.Javascript; +using DotVVM.Framework.Compilation.Javascript.Ast; using DotVVM.Framework.Utils; namespace DotVVM.Framework.Controls; @@ -254,11 +256,14 @@ private ICommandBinding CreateLoadDataDelegateCommandBinding( }); } - private static Expression CurrentGridDataSetExpression(Type datasetType) - { - var method = typeof(GridViewDataSetBindingProvider).GetMethod(nameof(GetCurrentGridDataSet))!.MakeGenericMethod(datasetType); - return Expression.Call(method); - } + public static CodeSymbolicParameter LoadDataDelegate = new CodeSymbolicParameter( + "LoadDataDelegate", + CodeParameterAssignment.FromExpression(JavascriptTranslator.KnockoutContextParameter.ToExpression().Member("$gridViewDataSetHelper").Member("loadDataSet")) + ); + public static CodeSymbolicParameter PostProcessorDelegate = new CodeSymbolicParameter( + "PostProcessorDelegate", + CodeParameterAssignment.FromExpression(JavascriptTranslator.KnockoutContextParameter.ToExpression().Member("$gridViewDataSetHelper").Member("postProcessor")) + ); /// /// A sentinel method which is translated to load the GridViewDataSet on the client side using the Load delegate. @@ -276,12 +281,6 @@ public static Task DataSetClientSideLoad Returns the DataSet we currently work on from the $context.$gridViewDataSetHelper.dataSet - public static T GetCurrentGridDataSet() where T : IBaseGridViewDataSet - { - throw new InvalidOperationException("This method cannot be called on the server!"); - } - private static Type GetOptionsConcreteType(Type dataSetConcreteType, out PropertyInfo optionsProperty) { if (!typeof(TDataSetInterface).IsAssignableFrom(dataSetConcreteType)) diff --git a/src/Framework/Framework/Controls/KnockoutHelper.cs b/src/Framework/Framework/Controls/KnockoutHelper.cs index e0438393ea..2d1bfb5a9f 100644 --- a/src/Framework/Framework/Controls/KnockoutHelper.cs +++ b/src/Framework/Framework/Controls/KnockoutHelper.cs @@ -114,20 +114,20 @@ public static void AddKnockoutForeachDataBind(this IHtmlWriter writer, string ex /// Generates a function expression that invokes the command with specified commandArguments. Creates code like `(...commandArguments) => dotvvm.postBack(...)` public static string GenerateClientPostbackLambda(string propertyName, ICommandBinding command, DotvvmBindableObject control, PostbackScriptOptions? options = null) { - options ??= new PostbackScriptOptions( - elementAccessor: "$element", - koContext: CodeParameterAssignment.FromIdentifier("$context", true) - ); + options ??= PostbackScriptOptions.KnockoutBinding; - var hasArguments = command is IStaticCommandBinding || command.CommandJavascript.EnumerateAllParameters().Any(p => p == CommandBindingExpression.CommandArgumentsParameter); - options.CommandArgs = hasArguments ? new CodeParameterAssignment(new ParametrizedCode("args", OperatorPrecedence.Max)) : default; + var addArguments = options.CommandArgs is null && (command is IStaticCommandBinding || command.CommandJavascript.EnumerateAllParameters().Any(p => p == CommandBindingExpression.CommandArgumentsParameter)); + if (addArguments) + { + options = options with { CommandArgs = new CodeParameterAssignment(new ParametrizedCode("args", OperatorPrecedence.Max)) }; + } // just few commands have arguments so it's worth checking if we need to clutter the output with argument propagation var call = KnockoutHelper.GenerateClientPostBackExpression( propertyName, command, control, options); - return hasArguments ? $"(...args)=>({call})" : $"()=>({call})"; + return addArguments ? $"(...args)=>({call})" : $"()=>({call})"; } /// Generates Javascript code which executes the specified command binding . @@ -205,6 +205,10 @@ string getHandlerScript() var adjustedExpression = JavascriptTranslator.AdjustKnockoutScriptContext(jsExpression, dataContextLevel: expression.FindDataContextTarget(control).stepsUp); + if (options.ParameterAssignment is {}) + { + adjustedExpression = adjustedExpression.AssignParameters(options.ParameterAssignment); + } // when the expression changes the dataContext, we need to override the default knockout context fo the command binding. CodeParameterAssignment knockoutContext; CodeParameterAssignment viewModel = default; diff --git a/src/Framework/Framework/Controls/PostbackScriptOptions.cs b/src/Framework/Framework/Controls/PostbackScriptOptions.cs index 745a99db01..9d1baf4033 100644 --- a/src/Framework/Framework/Controls/PostbackScriptOptions.cs +++ b/src/Framework/Framework/Controls/PostbackScriptOptions.cs @@ -1,25 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; using DotVVM.Framework.Compilation.Javascript; namespace DotVVM.Framework.Controls { /// Options for the method. - public class PostbackScriptOptions + public sealed record PostbackScriptOptions { /// 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. - public bool UseWindowSetTimeout { get; set; } + public bool UseWindowSetTimeout { get; init; } /// Return value of the event handler. If set to false, the script will also include event.stopPropagation() - public bool? ReturnValue { get; set; } - public bool IsOnChange { get; set; } + public bool? ReturnValue { get; init; } + public bool IsOnChange { get; init; } /// Javascript variable where the sender element can be found. Set to $element when in knockout binding. - public CodeParameterAssignment ElementAccessor { get; set; } + public CodeParameterAssignment ElementAccessor { get; init; } /// Javascript variable current knockout binding context can be found. By default, `ko.contextFor({elementAccessor})` is used - public CodeParameterAssignment? KoContext { get; set; } + public CodeParameterAssignment? KoContext { get; init; } /// Javascript expression returning an array of command arguments. - public CodeParameterAssignment? CommandArgs { get; set; } + public CodeParameterAssignment? CommandArgs { get; init; } /// When set to false, postback handlers will not be invoked for this command. - public bool AllowPostbackHandlers { get; } + public bool AllowPostbackHandlers { get; init; } /// Javascript expression returning AbortSignal which can be used to cancel the postback (it's a JS variant of CancellationToken). - public CodeParameterAssignment? AbortSignal { get; } + public CodeParameterAssignment? AbortSignal { get; init; } + public Func? ParameterAssignment { get; init; } /// 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. /// Return value of the event handler. If set to false, the script will also include event.stopPropagation() @@ -36,7 +40,8 @@ public PostbackScriptOptions(bool useWindowSetTimeout = false, CodeParameterAssignment? koContext = null, CodeParameterAssignment? commandArgs = null, bool allowPostbackHandlers = true, - CodeParameterAssignment? abortSignal = null) + CodeParameterAssignment? abortSignal = null, + Func? parameterAssignment = null) { this.UseWindowSetTimeout = useWindowSetTimeout; this.ReturnValue = returnValue; @@ -45,7 +50,27 @@ public PostbackScriptOptions(bool useWindowSetTimeout = false, this.KoContext = koContext; this.CommandArgs = commandArgs; this.AllowPostbackHandlers = allowPostbackHandlers; - AbortSignal = abortSignal; + this.AbortSignal = abortSignal; + this.ParameterAssignment = parameterAssignment; + } + + /// Default postback options, optimal for placing the script into a `onxxx` event attribute. + public static readonly PostbackScriptOptions JsEvent = new PostbackScriptOptions(); + public static readonly PostbackScriptOptions KnockoutBinding = new PostbackScriptOptions(elementAccessor: "$element", koContext: new CodeParameterAssignment("$context", OperatorPrecedence.Max, isGlobalContext: true)); + + public override string ToString() + { + var fields = new List(); + if (UseWindowSetTimeout) fields.Add("useWindowSetTimeout: true"); + if (ReturnValue != false) fields.Add($"returnValue: {(ReturnValue == true ? "true" : "null")}"); + if (IsOnChange) fields.Add("isOnChange: true"); + if (ElementAccessor.ToString() != "this") fields.Add($"elementAccessor: \"{ElementAccessor}\""); + if (KoContext != null) fields.Add($"koContext: \"{KoContext}\""); + if (CommandArgs != null) fields.Add($"commandArgs: \"{CommandArgs}\""); + if (!AllowPostbackHandlers) fields.Add("allowPostbackHandlers: false"); + if (AbortSignal != null) fields.Add($"abortSignal: \"{AbortSignal}\""); + if (ParameterAssignment != null) fields.Add($"parameterAssignment: \"{ParameterAssignment}\""); + return new StringBuilder("new PostbackScriptOptions(").AppendJoin(", ", fields.ToArray()).Append(")").ToString(); } } } diff --git a/src/Framework/Framework/Resources/Scripts/binding-handlers/appendable-data-pager.ts b/src/Framework/Framework/Resources/Scripts/binding-handlers/appendable-data-pager.ts index 453b34a664..9b409ae41a 100644 --- a/src/Framework/Framework/Resources/Scripts/binding-handlers/appendable-data-pager.ts +++ b/src/Framework/Framework/Resources/Scripts/binding-handlers/appendable-data-pager.ts @@ -1,6 +1,9 @@ -type AppendableDataPagerBinding = { +import { getStateManager } from "../dotvvm-base"; + +type AppendableDataPagerBinding = { autoLoadWhenInViewport: boolean, - loadNextPage: () => Promise + loadNextPage: () => Promise, + dataSet: any }; export default { @@ -15,9 +18,9 @@ export default { if (isLoading) return; let entry = entries[0]; - while (entry.isIntersecting) { - const dataSet = allBindingsAccessor.get("dotvvm-gridviewdataset").dataSet as DotvvmObservable; - if (dataSet.state.PagingOptions.IsLastPage) { + while (entry?.isIntersecting) { + const dataSet = valueAccessor().dataSet; + if (dataSet.PagingOptions().IsLastPage()) { return; } @@ -25,6 +28,8 @@ export default { try { await binding.loadNextPage(); + // getStateManager().doUpdateNow(); + // when the loading was finished, check whether we need to load another page entry = observer.takeRecords()[0]; }