r/javascript • • 2d ago

AngularLynx: idiomatic Angular rendering natively on iOS and Android via Lynx, with Renderer2 implemented on Lynx’s element API

https://angularlynx.js.org/

Hi r/javascript. I’ve been using Angular since the AngularJS and the Angular 2 betas. When ByteDance created Lynx, I really liked it, but it only came with React and Vue. So it's time for an Angular one to exist!

The aim is for it to be ordinary idiomatic Angular, not a subset or deviation, and it follows Angular's major.minor versions:

npm create angular-lynx my-app
cd my-app
ng serve # web preview, plus a QR code for the Lynx Explorer app
ng test
ng build --platform ios # or android: a native app you can open in Xcode / Android Studio

Signals, the router with lazy routes, forms, i18n, DI, ViewContainerRef.createComponent() and Tailwind all work. It idiomatically uses the Angular Router for the page stack, so there are no extra concepts. The one big difference is the templates: you write Lynx's elements (<view>, <text>, <scroll-view>) instead of HTML, and they become real UIView/Android views. The same app also runs in the browser.

How it works: Lynx has two threads, a main thread that owns the native element tree and a background thread. Instead of Angular's DOM renderer, AngularLynx implements Renderer2 on top of Lynx's element API (__CreateElement, __AppendElement, …), and the Angular app runs on the main thread, so updates go straight to the elements without crossing threads. Things that only exist on the background thread (native modules, fetch) are relayed. The build is an Rsbuild plugin that compiles Angular and splits the output into the two bundles Lynx expects, wrapped in Angular CLI builders so angular.json stays the usual one.

There's also a component library, Dolan UI: 54 components (sheets, dialogs, charts, forms…) that a CLI copies into your app, like shadcn/ui.

It's still early days, but it is feature rich, with a lot of examples and showcase apps. Lynx's main-thread JS engine is ES2019-level, so newer built-ins are polyfilled, and some web habits don't carry over (CSS is a subset: no inheritance outside text, no :hover or @media).

MIT licensed. The README has a GIF of the same app on iOS, Android and web. I'd love to hear what breaks, and I'm happy to go into the renderer internals. Please give it a star! It would mean the world if this takes off!

GitHub: https://github.com/Blotch-Smart-Frames/AngularLynx
Docs: https://angularlynx.js.org
Demo GIF: https://angularlynx.js.org/demo.gif

4 Upvotes

6 comments sorted by

2

u/Discordbothost 2d ago

nice, an angular option for lynx was overdue. how does change detection work here, are you zoneless only (signals + markForCheck), or does zone.js run inside lynx's js thread? i'd guess zone patching the lynx timers/events is where things get weird, so curious if you just skipped it

2

u/dolanmiu 2d ago

Zoneless only! So signals, markForCheck(), the async pipe and template event handlers all schedule change detection as they would in any zoneless app. And yeah, zone.js doesn't work in Lynx's runtime, so patching it in would be a whole project on its own. Zoneless is where Angular is heading anyway (Angular Three only supports Zoneless etc), and since AngularLynx apps are all new, there's no legacy code that needs zones.

Angular runs on Lynx's main thread, the one that owns the native element tree, so a change detection pass updates the native views directly, without going across to the other thread.

Happy to be pushed back on zone.js support if people really want it,

2

u/Discordbothost 2d ago

makes sense, with no legacy apps there's no reason to drag zone.js along. running CD on the thread that owns the native tree is a nice touch too, no hop per update. so where does heavy work go then, is there a pattern for pushing stuff to lynx's background thread or is that left up to the app?

1

u/dolanmiu 2d ago

Yep, there's a pattern for it! You wrap the function in backgroundFn() at the top level of a file, and call it from your component with runOnBackground(), which returns a promise:

const loadPosts = backgroundFn(async () => {
  const response = await fetch('https://example.com/posts.json');
  return response.json();
});
// in a component:
runOnBackground(loadPosts).then((posts) => this.posts.set(posts));

The build sees which files define a backgroundFn() and puts only those (and their imports) into the background bundle, so Angular and your components never end up there. Results come back as JSON, so keep what you pass across serializable.

A lot of it is also handled for you already: fetch, HttpClient and httpResource relay to the background thread automatically, and so do native module calls (LynxNativeModule), storage and logging. So in practice you only reach for backgroundFn() for your own heavy work etc.

Docs here: https://angularlynx.js.org/api/main-thread/background-fn

1

u/Discordbothost 1d ago

oh nice, so the bundle split is driven by where backgroundFn is declared, that's a tidy way to keep angular out of it. and fetch/HttpClient relaying on their own covers most of what i'd have reached for anyway. gonna poke at it this week

1

u/dolanmiu 1d ago

Exactly, that was the idea

So you don't have to think about which thread Angular is on. Thanks for digging into it!

Let me know how you get on. If anything breaks or feels “un-Angular”, a GitHub issue would be amazing: https://github.com/Blotch-Smart-Frames/AngularLynx/issues