@typebytes/ngx-template-streams
v1.2.1
Published
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) [![npm](https://img.shields.io/npm/v/@typebytes/ngx-template-streams.svg)](https://www.npmjs.com/package/@typebytes/ngx-template-
Downloads
43
Readme
ngx-template-streams
Take your Angular templates to the next level by embracing reactivity. This is a small and lightweight library that provides a simple DSL enabling event streams in templates. In other words, this library will supercharge your templates with Observables. Use a declerative syntax to create Observables from different event sources, such as native DOM events, or component outputs.
⚠️ Disclaimer ⚠️
This library is experimental and its goal is to explore how to create event streams from templates in Angular. To do this, we hook into the build process and apply HTML and TypeScript transformations, as well as patch some other internal APIs to make this work with AOT.
We like reactive programming and with this experiment we hope to push this forward within the Angular community and to help drive the adoption of a similar syntax or feature set that is integrated into Angular.
Can I use this now? Definitely! If at some point our implementation breaks, or Angular releases its own syntax, we will provide a schematic that will help you seamlessly migrate your code to keep the impact on your projects as small as possible.
Features
- ✅ Works with both
ViewEngine
andIvy
* - ✅ AOT compatible
- ✅ Easy to use syntax that is inspired by this proposal
- ✅ Ships with two reactive alternatives to
ViewChild
andViewChildren
(see API) - ✅ Can be used for native DOM events and component outputs
- ✅ Redefine the event payload (
$event
) - ✅ Works with our beloved
AsyncPipe
Notes
[1] If you want to use ngx-template-streams
with Ivy you have to use the latest version of ngx-template-streams
.
🙏 Credits
Big thanks to Filipe Silva, Craig Spence, Alexey Zuev and Manfred Steyer for his amazing ngx-build-plus library!
Table of contents
Quickstart
The most straightforward way to get started with this library is to use its ng add
schematic.
Simply run:
ng add @typebytes/ngx-template-streams
This will do all the heavy (actually not so heavy) lifting for your and add the library to your project.
Optionally you can also specifiy a project with --project <project-name>
.
The schematic will:
- ensure project dependency is placed in
package.json
- add ngx-build-plus as a
devDependency
- install necessary dependencies
- configure
serve
,build
, andtest
architects of your app (these will use a custom builder to allow for custom webpack configurations)
Once all that is done, we can take advantage of this library and define some event streams in our templates 🎉.
Alternative
If you want to use a more component- and code-centric way of listening for events on HTML elements or components, check out the @ObservableChild
and @ObservableChildren
decorator.
Syntax
The syntax is simple. Here's the full specification:
(*<event-name>)="<template-stream-name>[; $event = <payload>]"
*
marks the event binding as a template stream binding[]
denotes optional parts of the synax<placeholder>
represent placeholders you can fill in
More specifically there are 3 core building blocks:
- event name
- template stream name
- payload
The payload is optional and can litereally be anything as long as it follows the syntax above. So optional doesn't mean you can go wild and define the payload in whatever form you like. More on this here.
Now, let's check out how we can use this in our app 👇
Usage
Once you have installed the library, you can start using it. And that's very easy!
In order to create a template stream, you can use a slightly modified version of a regular event binding in Angular. Here's an example of a simple button with a click event:
<button (*click)="clicks$">Click Me (Stream)</button>
Instead of using a regular event binding, we are using a custom syntax that will be transformed into markup that Angular understands.
Important is that we indicate template streams by prefixing the event with an asterisk (*
). For the expression of the template stream we use the name of the Observable that will emit the click event.
Note: The $
sign is only a convention and is used to denote the property as an Observable
.
Next, we have to declare this property clicks$
on the component class. For that we can use a decorator provided by ngx-template-streams
called @ObservableEvent()
:
import { Component, OnInit } from '@angular/core';
import { ObservableEvent } from '@typebytes/ngx-template-streams';
import { Observable } from 'rxjs';
@Component({...})
export class AppComponent implements OnInit {
@ObservableEvent()
clicks$: Observable<any>;
ngOnInit() {
// we can either manually subscribe or use the async pipe
this.clicks$.subscribe(console.log);
}
}
Notice that we have declared the property using the @ObservableEvent
decorator and subscribe to the Observable in our ngOnInit
lifecycle hook. Alternatively we can also use this property in our template again and use the AsyncPipe
.
That's it! That's all it takes to create a very simple template stream!
The general syntax for creating a simple template stream inside the template is:
(*<event-name>)="<template-stream-name>; [payload]?"
The payload
part is optional. For more information check out Overwriting the event payload.
Overwriting the event payload
By default, the event payload will be $event
. Overwriting the payload is pretty straightforward and we only need to slightly extend our example from above.
So let's say we want the payload to be the string test
. Then we can define the payload as follows:
<button (*click)="clicks$; $event = 'test'">Click Me (Stream)</button>
Here we slightly extend the expression with an assignment of $event
. We can literally assign anything to $event
, from primitive values, to objects, properties from the component, and even function calls.
The general syntax for overwriting the payload is:
$event = <value>
Adding operators
We have decided not to add too much magic to this library and focus a bit more on clarity over brevity, type safety and explicitness. This means that in order to add operators to the template stream, we have to declare another variable and use the template stream property (here clicks$
) as the source.
For example:
import { Component, OnInit } from '@angular/core';
import { ObservableEvent } from '@typebytes/ngx-template-streams';
import { Observable } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
@Component({...})
export class AppComponent implements OnInit {
@ObservableEvent()
clicks$: Observable<any>;
// here we define a new property based on the template stream
// in order to add operators
debouncedClicks$ = this.clicks$.pipe(
debounceTime(400)
);
ngOnInit() {
this.debouncedClicks$.subscribe(console.log);
}
}
API
Besides the template syntax, ngx-template-streams
comes with one main building block, a @ObservableEvent()
decorator.
@ObservableEvent(subjectOrSubjectFactory)
Parameters
| Parameter | Type | Default | Description |
| ----------------------- | ---------------------------- | --------- | ----------------------------------------------------------------------------------------------------------- |
| subjectOrSubjectFactory | Subject
or Subject Factory | Subject
| Instance of a Subject or factory function to create the underlying source sequence for the template stream. |
For example, if we don't pass any parameters it will create a plain Subject
by default.
@Component({...})
export class AppComponent {
@ObservableEvent()
clicks$: Observable<any>;
}
We can also pass in an instance of a Subject
. This can be any type of Subject:
@Component({...})
export class AppComponent {
@ObservableEvent(new BehaviorSubject('INIT'))
clicks$: Observable<any>;
}
Or, we could also pass in a factory function that creates a Subject:
@Component({...})
export class AppComponent {
@ObservableEvent(() => {
return new Subject();
})
clicks$: Observable<any>;
}
@ObservableChild(selector, event, options)
The ObservableChild
is a reactive alternative to ViewChild
.
Parameters
| Parameter | Type | Default | Optional | Description |
| --------- | ------------------------------------------ | ------- | -------- | ----------- |
| selector | name or component type | / | | |
| event | event name or output | / | | |
| options | QueryOptions
& AddEventListenerOptions
| / | x | |
QueryOptions
(same options as with ViewChild
)
{
static?: boolean = false;
read?: any;
}
AddEventListenerOptions
{
once?: boolean;
passive?: boolean;
}
Example:
import { Component } from '@angular/core';
import { ObservableChild } from '@typebytes/ngx-template-streams';
@Component({
...,
template: `
<button #btn>Click Me</button>
`
})
export class AppComponent {
@ObservableChild('btn', 'click', { static: true, passive: true })
clicks$: Observable<any>;
/**
* We can only start to subscribe in 'ngOnInit' if the query is 'static'.
* Otherwise we have to use 'ngAfterViewInit' or from the template
* using the AsyncPipe.
*/
ngOnInit() {
this.clicks$.subscribe(console.log);
}
}
@ObservableChildren(selector, event, options)
The ObservableChildren
is a reactive alternative to ViewChildren
.
Parameters
| Parameter | Type | Default | Optional | Description |
| --------- | ------------------------- | ------- | -------- | ----------- |
| selector | name or component type | / | | |
| event | event name or output | / | | |
| options | AddEventListenerOptions
| / | x | |
Example:
import { Component } from '@angular/core';
import { ObservableChildren } from '@typebytes/ngx-template-streams';
@Component({
...,
template: `
<test-component></test-component>
<test-component></test-component>
<test-component></test-component>
`
})
export class AppComponent {
@ObservableChildren(TestComponent, 'myOutput')
aggregatedOutputs$: Observable<any>;
/**
* We can only start to subscribe in 'ngAfterViewInit' or from the
* template using the AsyncPipe.
*/
ngAfterViewInit() {
this.aggregatedOutputs$.subscribe(console.log);
}
}
Manual Installation
If you want to manually install this libray you first have to install ngx-build-plus as a devDependency
which allows us to extend the Angular CLI's default build behavior without ejecting:
ng add ngx-build-plus
Note: If you don't want to use the install schematic and need to know how you can manually install ngx-build-plus
, I would like redirect you to the official GitHub repo.
Next, we can install ngx-template-streams
and save it as a dependency of our project:
npm install @typebytes/ngx-template-streams -S
Now, we can update the angular.json
and add some extra configuration options to the build
, serve
and test
architect.
For each of those architect targets add the following additional options:
[...]
"architect": {
"build": {
[...],
"options": {
[...],
"extraWebpackConfig": "node_modules/@typebytes/ngx-template-streams/webpack/webpack.config.js",
"plugin": "~node_modules/@typebytes/ngx-template-streams/internal/plugin.js"
}
}
}
[...]
That's it! You are now ready to use template streams! 🎉
Why ⁉️
Because Observables rock 🤘. Everything is a stream and being able to also embrace reactivity in our templates improves the developer experience so that we don't have to constantly switch between different programming paradigms (imperative vs. functional reactive programming).
Also, this feature has been requested by the community for a long time and there is an open issue on GitHub since 2016.
With all the advances of different parts of the ecosystem including the Angular CLI, we wanted to take a stab and add this feature to the communities' toolbelt, allowing for more consistency in terms of programming style.
👷 Want to contribute?
If you want to file a bug, contribute some code, or improve our documentation, read up on our contributing guidelines and code of conduct, and check out open issues as well as open pull requests to avoid potential conflicts.
📄 Notes
This library has been well tested and works great in most use cases. However, there are a few things that we are aware of that we want to point out here. This is just to raise awareness that, in some special cases, you might notice some unexpected things.
Type Checker
For this library to work with AOT as well, we cannot run the type checker in a forked process. This has some performance drawbacks for large applications because TypeScript is synchronous and running the type checker in the main thread will slow down the compilation. We are aware of this and will investigate possible solutions to the underlying error, that is the forked type checker stops unexpectedly. If you have an idea, feel free to help us here. Any help is very much appreciated.
Formatting
When running your app in AOT mode, formatting (mostly whitespace in form of newlines) is not preserved. That's because AST transformations alone are not enough and the AOT compiler runs a type check that will error due to missing properties, even though the properties were created on the AST. We are talking about properties that a decorator (ObservableEvent
) will create at runtime. It's important to keep in mind that source files are immutable, this means any transformations are not reflected back to the actual text (sourceFile.getText()
) of the source file. However, this is important and therefore the current solution uses a printer to get an updated text version of the transformed AST which we then store back into the internal source file cache. Even though the formatting is not preserved, we believe it's not a deal breaker and shouldn't stop you from using this library. Serving your app with AOT enabled shouldn't be the default development environment and it should only be used to test your app. You can still look at the source maps, add breakpoints and debug your application. So no real downsides other than missing new lines. Nevertheless, we are still trying to find a "better" solution to this inconvenience. If you have ideas, please check out this issue.
FAQ
What if I already have a custom Webpack configuration?
If you are already using a custom webpack configuration to adjust the behavior of the build process, it's recommended
to follow the manual installation guide instead of using the ng add
schematic. We recommend to stick to ngx-build-plus as it's very convenient to work with and create a plugin that takes care of merging in your custom webpack config as well as the one provided by ngx-template-streams
. Finally, you have to call our build plugin (you'll find this in @typebytes/ngx-template-streams/internal/plugin.js
) to make sure the compiler plugin is correctly configured to allow template and AST transformations.
Do I need to unsubscribe from my event streams?
No, that's not necessary. The library will automatically complete and clean up every event stream for you when the component is destroyed.
What if I use TypeScript's strict mode?
If you are working with TypeScript's strict mode or have set strictPropertyInitialization
to true in your tsconfig.json
, you will experience an error using @ObservableEvent()
.
@ObservableEvent()
click$: Observable<MouseEvent>;
// ^ Property 'click$' has no initializer
// and is not definitely assigned in the constructor.
ngx-template-streams
ensures that your observable streams are initialized.
That's why you can easily get around this using the non-null-assertion operator of TypeScript. 👍
@ObservableEvent()
- click$: Observable<MouseEvent>;
+ click$!: Observable<MouseEvent>;
Versioning
ngx-template-streams
will be maintained under the Semantic Versioning guidelines. Releases are numbered with the following format:
<MAJOR>.<MINOR>.<PATCH>
- MAJOR versions indicate incompatible API changes,
- MINOR versions add functionality in a backwards-compatible manner, and
- PATCH versions introduce backwards-compatible bug fixes.
For more information on SemVer, please visit http://semver.org.
📄 Licence
MIT License (MIT) © Dominic Elm and Kwinten Pisman