diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlTreeHelper.cs b/src/Framework/Framework/Compilation/ControlTree/ControlTreeHelper.cs index 98b2d7b4b1..cace9e1cb6 100644 --- a/src/Framework/Framework/Compilation/ControlTree/ControlTreeHelper.cs +++ b/src/Framework/Framework/Compilation/ControlTree/ControlTreeHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using DotVVM.Framework.Compilation.Parser.Dothtml.Parser; @@ -27,6 +28,19 @@ public static bool HasPropertyValue(this IAbstractControl control, IPropertyDesc return value; } + public static Dictionary GetPropertyGroup(this IAbstractControl control, IPropertyGroupDescriptor group) + { + var result = new Dictionary(); + foreach (var prop in control.Properties) + { + if (prop.Key is IGroupedPropertyDescriptor member && member.PropertyGroup == group) + { + result.Add(member.GroupMemberName, prop.Value); + } + } + return result; + } + public static IPropertyDescriptor GetHtmlAttributeDescriptor(this IControlResolverMetadata metadata, string name) => metadata.GetPropertyGroupMember("", name); public static IPropertyDescriptor GetPropertyGroupMember(this IControlResolverMetadata metadata, string prefix, string name) diff --git a/src/Framework/Framework/Compilation/ControlTree/IAbstractControl.cs b/src/Framework/Framework/Compilation/ControlTree/IAbstractControl.cs index 5431488b32..7babee5d33 100644 --- a/src/Framework/Framework/Compilation/ControlTree/IAbstractControl.cs +++ b/src/Framework/Framework/Compilation/ControlTree/IAbstractControl.cs @@ -8,6 +8,7 @@ namespace DotVVM.Framework.Compilation.ControlTree public interface IAbstractControl : IAbstractContentNode { IEnumerable PropertyNames { get; } + IEnumerable> Properties { get; } bool TryGetProperty(IPropertyDescriptor property, [NotNullWhen(true)] out IAbstractPropertySetter? value); diff --git a/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedControl.cs b/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedControl.cs index 9f8cfcede7..135f70047b 100644 --- a/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedControl.cs +++ b/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedControl.cs @@ -18,6 +18,10 @@ public class ResolvedControl : ResolvedContentNode, IAbstractControl IEnumerable IAbstractControl.PropertyNames => Properties.Keys; + IEnumerable> IAbstractControl.Properties => + Properties.Select(p => new KeyValuePair(p.Key, p.Value)); + + public ResolvedControl(ControlResolverMetadata metadata, DothtmlNode? node, DataContextStack dataContext) : base(metadata, node, dataContext) { } diff --git a/src/Framework/Framework/Controls/JsComponent.cs b/src/Framework/Framework/Controls/JsComponent.cs index 68840f76a7..c1c910ae53 100644 --- a/src/Framework/Framework/Controls/JsComponent.cs +++ b/src/Framework/Framework/Controls/JsComponent.cs @@ -175,6 +175,20 @@ public static IEnumerable ValidateUsage(ResolvedControl contr (control.DothtmlNode as DothtmlElementNode)?.TagNameNode ); } + + var props = control.GetPropertyGroup(PropsGroupDescriptor); + var templates = control.GetPropertyGroup(TemplatesGroupDescriptor); + + foreach (var name in props.Keys.Intersect(templates.Keys)) + { + var templateElement = templates[name].DothtmlNode; + yield return new ControlUsageError( + $"JsComponent property and template must not share the same name ('{name}').", + DiagnosticSeverity.Error, + props[name].DothtmlNode, + (templateElement as DothtmlElementNode)?.TagNameNode ?? templateElement + ); + } } } diff --git a/src/Framework/Framework/Resources/Scripts/global-declarations.ts b/src/Framework/Framework/Resources/Scripts/global-declarations.ts index e875e05ef8..cf7ffc5b73 100644 --- a/src/Framework/Framework/Resources/Scripts/global-declarations.ts +++ b/src/Framework/Framework/Resources/Scripts/global-declarations.ts @@ -316,15 +316,26 @@ type DotvvmFileSize = { } type DotvvmJsComponent = { - updateProps(p: { [key: string]: any }): void - dispose(): void + /** Called after each update of dotvvm.state which changes any of the bound properties. Only the changed properties are listed in the `updatedProps` argument. */ + updateProps(updatedProps: { [key: string]: any }): void + /** Called after the HTMLElement is removed from DOM. + * The component does not need to remove any child elements, but should clean any external data, such as subscription to dotvvm events */ + dispose?(): void } type DotvvmJsComponentFactory = { + /** Initializes the component on the specified HTMLElement. + * @param element The root HTMLElement of this component + * @param props An object listing all constants and `value` bindings from the `dot:JsComponent` instance + * @param commands An object listing all `command` and `staticCommand` bindings from the `dot:JsComponent` instance + * @param templates An object listing all content properties of the `dot:JsComponent`. The template is identified using its HTML id attribute, it can be rendered using ko.renderTemplate, KnockoutTemplateReactComponent or KnockoutTemplateSvelteComponent + * @param setProps A function which will attempt to write a value back into the bound property. Only certain `value` bindings can be updated, an exception is thown if it isn't possible + * @returns An object which will be notified about subsequent changes to the bound properties and when the component + */ create( element: HTMLElement, props: { [key: string]: any }, commands: { [key: string]: (args: any[]) => Promise }, - templates: { [key: string]: string }, // TODO + templates: { [key: string]: string }, setProps: (p: { [key: string]: any }) => void ): DotvvmJsComponent }