diff --git a/apps/dart_quotes/lib/main.client.options.dart b/apps/dart_quotes/lib/main.client.options.dart index 166cb5999..3de487955 100644 --- a/apps/dart_quotes/lib/main.client.options.dart +++ b/apps/dart_quotes/lib/main.client.options.dart @@ -40,8 +40,8 @@ ClientOptions get defaultClientOptions => ClientOptions( clients: { 'quote_like_button': ClientLoader( (p) => _quote_like_button.QuoteLikeButton( - id: p['id'] as String, - initialCount: p['initialCount'] as int, + id: p.get('id'), + initialCount: p.get('initialCount'), ), loader: _quote_like_button.loadLibrary, ), diff --git a/apps/dart_quotes_server/lib/main.client.options.dart b/apps/dart_quotes_server/lib/main.client.options.dart index c785fd059..728d9c8c4 100644 --- a/apps/dart_quotes_server/lib/main.client.options.dart +++ b/apps/dart_quotes_server/lib/main.client.options.dart @@ -29,8 +29,8 @@ ClientOptions get defaultClientOptions => ClientOptions( clients: { 'quote_like_button': ClientLoader( (p) => _quote_like_button.QuoteLikeButton( - id: p['id'] as int, - initialCount: p['initialCount'] as int, + id: p.get('id'), + initialCount: p.get('initialCount'), ), loader: _quote_like_button.loadLibrary, ), diff --git a/apps/fluttercon/lib/main.client.options.dart b/apps/fluttercon/lib/main.client.options.dart index 6122092ea..42855bdef 100644 --- a/apps/fluttercon/lib/main.client.options.dart +++ b/apps/fluttercon/lib/main.client.options.dart @@ -38,7 +38,7 @@ ClientOptions get defaultClientOptions => ClientOptions( clients: { 'like_button': ClientLoader( (p) => _like_button.LikeButton( - session: _session.SessionCodex.decode(p['session'] as String), + session: _session.SessionCodex.decode(p.get('session')), ), loader: _like_button.loadLibrary, ), diff --git a/apps/website/lib/main.client.options.dart b/apps/website/lib/main.client.options.dart index 1db1038e9..5a871a38d 100644 --- a/apps/website/lib/main.client.options.dart +++ b/apps/website/lib/main.client.options.dart @@ -37,7 +37,7 @@ import 'package:website/pages/home/5_community/components/sponsors_list.dart' ClientOptions get defaultClientOptions => ClientOptions( clients: { 'header': ClientLoader( - (p) => _header.Header(showHome: p['showHome'] as bool), + (p) => _header.Header(showHome: p.get('showHome')), loader: _header.loadLibrary, ), 'install_command': ClientLoader( diff --git a/apps/website/lib/pages/home/0_hero/components/meet_jaspr_button.dart b/apps/website/lib/pages/home/0_hero/components/meet_jaspr_button.dart index 11688b3ea..412623af1 100644 --- a/apps/website/lib/pages/home/0_hero/components/meet_jaspr_button.dart +++ b/apps/website/lib/pages/home/0_hero/components/meet_jaspr_button.dart @@ -54,7 +54,7 @@ class MeetJasprButtonState extends State { Component build(BuildContext context) { if (notifier.done) { return div(id: 'meet-jaspr-button', [ - .wrapElement( + .apply( events: { 'click': (event) { event.preventDefault(); @@ -77,7 +77,7 @@ class MeetJasprButtonState extends State { } return div(id: 'meet-jaspr-button', [ - .wrapElement( + .apply( classes: touchTimer != null ? 'active' : null, events: { 'mousemove': (event) { diff --git a/apps/website/lib/pages/home/2_devex/components/counter_button.dart b/apps/website/lib/pages/home/2_devex/components/counter_button.dart index 91edbdb7e..750f36f4d 100644 --- a/apps/website/lib/pages/home/2_devex/components/counter_button.dart +++ b/apps/website/lib/pages/home/2_devex/components/counter_button.dart @@ -51,7 +51,7 @@ class CounterButtonState extends State { Component build(BuildContext context) { return div(classes: 'counter-container', [ Particles(particles: particles), - .wrapElement( + .apply( events: { 'click': (event) { event.preventDefault(); diff --git a/docs.json b/docs.json index 0fda0c57a..5f9a157bf 100644 --- a/docs.json +++ b/docs.json @@ -154,6 +154,10 @@ "title": "💙 Flutter Embedding", "href": "/going_further/flutter_embedding" }, + { + "title": "🖥️ Server Components", + "href": "/going_further/server_components" + }, { "title": "🪐 Custom Backend", "href": "/going_further/backend" diff --git a/docs/api/utils/at_client.mdx b/docs/api/utils/at_client.mdx index f5abffc1f..683a73b6b 100644 --- a/docs/api/utils/at_client.mdx +++ b/docs/api/utils/at_client.mdx @@ -102,6 +102,20 @@ Learn more about how to set up serialization for custom data types using the [`@ With this setup you can use any class as the parameter of a `@client` component. +### Server Components (Passing Components) + +Parameters can also be other components (type `Component`, including `List`s and `Map`s thereof). When you pass a component as a parameter to a `@client` component, it is treated as a **Server Component**: + +- It is rendered **only on the server**. +- Its Dart code is **not** compiled to JavaScript and **never** executes on the client. +- Its pre-rendered HTML structure is embedded directly into the `@client` component's output and is automatically mounted on the client during hydration. + +This is extremely useful when building layout components (like modals, sidebars, or tabs) that wrap large parts of your website that don't need client-side interactivity, thereby keeping your client bundle small. + + + For a detailed explanation of this design pattern, including advanced examples like Tab bars and component scope transitions, see the [Server Components Guide](/going_further/server_components). + + ## How it works The following happens when you use a `@client` component: @@ -114,11 +128,10 @@ The following happens when you use a `@client` component: 2. The component is built and pre-rendered normally. 3. Jaspr adds a html marker (``) around your components output. -4. Jaspr adds the components js target as a `\n' + ' \n' + ' \n' + '
Hello
\n' + ' \n' + ' \n' + '\n' + '', + ), + ); + }); }); } diff --git a/packages/jaspr/test/server/inherited_dom_wrapper_test.dart b/packages/jaspr/test/server/inherited_dom_wrapper_test.dart new file mode 100644 index 000000000..f44374f33 --- /dev/null +++ b/packages/jaspr/test/server/inherited_dom_wrapper_test.dart @@ -0,0 +1,333 @@ +@TestOn('vm') +library; + +import 'dart:convert'; + +import 'package:jaspr/dom.dart'; +import 'package:jaspr/server.dart'; +import 'package:jaspr_test/server_test.dart'; + +void main() { + group('Component.apply', () { + setUpAll(() { + Jaspr.initializeApp(); + }); + + test('renders single applied element', () async { + final result = await renderComponent( + Component.apply( + id: 'test', + classes: 'example', + styles: Styles(color: Colors.red), + attributes: {'aria-label': 'test'}, + child: div([]), + ), + standalone: true, + ); + + expect( + result.body, + decodedMatches('
\n'), + ); + }); + + test('merges component props with inherited props', () async { + final result = await renderComponent( + Component.apply( + id: 'test', + classes: 'example', + styles: Styles(color: Colors.red, backgroundColor: Colors.blue), + attributes: {'aria-label': 'test'}, + child: div( + id: 'main', + classes: 'container', + styles: Styles(color: Colors.green, display: Display.flex), + attributes: {'data-test': 'example'}, + [], + ), + ), + standalone: true, + ); + + expect( + result.body, + decodedMatches( + '
\n', + ), + ); + }); + + test('outer apply can target inner apply', () async { + final result = await renderComponent( + Component.apply( + target: ApplyTarget.childWith(id: 'id-1'), + classes: 'outer', + child: Component.apply( + id: 'id-1', + classes: 'inner', + child: div([]), + ), + ), + standalone: true, + ); + + expect( + result.body, + decodedMatches( + '
\n', + ), + ); + }); + + test('can target only direct child', () async { + final result = await renderComponent( + Component.apply( + id: 'test', + classes: 'example', + child: div([ + p([Component.text('Hello World')]), + ]), + ), + standalone: true, + ); + + expect( + result.body, + decodedMatches( + '

Hello World

\n', + ), + ); + }); + + test('can target all descendants', () async { + final result = await renderComponent( + Component.apply( + target: ApplyTarget.descendantWith(), + classes: 'example', + child: div([ + p([Component.text('Hello World')]), + ]), + ), + standalone: true, + ); + + expect( + result.body, + decodedMatches( + '

Hello World

\n', + ), + ); + }); + + test('merges properties of multiple applies', () async { + final result = await renderComponent( + Component.apply( + id: 'id-1', + classes: 'example', + attributes: {'data-test': 'test-1'}, + child: Component.apply( + id: 'id-2', + classes: 'container', + attributes: {'aria-label': 'test-2'}, + child: div([ + p([]), + ]), + ), + ), + standalone: true, + ); + + expect( + result.body, + decodedMatches( + '

\n', + ), + ); + }); + + test('can target descendants by tag', () async { + final result = await renderComponent( + Component.apply( + target: ApplyTarget.descendantWith(tag: 'p'), + classes: 'example', + child: div([ + p([]), + div([ + p([]), + ]), + ]), + ), + standalone: true, + ); + + expect( + result.body, + decodedMatches( + '

\n', + ), + ); + }); + + test('can target descendants by id', () async { + final result = await renderComponent( + Component.apply( + target: ApplyTarget.descendantWith(id: 'id-1'), + classes: 'example', + child: div([ + div(id: 'id-1', []), + div(id: 'id-2', []), + ]), + ), + standalone: true, + ); + + expect( + result.body, + decodedMatches( + '
\n', + ), + ); + }); + + test('can target descendants by class', () async { + final result = await renderComponent( + Component.apply( + target: ApplyTarget.descendantWith(classes: {'example'}), + attributes: {'data-test': 'test'}, + child: div([ + div(classes: 'example', []), + div(classes: 'example other', []), + div(classes: 'other', []), + ]), + ), + standalone: true, + ); + + expect( + result.body, + decodedMatches( + '
\n' + '
\n' + '
\n' + '
\n' + '
\n', + ), + ); + + final result2 = await renderComponent( + Component.apply( + target: ApplyTarget.descendantWith(classes: {'example', 'other'}), + attributes: {'data-test': 'test'}, + child: div([ + div(classes: 'example', []), + div(classes: 'example other', []), + div(classes: 'example other variant', []), + div(classes: 'other', []), + ]), + ), + standalone: true, + ); + + expect( + result2.body, + decodedMatches( + '
\n' + '
\n' + '
\n' + '
\n' + '
\n' + '
\n', + ), + ); + }); + + test('can target descendants by complex selector', () async { + final result = await renderComponent( + Component.apply( + target: ApplyTarget.descendantWith(tag: 'p', id: 'id-1', classes: {'example'}), + attributes: {'data-test': 'test'}, + child: div([ + p([]), + p(id: 'id-1', []), + p(classes: 'example', []), + p(id: 'id-1', classes: 'example', []), + p(id: 'id-1', classes: 'example other', []), + ]), + ), + standalone: true, + ); + + expect( + result.body, + decodedMatches( + '
\n' + '

\n' + '

\n' + '

\n' + '

\n' + '

\n' + '
\n', + ), + ); + }); + + test('can target descendants with multiple applies', () async { + final result = await renderComponent( + Component.apply( + target: ApplyTarget.descendantWith(classes: {'example'}), + attributes: {'data-test-1': 'test'}, + child: Component.apply( + target: ApplyTarget.childWith(tag: 'div'), + attributes: {'data-test-2': 'test'}, + child: div(classes: 'example', [ + div([]), + div(classes: 'example', []), + ]), + ), + ), + standalone: true, + ); + + expect( + result.body, + decodedMatches( + '
\n', + ), + ); + }); + + testComponents('renders single applied element', (tester) async { + String data = 'test'; + late void Function() callSetState; + + final child = div([]); + + tester.pumpComponent( + StatefulBuilder( + builder: (context, setState) { + callSetState = () => setState(() {}); + return Component.apply( + classes: 'example', + attributes: {'data': data}, + child: child, + ); + }, + ), + ); + + expect(find.tag('div'), findsOneComponent); + final renderObject = (find.tag('div').evaluate().first as RenderObjectElement).renderObject; + expect(renderObject, isA().having((e) => e.attributes, 'attributes', {'data': 'test'})); + + data = 'test-2'; + callSetState(); + await tester.pump(); + + expect(renderObject, isA().having((e) => e.attributes, 'attributes', {'data': 'test-2'})); + }); + }); +} + +TypeMatcher> decodedMatches(dynamic string) { + return isA>().having((e) => utf8.decode(e), 'decoded', string); +} diff --git a/packages/jaspr_builder/lib/src/client/client_module_builder.dart b/packages/jaspr_builder/lib/src/client/client_module_builder.dart index b00fe2897..f343efdb9 100644 --- a/packages/jaspr_builder/lib/src/client/client_module_builder.dart +++ b/packages/jaspr_builder/lib/src/client/client_module_builder.dart @@ -139,7 +139,7 @@ class ClientModule { }; String componentFactory() { - return '[[$import]].$name(${params.where((p) => !p.isNamed).map((p) => p.decoder).followedBy(params.where((p) => p.isNamed).map((p) => '${p.name}: ${p.decoder}')).join(', ')})'; + return '[[$import]].$name(${params.where((p) => !p.isNamed).map((p) => '${p.decoder}, ').followedBy(params.where((p) => p.isNamed).map((p) => '${p.name}: ${p.decoder},')).join(' ')})'; } } @@ -172,7 +172,7 @@ List getParamsFor(ClassElement e, Codecs codecs) { for (final param in params) { if (!param.isInitializingFormal) { throw UnsupportedError( - 'Client components only support initializing formal constructor parameters. ' + 'Client components only support initializing formal constructor parameters.\n' 'Failing element: ${e.name}.${constr.name}($param)', ); } @@ -180,12 +180,12 @@ List getParamsFor(ClassElement e, Codecs codecs) { return params.map((p) { try { - final decoder = codecs.getDecoderFor(p.type, "p['${p.name}']"); + final decoder = codecs.getDecoderFor(p.type, "p.get<__CAST__>('${p.name}')", true); final encoder = codecs.getEncoderFor(p.type, 'c.${p.name}'); return ClientParam(name: p.name ?? '', isNamed: p.isNamed, decoder: decoder, encoder: encoder); } on InvalidParameterException catch (_) { throw UnsupportedError( - '@client components only support parameters of primitive serializable types or types that define @decoder and @encoder methods. ' + '@client components only support parameters of primitive serializable types, Components or types that define @decoder and @encoder methods.\n' 'Failing parameter: [$p] in ${e.name}.${constr.name}()', ); } diff --git a/packages/jaspr_builder/lib/src/codec/codecs.dart b/packages/jaspr_builder/lib/src/codec/codecs.dart index 7c206eb1a..7a4a874a5 100644 --- a/packages/jaspr_builder/lib/src/codec/codecs.dart +++ b/packages/jaspr_builder/lib/src/codec/codecs.dart @@ -21,9 +21,9 @@ extension CodecLoader on BuildStep { } extension CodecReader on Codecs { - String getDecoderFor(DartType type, String getter) { - final decoder = type.acceptWithArgument(DecoderVisitor(this), getter); - return decoder ?? getter; + String getDecoderFor(DartType type, String getter, bool allowComponent) { + final decoder = type.acceptWithArgument(DecoderVisitor(this, allowComponent), getter); + return decoder.decoder.replaceAll('__CAST__', decoder.cast); } String getEncoderFor(DartType type, String getter) { @@ -75,56 +75,83 @@ class EncoderVisitor extends UnifyingTypeVisitorWithArgument { } } -class DecoderVisitor extends UnifyingTypeVisitorWithArgument { - DecoderVisitor(this.codecs); +typedef DecoderResult = ({ + String decoder, + String cast, +}); + +class DecoderVisitor extends UnifyingTypeVisitorWithArgument { + DecoderVisitor(this.codecs, this.allowComponent); + final Codecs codecs; + final bool allowComponent; @override - String? visitDartType(DartType type, String argument) { + DecoderResult visitDartType(DartType type, String argument) { if (!type.isDartPrimitive) { throw const InvalidParameterException(); } - return '$argument as ${type.getDisplayString()}'; + return (decoder: argument, cast: type.getDisplayString()); } @override - String? visitInterfaceType(InterfaceType type, String argument) { + DecoderResult visitInterfaceType(InterfaceType type, String argument) { if (type.isDartCoreList) { final nullCheck = type.nullabilitySuffix == NullabilitySuffix.question ? '?' : ''; - final base = '($argument as List$nullCheck)'; if (type.typeArguments.first case DynamicType() || DartType(isDartCoreObject: true, nullabilitySuffix: NullabilitySuffix.question)) { - return base; + return (decoder: argument, cast: 'List$nullCheck'); } final nested = type.typeArguments.first.acceptWithArgument(this, 'i'); - if (RegExp(r'^i as (.*)$').firstMatch(nested ?? '') case final match?) { - return '$base$nullCheck.cast<${match.group(1)!}>()'; - } else { - return '$base$nullCheck.map((i) => $nested).toList()'; + final nestedNullCheck = + !nested.cast.endsWith('?') && type.typeArguments.first.nullabilitySuffix == NullabilitySuffix.question + ? '?' + : ''; + var decoder = argument.endsWith(' as __CAST__') ? '($argument)$nullCheck' : '$argument$nullCheck'; + decoder += '.cast<${nested.cast}$nestedNullCheck>()'; + if (nested.decoder != 'i') { + decoder += '.map((i) => ${nested.decoder}).toList()'; } + return (decoder: decoder, cast: 'List$nullCheck'); } else if (type.isDartCoreMap) { if (!type.typeArguments.first.isDartCoreString) { throw const InvalidParameterException(); } final nullCheck = type.nullabilitySuffix == NullabilitySuffix.question ? '?' : ''; - final base = '($argument as Map$nullCheck)'; if (type.typeArguments[1] case DynamicType() || DartType(isDartCoreObject: true, nullabilitySuffix: NullabilitySuffix.question)) { - return base; + return (decoder: argument, cast: 'Map$nullCheck'); } final nested = type.typeArguments[1].acceptWithArgument(this, 'v'); - if (RegExp(r'^v as (.*)$').firstMatch(nested ?? '') case final match?) { - return '$base$nullCheck.cast()'; - } else { - return '$base$nullCheck.map((k, v) => MapEntry(k, $nested))'; + final nestedNullCheck = + !nested.cast.endsWith('?') && type.typeArguments[1].nullabilitySuffix == NullabilitySuffix.question + ? '?' + : ''; + var decoder = argument.endsWith(' as __CAST__') ? '($argument)$nullCheck' : '$argument$nullCheck'; + decoder += '.cast()'; + if (nested.decoder != 'v') { + decoder += '.map((k, v) => MapEntry(k, ${nested.decoder}))'; } + return (decoder: decoder, cast: 'Map$nullCheck'); } else if (codecs[type.element.name] case final codec?) { + final decoderCall = '[[${codec.import}]].${codec.extension ?? codec.name}.${codec.decoder}'; + final argRaw = argument.endsWith(' as __CAST__') + ? argument.substring(0, argument.length - ' as __CAST__'.length) + : argument; + + final decoder = type.nullabilitySuffix == NullabilitySuffix.question + ? '$argRaw != null ? $decoderCall($argument) : null' + : '$decoderCall($argument)'; + + return (decoder: decoder, cast: codec.rawType); + } else if (allowComponent && + type.element.name == 'Component' && + type.element.library.identifier == 'package:jaspr/src/framework/framework.dart') { if (type.nullabilitySuffix == NullabilitySuffix.question) { - return '$argument != null ? [[${codec.import}]].${codec.extension ?? codec.name}.${codec.decoder}($argument as ${codec.rawType}) : null'; - } else { - return '[[${codec.import}]].${codec.extension ?? codec.name}.${codec.decoder}($argument as ${codec.rawType})'; + return (decoder: 'p.mountOrNull($argument)', cast: 'String?'); } + return (decoder: 'p.mount($argument)', cast: 'String'); } return super.visitInterfaceType(type, argument); } diff --git a/packages/jaspr_builder/lib/src/sync/sync_mixins_builder.dart b/packages/jaspr_builder/lib/src/sync/sync_mixins_builder.dart index 53254d8af..a91cc5e2e 100644 --- a/packages/jaspr_builder/lib/src/sync/sync_mixins_builder.dart +++ b/packages/jaspr_builder/lib/src/sync/sync_mixins_builder.dart @@ -128,11 +128,11 @@ class SyncMixinsBuilder implements Builder { final decoders = fields .map((f) { try { - final decoder = codecs.getDecoderFor(f.type, "value['${f.name}']"); + final decoder = codecs.getDecoderFor(f.type, "value['${f.name}'] as __CAST__", false); return '${f.name} = $decoder;'; } on InvalidParameterException catch (_) { throw UnsupportedError( - 'Fields annotated with @sync must have a primitive serializable type or a type that defines @decoder and @encoder methods. ' + 'Fields annotated with @sync must have a primitive serializable type or a type that defines @decoder and @encoder methods.\n' 'Failing field: [$f] in ${clazz.name}', ); } diff --git a/packages/jaspr_builder/test/client/client_annotation_test.dart b/packages/jaspr_builder/test/client/client_annotation_test.dart index 8f80ff2e1..e7fac9f15 100644 --- a/packages/jaspr_builder/test/client/client_annotation_test.dart +++ b/packages/jaspr_builder/test/client/client_annotation_test.dart @@ -9,6 +9,7 @@ import 'sources/client_basic.dart'; import 'sources/client_invalid.dart'; import 'sources/client_model_class.dart'; import 'sources/client_model_extension.dart'; +import 'sources/client_with_server_components.dart'; void main() { group('client annotation', () { @@ -71,7 +72,7 @@ void main() { expect( errorLog, equals( - 'ClientModuleBuilder on lib/component_basic.dart:\nClient components only support initializing formal constructor parameters. ' + 'ClientModuleBuilder on lib/component_basic.dart:\nClient components only support initializing formal constructor parameters.\n' 'Failing element: Component.new(String a)', ), ); @@ -95,12 +96,24 @@ void main() { expect( errorLog, equals( - 'ClientModuleBuilder on lib/component_basic.dart:\n@client components only support parameters of primitive serializable types or types that define @decoder and @encoder methods. Failing parameter: [DateTime time] in Component.new()', + 'ClientModuleBuilder on lib/component_basic.dart:\n@client components only support parameters of primitive serializable types, Components or types that define @decoder and @encoder methods.\n' + 'Failing parameter: [DateTime time] in Component.new()', ), ); }); }); + group('on component with server components as params', () { + test('generates json module', () async { + await testBuilder( + ClientModuleBuilder(BuilderOptions({})), + clientWithServerComponentsSources, + outputs: {...clientWithServerComponentsModuleOutputs}, + readerWriter: reader, + ); + }); + }); + test('generates bundle', () async { await testBuilder( ClientsBundleBuilder(BuilderOptions({})), @@ -108,6 +121,7 @@ void main() { ...clientBasicModuleOutputs, ...clientModelClassModuleOutputs, ...clientModelExtensionModuleOutputs, + ...clientWithServerComponentsModuleOutputs, }, outputs: clientBundleOutputs, readerWriter: reader, diff --git a/packages/jaspr_builder/test/client/sources/bundle.dart b/packages/jaspr_builder/test/client/sources/bundle.dart index 9de7f901b..b0acc00b5 100644 --- a/packages/jaspr_builder/test/client/sources/bundle.dart +++ b/packages/jaspr_builder/test/client/sources/bundle.dart @@ -1,45 +1,15 @@ import 'dart:convert'; +import 'client_basic.dart'; +import 'client_model_class.dart'; +import 'client_model_extension.dart'; +import 'client_with_server_components.dart'; + final clientBundleOutputs = { 'site|lib/clients.bundle.json': jsonEncode([ - { - 'name': 'Component', - 'id': ['site', 'lib/component_basic.dart'], - 'import': 'package:site/component_basic.dart', - 'params': [ - {'name': 'a', 'isNamed': false, 'decoder': "p['a'] as String", 'encoder': 'c.a'}, - {'name': 'b', 'isNamed': true, 'decoder': "p['b'] as int", 'encoder': 'c.b'}, - {'name': 'c', 'isNamed': true, 'decoder': "p['c'] as double?", 'encoder': 'c.c'}, - {'name': 'd', 'isNamed': true, 'decoder': "p['d'] as bool", 'encoder': 'c.d'}, - ], - }, - { - 'name': 'Component', - 'id': ['site', 'lib/component_model_class.dart'], - 'import': 'package:site/component_model_class.dart', - 'params': [ - {'name': 'a', 'isNamed': false, 'decoder': "p['a'] as String", 'encoder': 'c.a'}, - { - 'name': 'b', - 'isNamed': true, - 'decoder': "[[package:site/model_class.dart]].ModelA.fromRaw(p['b'] as Map)", - 'encoder': 'c.b.toRaw()', - }, - ], - }, - { - 'name': 'Component', - 'id': ['site', 'lib/component_model_extension.dart'], - 'import': 'package:site/component_model_extension.dart', - 'params': [ - {'name': 'a', 'isNamed': false, 'decoder': "p['a'] as String", 'encoder': 'c.a'}, - { - 'name': 'b', - 'isNamed': true, - 'decoder': "[[package:site/model_extension.dart]].ModelBCodec.fromRaw(p['b'] as Map)", - 'encoder': '[[package:site/model_extension.dart]].ModelBCodec(c.b).toRaw()', - }, - ], - }, + clientWithServerComponentsModuleData, + clientBasicModuleData, + clientModelClassModuleData, + clientModelExtensionModuleData, ]), }; diff --git a/packages/jaspr_builder/test/client/sources/client_basic.dart b/packages/jaspr_builder/test/client/sources/client_basic.dart index 5e2f31750..0a708a429 100644 --- a/packages/jaspr_builder/test/client/sources/client_basic.dart +++ b/packages/jaspr_builder/test/client/sources/client_basic.dart @@ -19,16 +19,18 @@ const clientBasicSources = { ''', }; +final clientBasicModuleData = { + 'name': 'Component', + 'id': ['site', 'lib/component_basic.dart'], + 'import': 'package:site/component_basic.dart', + 'params': [ + {'name': 'a', 'isNamed': false, 'decoder': "p.get('a')", 'encoder': 'c.a'}, + {'name': 'b', 'isNamed': true, 'decoder': "p.get('b')", 'encoder': 'c.b'}, + {'name': 'c', 'isNamed': true, 'decoder': "p.get('c')", 'encoder': 'c.c'}, + {'name': 'd', 'isNamed': true, 'decoder': "p.get('d')", 'encoder': 'c.d'}, + ], +}; + final clientBasicModuleOutputs = { - 'site|lib/component_basic.client.module.json': jsonEncode({ - 'name': 'Component', - 'id': ['site', 'lib/component_basic.dart'], - 'import': 'package:site/component_basic.dart', - 'params': [ - {'name': 'a', 'isNamed': false, 'decoder': "p['a'] as String", 'encoder': 'c.a'}, - {'name': 'b', 'isNamed': true, 'decoder': "p['b'] as int", 'encoder': 'c.b'}, - {'name': 'c', 'isNamed': true, 'decoder': "p['c'] as double?", 'encoder': 'c.c'}, - {'name': 'd', 'isNamed': true, 'decoder': "p['d'] as bool", 'encoder': 'c.d'}, - ], - }), + 'site|lib/component_basic.client.module.json': jsonEncode(clientBasicModuleData), }; diff --git a/packages/jaspr_builder/test/client/sources/client_model_class.dart b/packages/jaspr_builder/test/client/sources/client_model_class.dart index 586041ea7..b29863d84 100644 --- a/packages/jaspr_builder/test/client/sources/client_model_class.dart +++ b/packages/jaspr_builder/test/client/sources/client_model_class.dart @@ -23,19 +23,21 @@ final clientModelClassSources = { ...codecBundleOutputs, }; +final clientModelClassModuleData = { + 'name': 'Component', + 'id': ['site', 'lib/component_model_class.dart'], + 'import': 'package:site/component_model_class.dart', + 'params': [ + {'name': 'a', 'isNamed': false, 'decoder': "p.get('a')", 'encoder': 'c.a'}, + { + 'name': 'b', + 'isNamed': true, + 'decoder': "[[package:site/model_class.dart]].ModelA.fromRaw(p.get>('b'))", + 'encoder': 'c.b.toRaw()', + }, + ], +}; + final clientModelClassModuleOutputs = { - 'site|lib/component_model_class.client.module.json': jsonEncode({ - 'name': 'Component', - 'id': ['site', 'lib/component_model_class.dart'], - 'import': 'package:site/component_model_class.dart', - 'params': [ - {'name': 'a', 'isNamed': false, 'decoder': "p['a'] as String", 'encoder': 'c.a'}, - { - 'name': 'b', - 'isNamed': true, - 'decoder': "[[package:site/model_class.dart]].ModelA.fromRaw(p['b'] as Map)", - 'encoder': 'c.b.toRaw()', - }, - ], - }), + 'site|lib/component_model_class.client.module.json': jsonEncode(clientModelClassModuleData), }; diff --git a/packages/jaspr_builder/test/client/sources/client_model_extension.dart b/packages/jaspr_builder/test/client/sources/client_model_extension.dart index 5e13e85d6..37443bf7a 100644 --- a/packages/jaspr_builder/test/client/sources/client_model_extension.dart +++ b/packages/jaspr_builder/test/client/sources/client_model_extension.dart @@ -23,19 +23,21 @@ final clientModelExtensionSources = { ...codecBundleOutputs, }; +final clientModelExtensionModuleData = { + 'name': 'Component', + 'id': ['site', 'lib/component_model_extension.dart'], + 'import': 'package:site/component_model_extension.dart', + 'params': [ + {'name': 'a', 'isNamed': false, 'decoder': "p.get('a')", 'encoder': 'c.a'}, + { + 'name': 'b', + 'isNamed': true, + 'decoder': "[[package:site/model_extension.dart]].ModelBCodec.fromRaw(p.get>('b'))", + 'encoder': '[[package:site/model_extension.dart]].ModelBCodec(c.b).toRaw()', + }, + ], +}; + final clientModelExtensionModuleOutputs = { - 'site|lib/component_model_extension.client.module.json': jsonEncode({ - 'name': 'Component', - 'id': ['site', 'lib/component_model_extension.dart'], - 'import': 'package:site/component_model_extension.dart', - 'params': [ - {'name': 'a', 'isNamed': false, 'decoder': "p['a'] as String", 'encoder': 'c.a'}, - { - 'name': 'b', - 'isNamed': true, - 'decoder': "[[package:site/model_extension.dart]].ModelBCodec.fromRaw(p['b'] as Map)", - 'encoder': '[[package:site/model_extension.dart]].ModelBCodec(c.b).toRaw()', - }, - ], - }), + 'site|lib/component_model_extension.client.module.json': jsonEncode(clientModelExtensionModuleData), }; diff --git a/packages/jaspr_builder/test/client/sources/client_with_server_components.dart b/packages/jaspr_builder/test/client/sources/client_with_server_components.dart new file mode 100644 index 000000000..77169118f --- /dev/null +++ b/packages/jaspr_builder/test/client/sources/client_with_server_components.dart @@ -0,0 +1,45 @@ +import 'dart:convert'; + +const clientWithServerComponentsSources = { + 'site|lib/client_with_sc.dart': ''' + import 'package:jaspr/jaspr.dart'; + + @client + class ClientWithSC extends StatelessComponent { + ClientWithSC({required this.child, this.children = const [], required this.childrenMap = const {}, super.key}); + + final Component child; + final List children; + final Map childrenMap; + + @override + Component build(BuildContext context) => text(''); + } + ''', +}; + +final clientWithServerComponentsModuleData = { + 'name': 'ClientWithSC', + 'id': ['site', 'lib/client_with_sc.dart'], + 'import': 'package:site/client_with_sc.dart', + 'params': [ + {'name': 'child', 'isNamed': true, 'decoder': "p.mount(p.get('child'))", 'encoder': 'c.child'}, + { + 'name': 'children', + 'isNamed': true, + 'decoder': "p.get>('children').cast().map((i) => p.mount(i)).toList()", + 'encoder': 'c.children', + }, + { + 'name': 'childrenMap', + 'isNamed': true, + 'decoder': + "p.get>('childrenMap').cast().map((k, v) => MapEntry(k, p.mount(v)))", + 'encoder': 'c.childrenMap', + }, + ], +}; + +final clientWithServerComponentsModuleOutputs = { + 'site|lib/client_with_sc.client.module.json': jsonEncode(clientWithServerComponentsModuleData), +}; diff --git a/packages/jaspr_builder/test/options/client_options_test.dart b/packages/jaspr_builder/test/options/client_options_test.dart index 4d5c5a8cd..d4ccb85ed 100644 --- a/packages/jaspr_builder/test/options/client_options_test.dart +++ b/packages/jaspr_builder/test/options/client_options_test.dart @@ -7,7 +7,7 @@ import 'sources/client_options.dart'; import 'sources/client_options_other.dart'; void main() { - group('jaspr options builder', () { + group('client options builder', () { late TestReaderWriter reader; setUp(() async { diff --git a/packages/jaspr_builder/test/options/server_options_test.dart b/packages/jaspr_builder/test/options/server_options_test.dart index 9a79a4013..fc5f390fc 100644 --- a/packages/jaspr_builder/test/options/server_options_test.dart +++ b/packages/jaspr_builder/test/options/server_options_test.dart @@ -8,7 +8,7 @@ import 'sources/server_options_other.dart'; import 'sources/server_options_standalone.dart'; void main() { - group('jaspr options builder', () { + group('server options builder', () { late TestReaderWriter reader; setUp(() async { diff --git a/packages/jaspr_builder/test/options/sources/client_options.dart b/packages/jaspr_builder/test/options/sources/client_options.dart index 749ddeb3c..7908c2c6a 100644 --- a/packages/jaspr_builder/test/options/sources/client_options.dart +++ b/packages/jaspr_builder/test/options/sources/client_options.dart @@ -9,6 +9,7 @@ final clientOptionsSources = { import 'package:site/component_basic.dart'; // import 'package:site/component_model_class.dart'; import 'package:site/component_model_extension.dart'; + import 'package:site/client_with_sc.dart'; void main() {} ''', @@ -28,6 +29,7 @@ final clientOptionsOutputs = { '\n' 'import \'package:jaspr/client.dart\';\n' '\n' + 'import \'package:site/client_with_sc.dart\' deferred as _client_with_sc;\n' 'import \'package:site/component_basic.dart\' deferred as _component_basic;\n' 'import \'package:site/component_model_extension.dart\'\n' ' deferred as _component_model_extension;\n' @@ -51,19 +53,36 @@ final clientOptionsOutputs = { '/// ```\n' 'ClientOptions get defaultClientOptions => ClientOptions(\n' ' clients: {\n' + ' \'client_with_sc\': ClientLoader(\n' + ' (p) => _client_with_sc.ClientWithSC(\n' + ' child: p.mount(p.get(\'child\')),\n' + ' children: p\n' + ' .get>(\'children\')\n' + ' .cast()\n' + ' .map((i) => p.mount(i))\n' + ' .toList(),\n' + ' childrenMap: p\n' + ' .get>(\'childrenMap\')\n' + ' .cast()\n' + ' .map((k, v) => MapEntry(k, p.mount(v))),\n' + ' ),\n' + ' loader: _client_with_sc.loadLibrary,\n' + ' ),\n' ' \'component_basic\': ClientLoader(\n' ' (p) => _component_basic.Component(\n' - ' p[\'a\'] as String,\n' - ' b: p[\'b\'] as int,\n' - ' c: p[\'c\'] as double?,\n' - ' d: p[\'d\'] as bool,\n' + ' p.get(\'a\'),\n' + ' b: p.get(\'b\'),\n' + ' c: p.get(\'c\'),\n' + ' d: p.get(\'d\'),\n' ' ),\n' ' loader: _component_basic.loadLibrary,\n' ' ),\n' ' \'component_model_extension\': ClientLoader(\n' ' (p) => _component_model_extension.Component(\n' - ' p[\'a\'] as String,\n' - ' b: _model_extension.ModelBCodec.fromRaw(p[\'b\'] as Map),\n' + ' p.get(\'a\'),\n' + ' b: _model_extension.ModelBCodec.fromRaw(\n' + ' p.get>(\'b\'),\n' + ' ),\n' ' ),\n' ' loader: _component_model_extension.loadLibrary,\n' ' ),\n' diff --git a/packages/jaspr_builder/test/options/sources/client_options_other.dart b/packages/jaspr_builder/test/options/sources/client_options_other.dart index 1811f1b30..f3aa60060 100644 --- a/packages/jaspr_builder/test/options/sources/client_options_other.dart +++ b/packages/jaspr_builder/test/options/sources/client_options_other.dart @@ -21,6 +21,7 @@ final clientOptionsOtherOutputs = { '\n' 'import \'package:jaspr/client.dart\';\n' '\n' + 'import \'package:site/client_with_sc.dart\' deferred as _client_with_sc;\n' 'import \'package:site/component_basic.dart\' deferred as _component_basic;\n' 'import \'package:site/component_model_class.dart\'\n' ' deferred as _component_model_class;\n' @@ -47,26 +48,43 @@ final clientOptionsOtherOutputs = { '/// ```\n' 'ClientOptions get defaultClientOptions => ClientOptions(\n' ' clients: {\n' + ' \'client_with_sc\': ClientLoader(\n' + ' (p) => _client_with_sc.ClientWithSC(\n' + ' child: p.mount(p.get(\'child\')),\n' + ' children: p\n' + ' .get>(\'children\')\n' + ' .cast()\n' + ' .map((i) => p.mount(i))\n' + ' .toList(),\n' + ' childrenMap: p\n' + ' .get>(\'childrenMap\')\n' + ' .cast()\n' + ' .map((k, v) => MapEntry(k, p.mount(v))),\n' + ' ),\n' + ' loader: _client_with_sc.loadLibrary,\n' + ' ),\n' ' \'component_basic\': ClientLoader(\n' ' (p) => _component_basic.Component(\n' - ' p[\'a\'] as String,\n' - ' b: p[\'b\'] as int,\n' - ' c: p[\'c\'] as double?,\n' - ' d: p[\'d\'] as bool,\n' + ' p.get(\'a\'),\n' + ' b: p.get(\'b\'),\n' + ' c: p.get(\'c\'),\n' + ' d: p.get(\'d\'),\n' ' ),\n' ' loader: _component_basic.loadLibrary,\n' ' ),\n' ' \'component_model_class\': ClientLoader(\n' ' (p) => _component_model_class.Component(\n' - ' p[\'a\'] as String,\n' - ' b: _model_class.ModelA.fromRaw(p[\'b\'] as Map),\n' + ' p.get(\'a\'),\n' + ' b: _model_class.ModelA.fromRaw(p.get>(\'b\')),\n' ' ),\n' ' loader: _component_model_class.loadLibrary,\n' ' ),\n' ' \'component_model_extension\': ClientLoader(\n' ' (p) => _component_model_extension.Component(\n' - ' p[\'a\'] as String,\n' - ' b: _model_extension.ModelBCodec.fromRaw(p[\'b\'] as Map),\n' + ' p.get(\'a\'),\n' + ' b: _model_extension.ModelBCodec.fromRaw(\n' + ' p.get>(\'b\'),\n' + ' ),\n' ' ),\n' ' loader: _component_model_extension.loadLibrary,\n' ' ),\n' diff --git a/packages/jaspr_builder/test/sync/sources/sync_model_class.dart b/packages/jaspr_builder/test/sync/sources/sync_model_class.dart index 728c818e8..9328316d4 100644 --- a/packages/jaspr_builder/test/sync/sources/sync_model_class.dart +++ b/packages/jaspr_builder/test/sync/sources/sync_model_class.dart @@ -72,27 +72,22 @@ final syncModelClassOutputs = { ' : null;\n' ' m2 = _model_class.ModelA.fromRaw(value[\'m2\'] as Map);\n' ' m3 = (value[\'m3\'] as List)\n' - ' .map((i) => _model_class.ModelA.fromRaw(i as Map))\n' + ' .cast>()\n' + ' .map((i) => _model_class.ModelA.fromRaw(i))\n' ' .toList();\n' ' m4 = (value[\'m4\'] as List)\n' - ' .map(\n' - ' (i) => i != null\n' - ' ? _model_class.ModelA.fromRaw(i as Map)\n' - ' : null,\n' - ' )\n' + ' .cast?>()\n' + ' .map((i) => i != null ? _model_class.ModelA.fromRaw(i) : null)\n' ' .toList();\n' - ' m5 = (value[\'m5\'] as Map).map(\n' - ' (k, v) =>\n' - ' MapEntry(k, _model_class.ModelA.fromRaw(v as Map)),\n' - ' );\n' - ' m6 = (value[\'m6\'] as Map).map(\n' - ' (k, v) => MapEntry(\n' - ' k,\n' - ' v != null\n' - ' ? _model_class.ModelA.fromRaw(v as Map)\n' - ' : null,\n' - ' ),\n' - ' );\n' + ' m5 = (value[\'m5\'] as Map)\n' + ' .cast>()\n' + ' .map((k, v) => MapEntry(k, _model_class.ModelA.fromRaw(v)));\n' + ' m6 = (value[\'m6\'] as Map)\n' + ' .cast?>()\n' + ' .map(\n' + ' (k, v) =>\n' + ' MapEntry(k, v != null ? _model_class.ModelA.fromRaw(v) : null),\n' + ' );\n' ' }\n' '\n' ' @override\n' diff --git a/packages/jaspr_builder/test/sync/sources/sync_model_extension.dart b/packages/jaspr_builder/test/sync/sources/sync_model_extension.dart index c462880d0..c384c35b0 100644 --- a/packages/jaspr_builder/test/sync/sources/sync_model_extension.dart +++ b/packages/jaspr_builder/test/sync/sources/sync_model_extension.dart @@ -77,32 +77,24 @@ final syncModelExtensionOutputs = { ' value[\'m2\'] as Map,\n' ' );\n' ' m3 = (value[\'m3\'] as List)\n' - ' .map(\n' - ' (i) =>\n' - ' _model_extension.ModelBCodec.fromRaw(i as Map),\n' - ' )\n' + ' .cast>()\n' + ' .map((i) => _model_extension.ModelBCodec.fromRaw(i))\n' ' .toList();\n' ' m4 = (value[\'m4\'] as List)\n' - ' .map(\n' - ' (i) => i != null\n' - ' ? _model_extension.ModelBCodec.fromRaw(i as Map)\n' - ' : null,\n' - ' )\n' + ' .cast?>()\n' + ' .map((i) => i != null ? _model_extension.ModelBCodec.fromRaw(i) : null)\n' ' .toList();\n' - ' m5 = (value[\'m5\'] as Map).map(\n' - ' (k, v) => MapEntry(\n' - ' k,\n' - ' _model_extension.ModelBCodec.fromRaw(v as Map),\n' - ' ),\n' - ' );\n' - ' m6 = (value[\'m6\'] as Map).map(\n' - ' (k, v) => MapEntry(\n' - ' k,\n' - ' v != null\n' - ' ? _model_extension.ModelBCodec.fromRaw(v as Map)\n' - ' : null,\n' - ' ),\n' - ' );\n' + ' m5 = (value[\'m5\'] as Map)\n' + ' .cast>()\n' + ' .map((k, v) => MapEntry(k, _model_extension.ModelBCodec.fromRaw(v)));\n' + ' m6 = (value[\'m6\'] as Map)\n' + ' .cast?>()\n' + ' .map(\n' + ' (k, v) => MapEntry(\n' + ' k,\n' + ' v != null ? _model_extension.ModelBCodec.fromRaw(v) : null,\n' + ' ),\n' + ' );\n' ' }\n' '\n' ' @override\n' diff --git a/packages/jaspr_builder/test/sync/sync_annotation_test.dart b/packages/jaspr_builder/test/sync/sync_annotation_test.dart index 9c935639f..ce775eacd 100644 --- a/packages/jaspr_builder/test/sync/sync_annotation_test.dart +++ b/packages/jaspr_builder/test/sync/sync_annotation_test.dart @@ -81,7 +81,8 @@ void main() { expect( errorLog, equals( - 'SyncMixinsBuilder on lib/component.dart:\nFields annotated with @sync must have a primitive serializable type or a type that defines @decoder and @encoder methods. Failing field: [Map a] in ComponentState', + 'SyncMixinsBuilder on lib/component.dart:\nFields annotated with @sync must have a primitive serializable type or a type that defines @decoder and @encoder methods.\n' + 'Failing field: [Map a] in ComponentState', ), ); }); @@ -104,7 +105,8 @@ void main() { expect( errorLog, equals( - 'SyncMixinsBuilder on lib/component.dart:\nFields annotated with @sync must have a primitive serializable type or a type that defines @decoder and @encoder methods. Failing field: [DateTime time] in ComponentState', + 'SyncMixinsBuilder on lib/component.dart:\nFields annotated with @sync must have a primitive serializable type or a type that defines @decoder and @encoder methods.\n' + 'Failing field: [DateTime time] in ComponentState', ), ); }); diff --git a/packages/jaspr_cli/lib/src/commands/dev_command.dart b/packages/jaspr_cli/lib/src/commands/dev_command.dart index 8acc08d55..2e27b5f72 100644 --- a/packages/jaspr_cli/lib/src/commands/dev_command.dart +++ b/packages/jaspr_cli/lib/src/commands/dev_command.dart @@ -32,10 +32,11 @@ abstract class DevCommand extends BaseCommand with ProxyHelper, FlutterHelper { 'mode', abbr: 'm', help: 'Sets the reload/refresh mode.', - allowed: ['reload', 'refresh'], + allowed: ['reload', 'refresh', 'none'], allowedHelp: { 'reload': 'Reloads js modules without server reload (loses current state)', 'refresh': 'Performs a full page refresh and server reload', + 'none': 'Does not perform any reloads', }, defaultsTo: 'refresh', ); @@ -355,13 +356,19 @@ abstract class DevCommand extends BaseCommand with ProxyHelper, FlutterHelper { ], ]; + final reload = switch (mode) { + 'reload' => ReloadConfiguration.hotRestart, + 'refresh' => ReloadConfiguration.liveReload, + _ => ReloadConfiguration.none, + }; + final workflow = await ClientWorkflow.start( proxyPort, buildArgs, logger, guardResource, enableDebugging: launchInChrome, - reload: mode == 'reload' ? ReloadConfiguration.hotRestart : ReloadConfiguration.liveReload, + reload: reload, ); if (workflow == null) { return null; diff --git a/packages/jaspr_content/lib/components/_internal/zoomable_image.dart b/packages/jaspr_content/lib/components/_internal/zoomable_image.dart index c249c0691..aef37006b 100644 --- a/packages/jaspr_content/lib/components/_internal/zoomable_image.dart +++ b/packages/jaspr_content/lib/components/_internal/zoomable_image.dart @@ -50,14 +50,10 @@ class ZoomableImage extends StatefulComponent { } class _ZoomableImageState extends State with ViewTransitionMixin { - static int _dialogCount = 0; - - late final HTMLDialogElement dialog; + final GlobalNodeKey dialogKey = GlobalNodeKey(); final GlobalNodeKey imageKey = GlobalNodeKey(); - StreamSubscription? _cancelSub; StreamSubscription? _resizeSub; - void Function(void Function())? dialogSetState; bool zoomed = false; bool isResize = false; @@ -71,27 +67,7 @@ class _ZoomableImageState extends State with ViewTransitionMixin if (kIsWeb) { context.binding.addPostFrameCallback(() { - dialog = HTMLDialogElement() - ..id = 'dzm${_dialogCount++}' - ..className = 'zoom-modal'; - window.document.body!.appendChild(dialog); - - _cancelSub = EventStreamProviders.cancelEvent.forTarget(dialog).listen((e) { - e.preventDefault(); - zoomOut(); - }); - updateImageOffset(false); - - (runApp as dynamic)( - StatefulBuilder( - builder: (context, setState) { - dialogSetState = setState; - return _buildDialog(context); - }, - ), - attachTo: '#${dialog.id}', - ); }); _resizeSub = EventStreamProviders.resizeEvent.forTarget(window).listen((_) { @@ -128,89 +104,88 @@ class _ZoomableImageState extends State with ViewTransitionMixin ); this.isResize = isResize; }); - dialogSetState?.call(() {}); } void zoomIn() { updateImageOffset(false); - setState(() { - zoomed = true; - }); - dialogSetState?.call(() {}); + setState(() => zoomed = true); document.body!.style.overflow = 'hidden'; - dialog.showModal(); + dialogKey.currentNode?.showModal(); } void zoomOut() async { - setState(() { - zoomed = false; - }); - dialogSetState?.call(() {}); + setState(() => zoomed = false); await Future.delayed(const Duration(milliseconds: 300)); if (zoomed) return; - dialog.close(); document.body!.style.overflow = 'auto'; + dialogKey.currentNode?.close(); } @override void dispose() { - _cancelSub?.cancel(); _resizeSub?.cancel(); - dialog.remove(); super.dispose(); } @override Component build(BuildContext context) { - return figure(classes: 'image zoomable', [ - img( - key: imageKey, - src: component.src, - alt: component.alt ?? component.caption, - styles: zoomed ? Styles(visibility: Visibility.hidden) : null, - events: events( - onClick: () { - zoomIn(); - }, + return Component.fragment([ + figure(classes: 'image zoomable', [ + img( + key: imageKey, + src: component.src, + alt: component.alt ?? component.caption, + styles: zoomed ? Styles(visibility: Visibility.hidden) : null, + events: events( + onClick: () { + zoomIn(); + }, + ), ), - ), - if (component.caption != null) figcaption([Component.text(component.caption!)]), + if (component.caption != null) figcaption([Component.text(component.caption!)]), + ]), + _buildDialog(context), ]); } Component _buildDialog(BuildContext context) { - return div( - classes: 'image-wrapper', + return dialog( + key: dialogKey, + classes: 'zoom-modal not-content', + open: dialogKey.currentNode?.open ?? false, events: { - 'click': (_) { - zoomOut(); - }, - 'wheel': (_) { + 'cancel': (e) { + e.preventDefault(); zoomOut(); }, }, [ - img( - src: component.src, - alt: component.alt ?? component.caption, - styles: Styles( - position: Position.absolute(top: sourceOffset.y.px, left: sourceOffset.x.px), - width: targetOffset.width.px, - height: targetOffset.height.px, - transform: Transform.combine( - zoomed - ? [ - Transform.translate( - x: (-sourceOffset.x + targetOffset.x).px, - y: (-sourceOffset.y + targetOffset.y).px, - ), - Transform.scale(1), - ] - : [Transform.translate(x: 0.px, y: 0.px), Transform.scale(sourceOffset.scale)], + div( + classes: 'image-wrapper', + events: { + 'click': (_) => zoomOut(), + 'wheel': (_) => zoomOut(), + }, + [ + img( + src: component.src, + alt: component.alt ?? component.caption, + styles: Styles( + position: Position.absolute(top: sourceOffset.y.px, left: sourceOffset.x.px), + width: targetOffset.width.px, + height: targetOffset.height.px, + transform: Transform.combine([ + Transform.translate( + x: zoomed ? (-sourceOffset.x + targetOffset.x).px : 0.px, + y: zoomed ? (-sourceOffset.y + targetOffset.y).px : 0.px, + ), + Transform.scale(zoomed ? 1 : sourceOffset.scale), + ]), + raw: {if (isResize) 'transition': 'none'}, + ), ), - raw: {if (isResize) 'transition': 'none'}, - ), + ], ), ], ); diff --git a/packages/jaspr_content/lib/src/route_loader/route_loader.dart b/packages/jaspr_content/lib/src/route_loader/route_loader.dart index 9e633c38c..03e35db17 100644 --- a/packages/jaspr_content/lib/src/route_loader/route_loader.dart +++ b/packages/jaspr_content/lib/src/route_loader/route_loader.dart @@ -167,6 +167,7 @@ abstract class RouteLoaderBase implements RouteLoader { void invalidateSource(T source, {bool rebuild = true}) { source.invalidate(rebuild: rebuild); + ServerApp.reloadClients(source.url); } void invalidateRoutes() { diff --git a/packages/jaspr_test/lib/src/testers/client_tester.dart b/packages/jaspr_test/lib/src/testers/client_tester.dart index 5f5821ccf..65f2abfdf 100644 --- a/packages/jaspr_test/lib/src/testers/client_tester.dart +++ b/packages/jaspr_test/lib/src/testers/client_tester.dart @@ -4,6 +4,8 @@ import 'dart:js_interop'; import 'package:jaspr/client.dart'; // ignore: implementation_imports import 'package:jaspr/src/dom/type_checks.dart'; +// ignore: implementation_imports, invalid_use_of_visible_for_testing_member +import 'package:jaspr/src/framework/framework.dart' show GlobalComponentsBinding; import 'package:meta/meta.dart'; import 'package:test/test.dart'; import 'package:universal_web/web.dart' as web; @@ -36,6 +38,9 @@ void testClient( // Clear all nodes web.document.body?.replaceChildren([].toJS); + + // ignore: invalid_use_of_visible_for_testing_member + GlobalComponentsBinding.clearGlobalKeys(); }, skip: skip, timeout: timeout, diff --git a/pubspec.lock b/pubspec.lock index 2e767f70f..4d7c9c00b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1171,10 +1171,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mime: dependency: transitive description: @@ -1824,26 +1824,26 @@ packages: dependency: transitive description: name: test - sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" + sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20" url: "https://pub.dev" source: hosted - version: "1.30.0" + version: "1.31.0" test_api: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" test_core: dependency: transitive description: name: test_core - sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" + sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34" url: "https://pub.dev" source: hosted - version: "0.6.16" + version: "0.6.17" test_reflective_loader: dependency: transitive description: