Skip to content
Draft
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
49f3ad6
Merge branch 'main' into feat/server-components
Jun 14, 2024
2b9bbfd
add server components setup
Jun 14, 2024
497460f
finish server components
Jun 29, 2024
9ad1735
update server_components experiment
Jul 1, 2024
842b897
Merge branch 'main' into feat/server-components
Jul 3, 2024
0d13a83
Merge branch 'main' into feat/server-components
schultek Feb 25, 2025
35ce3d8
fix merge issues
schultek Feb 25, 2025
0f2d046
fixes
schultek Feb 25, 2025
227648b
Merge branch 'main' into feat/server-components
schultek Sep 25, 2025
a732436
fix server comp experiment
schultek Sep 25, 2025
bb2e0d2
fix nested client components
schultek Sep 28, 2025
4230c82
extend server component handling
schultek Nov 2, 2025
6e499de
add stateful reload feature
schultek Nov 7, 2025
9619db0
Merge branch 'main' into feat/server-components
schultek Dec 8, 2025
1f9cc50
Merge branch 'main' into feat/server-components
schultek Dec 11, 2025
0c0cbe2
Merge branch 'main' into feat/server-components
schultek Jan 5, 2026
c08515a
add reload event handling
schultek Jan 7, 2026
f0073ce
add onReload feature
schultek Jan 8, 2026
7bb7ed6
update experiment
schultek Jan 8, 2026
7053147
Merge branch 'main' into feat/server-components
schultek Feb 15, 2026
2034181
make reload private
schultek Mar 16, 2026
e46ec38
apply review
schultek Mar 18, 2026
95719cd
Feat: Component.apply with target (#777)
schultek Apr 20, 2026
f5bbd92
Merge branch 'main' into feat/server-components
schultek Apr 20, 2026
8f4ba5b
apply review
schultek May 4, 2026
4b6b491
Merge branch 'main' into feat/server-components
schultek May 4, 2026
222ba09
add testcases for server components
schultek May 26, 2026
32afb3e
fix formatting
schultek May 26, 2026
01e018c
add reload test
schultek May 26, 2026
1ad049e
add server component docs
schultek May 26, 2026
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: 2 additions & 2 deletions apps/dart_quotes/lib/main.client.options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>('id'),
initialCount: p.get<int>('initialCount'),
),
loader: _quote_like_button.loadLibrary,
),
Expand Down
4 changes: 2 additions & 2 deletions apps/dart_quotes_server/lib/main.client.options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>('id'),
initialCount: p.get<int>('initialCount'),
),
loader: _quote_like_button.loadLibrary,
),
Expand Down
2 changes: 1 addition & 1 deletion apps/fluttercon/lib/main.client.options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>('session')),
),
loader: _like_button.loadLibrary,
),
Expand Down
2 changes: 1 addition & 1 deletion apps/website/lib/main.client.options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool>('showHome')),
loader: _header.loadLibrary,
),
'install_command': ClientLoader(
Expand Down
32 changes: 27 additions & 5 deletions docs/concepts/components.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ In every aspect, this component behaves the same as Flutter's `InheritedWidget`.

## Foundation Components

Jaspr has three foundational component types, which are accessible through factory constructors on the `Component` class: `Component.element()`, `Component.text()` and `Component.fragment()`. Additionally, there are two more `Component.empty()` and `Component.wrapElement()` which are all explained below.
Jaspr has three foundational component types, which are accessible through factory constructors on the `Component` class: `Component.element()`, `Component.text()` and `Component.fragment()`. Additionally, there are two more `Component.empty()` and `Component.apply()` which are all explained below.

<Info>
Unlike Flutter, Jaspr has a fixed number of foundational components which make up all other components and represent the core building blocks of HTML: element nodes and text nodes. Flutter is different because it allows you to write custom render objects with your own layouting and painting logic. This isn't possible in Jaspr because with HTML, the browser handles both layouting and painting.
Expand Down Expand Up @@ -210,14 +210,14 @@ This is useful when you want to return "nothing" from a build method.
final component = Component.empty();
```

### Component.wrapElement()
### Component.apply()

A component which applies its attributes and parameters (like `classes`, `styles`,) etc.) to its direct child element(s).
A component which applies its attributes and parameters (like `classes`, `styles`,) etc.) to its target element(s).

This does not create a HTML element itself. All properties are merged with the respective child element's properties, with the child's properties taking precedence where there are conflicts.
This does not create a HTML element itself. All properties are merged with the respective target element's properties, with the target's properties taking precedence where there are conflicts.

```dart
final component = Component.wrapElement(
final component = Component.apply(
classes: 'wrapping-class',
styles: Styles(backgroundColor: Colors.blue, padding: Padding.all(8.px)),
child: div(
Expand All @@ -238,6 +238,28 @@ The above component renders the following HTML:
</div>
```

By default, `Component.apply()` targets all of its direct children. You can change this by using the `ApplyTarget target` parameter to target elements, either direct children or descendants, by tag, id or class names.

```dart
final component = Component.apply(
target: ApplyTarget.descendantWith(tag: 'p'),
styles: Styles(color: Colors.blue),
child: div([
p([
.text('Hello World'),
]),
]),
);
```

The above component renders the following HTML:

```html
<div>
<p style="color: blue;">Hello World</p>
</div>
```

## Formatting Whitespace

When pre-rendering your components in **server** and **static** mode, Jaspr will output cleanly formatted html on a best-effort basis. This means it will add newlines and indentations to your html element, while trying to not affect the way the html is rendered.
Expand Down
2 changes: 1 addition & 1 deletion examples/backend_dart_frog/lib/main.client.options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ ClientOptions get defaultClientOptions => ClientOptions(
loader: _counter.loadLibrary,
),
'hello': ClientLoader(
(p) => _hello.Hello(name: p['name'] as String),
(p) => _hello.Hello(name: p.get<String>('name')),
loader: _hello.loadLibrary,
),
},
Expand Down
6 changes: 6 additions & 0 deletions experiments/server-components/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Files and directories created by pub.
.dart_tool/
.packages

# Conventional directory for build output.
build/
3 changes: 3 additions & 0 deletions experiments/server-components/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# server_components

A basic pure-dart web app with ssr & automatic client hydration.
1 change: 1 addition & 0 deletions experiments/server-components/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:lints/recommended.yaml
45 changes: 45 additions & 0 deletions experiments/server-components/lib/counter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import 'package:jaspr/dom.dart';
import 'package:jaspr/jaspr.dart';

@client
class Counter extends StatefulComponent {
const Counter({this.step = 1, this.child, super.key});

final int step;
final Component? child;

@override
State<StatefulComponent> createState() => CounterState();
}

class CounterState extends State<Counter>
with SyncStateMixin<Counter, Map<String, dynamic>> {
int count = 0;

@override
Map<String, dynamic> getState() {
return {'stepDouble': component.step * 2};
}

@override
void updateState(Map<String, dynamic> value) {
print("GOT SYNC: $value");
}

@override
Component build(BuildContext context) {
print("Building Counter with count=$count and step=${component.step}");
return div(classes: 'client', [
.text('$count '),
button(
onClick: () {
setState(() {
count += component.step;
});
},
[.text("Increase by ${component.step}")],
),
if (component.child != null) component.child!,
]);
}
}
26 changes: 26 additions & 0 deletions experiments/server-components/lib/main.client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// The entrypoint for the **client** environment.
//
// The [main] method will only be executed on the client after loading the page.

// Client-specific Jaspr import.
import 'package:jaspr/client.dart';

// This file is generated automatically by Jaspr, do not remove or edit.
import 'main.client.options.dart';

void main() {
// Initializes the client environment with the generated default options.
Jaspr.initializeApp(
options: defaultClientOptions,
);

// Starts the app.
//
// [ClientApp] automatically loads and renders all components annotated with @client.
//
// You can wrap this with additional [InheritedComponent]s to share state across multiple
// @client components if needed.
runApp(
const ClientApp(),
);
}
42 changes: 42 additions & 0 deletions experiments/server-components/lib/main.client.options.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// dart format off
// ignore_for_file: type=lint

// GENERATED FILE, DO NOT MODIFY
// Generated with jaspr_builder

import 'package:jaspr/client.dart';

import 'package:server_components/counter.dart' deferred as _counter;
import 'package:server_components/minicounter.dart' deferred as _minicounter;

/// Default [ClientOptions] for use with your Jaspr project.
///
/// Use this to initialize Jaspr **before** calling [runApp].
///
/// Example:
/// ```dart
/// import 'main.client.options.dart';
///
/// void main() {
/// Jaspr.initializeApp(
/// options: defaultClientOptions,
/// );
///
/// runApp(...);
/// }
/// ```
ClientOptions get defaultClientOptions => ClientOptions(
clients: {
'counter': ClientLoader(
(p) => _counter.Counter(
step: p.get<int>('step'),
child: p.mountOrNull(p.get<String?>('child')),
),
loader: _counter.loadLibrary,
),
'minicounter': ClientLoader(
(p) => _minicounter.MiniCounter(),
loader: _minicounter.loadLibrary,
),
},
);
38 changes: 38 additions & 0 deletions experiments/server-components/lib/main.server.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:jaspr/dom.dart';
import 'package:jaspr/server.dart';
import 'main.server.options.dart';

import 'counter.dart';
import 'minicounter.dart';

void main() {
Jaspr.initializeApp(options: defaultServerOptions);

runApp(
Document(
title: 'server_components',
body: Builder(
builder: (context) {
var time = DateTime.now();
return div(classes: 'server', [
p([.text('Server Time: $time')]),
Counter(step: time.second, child: null),
MiniCounter(),
]);
},
),
),
);
}

@css
List<StyleRule> get styles => [
css('.server').styles(
padding: Padding.all(2.px),
border: Border.all(color: Colors.red),
),
css('.client').styles(
padding: Padding.all(2.px),
border: Border.all(color: Colors.green),
),
];
45 changes: 45 additions & 0 deletions experiments/server-components/lib/main.server.options.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// dart format off
// ignore_for_file: type=lint

// GENERATED FILE, DO NOT MODIFY
// Generated with jaspr_builder

import 'package:jaspr/server.dart';
import 'package:server_components/counter.dart' as _counter;
import 'package:server_components/main.server.dart' as _main$server;
import 'package:server_components/minicounter.dart' as _minicounter;

/// Default [ServerOptions] for use with your Jaspr project.
///
/// Use this to initialize Jaspr **before** calling [runApp].
///
/// Example:
/// ```dart
/// import 'main.server.options.dart';
///
/// void main() {
/// Jaspr.initializeApp(
/// options: defaultServerOptions,
/// );
///
/// runApp(...);
/// }
/// ```
ServerOptions get defaultServerOptions => ServerOptions(
clientId: 'main.client.dart.js',
clients: {
_counter.Counter: ClientTarget<_counter.Counter>(
'counter',
params: __counterCounter,
),
_minicounter.MiniCounter: ClientTarget<_minicounter.MiniCounter>(
'minicounter',
),
},
styles: () => [..._main$server.styles],
);

Map<String, Object?> __counterCounter(_counter.Counter c) => {
'step': c.step,
'child': c.child,
};
29 changes: 29 additions & 0 deletions experiments/server-components/lib/minicounter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'package:jaspr/dom.dart';
import 'package:jaspr/jaspr.dart';

@client
class MiniCounter extends StatefulComponent {
const MiniCounter({super.key});

@override
State<StatefulComponent> createState() => MiniCounterState();
}

class MiniCounterState extends State<MiniCounter> {
int count = 0;

@override
Component build(BuildContext context) {
return span(classes: 'client', [
button(

onClick: () {
setState(() {
count++;
});
},
[.text("Count: $count")],
),
]);
}
}
Loading
Loading