Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: render adapters #174

Merged
merged 6 commits into from
Feb 17, 2024
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
4 changes: 3 additions & 1 deletion packages/jaspr/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## Unreleased patch
## Unreleased minor

- Fixed bug with `DomValidator`.
- `Document` is no longer required when using server-side rendering.
- Improved how `@client` components are hydrated.

## 0.10.0

Expand Down
17 changes: 8 additions & 9 deletions packages/jaspr/lib/src/browser/browser_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import '../framework/framework.dart';
import 'dom_render_object.dart';
import 'js_data.dart';

final _queryReg = RegExp(r'^(.*?)(?:\((\d+):(\d+)\))?$');

/// Global component binding for the browser
class BrowserAppBinding extends AppBinding with ComponentsBinding {
@override
Expand All @@ -21,22 +19,23 @@ class BrowserAppBinding extends AppBinding with ComponentsBinding {
Uri get currentUri => Uri.parse(window.location.href.substring(window.location.origin.length));

late String attachTarget;
late (Node, Node)? attachBetween;

@override
Future<void> attachRootComponent(Component app, {String attachTo = 'body'}) {
Future<void> attachRootComponent(Component app, {String attachTo = 'body', (Node, Node)? attachBetween}) {
_loadRawState();
attachTarget = attachTo;
this.attachBetween = attachBetween;
return super.attachRootComponent(app);
}

@override
RenderObject createRootRenderObject() {
var attachMatch = _queryReg.firstMatch(attachTarget)!;
var target = attachMatch.group(1)!;
var from = int.tryParse(attachMatch.group(2) ?? '');
var to = int.tryParse(attachMatch.group(3) ?? '');

return RootDomRenderObject(document.querySelector(target)!, from, to);
if (attachBetween case (var start, var end)) {
return RootDomRenderObject.between(start, end);
} else {
return RootDomRenderObject(document.querySelector(attachTarget)!);
}
}

final Map<String, dynamic> _rawState = {};
Expand Down
56 changes: 42 additions & 14 deletions packages/jaspr/lib/src/browser/clients.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import 'dart:async';
import 'dart:convert';
import 'dart:html';

import '../framework/framework.dart';
import 'js_data.dart';
import 'run_app.dart';
import 'browser_binding.dart';

typedef ClientLoader = FutureOr<ClientBuilder> Function();
typedef ClientBuilder = Component Function(ConfigParams);
Expand Down Expand Up @@ -40,25 +41,52 @@ ClientLoader loadClient(Future<void> Function() loader, ClientBuilder builder) {
return () => loader().then((_) => builder);
}

void _runClient(ClientBuilder builder, ConfigParams params, String id) {
runApp(builder(params), attachTo: id);
void _runClient(ClientBuilder builder, ConfigParams params, (Node, Node) between) {
BrowserAppBinding().attachRootComponent(builder(params), attachBetween: between);
}

final _compStartRegex = RegExp(r'^\s*\$(\S+)(?:\s+data=(.*))?\s*$');
final _compEndRegex = RegExp(r'^\s*/\$(\S+)\s*$');

void _applyClients(FutureOr<ClientBuilder> Function(String) fn) {
var comps = jasprConfig.comp;
if (comps == null) return;
var iterator = NodeIterator(document, NodeFilter.SHOW_COMMENT);

for (var comp in comps) {
var builder = fn(comp.name);
if (builder is ClientBuilder) {
_runClient(builder, ConfigParams(comp.params), comp.id);
} else {
builder.then((b) => _runClient(b, ConfigParams(comp.params), comp.id));
List<(String, String?, Node)> nodes = [];

Comment? currNode;
while ((currNode = iterator.nextNode() as Comment?) != null) {
var value = currNode!.nodeValue ?? '';
var match = _compStartRegex.firstMatch(value);
if (match != null) {
var name = match.group(1)!;
var data = match.group(2);

nodes.add((name, data, currNode));
}

match = _compEndRegex.firstMatch(value);
if (match != null) {
var name = match.group(1)!;

if (nodes.last.$1 == name) {
var comp = nodes.removeLast();
var start = comp.$3;
assert(start.parentNode == currNode.parentNode);

var between = (start, currNode);
var params = ConfigParams(jsonDecode(comp.$2 ?? '{}'));

var builder = fn(name);
if (builder is ClientBuilder) {
_runClient(builder, params, between);
} else {
builder.then((b) => _runClient(b, params, between));
}
}
}
}
}

void runAppWithParams(ClientBuilder appBuilder) {
var appConfig = jasprConfig.comp?.firstOrNull;
_runClient(appBuilder, ConfigParams(appConfig?.params ?? {}), appConfig?.id ?? 'body');
_applyClients((_) => appBuilder);
}
34 changes: 18 additions & 16 deletions packages/jaspr/lib/src/browser/dom_render_object.dart
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,8 @@ class DomRenderObject extends RenderObject {
if (childNode == null) return;

var afterNode = after?.node;
if ((afterNode, parent) case (null, RootDomRenderObject p) when p.from != null && p.from! > 0) {
afterNode = p.container.childNodes[p.from! - 1];
if (afterNode == null && parent is RootDomRenderObject) {
afterNode = parent.beforeStart;
}

if (childNode.previousNode == afterNode && childNode.parentNode == parentNode) {
Expand Down Expand Up @@ -273,21 +273,23 @@ class DomRenderObject extends RenderObject {
}

class RootDomRenderObject extends DomRenderObject {
final html.Element container;
final int? from;
final int? to;

RootDomRenderObject(this.container, this.from, this.to) {
Iterable<Node> nodes = container.nodes;
if (kDebugMode) {
nodes = nodes.where((node) => node is! html.Text || (node.text ?? '').trim().isNotEmpty);
}
nodes = nodes.skip(from ?? 0);
if (to != null) {
nodes = nodes.take(to! - (from ?? 0));
}
final Node container;
late final Node? beforeStart;

RootDomRenderObject(this.container, [List<Node>? nodes]) {
node = container;
toHydrate = nodes.toList();
toHydrate = nodes ?? container.nodes;
beforeStart = toHydrate.firstOrNull?.previousNode;
}

factory RootDomRenderObject.between(Node start, Node end) {
var nodes = <Node>[];
Node? curr = start.nextNode;
while (curr != null && curr != end) {
nodes.add(curr);
curr = curr.nextNode;
}
return RootDomRenderObject(start.parentNode!, nodes);
}
}

Expand Down
4 changes: 0 additions & 4 deletions packages/jaspr/lib/src/browser/js_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ class JasprConfig {
Map<String, dynamic>? get sync {
return decodeConfig(_config?.sync);
}

List<ComponentConfig>? get comp {
return _config?.comps?.map((c) => ComponentConfig._(c)).toList();
}
}

class ComponentConfig {
Expand Down
8 changes: 6 additions & 2 deletions packages/jaspr/lib/src/foundation/options.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:convert';

import '../framework/framework.dart';

/// Main class for initializing the jaspr framework.
Expand Down Expand Up @@ -34,7 +36,9 @@ class ClientTarget<T extends Component> {

const ClientTarget(this.name, {this.params});

Map<String, dynamic> encode(T component) {
return {'name': name, if (params != null) 'params': params!(component)};
String dataFor(T component) {
if (params == null) return '';

return 'data=${HtmlEscape(HtmlEscapeMode(escapeLtGt: true)).convert(jsonEncode(params!(component)))}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import '../../framework/framework.dart';
import '../child_nodes.dart';
import '../markup_render_object.dart';
import '../server_binding.dart';
import 'client_script_adapter.dart';
import 'element_boundary_adapter.dart';

class ClientComponentAdapter extends ElementBoundaryAdapter {
ClientComponentAdapter(this.name, this.data, super.element);

final String name;
final String data;

@override
void processBoundary(ChildListRange range) {
range.start.insertNext(ChildNodeData(MarkupRenderObject()..updateText('<!-- \$$name $data -->', true)));
range.end.insertPrev(ChildNodeData(MarkupRenderObject()..updateText('<!-- /\$$name -->', true)));
}
}

class ClientComponentRegistry extends ObserverComponent {
ClientComponentRegistry({required super.child, super.key});

@override
ObserverElement createElement() => ClientComponentRegistryElement(this);
}

class ClientComponentRegistryElement extends ObserverElement {
ClientComponentRegistryElement(super.component);

bool _didAddClientScript = false;
final List<Element> _clientElements = [];

@override
void willRebuildElement(Element element) {
var binding = this.binding as ServerAppBinding;

if (!_didAddClientScript) {
(binding).addRenderAdapter(ClientScriptAdapter(binding, _clientElements));
_didAddClientScript = true;
}

var entry = binding.options.targets[element.component.runtimeType];

if (entry == null) {
return;
}

var isClientBoundary = true;
element.visitAncestorElements((e) {
return isClientBoundary = !_clientElements.contains(e);
});

if (!isClientBoundary) {
return;
}

_clientElements.add(element);
binding.addRenderAdapter(ClientComponentAdapter(entry.name, entry.dataFor(element.component), element));
}

@override
void didRebuildElement(Element element) {}

@override
void didUnmountElement(Element element) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import '../../framework/framework.dart';
import '../markup_render_object.dart';
import '../server_binding.dart';
import 'head_scope_adapter.dart';

class ClientScriptAdapter extends HeadScopeAdapter {
ClientScriptAdapter(this.binding, this.clientElements);

final ServerAppBinding binding;
final List<Element> clientElements;

@override
void applyHead(MarkupRenderObject head) {
if (clientElements.isEmpty) {
return;
}

String source;
if (clientElements.length == 1) {
var entry = binding.options.targets[clientElements.first.component.runtimeType]!;
source = '${entry.name}.client';
} else {
source = 'main.clients';
}

head.children.insertBefore(
head.createChildRenderObject()
..updateElement('script', null, null, null, {'src': '$source.dart.js', 'defer': ''}, null),
);
}
}
50 changes: 50 additions & 0 deletions packages/jaspr/lib/src/server/adapters/document_adapter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import '../markup_render_object.dart';
import '../server_binding.dart';

class DocumentAdapter extends RenderAdapter {
@override
void prepare() {}

@override
void apply(MarkupRenderObject root) {
var html = root.children.findWhere((c) => c.tag == 'html')?.node;
if (html == null) {
var range = root.children.range();
root.children.insertAfter(
html = root.createChildRenderObject()
..tag = 'html'
..children.insertRangeAfter(range),
);
}

var head = html.children.findWhere((c) => c.tag == 'head');
var body = html.children.findWhere((c) => c.tag == 'body');

if (body == null && head == null) {
var range = html.children.range();
html.children.insertAfter(html.createChildRenderObject()..tag = 'head');
html.children.insertBefore(html.createChildRenderObject()
..tag = 'body'
..children.insertRangeAfter(range));
} else if (body != null && head == null) {
html.children.insertAfter(html.createChildRenderObject()..tag = 'head');
} else if (body == null && head != null) {
var rangeBefore = html.children.range(endBefore: head);
var rangeAfter = html.children.range(startAfter: head);

var body = html.createChildRenderObject()..tag = 'body';
body.children
..insertRangeAfter(rangeAfter)
..insertRangeAfter(rangeBefore);
html.children.insertAfter(body, after: head.node);
}

var hasDoctype =
root.children.findWhere((r) => r.text != null && r.text!.startsWith('<!DOCTYPE') && (r.rawHtml ?? false)) !=
null;

if (!hasDoctype) {
root.children.insertAfter(root.createChildRenderObject()..updateText('<!DOCTYPE html>', true));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'dart:async';

import '../../framework/framework.dart';
import '../child_nodes.dart';
import '../markup_render_object.dart';
import '../server_binding.dart';

abstract class ElementBoundaryAdapter extends RenderAdapter {
ElementBoundaryAdapter(this.element);

final Element element;

late ChildListRange range;

@override
FutureOr<void> prepare() {
var parent = element.parentRenderObjectElement!.renderObject as MarkupRenderObject;
range = parent.children.wrapElement(element);
}

@override
void apply(MarkupRenderObject root) {
return processBoundary(range);
}

void processBoundary(ChildListRange range);
}
Loading
Loading