Is your feature request related to a problem? Please describe.
To set focus on an element from Blazor, currently requires and ElementReference (or an id) and a JSInterop call in OnAfterRenderAsync.
It would be nice to be able to do this declaratively by using the "autofocus" attribute.
This is a capability of HTML, but does not work for SPA applications where the elements are inserted into an existing DOM.
The addition of an ability to have autofocus on newly created elements would make the SPA developer experience much simpler and provide a better result for the end user of the application.
Describe the solution you'd like
The Blazor application renders an element with the autofocus attribute and that triggers the BrowserRenderer to call the focus() method on the newly create element.
<button @onclick=@(MyClickHandler) autofocus>Click Me</button>
This should only cover initial element creation to maintain consistency with normal html autofocus.
Additional context
The BrowserRenderer used in Blazor can be modified in a manner similar to this (proof of concept testing confirms this at a superficial level) to provide autofocus on element creation.
private insertElement(batch: RenderBatch, componentId: number, parent: LogicalElement, childIndex: number, frames: ArrayValues<RenderTreeFrame>, frame: RenderTreeFrame, frameIndex: number) {
const frameReader = batch.frameReader;
const tagName = frameReader.elementName(frame)!;
const newDomElementRaw = tagName === 'svg' || isSvgElement(parent) ?
document.createElementNS('http://www.w3.org/2000/svg', tagName) :
document.createElement(tagName);
const newElement = toLogicalElement(newDomElementRaw);
insertLogicalChild(newDomElementRaw, parent, childIndex);
+ // Handle autofocus
+ let wantsFocus: boolean = false;
// Apply attributes
const descendantsEndIndexExcl = frameIndex + frameReader.subtreeLength(frame);
for (let descendantIndex = frameIndex + 1; descendantIndex < descendantsEndIndexExcl; descendantIndex++) {
const descendantFrame = batch.referenceFramesEntry(frames, descendantIndex);
if (frameReader.frameType(descendantFrame) === FrameType.attribute) {
this.applyAttribute(batch, componentId, newDomElementRaw, descendantFrame);
+ // Handle autofocus
+ let attrName = batch.frameReader.attributeName(descendantFrame);
+ wantsFocus = ( attrName === 'autofocus' );
} else {
// As soon as we see a non-attribute child, all the subsequent child frames are
// not attributes, so bail out and insert the remnants recursively
this.insertFrameRange(batch, componentId, newElement, 0, frames, descendantIndex, descendantsEndIndexExcl);
break;
}
}
+ if (wantsFocus) { // Handle autofocus
+ newDomElementRaw.focus();
+ }
// We handle setting 'value' on a <select> in two different ways:
// [1] When inserting a corresponding <option>, in case you're dynamically adding options
// [2] After we finish inserting the <select>, in case the descendant options are being
// added as an opaque markup block rather than individually
// Right here we implement [2]
if (newDomElementRaw instanceof HTMLSelectElement && selectValuePropname in newDomElementRaw) {
const selectValue = newDomElementRaw[selectValuePropname];
newDomElementRaw.value = selectValue;
delete newDomElementRaw[selectValuePropname];
}
}
Link to gist with full source : https://gist.github.com/SQL-MisterMagoo/949f2aff8aa0006ab6843bcedd14dd62/revisions
EDIT: 30/11/2019 Section below should be considered removed from this request as it was flawed
~~### Additional context
At this point in the code, it would be simple to add another case statement to handle autofocus
https://github.com/aspnet/AspNetCore/blob/32a2cc594363672ccfe7644a649f77a8bfc9c4a8/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts#L311
private tryApplySpecialProperty(batch: RenderBatch, element: Element, attributeName: string, attributeFrame: RenderTreeFrame | null) {
switch (attributeName) {
case 'value':
return this.tryApplyValueProperty(batch, element, attributeFrame);
case 'checked':
return this.tryApplyCheckedProperty(batch, element, attributeFrame);
/* ** Suggested addition ** */
case 'autofocus': {
element.focus();
return true;
}
default: {
if (attributeName.startsWith(internalAttributeNamePrefix)) {
this.applyInternalAttribute(batch, element, attributeName.substring(internalAttributeNamePrefix.length), attributeFrame);
return true;
}
return false;
}
}
}
```~~
Is your feature request related to a problem? Please describe.
To set focus on an element from Blazor, currently requires and ElementReference (or an id) and a JSInterop call in OnAfterRenderAsync.
It would be nice to be able to do this declaratively by using the "autofocus" attribute.
This is a capability of HTML, but does not work for SPA applications where the elements are inserted into an existing DOM.
The addition of an ability to have
autofocuson newly created elements would make the SPA developer experience much simpler and provide a better result for the end user of the application.Describe the solution you'd like
The Blazor application renders an element with the autofocus attribute and that triggers the BrowserRenderer to call the focus() method on the newly create element.
This should only cover initial element creation to maintain consistency with normal html
autofocus.Additional context
The BrowserRenderer used in Blazor can be modified in a manner similar to this (proof of concept testing confirms this at a superficial level) to provide
autofocuson element creation.private insertElement(batch: RenderBatch, componentId: number, parent: LogicalElement, childIndex: number, frames: ArrayValues<RenderTreeFrame>, frame: RenderTreeFrame, frameIndex: number) { const frameReader = batch.frameReader; const tagName = frameReader.elementName(frame)!; const newDomElementRaw = tagName === 'svg' || isSvgElement(parent) ? document.createElementNS('http://www.w3.org/2000/svg', tagName) : document.createElement(tagName); const newElement = toLogicalElement(newDomElementRaw); insertLogicalChild(newDomElementRaw, parent, childIndex); + // Handle autofocus + let wantsFocus: boolean = false; // Apply attributes const descendantsEndIndexExcl = frameIndex + frameReader.subtreeLength(frame); for (let descendantIndex = frameIndex + 1; descendantIndex < descendantsEndIndexExcl; descendantIndex++) { const descendantFrame = batch.referenceFramesEntry(frames, descendantIndex); if (frameReader.frameType(descendantFrame) === FrameType.attribute) { this.applyAttribute(batch, componentId, newDomElementRaw, descendantFrame); + // Handle autofocus + let attrName = batch.frameReader.attributeName(descendantFrame); + wantsFocus = ( attrName === 'autofocus' ); } else { // As soon as we see a non-attribute child, all the subsequent child frames are // not attributes, so bail out and insert the remnants recursively this.insertFrameRange(batch, componentId, newElement, 0, frames, descendantIndex, descendantsEndIndexExcl); break; } } + if (wantsFocus) { // Handle autofocus + newDomElementRaw.focus(); + } // We handle setting 'value' on a <select> in two different ways: // [1] When inserting a corresponding <option>, in case you're dynamically adding options // [2] After we finish inserting the <select>, in case the descendant options are being // added as an opaque markup block rather than individually // Right here we implement [2] if (newDomElementRaw instanceof HTMLSelectElement && selectValuePropname in newDomElementRaw) { const selectValue = newDomElementRaw[selectValuePropname]; newDomElementRaw.value = selectValue; delete newDomElementRaw[selectValuePropname]; } }Link to gist with full source : https://gist.github.com/SQL-MisterMagoo/949f2aff8aa0006ab6843bcedd14dd62/revisions
EDIT: 30/11/2019 Section below should be considered removed from this request as it was flawed
~~### Additional context
At this point in the code, it would be simple to add another case statement to handle
autofocushttps://github.com/aspnet/AspNetCore/blob/32a2cc594363672ccfe7644a649f77a8bfc9c4a8/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts#L311