[web][docs] Improve HtmlElementView widget docs. (#145192)

This PR expands the `HtmlElementView` widget DartDocs, with the following sections:

* Usage: How to use the widget, two ways:
  * The `HtmlElementView.fromTagName` constructor
  * The `PlatformViewRegistry` way
* Lifecycle: There's an `onCreated` callback on the widget. When does it get called?
* HTML Lifecycle: How to listen to events coming from the DOM.
* Visibility: what is the `isVisible` property for?

Small additional tweaks here and there to mention common pitfalls of using HtmlElementView on the web, and mentions to workarounds, like `package:pointer_interceptor`.

## Issues

* Fixes: https://github.com/flutter/flutter/issues/143922
* Fixes: https://github.com/flutter/flutter/issues/49634
* Related: https://github.com/flutter/website/issues/5063
This commit is contained in:
David Iglesias 2024-03-22 17:53:26 -07:00 committed by GitHub
parent 6836b0445a
commit df75d249b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -19,6 +19,8 @@ import 'framework.dart';
// PlatformViewController createFooWebView(PlatformViewCreationParams params) { return (null as dynamic) as PlatformViewController; }
// Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers = <Factory<OneSequenceGestureRecognizer>>{};
// late PlatformViewController _controller;
// void myOnElementCreated(Object element) {}
// void myOnPlatformViewCreated(int viewId) {}
/// Embeds an Android view in the Widget hierarchy.
///
@ -358,49 +360,291 @@ class AppKitView extends _DarwinView {
State<AppKitView> createState() => _AppKitViewState();
}
/// Callback signature for when the platform view's DOM element was created.
/// The signature of the function that gets called when the [HtmlElementView]
/// DOM element is created.
///
/// [element] is the DOM element that was created.
///
/// Also see [HtmlElementView.fromTagName] that uses this callback
/// signature.
/// This callback is called before [element] is attached to the DOM, so it can
/// be modified as needed by the Flutter web application.
///
/// See [HtmlElementView.fromTagName] that receives a callback of this type.
///
/// {@template flutter.widgets.web.JSInterop.object}
/// Flutter uses type `Object` so this API doesn't force any JS interop API
/// implementation to Flutter users. This `element` can be cast to any compatible
/// JS interop type as needed. For example: `JSAny` (from `dart:js_interop`),
/// `HTMLElement` (from `package:web`) or any custom JS interop definition.
/// See "Next-generation JS interop": https://dart.dev/interop/js-interop
/// {@endtemplate}
typedef ElementCreatedCallback = void Function(Object element);
/// Embeds an HTML element in the Widget hierarchy in Flutter Web.
/// Embeds an HTML element in the Widget hierarchy in Flutter web.
///
/// *NOTE*: This only works in Flutter Web. To embed web content on other
/// platforms, consider using the `flutter_webview` plugin.
///
/// Embedding HTML is an expensive operation and should be avoided when a
/// Flutter equivalent is possible.
///
/// The embedded HTML is painted just like any other Flutter widget and
/// transformations apply to it as well. This widget should only be used in
/// Flutter Web.
/// The embedded HTML is laid out like any other Flutter widget and
/// transformations (like opacity, and clipping) apply to it as well.
///
/// {@macro flutter.widgets.AndroidView.layout}
///
/// Due to security restrictions with cross-origin `<iframe>` elements, Flutter
/// cannot dispatch pointer events to an HTML view. If an `<iframe>` is the
/// target of an event, the window containing the `<iframe>` is not notified
/// of the event. In particular, this means that any pointer events which land
/// on an `<iframe>` will not be seen by Flutter, and so the HTML view cannot
/// participate in gesture detection with other widgets.
/// Embedding HTML is a _potentially expensive_ operation and should be avoided
/// when a Flutter equivalent is possible. (See **`isVisible` parameter** below.)
/// This widget is useful to integrate native HTML elements to a Flutter web app,
/// like a `<video>` tag, or a `<div>` where a [Google Map](https://pub.dev/packages/google_maps_flutter)
/// can be rendered.
///
/// The way we enable accessibility on Flutter for web is to have a full-page
/// button which waits for a double tap. Placing this full-page button in front
/// of the scene would cause platform views not to receive pointer events. The
/// tradeoff is that by placing the scene in front of the semantics placeholder
/// will cause platform views to block pointer events from reaching the
/// placeholder. This means that in order to enable accessibility, you must
/// double tap the app *outside of a platform view*. As a consequence, a
/// full-screen platform view will make it impossible to enable accessibility.
/// Make sure that your HTML views are sized no larger than necessary, or you
/// may cause difficulty for users trying to enable accessibility.
/// This widget **only works on Flutter web.** To embed web content on other
/// platforms, consider using the [`webview_flutter` plugin](https://pub.dev/packages/webview_flutter).
///
/// {@macro flutter.widgets.AndroidView.lifetime}
/// ## Usage
///
/// There's two ways to use the `HtmlElementView` widget:
///
/// ### `HtmlElementView.fromTagName`
///
/// The [HtmlElementView.fromTagName] constructor creates the HTML element
/// specified by `tagName`, and passes it to the `onElementCreated` callback
/// where it can be customized:
///
/// ```dart
/// // In a `build` method...
/// HtmlElementView.fromTagName(
/// tagName: 'div',
/// onElementCreated: myOnElementCreated,
/// );
/// ```
///
/// The example creates a `<div>` element, then calls the `onElementCreated`
/// callback with the created `<div>`, so it can be customized **before** it is
/// attached to the DOM.
///
/// (See more details about `onElementCreated` in the **Lifecycle** section below.)
///
/// ### Using the `PlatformViewRegistry`
///
/// The primitives used to implement [HtmlElementView.fromTagName] are available
/// for general use through `dart:ui_web`'s `platformViewRegistry`.
///
/// Creating an `HtmlElementView` through these primitives is a two step process:
///
/// #### 1. `registerViewFactory`
///
/// First, a `viewFactory` function needs to be registered for a given `viewType`.
/// Flutter web will call this factory function to create the `element` that will
/// be attached later:
///
/// ```dart
/// import 'dart:ui_web' as ui_web;
/// import 'package:web/web.dart' as web;
///
/// void registerRedDivFactory() {
/// ui_web.platformViewRegistry.registerViewFactory(
/// 'my-view-type',
/// (int viewId, {Object? params}) {
/// // Create and return an HTML Element from here
/// final web.HTMLDivElement myDiv = web.HTMLDivElement()
/// ..id = 'some_id_$viewId'
/// ..style.backgroundColor = 'red'
/// ..style.width = '100%'
/// ..style.height = '100%';
/// return myDiv;
/// },
/// );
/// }
/// ```
///
/// `registerViewFactory` **must** be called outside of `build` methods, so the
/// registered function is available when `build` happens.
///
/// See the different types of functions that can be used as `viewFactory`:
///
/// * [`typedef ui_web.PlatformViewFactory`](https://api.flutter.dev/flutter/dart-ui_web/PlatformViewFactory.html)
/// * [`typedef ui_web.ParameterizedPlatformViewFactory`](https://api.flutter.dev/flutter/dart-ui_web/ParameterizedPlatformViewFactory.html)
///
/// #### 2. `HtmlElementView` widget
///
/// Once a factory is registered, an `HtmlElementView` widget of `viewType` can
/// be added to the widget tree, like so:
///
/// ```dart
/// // In a `build` method...
/// const HtmlElementView(
/// viewType: 'my-view-type',
/// onPlatformViewCreated: myOnPlatformViewCreated,
/// creationParams: <String, Object?>{
/// 'key': 'someValue',
/// },
/// );
/// ```
///
/// [viewType] **must** match the value used to `registerViewFactory` before.
///
/// [creationParams] (optional) will be passed to your `viewFactory` function,
/// if it accepts them.
///
/// [onPlatformViewCreated] will be called with the `viewId` of the platform
/// view (`element`) created by the `viewFactory`, before it gets attached to
/// the DOM.
///
/// The `viewId` can be used to retrieve the created `element` (The same one
/// passed to `onElementCreated` in [HtmlElementView.fromTagName]) with the
/// `ui_web.platformViewRegistry.`[`getViewById` method](https://api.flutter.dev/flutter/dart-ui_web/PlatformViewRegistry/getViewById.html).
///
/// (See more details about `onPlatformViewCreated` in the **Lifecycle** section
/// below.)
///
/// ## Lifecycle
///
/// `HtmlElementView` widgets behave like any other Flutter stateless widget, but
/// with an additional lifecycle method: `onPlatformViewCreated` / `onElementCreated`
/// (depending on the constructor, see **Usage** above).
///
/// The difference between the two callbacks is the parameter they receive:
///
/// * `onPlatformViewCreated` will be called with the created `viewId` as a parameter,
/// and needs `ui_web.platformViewRegistry.getViewById` to retrieve the created
/// element (See [PlatformViewCreatedCallback]).
/// * `onElementCreated` will be called with the created `element` directly,
/// skipping its `viewId` (See [ElementCreatedCallback]).
///
/// Both callbacks are called **after** the HTML `element` has been created, but
/// **before** it's attached to the DOM.
///
/// ### HTML Lifecycle
///
/// The Browser DOM APIs have additional HTML lifecycle callbacks for the root
/// `element` of an `HtmlElementView`.
///
/// #### Element Attached To The DOM
///
/// It is common for JS code to locate the DOM elements they need with a
/// selector, rather than accepting said DOM elements directly. In those cases,
/// the `element` **must** be attached to the DOM for the selector to work.
///
/// The example below demonstrates **how to create an `onElementAttached` function**
/// that gets called when the root `element` is attached to the DOM using a
/// `ResizeObserver` through `package:web` from the `onElementCreated` lifecycle
/// method:
///
/// ```dart
/// import 'dart:js_interop';
/// import 'package:web/web.dart' as web;
///
/// // Called after `element` is attached to the DOM.
/// void onElementAttached(web.HTMLDivElement element) {
/// final web.Element? located = web.document.querySelector('#someIdThatICanFindLater');
/// assert(located == element, 'Wrong `element` located!');
/// // Do things with `element` or `located`, or call your code now...
/// element.style.backgroundColor = 'green';
/// }
///
/// void onElementCreated(Object element) {
/// element as web.HTMLDivElement;
/// element.style.backgroundColor = 'red';
/// element.id = 'someIdThatICanFindLater';
///
/// // Create the observer
/// final web.ResizeObserver observer = web.ResizeObserver((
/// JSArray<web.ResizeObserverEntry> entries,
/// web.ResizeObserver observer,
/// ) {
/// if (element.isConnected) {
/// // The observer is done, disconnect it.
/// observer.disconnect();
/// // Call our callback.
/// onElementAttached(element);
/// }
/// }.toJS);
///
/// // Connect the observer.
/// observer.observe(element);
/// }
/// ```
///
/// * Read more about [`ResizeObserver` in the MDN](https://developer.mozilla.org/en-US/docs/Web/API/Resize_Observer_API).
///
/// #### Other Observers
///
/// The example above uses a `ResizeObserver` because it can be applied directly
/// to the `element` that is about to be attached. Another observer that could
/// be used for this (with a little bit more code) would be a
/// [`MutationObserver`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver).
///
/// The `MutationObserver` requires the parent element in which the `HtmlElementView`
/// is going to be inserted. A safe way to retrieve a parent element for the
/// platform view is to retrieve the `hostElement` of the [FlutterView] where the
/// `HtmlElementView` is being rendered.
///
/// The `hostElement` of the current [FlutterView] can be retrieved through:
///
/// ```dart
/// import 'dart:js_interop';
/// import 'dart:ui_web' as ui_web;
/// import 'package:flutter/widgets.dart';
///
/// void useHostElement(BuildContext context) {
/// final int flutterViewId = View.of(context).viewId;
/// final JSAny? hostElement = ui_web.views.getHostElement(flutterViewId);
/// // Use `package:web` with `hostElement`...
/// }
/// ```
///
/// **Important:** `FlutterView.viewId` and the `viewId` parameter passed to
/// the `viewFactory` identify **different objects**:
///
/// * `flutterViewId` (from `View.of(context)`) represents the [FlutterView]
/// where the web app is currently rendering.
/// * `viewId` (passed to the `viewFactory` function) represents a unique ID
/// for the `HtmlElementView` instance that is being attached to the app.
///
/// Read more about [FlutterView] on Flutter's API docs:
///
/// * [`View.of`](https://api.flutter.dev/flutter/widgets/View/of.html)
/// * [`getHostElement`](https://main-api.flutter.dev/flutter/dart-ui_web/FlutterViewManagerProxy/getHostElement.html)
///
/// ## Pointer events
///
/// In order for the `HtmlElementView` contents to be interactive, they're allowed
/// to handle `pointer-events`. This may result in Flutter missing some events
/// because they've been handled by the `HtmlElementView`, and not seen by
/// Flutter.
///
/// [`package:pointer_interceptor`](https://pub.dev/packages/pointer_interceptor)
/// may help in some cases where Flutter content needs to be overlaid on top of
/// an `HtmlElementView`. Alternatively, the `pointer-events: none` property can
/// be set `onElementCreated`; but that will prevent **ALL** interactions with
/// the underlying HTML content.
///
/// If the `HtmlElementView` is an `<iframe>` element, Flutter will not receive
/// pointer events that land in the `<iframe>` (click/tap, drag, drop, etc.)
/// In those cases, the `HtmlElementView` will seem like it's _swallowing_
/// the events and not participating in Flutter's gesture detection.
///
/// ## `isVisible` parameter
///
/// Rendering custom HTML content (from `HtmlElementView`) in between `canvas`
/// pixels means that the Flutter web engine needs to _split_ the canvas drawing
/// into elements drawn _behind_ the HTML content, and those drawn _above_ it.
///
/// In the Flutter web engine, each of these _splits of the canvas to sandwich
/// HTML content in between_ is referred to as an **overlay**.
///
/// Each _overlay_ present in a scene has implications both in memory and
/// execution performance, and it is best to minimize their amount; browsers
/// support a limited number of _overlays_ on a single scene at a given time.
///
/// `HtmlElementView` objects have an `isVisible` property that can be passed
/// through `registerViewFactory`, or `fromTagName`. `isVisible` refers
/// to whether the `HtmlElementView` will paint pixels on the screen or not.
///
/// Correctly defining this value helps the Flutter web rendering engine optimize
/// the amount of _overlays_ it'll need to render a particular scene.
///
/// In general, `isVisible` should be left to its default value of `true`, but
/// in some `HtmlElementView`s (like the `pointer_interceptor` or `Link` widget),
/// it can be set to `false`, so the engine doesn't _waste_ an overlay to render
/// Flutter content on top of views that don't paint any pixels.
class HtmlElementView extends StatelessWidget {
/// Creates a platform view for Flutter Web.
/// Creates a platform view for Flutter web.
///
/// `viewType` identifies the type of platform view to create.
const HtmlElementView({
@ -419,6 +663,7 @@ class HtmlElementView extends StatelessWidget {
///
/// [onElementCreated] is called when the DOM element is created. It can be
/// used by the app to customize the element by adding attributes and styles.
/// This method is called *before* the element is attached to the DOM.
factory HtmlElementView.fromTagName({
Key? key,
required String tagName,
@ -439,6 +684,8 @@ class HtmlElementView extends StatelessWidget {
/// Callback to invoke after the platform view has been created.
///
/// This method is called *before* the platform view is attached to the DOM.
///
/// May be null.
final PlatformViewCreatedCallback? onPlatformViewCreated;