Zap components are defined in
.zap files. They consist of three sections, all of which are optional: scripts, styles and markup:
<script> // Dart code for the component </script> <style> /* you can put scoped component css here */ </style> <!-- Markup as HTML goes here -->
<script> tag contains Dart code to run when a component is initialized. All variables declared there are bound to the component, and visible in the component's markup. Changes to variables are tracked by the compiler, and will cause usages to rebuild automatically.
To define a property, annotate a variable with
Properties can be given befault values by adding an initializer to the variable. The default value doesn't have to be a constant.
A component's state can be changed by assigning to a variable declared in a
<script> block. The component will automatically update references to changed variables.
Note that only assignments are tracked as updates. Mutable objects work poorly with zap, as they can change state without re-assignments. For more complex state management needs, consider using riverpod.
Any top-level statement can be made reactive by prefixing it with a
$: label. Reactive statements run once as the component initializes, and then again whenever the values that they depend on have changed.
<script> import 'dart:html'; @prop String title; // this will update `document.title` whenever the `title` prop changes $: document.title = title; $: print(`multiple statements can be combined`); print(`the current title is $itle}`); } </script>
Only variables that directly apear within the
$: block will become dependencies of the reactive statement. Variables that might be used in called methods are not considered, for instance.
A script tag with a
context="library" attribute can be used to define Dart code that is not bound to the component. It can be used to define classes or global fields. Variables defined in this script are not reactive.
<script context="library"> var totalComponents = 0; // this allows an importer to do e.g. `import 'example.zap' show alertTotal` void alertTotal() window.alert(totalComponents); } </script> <script> totalComponents += 1; print('total number of times this component has been created: $totalComponents'); </script>
CSS inside a
<style> block will be scoped to that component. This works by adding a class to affected elements, which is based on a hash of the component styles.
Zap uses sass to parse
<style> blocks, so Sass directives are supported out of the box.
Zap components can include regular HTML elements. When imported, other zap components can be included by using the basename of the file they were declared in as a tag:
<script> import 'headers.dart'; </script> <headers />
Attributes and properties
By default, attributes work just like in regular HTML.
<div class="foo"> <button disabled>can't touch this</button> </div>
Attribute values can contain Dart expressions.
<a href="page/}">page }</a>
Or they can be Dart expressions.
Properties can be set on components by providing them as attributes.
Inside of text or attributes, any Dart expression may be used in braces.
<h1>Hello ame}!</h1> <p>} + } = + b}.</p>
The expression will be evaluated and the result of calling
toString() on the result is escaped and inserted into the document. When component variables used inside an expression change, the expression is re-evaluated.
Content that is conditionally rendered can be wrapped in an if block.
if answer === 42} <p>what was the question?</p> if}
Additional conditions can be added with
else if expression}, optionally ending in an
#if porridge.temperature > 100} <p>too hot!</p> else if 80 > porridge.temperature} <p>too cold!</p> else} <p>just right!</p> if}
As you'd expect, the expression used in an
else if must be of type
for block iterates over values:
<script> const potentialQuestions = [ 'What is that beautiful house?', 'Where does that highway go to?', 'Am I right? Am I wrong?', 'My God! What have I done?', ]; </script> <h1>Potential questions</h1> <ul> for question in potentialQuestions} <li>uestion}</li> for} </ul>
The type of the expression being iterated over must have a type of
Iterable<T>. The type of the loop variable is then inferred as
T. It is possible to keep track of indices by adding another loop variable:
for question, i in potentialQuestions} question } if i != potentialQuestions.length - 1} <hr> if} for}
await ...} and
await each ...}
await x from expr } block awaits the expression
expr, which has to be of type
FutureOr<T>. The type of the variable
x is then inferred to
await each x from expr} block listens to a stream
expr, which has to be of the type
Stream<T>. The type of the variable
x is again inferred to
<script> import 'dart:async'; final stream = StreamController<String>().stream; </script> await each snapshot from stream} Hello napshot} await} await snapshot from stream.first} Hello napshot} await}
Text written in zap components is escaped, so binding a string containing HTML characters would escape characters like
> in the component.
html } block can be used to insert a Dart-expression without any sanitazion. For example,
html "<script>alert('hacks');</script>"} will do what you may fear it does.
html} blocks should not be used with untrusted data. Please note that the expression inside a block must be a full HTML node - things like
html '<div>'}contenthtml '</div>'} won't work because
</div> isn't valid HTML on its own. As contents are written directly into the DOM, zap components also can't be used inside
html } blocks.
In addition to regular attributes, elements can have directives, which control the element's behavior in some way.
on: directive, a listener for DOM or component events can be registered. The value of the directive must either be a
void Function() or a
void Function(EventType), where
EventType depends on the name of the event used. For instance,
on:Click delivers a
<script> var count = 0; void handleClick() => count++; </script> <button on:click=andleClick}>count: ount}</button>
Handlers can also be declare inline:
<button on:click=") => count++}">count: ount}</button>
Modifiers can be added to the event stream with the
<form on:submit|preventDefault=andleSubmit}> <!-- the `submit` event's default is prevented, so the page won't reload --> </form>
The following modifiers are available:
event.preventDefault()before running the handler.
event.stopProgataion(), preventing the event from reaching the next element.
passive: TODO this is currently a noop
capture: Fires the handler during the capture phase instead of the bubbling phase.
once: Remove the handler after the first time it runs.
self: Only trigger the handler if
event.targetis the element itself.
trusted. Only trigger the handler if
Modifiers can be chained together, e.g.
It is possible to have multiple event listeners for the same event.
Data ordinarily flows down, from parent to child. The
bind: directive allows data to flow the other way, from chil to parent. Most bindings are specific to particular elements.
The simplest bindings reflect the value of a property, such as
<input bind:value=ame}> <textarea bind:value=ext}></textarea> <input type="checkbox" bind:checked=es}>
Zap does not currently support the shorthand syntax or known types other than
String. Contributions are welcome!
To get a reference to a DOM node, use
<script> late CanvasElement canvas; onMount(() => const ctx = canvasElement.getContext('2d'); drawStuff(ctx); }); self.onMount(() final context = canvas.context2D; drawStuff(context); }); </script> <canvas bind:this=anvasElement}></canvas>
Components can have child content, in the same way that elements can.
The content is exposed in the child component using the
<slot> element, which can contain fallback content that is rendered if no children are provided.
<!-- widget.zap --> <div> <slot> this fallback content will be rendered when no content is provided, like in the first example </slot> </div> <!-- app.zap --> <widget></widget> <!-- this component will render the default content --> <widget> <p>this is some child content that will overwrite the default slot content</p> </widget>
Named slots allow consumers to target specific areas. They can also have fallback content.
<!-- widget.zap --> <div> <slot name="header">No header was provided</slot> <p>Some content between header and footer</p> <slot name="footer"></slot> </div> <!-- app.zap --> <widget> <h1 slot="header">Hello</h1> <p slot="footer">Copyright (c) 2022 Zap Industries</p> </widget>
<zap:fragment> tag can be used to place multiple DOM nodes in a slot without wrapping it in a container element:
<widget> <h1 slot="header">Hello</h1> <zap:fragment slot="footer"> <p>All rights reserved.</p> <p>Copyright (c) 2022 Zap Industries</p> </zap:fragment> </widget>
To include a child component that's not known statically, the special
<zap:component> tag can be used:
Inside a component script, you can use the special
self variable to refer to the component. It contains the following important members:
Map<Object?, Object?> get context:
void emitEvent(Event event): Allows emitting custom component events that parent components can listen to with the
Future<void> get tick: A future completing after pending state changes have been applied. If no state changes are scheduled, the returned future returns in a new microtask.
To support reactivity, especially with complex flows across components, zap provides a concept of
Watchable is essentially a stream that never emits errors, and always provides the latest value through a
final currentTime = Watchable.stream( Stream.periodic(const Duration(seconds: 1), (_) => DateTime.now()), DateTime.now() );
Inside components, the
watch function can be used to read the current value of a
Watchable. When the watchable updates, so will the parts of the component reading that watchable.
The current time is atch(currentTime)}.
Conceptually, watchables are similar to stores in Svelte. In fact, the
WritableWatchable class is very similar to a writable store in Svelte:
<script> import 'package:zap/zap.dart'; final counter = WritableWatchable(0); var currentCounter = watch(counter); void handleClick() => counter.value++; </script> <button on:click=andleClick}> You've clicked this button counter } counter == 1 ? 'time' : 'times' } </button>
While writable watchables are less convenient than a simple reactive variable, they make it easy to share state between two components.
Zap has a first-class package providing integration with Riverpod, a reactive framework for state management.
To use it, depend on
dependencies: riverpod_zap: hosted: https://simonbinder.eu
To use riverpod providers in zap components, import
package:riverpod_zap/riverpod.dart. It provides the following extensions for components:
ProviderContainer self.riverpodContainer: The container of the closest riverpod scope surrounding this component.
T self.read<T>(ProviderBase<T> provider): Reads the value of a provider.
Watchable<State> use<T>(ProviderListenable<T> provider): Obtains the updating value of a provider as a
Watchable. It can be used together with watch to automatically listen for the latest value of a provider.
To register a new riverpod scope, use the
<riverpod-scope> tag in a component. Child elements will use that scope then. You can add provider overrides with the
overrides property to that tag.