Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

[web] Support platform view creation params #42255

Merged
merged 5 commits into from
Jun 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 13 additions & 19 deletions lib/web_ui/lib/src/engine/platform_views/content_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:ui/ui_web/src/ui_web.dart' as ui_web;

import '../browser_detection.dart';
import '../dom.dart';
import '../embedder.dart';
import '../util.dart';
import 'slots.dart';

/// A function which takes a unique `id` and some `params` and creates an HTML element.
///
/// This is made available to end-users through dart:ui in web.
typedef ParameterizedPlatformViewFactory = DomElement Function(
int viewId, {
Object? params,
});

/// A function which takes a unique `id` and creates an HTML element.
///
/// This is made available to end-users through dart:ui in web.
typedef PlatformViewFactory = DomElement Function(int viewId);

/// This class handles the lifecycle of Platform Views in the DOM of a Flutter Web App.
///
/// There are three important parts of Platform Views. This class manages two of
Expand Down Expand Up @@ -79,8 +68,13 @@ class PlatformViewManager {
/// `factoryFunction` needs to be a [PlatformViewFactory].
bool registerFactory(String viewType, Function factoryFunction,
{bool isVisible = true}) {
assert(factoryFunction is PlatformViewFactory ||
factoryFunction is ParameterizedPlatformViewFactory);
assert(
factoryFunction is ui_web.PlatformViewFactory ||
factoryFunction is ui_web.ParameterizedPlatformViewFactory,
'Factory signature is invalid. Expected either '
'{${ui_web.PlatformViewFactory}} or {${ui_web.ParameterizedPlatformViewFactory}} '
'but got: {${factoryFunction.runtimeType}}',
);

if (_factories.containsKey(viewType)) {
return false;
Expand Down Expand Up @@ -132,11 +126,11 @@ class PlatformViewManager {
final Function factoryFunction = _factories[viewType]!;
final DomElement content;

if (factoryFunction is ParameterizedPlatformViewFactory) {
content = factoryFunction(viewId, params: params);
if (factoryFunction is ui_web.ParameterizedPlatformViewFactory) {
content = factoryFunction(viewId, params: params) as DomElement;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this assert(content is DomElement, 'Cool error message'), rather than just the cast? That way we may be able to provide a better error message than the default one coming from Dart?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good idea!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I don't think DomElement is a real type at runtime, so I'm not sure what content is DomElement is going to check. cc @joshualitt any ideas?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to submit this as is. I can do a follow up PR if you still feel strongly about it.

} else {
factoryFunction as PlatformViewFactory;
content = factoryFunction(viewId);
factoryFunction as ui_web.PlatformViewFactory;
content = factoryFunction(viewId) as DomElement;
}

_ensureContentCorrectlySized(content, viewType);
Expand Down
4 changes: 2 additions & 2 deletions lib/web_ui/lib/src/engine/platform_views/message_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class PlatformViewMessageHandler {
final Map<dynamic, dynamic> args = methodCall.arguments as Map<dynamic, dynamic>;
final int viewId = args.readInt('id');
final String viewType = args.readString('viewType');
final Object? params = args['params'];

if (!_contentManager.knowsViewType(viewType)) {
callback(_codec.encodeErrorEnvelope(
Expand All @@ -89,11 +90,10 @@ class PlatformViewMessageHandler {
return;
}

// TODO(hterkelsen): How can users add extra `args` from the HtmlElementView widget?
final DomElement content = _contentManager.renderContent(
viewType,
viewId,
args,
params,
);

// For now, we don't need anything fancier. If needed, this can be converted
Expand Down
23 changes: 17 additions & 6 deletions lib/web_ui/lib/ui_web/src/ui_web/platform_view_registry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,32 @@

import 'package:ui/src/engine.dart';

/// A function which takes a unique `id` and some `params` and creates an HTML
/// element.
typedef ParameterizedPlatformViewFactory = Object Function(
int viewId, {
Object? params,
});

/// A function which takes a unique `id` and creates an HTML element.
typedef PlatformViewFactory = Object Function(int viewId);

/// The platform view registry for this app.
final PlatformViewRegistry platformViewRegistry = PlatformViewRegistry();

/// A registry for factories that create platform views.
class PlatformViewRegistry {
/// Register [viewTypeId] as being creating by the given [viewFactory].
/// [viewFactory] can be any function that takes an integer and returns an
/// `HTMLElement` DOM object.
/// Register [viewType] as being created by the given [viewFactory].
///
/// [viewFactory] can be any function that takes an integer and optional
/// `params` and returns an `HTMLElement` DOM object.
bool registerViewFactory(
String viewTypeId,
Object Function(int viewId) viewFactory, {
String viewType,
Function viewFactory, {
bool isVisible = true,
}) {
return platformViewManager.registerFactory(
viewTypeId,
viewType,
viewFactory,
isVisible: isVisible,
);
Expand Down
79 changes: 78 additions & 1 deletion lib/web_ui/test/engine/platform_views/message_handler_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ void main() {

const MethodCodec codec = StandardMethodCodec();

typedef PlatformViewFactoryCall = ({int viewId, Object? params});

void testMain() {
group('PlatformViewMessageHandler', () {
group('handlePlatformViewCall', () {
Expand Down Expand Up @@ -109,6 +111,80 @@ void testMain() {
reason:
'The response should be a success envelope, with null in it.');
});

test('passes creation params to the factory', () async {
final List<PlatformViewFactoryCall> factoryCalls = <PlatformViewFactoryCall>[];
contentManager.registerFactory(viewType, (int viewId, {Object? params}) {
factoryCalls.add((viewId: viewId, params: params));
return createDomHTMLDivElement();
});
final PlatformViewMessageHandler messageHandler = PlatformViewMessageHandler(
contentManager: contentManager,
);

final List<Completer<ByteData?>> completers = <Completer<ByteData?>>[];

completers.add(Completer<ByteData?>());
messageHandler.handlePlatformViewCall(
_getCreateMessage(viewType, 111),
completers.last.complete,
);

completers.add(Completer<ByteData?>());
messageHandler.handlePlatformViewCall(
_getCreateMessage(viewType, 222, <dynamic, dynamic>{'foo': 'bar'}),
completers.last.complete,
);

completers.add(Completer<ByteData?>());
messageHandler.handlePlatformViewCall(
_getCreateMessage(viewType, 333, 'foobar'),
completers.last.complete,
);

completers.add(Completer<ByteData?>());
messageHandler.handlePlatformViewCall(
_getCreateMessage(viewType, 444, <dynamic>[1, null, 'str']),
completers.last.complete,
);

final List<ByteData?> responses = await Future.wait(
completers.map((Completer<ByteData?> c) => c.future),
);

for (final ByteData? response in responses) {
expect(
codec.decodeEnvelope(response!),
isNull,
reason: 'The response should be a success envelope, with null in it.',
);
}

expect(factoryCalls, hasLength(4));
expect(factoryCalls[0].viewId, 111);
expect(factoryCalls[0].params, isNull);
expect(factoryCalls[1].viewId, 222);
expect(factoryCalls[1].params, <dynamic, dynamic>{'foo': 'bar'});
Comment on lines +166 to +167
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need an extra case where the param passed is not a map, but a string or an int, or some other simple Object?

expect(factoryCalls[2].viewId, 333);
expect(factoryCalls[2].params, 'foobar');
expect(factoryCalls[3].viewId, 444);
expect(factoryCalls[3].params, <dynamic>[1, null, 'str']);
});

test('fails if the factory returns a non-DOM object', () async {
contentManager.registerFactory(viewType, (int viewId) {
// Return an object that's not a DOM element.
return Object();
});

final PlatformViewMessageHandler messageHandler =
PlatformViewMessageHandler(contentManager: contentManager);
final ByteData? message = _getCreateMessage(viewType, viewId);

expect(() {
messageHandler.handlePlatformViewCall(message, (_) {});
}, throwsA(isA<TypeError>()));
});
});

group('"dispose" message', () {
Expand Down Expand Up @@ -162,12 +238,13 @@ class _FakePlatformViewManager extends PlatformViewManager {
}
}

ByteData? _getCreateMessage(String viewType, int viewId) {
ByteData? _getCreateMessage(String viewType, int viewId, [Object? params]) {
return codec.encodeMethodCall(MethodCall(
'create',
<String, dynamic>{
'id': viewId,
'viewType': viewType,
if (params != null) 'params': params,
},
));
}
Expand Down