diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Interop/InvokeWithJsonMarshalling.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Interop/InvokeWithJsonMarshalling.ts index f98c4b22a8..0e4018fdec 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Interop/InvokeWithJsonMarshalling.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Interop/InvokeWithJsonMarshalling.ts @@ -1,11 +1,14 @@ import { platform } from '../Environment'; import { System_String } from '../Platform/Platform'; import { getRegisteredFunction } from './RegisteredFunction'; +import { getElementByCaptureId } from '../Rendering/ElementReferenceCapture'; + +const elementRefKey = '_blazorElementRef'; // Keep in sync with ElementRef.cs export function invokeWithJsonMarshalling(identifier: System_String, ...argsJson: System_String[]) { const identifierJsString = platform.toJavaScriptString(identifier); const funcInstance = getRegisteredFunction(identifierJsString); - const args = argsJson.map(json => JSON.parse(platform.toJavaScriptString(json))); + const args = argsJson.map(json => JSON.parse(platform.toJavaScriptString(json), jsonReviver)); const result = funcInstance.apply(null, args); if (result !== null && result !== undefined) { const resultJson = JSON.stringify(result); @@ -14,3 +17,11 @@ export function invokeWithJsonMarshalling(identifier: System_String, ...argsJson return null; } } + +function jsonReviver(key: string, value: any): any { + if (value && typeof value === 'object' && value.hasOwnProperty(elementRefKey) && typeof value[elementRefKey] === 'number') { + return getElementByCaptureId(value[elementRefKey]); + } + + return value; +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts index 51ae8ee04a..2a95198c5b 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts @@ -5,6 +5,7 @@ import { platform } from '../Environment'; import { EventDelegator } from './EventDelegator'; import { EventForDotNet, UIEventArgs } from './EventForDotNet'; import { LogicalElement, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement } from './LogicalElements'; +import { applyCaptureIdToElement } from './ElementReferenceCapture'; const selectValuePropname = '_blazorSelectValue'; let raiseEventMethod: MethodHandle; let renderComponentMethod: MethodHandle; @@ -138,6 +139,13 @@ export class BrowserRenderer { return 1; case FrameType.region: return this.insertFrameRange(componentId, parent, childIndex, frames, frameIndex + 1, frameIndex + renderTreeFrame.subtreeLength(frame)); + case FrameType.elementReferenceCapture: + if (parent instanceof Element) { + applyCaptureIdToElement(parent, renderTreeFrame.elementReferenceCaptureId(frame)); + return 0; // A "capture" is a child in the diff, but has no node in the DOM + } else { + throw new Error('Reference capture frames can only be children of element frames.'); + } default: const unknownType: never = frameType; // Compile-time verification that the switch was exhaustive throw new Error(`Unknown frame type: ${unknownType}`); @@ -252,16 +260,27 @@ export class BrowserRenderer { childIndex += numChildrenInserted; // Skip over any descendants, since they are already dealt with recursively - const subtreeLength = renderTreeFrame.subtreeLength(frame); - if (subtreeLength > 1) { - index += subtreeLength - 1; - } + index += countDescendantFrames(frame); } return (childIndex - origChildIndex); // Total number of children inserted } } +function countDescendantFrames(frame: RenderTreeFramePointer): number { + switch (renderTreeFrame.frameType(frame)) { + // The following frame types have a subtree length. Other frames may use that memory slot + // to mean something else, so we must not read it. We should consider having nominal subtypes + // of RenderTreeFramePointer that prevent access to non-applicable fields. + case FrameType.component: + case FrameType.element: + case FrameType.region: + return renderTreeFrame.subtreeLength(frame) - 1; + default: + return 0; + } +} + function isCheckbox(element: Element) { return element.tagName === 'INPUT' && element.getAttribute('type') === 'checkbox'; } diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/ElementReferenceCapture.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/ElementReferenceCapture.ts new file mode 100644 index 0000000000..ab28491c1a --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/ElementReferenceCapture.ts @@ -0,0 +1,12 @@ +export function applyCaptureIdToElement(element: Element, referenceCaptureId: number) { + element.setAttribute(getCaptureIdAttributeName(referenceCaptureId), ''); +} + +export function getElementByCaptureId(referenceCaptureId: number) { + const selector = `[${getCaptureIdAttributeName(referenceCaptureId)}]`; + return document.querySelector(selector); +} + +function getCaptureIdAttributeName(referenceCaptureId: number) { + return `_bl_${referenceCaptureId}`; +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderTreeFrame.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderTreeFrame.ts index 4d57ae8182..8b66d92e18 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderTreeFrame.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderTreeFrame.ts @@ -14,6 +14,7 @@ export const renderTreeFrame = { // The properties and memory layout must be kept in sync with the .NET equivalent in RenderTreeFrame.cs frameType: (frame: RenderTreeFramePointer) => platform.readInt32Field(frame, 4) as FrameType, subtreeLength: (frame: RenderTreeFramePointer) => platform.readInt32Field(frame, 8) as FrameType, + elementReferenceCaptureId: (frame: RenderTreeFramePointer) => platform.readInt32Field(frame, 8), componentId: (frame: RenderTreeFramePointer) => platform.readInt32Field(frame, 12), elementName: (frame: RenderTreeFramePointer) => platform.readStringField(frame, 16), textContent: (frame: RenderTreeFramePointer) => platform.readStringField(frame, 16), @@ -29,6 +30,7 @@ export enum FrameType { attribute = 3, component = 4, region = 5, + elementReferenceCapture = 6, } // Nominal type to ensure only valid pointers are passed to the renderTreeFrame functions. diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs index f04dd3c203..581c3f6cda 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs @@ -54,6 +54,10 @@ namespace Microsoft.AspNetCore.Blazor.Razor public static readonly string AddAttribute = nameof(AddAttribute); + public static readonly string AddElementReferenceCapture = nameof(AddElementReferenceCapture); + + public static readonly string AddComponentReferenceCapture = nameof(AddComponentReferenceCapture); + public static readonly string Clear = nameof(Clear); public static readonly string GetFrames = nameof(GetFrames); @@ -91,5 +95,10 @@ namespace Microsoft.AspNetCore.Blazor.Razor { public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.Components.EventHandlerAttribute"; } + + public static class ElementRef + { + public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.ElementRef"; + } } } diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs index 03759263d2..2fd884ad35 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs @@ -482,6 +482,39 @@ namespace Microsoft.AspNetCore.Blazor.Razor } } + public override void WriteReferenceCapture(CodeRenderingContext context, RefExtensionNode refNode) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (refNode == null) + { + throw new ArgumentNullException(nameof(refNode)); + } + + // The runtime node writer moves the call elsewhere. At design time we + // just want sufficiently similar code that any unknown-identifier or type + // errors will be equivalent + var captureTypeName = refNode.IsComponentCapture + ? refNode.ComponentCaptureTypeName + : BlazorApi.ElementRef.FullTypeName; + WriteCSharpCode(context, new CSharpCodeIntermediateNode + { + Source = refNode.Source, + Children = + { + refNode.IdentifierToken, + new IntermediateToken + { + Kind = TokenKind.CSharp, + Content = $" = default({captureTypeName});" + } + } + }); + } + private void WriteCSharpToken(CodeRenderingContext context, IntermediateToken token) { if (string.IsNullOrWhiteSpace(token.Content)) diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs index b275f388a9..4e9d526ca8 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs @@ -76,11 +76,13 @@ namespace Microsoft.AspNetCore.Blazor.Razor builder.Features.Add(new BindLoweringPass()); builder.Features.Add(new EventHandlerLoweringPass()); builder.Features.Add(new ComponentLoweringPass()); + builder.Features.Add(new RefLoweringPass()); builder.Features.Add(new OrphanTagHelperLoweringPass()); builder.Features.Add(new ComponentTagHelperDescriptorProvider()); builder.Features.Add(new BindTagHelperDescriptorProvider()); builder.Features.Add(new EventHandlerTagHelperDescriptorProvider()); + builder.Features.Add(new RefTagHelperDescriptorProvider()); if (isDeclarationOnlyCompile) { diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorMetadata.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorMetadata.cs index 11899e3c9f..95f2cb52b4 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorMetadata.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorMetadata.cs @@ -43,5 +43,12 @@ namespace Microsoft.AspNetCore.Blazor.Razor public readonly static string TagHelperKind = "Blazor.EventHandler"; } + + public static class Ref + { + public readonly static string TagHelperKind = "Blazor.Ref"; + + public static readonly string RuntimeName = "Blazor.None"; + } } } diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorNodeWriter.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorNodeWriter.cs index d60affa49d..198a6e6a77 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorNodeWriter.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorNodeWriter.cs @@ -27,5 +27,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor public abstract void WriteComponentBody(CodeRenderingContext context, ComponentBodyExtensionNode node); public abstract void WriteComponentAttribute(CodeRenderingContext context, ComponentAttributeExtensionNode node); + + public abstract void WriteReferenceCapture(CodeRenderingContext context, RefExtensionNode node); } } diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs index 9da89c43a4..a6a0da3f7e 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs @@ -30,6 +30,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor private string _unconsumedHtml; private List _currentAttributeValues; private IDictionary _currentElementAttributes = new Dictionary(); + private List _currentElementRefCaptures = new List(); private int _sourceSequence = 0; private struct PendingAttribute @@ -269,6 +270,15 @@ namespace Microsoft.AspNetCore.Blazor.Razor _currentElementAttributes.Clear(); } + if (_currentElementRefCaptures.Count > 0) + { + foreach (var refNode in _currentElementRefCaptures) + { + WriteAddReferenceCaptureCall(context, refNode); + } + _currentElementRefCaptures.Clear(); + } + _scopeStack.OpenScope( tagName: nextTag.Data, isComponent: false); } @@ -306,6 +316,39 @@ namespace Microsoft.AspNetCore.Blazor.Razor } } + private void WriteAddReferenceCaptureCall(CodeRenderingContext context, RefExtensionNode refNode) + { + var codeWriter = context.CodeWriter; + + var methodName = refNode.IsComponentCapture + ? nameof(BlazorApi.RenderTreeBuilder.AddComponentReferenceCapture) + : nameof(BlazorApi.RenderTreeBuilder.AddElementReferenceCapture); + codeWriter + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{methodName}") + .Write((_sourceSequence++).ToString()) + .WriteParameterSeparator(); + + const string refCaptureParamName = "__value"; + using (var lambdaScope = codeWriter.BuildLambda(refCaptureParamName)) + { + var typecastIfNeeded = refNode.IsComponentCapture ? $"({refNode.ComponentCaptureTypeName})" : string.Empty; + WriteCSharpCode(context, new CSharpCodeIntermediateNode + { + Source = refNode.Source, + Children = + { + refNode.IdentifierToken, + new IntermediateToken { + Kind = TokenKind.CSharp, + Content = $" = {typecastIfNeeded}{refCaptureParamName};" + } + } + }); + } + + codeWriter.WriteEndMethodInvocation(); + } + private void RejectDisallowedHtmlTags(IntermediateNode node, HtmlTagToken tagToken) { // Disallow