惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

爱范儿
爱范儿
大猫的无限游戏
大猫的无限游戏
WordPress大学
WordPress大学
C
Cyber Attacks, Cyber Crime and Cyber Security
D
DataBreaches.Net
G
Google Developers Blog
博客园 - Franky
V
V2EX
博客园 - 叶小钗
D
Docker
The GitHub Blog
The GitHub Blog
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
H
Hackread – Cybersecurity News, Data Breaches, AI and More
B
Blog RSS Feed
月光博客
月光博客
M
MIT News - Artificial intelligence
F
Fortinet All Blogs
Microsoft Azure Blog
Microsoft Azure Blog
人人都是产品经理
人人都是产品经理
IT之家
IT之家
Google DeepMind News
Google DeepMind News
Apple Machine Learning Research
Apple Machine Learning Research
V
Visual Studio Blog
博客园 - 司徒正美
Stack Overflow Blog
Stack Overflow Blog
罗磊的独立博客
J
Java Code Geeks
U
Unit 42
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 聂微东
T
Tailwind CSS Blog
T
The Blog of Author Tim Ferriss
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Blog — PlanetScale
Blog — PlanetScale
Jina AI
Jina AI
C
Check Point Blog
Y
Y Combinator Blog
MyScale Blog
MyScale Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
宝玉的分享
宝玉的分享
B
Blog
小众软件
小众软件
云风的 BLOG
云风的 BLOG
I
InfoQ
Recorded Future
Recorded Future
酷 壳 – CoolShell
酷 壳 – CoolShell
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
美团技术团队

Show HN

GitHub - flightdeckhq/flightdeck: Observability and control plane for AI agents. CSP Radar GitHub - Light-Heart-Labs/DreamServer: Turn your PC, Mac, or Linux box into an AI server. LLM inference, chat UI, voice, agents, workflows, RAG, and image generation. GitHub - Diplomat-ai/diplomat-agent-ts: What can your TypeScript AI agent do to the real world? Scan your code. See which tool calls have zero checks Code Block Selector - Visual Studio Marketplace Prometheus dependency graph — interactive showcase | Riftmap Show HN: I made a vi-like modal keyboard plugin for Figma GitHub - run-llama/liteparse: A fast, helpful, and open-source document parser GitHub - dalemyers/Roar: A macOS CLI tool for notifications GitHub - district-solutions/open-agent-tools-coder: Enables small-to-large self-hosted ai models to use local source code when running tool-calling agentic workloads. We actively data mine 20,900+ (2+ TB) popular github repos using large and small ai models to create reuseable: json, markdown and parquet files for local-first tool-calling models. GitHub - progapandist/stripeek: A local TUI proxy for real-time Stripe API debugging, built for navigating complex payloads fast. GitHub - sir1st/hermes-desktop: All-in-one cross-platform desktop app for Hermes Agent — bundles Python + hermes-agent + hermes-web-ui GitHub - astefanutti/shaderbang: Shebang for Shaders Show HN: Generate Claude Code Workflows using Spec Driven Development approach GitHub - nixys/nxs-universal-chart: The Helm chart you can use to install any of your applications into Kubernetes/OpenShift Show HN: AI agents for UK GDAD PCF roles and their skills The Two Pillars: Mixer Mode and Meta-Software in the Reorganization of Software Work After AI GitHub - JaiCode08/teleport-env What 1,000+ Harness Experiments Taught Me About Self-Improving Agents Show HN: Liiists, a Markdown-first, iOS and CLI list app SwiperTab – Get this Extension for 🦊 Firefox (en-US) GitHub - kouhxp/fftext: Summarize, explain, fact-check, or translate any text, URL, or file. No GPU. No cloud. One command GitHub - sweetpad-dev/sweetpad: Develop Swift/iOS projects using VSCode GitHub - dogmaticdev/IRON: IRON a.k.a. Intermediate Representation Object Notation is a Interpreter/Database that is used to create Programming Languages. GitHub - sjhalani7/vaen: Package your AI coding harness into a portable .agent file, and share it across repos, teams, & the community without ever having to copy-paste instructions, skills, MCP config, or secrets. Show HN: Gandalf the Grader Show HN: Citadeld – replay any CI failure locally from a single file GitHub - tdortman/cuSBF: High-Performance GPU Super Bloom Filter coral-ai/claude-code-token-xray at main · Coral-Bricks-AI/coral-ai GitHub - ulyssestenn/funes: Funes is a Git-based framework for LLM-managed knowledge work: an AI Librarian ingests raw sources, builds an interlinked Markdown knowledge base, and uses it to produce cited reports, analyses, and other outputs. GitHub - ThatXliner/gah: Git Add Hunk, built for agents to use GitHub - harmont-dev/harmont-cli: Command-line client for the Harmont CI platform GitHub - brooksmcmillin/mcp-authflow: OAuth 2.0 Authorization Server framework for MCP servers GitHub - javaid-codes/audit-supply-chain-agents GitHub - amorey/gochan: A small library of common channel architectures for Go, inspired by Rust GitHub - arifozgun/OpenGem: Free, Open-Source AI API Gateway with Gemini, OpenAI & Anthropic Compatibility in 1 file GitHub - Pranesh950/BioPetals: 🌸 Run BIOxAI models at home, BitTorrent-style. Fine-tuning and inference up to 10x faster than offloading GitHub - cnguyen14/bounty-doctor: Diagnose a GitHub bounty issue before you waste hours: detects honeypot scam repos, AI-bot attempt swarms, and stale contests. Show HN: CoreMCP – MCP Server for On-Prem DBs Show HN: KittyHTML – Render HTML/CSS as an inline image in your terminal GitHub - bingud/filemat: Web-based file manager Show HN: TruthLens – Free multi-signal deepfake image detector GitHub - apexlocal-jz/claude-usage-tray: Windows system-tray app showing your Claude Code rate-limit usage at a glance. Zero deps, ~300 lines of PowerShell. Cross-IDE (works regardless of VS Code, Cursor, plain terminal). Release v0.1.2.1 · kouhxp/yapsnap GitHub - noopolis/moltnet: Self-hostable chat network for AI agents. Pre-built bridges for Claude Code, Codex, and the Claws. Rooms, DMs, history. No Slack bots, no Matrix, no glue code. GitHub - tamerh/enju: Coordinating Humans, AI Agents, and Compute as Peers on a Shared Workflow Graph Show HN: Continuity-auth – Respect-weighted rate limits for the open web GitHub - luml-ai/luml: AI lifecycle platform where engineers and agents track experiments, train models, and ship to production. GitHub - mrdanielcasper/CoreTex: A UNIX-inspired, biomimetic, flat-file AI harness and knowledge engine. GitHub - clemg/pierre-github: Pierre's diffs.com and trees.software for Github GitHub - lyriks-io/unspaghettit: Behavior-driven AI development without prompt spaghetti. GitHub - sofumel/claude-handoff-revive: Resume Claude Code work after rate/usage/context limits without replaying the prior transcript. Auto-saves at 90%/95% usage. Plugin-installable, 10 languages. GitHub - dotexorg/saferpc: Typed, end-to-end encrypted RPC over any bidirectional channel. GitHub - BeeZeeAgent/beezee: Agent harness orchestration Legato Next.js Boilerplate for Internal Tools · CoreUI GitHub - clark-labs-inc/clark-hash: Clark Hash, 32x smaller searchable sketches for embeddings GitHub - ZeroPointRepo/youtube-mcp: The fastest YouTube transcript + YouTube search MCP for AI agents. Try for free. Typing Mastery — climb toward 100+ WPM, deliberately GitHub - Andebugulin/Awareen GitHub - fayzan123/claude-workflow-composer: Visual desktop app for composing multi-agent coding workflows. Drag agents, attach skills and MCPs, wire handoffs, export to .claude/ GitHub - harshaneel/humanize: Best static AI text humanizer. Two research-grounded skills that work in any LLM (Claude, ChatGPT, Gemini, Codex): humanize beats perplexity-based detectors, ai-check produces forensic scoring with evidence-quoted flags. Nine levers, 50+ peer-reviewed sources, 2024-2026 detection literature. GitHub - StackOneHQ/stack-nudge GitHub - nodes-app/swift-markdown-engine: A native AppKit Markdown editor for macOS, built on TextKit 2 and bridged to SwiftUI. We hardened an LLM agent. Each defense we added made it more exploitable. GitHub - alkait/WhatsKept: Agent-queryable WhatsApp history from an iOS backup — a single Go binary. GitHub - octelium/cordium: Open-source, general-purpose sandbox platform for devs and AI agents that provides identity-based secure access to infrastructure without credentials. WAR.GOV/UFO Microfilm5 GitHub - scosman/videowright: Build animated explainer videos with your coding agent GitHub - dipankar/dscode: The code editor you can take apart. GitHub - zoharbabin/web-researcher-mcp: MCP server (Go) for AI assistants: web search, content extraction, academic/patent/news research. Multi-provider routing, 4-tier scraping, search lenses. Works with Claude, Cursor, and any MCP client. GitHub - ruvnet/RuView: π RuView turns commodity WiFi signals into real-time spatial intelligence, vital sign monitoring, and presence detection — all without a single pixel of video. GitHub - scanaislop/aislop: Catch the slop AI coding agents leave in your code: narrative comments, swallowed exceptions, as-any casts, dead code, oversized functions. 50+ rules across 7 languages (TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP). Sub-second, deterministic, no LLM at runtime. MIT-licensed. GitHub - kouhxp/cheap-im: CPU-only voice agent approximating Thinking Machines' Interaction Models demo GitHub - unprovable/OrchidMantis: Orchid Mantis — standalone framework for Zero-Knowledge Proofs of eXploit (ZKPoX). GitHub - MarcellM01/TinySearch: Shrink the web for your local LLMs! GitHub - pileax-ai/pileax: PileaX is an all-in-one AI knowledge base system. 🍀 GitHub - TangibleResearch/Halgorithem: A Algo designed to detect AI Hallucitions GitHub - DO-SAY-GO/freelang: I love freelang GitHub - CarpseDeam/Aura-IDE: An AI coding harness that shaped itself - Planner/Worker agents, repo awareness, surgical edits, validation, recovery, and safe diff approvals. GitHub - chojs23/concord: A feature-rich TUI client for Discord GitHub - tommyjepsen/awesome-ux-skills: UX & AI Product designs skills you can use today in Claude Code GitHub - aerf-spec/aerf: Agent Evidence Receipt Format (AERF) — an open specification for tamper-evident, independently verifiable records of AI agent actions. GitHub - kklimuk/docx-cli: CLI for AI agents (Claude, Codex) to read, edit, and comment on .docx files with full format fidelity. GitHub - Jwrede/tokentoll: Catch LLM cost changes in code review. Infracost for LLM spend. GitHub - samchon/ttsc: A `typescript-go` toolchain for compiler-powered plugins and type-safe execution + 500x faster lint integrated into compiler GitHub - Higangssh/homebutler: 🏠 Manage your homelab from chat. Single binary, zero dependencies. GitHub - olalie/tapmap: See where your computer connects and what stands out on a live world map. GitHub - matisiekpl/neond: DX-focused control plane for Postgres dedicated to non-critical workloads. Your postgres:latest replacement 🐘 GitHub - Diplomat-ai/diplomat-agent: What can your AI agent do to the real world? Scan your code. See which tool calls have zero checks GitHub - Bajusz15/beacon: Open-source agent for secure remote access, monitoring, and deploys across home-lab and self-hosted machines like Raspberry Pi, N100, or any Linux server. Open web based TTY or tunnel Home Assistant and other local services securely without opening ports. BigTech AI News - Chrome 应用商店 GitHub - vinhnx/VTCode: VT Code is an open-source coding agent with LLM-native code understanding and robust shell safety. Supports multiple LLM providers with automatic failover and efficient context management. GitHub - michaelaz774/decision-engine: A decision operating system for startup founders, powered by Claude Code. Synthesizes wisdom from 25+ legendary founders and investors into interactive AI-driven decision frameworks. GitHub - Chrilleweb/dotenv-diff: Validate environment variable usage in your codebase GitHub - Lumen-Labs/brainapi2: BrainAPI is a knowledge graph–powered AI memory layer that transforms unstructured data into structured knowledge, enabling intelligent search, recommendations, and contextual memory for AI agents and applications. GitHub - familiar-software/familiar: Let AI watch you work. Familiar lets your AI update its memory, skills, and knowledge by watching your screen. GitHub - skorotkiewicz/rudo: A small, elegant dock for Wayland GitHub - muxshed/shed: One stream in, or many. Every destination, simultaneously. No cloud middleman, no per-channel fees, no limits. make sidebar/address bar rounded corner toggleable
GitHub - fynyky/elemental: Simple reactive front-end library
fynyky · 2026-05-29 · via Show HN

Elemental is a simple front-end library that lets you build reactive UIs declaratively using plain JavaScript. No special syntax to learn or complex frameworks to build around, just normal nested functions.

Here's a quick example of what Elemental does:

import { Reactor, ob, el } from '@fynyky/elemental'

const rx = new Reactor({ name: 'Anakin' })

el(document.body,
  el('main',
    el('h1', 'Hello World!'),
    el('h2', (x) => { x.id = 'foo' }, () => 'returned text'),
    el('div.note', ['this', 'is', 'an', 'array']),
    el('p.greeting', ob(() => ('My name is ' + rx.name)))
  )
)
// <main>
//   <h1>Hello World!</h1>
//   <h2 id="foo">returned text</h2>
//   <div class="note">thisisanarray</div>
//   <p class="greeting">My name is Anakin</p>
// </main>

rx.name = 'Darth'
//   <p class="greeting">My name is Anakin</p>
// Changes to
//   <p class="greeting">My name is Darth</p>
  • el is a function that creates elements then attaches children to them
    • The first argument is a CSS selector string describing the element to create, or an existing element to append to
    • Subsequent arguments are appended as children
    • Functions are run given their parent, and their return values are appended
    • Observer functions do the same, but their children get replaced when updated
  • ob is shorthand for new Observer
  • An Observer is a function that automatically tracks reactive variables that it uses, and retriggers if they get updated.
  • A Reactor is an object that stores reactive variables
  • When a Reactor is updated it automatically retriggers the dependent Observer functions

Elemental is designed to be unobtrusive and unopinionated.

  • No special syntax to learn. Everything is just plain JavaScript
  • No need to manually declare listeners. Elemental automatically keeps track of all that
  • No complex framework internals to debug. It just appends elements and runs functions when needed
  • Use it for the whole front-end or just a few components. Elements created by Elemental are just normal DOM elements, and any variable can be easily replaced with a reactive one without changing the rest of your codebase.

Elemental is built on top of Reactor.js

Check out the demo repo for examples.

Installation

Elemental is available on npm. Install it by running:

$ npm install @fynyky/elemental

Import it using:

import { 
  el,
  ob,
  attr,
  on,
  bind,
  Reactor,
  Observer,
  hide,
  batch,
  shuck
} from '@fynyky/elemental'

It is also available directly from unpkg. You can import it in JavaScript using

import { el, attr, on, bind, ob, Reactor, Observer, hide, batch, shuck } from 'https://unpkg.com/@fynyky/elemental'

Elements

The function el(description, children...) builds DOM elements. It appends the children arguments to a parent element created/referenced by the description.

The description can be a String or an existing Element. If given a String, it creates a new element using CSS selector syntax: the tag comes first, followed by .class and #id segments.

el('button.foo')    // <button class="foo"></button>
el('input#bar.baz') // <input id="bar" class="baz">
el('div.foo.bar')   // <div class="foo bar"></div>
el('section')       // <section></section>

If the tag is omitted, it defaults to div.

el('.card')            // <div class="card"></div>
el('#hero')            // <div id="hero"></div>

If given an existing Element it does nothing on its own but uses the provided element as a target for applying the children arguments. For example you can append things to the document body by doing

el(document.body, 'hello world')
<html>
  <body>hello world</body>
</html>

You can also use document.querySelector to find an existing element:

el(document.querySelector('#app'), 'hello world')
el(document.querySelector('.container'), el('p', 'content'))

String arguments provided as children are appended as text nodes

Element arguments are just appended directly

el('h1', document.createElement('div'))

Since el itself returns elements, this allows nesting of el calls to declaratively create the DOM

el(document.body,
  el('main', 
    el('h1', 'Title Text'),
    el('p', 'Paragraph text'),
  )
)
<html>
  <body>
    <main>
      <h1>Title Text</h1>
      <p>Paragraph text</p>
    </main>
  </body>
</html>

Function arguments are run in the context of the parent. This allows arbitrary manipulation of the parent such as attaching listeners, setting styles, etc.

el('h1', function() {
  this.id = 'foo'
  this.onclick = () => console.log('clicked!')
  this.style.color = 'red'
})

The parent is also provided as the first argument to the function when it is called. This allows arrow functions to work.

el('h1', x => {
  x.id = 'foo'
  x.onclick = () => console.log('clicked!')
  x.style.color = 'red'
})

If the function returns a value, that value is appended as a child.

el('h1', 
  x => {
    return 'some text'
  }, 
  x => ' more text'
)
<h1>some text more text</h1>

The attr(attribute, value) function is provided as a shorthand for

$ => { $.setAttribute(attribute, value) }

This allows easy setting of attributes like this

el('h1', attr('id', 'foo'))

Similarly the on(event, fn) function is provided as a shorthand for

$ => { $.addEventListener(event, fn) }

This allows easy attaching of event listeners like this

el('button', on('click', () => console.log('clicked!')))

Similarly the bind(reactor, key) function is provided as a shorthand for

$ => {
  $.addEventListener('input', () => { reactor[key] = $.value })
  return new Observer(() => { $.value = reactor[key] })
}

This allows for easy 2-way binding for input fields and Reactor objects.

const rx = new Reactor({ name: 'foo' })
el('input', bind(rx, 'name'))

Iterable arguments (arrays, Sets, NodeLists, generators, etc.) are flattened and their elements recursively appended.

el('h1', [
  'some text',
  document.createElement('div'),
  x => 'boop'
])
<h1>some text<div></div>boop</h1>

null and undefined children are silently ignored. This makes conditional rendering straightforward.

const showTitle = false
el('div',
  showTitle ? el('h1', 'Title') : null,  // null is ignored
  el('p', 'Always shown')
)
<div><p>Always shown</p></div>

Promise arguments create a comment placeholder.

let resolve
const somePromise = new Promise(r => { resolve = r })
el('h1', somePromise)
<h1><!-- promisePlaceholder --></h1>

When the promise resolves this placeholder is replaced with the resolved value.


Observer functions from Elemental are handled very similarly to functions. They are executed in the context of the parent. However they also leave a set of comments bookmarking the children they produce.

const rx = new Reactor({ name: 'foo' })
el('h1', ob(() => rx.name))
<h1>
  <!-- observerStart -->
  foo
  <!-- observerEnd -->
</h1>

When the observer is retriggered, everything between the bookmarks is removed and replaced with the new output.

<h1>
  <!-- observerStart -->
  bar
  <!-- observerEnd -->
</h1>

Read below for more details on how observers work.

Note: for observers attached as children via el they are deactivated automatically when their parent is not attached to the document. This should not affect most use cases since the observer is reactivated when the parent is reattached to the document. But note that unattached elements just being held in memory will not be changing.

Reactors

A Reactor is an object wrapper that automatically tracks Observer functions that read its properties and notifies the observers when those properties are updated.

You create a new reactor by calling its constructor:

const reactor = new Reactor()

You can also wrap an existing object with a reactor by passing it to the constructor. Changes to the reactor are passed through to the underlying object:

const reactor = new Reactor({
  foo: "bar"
})

Reactors behave mostly like plain JavaScript objects:

const reactor = new Reactor({
  foo: "bar"
})
// You can get and set properties as usual
reactor.foo // "bar"
reactor.cow = "moo"
// defineProperty works normally as well
Object.defineProperty(reactor, "milk", {
  get() { return "chocolate" }
})
reactor.milk // "chocolate"
// delete works too
delete reactor.foo
reactor.foo // undefined

The key difference of Reactor objects is that they track when one of their properties is read by an Observer function and will notify that observer when the property is updated:

const reactor = new Reactor({ foo: "bar" })

new Observer(() => {
  console.log("foo is ", reactor.foo)
})() // prints "foo is bar"

reactor.foo = "moo" // prints "foo is moo"

Object.defineProperty(reactor, "foo", {
  get() { return "meow" }
}) // prints "foo is meow"

delete reactor.foo // prints "foo is undefined"

Tracking is property-specific, so observers will not trigger if a different property is updated:

const reactor = new Reactor({
  foo: "bar",
  moo: "mar"
})

new Observer(() => {
  console.log("foo tracker is now", reactor.foo)
})() // prints "foo tracker is now bar"

new Observer(() => {
  console.log("moo tracker is now", reactor.moo)
})() // prints "moo tracker is now mar"

reactor.foo = "bar2" // prints "foo tracker is now bar2"
reactor.moo = "mar2" // prints "moo tracker is now mar2"
reactor.goo = "goop" // does not trigger any observers

If reading a reactor's property returns an object, that object is recursively wrapped in a reactor before being returned. This allows observers to track dependencies in nested objects easily:

const reactor = new Reactor({
  outer: {
    inner: "cake"
  }
})

new Observer(() => {
  console.log("inner value is ", reactor.outer.inner)
})() // prints "inner value is cake"

Reactors are implemented using Proxy objects. This means reactors created from scratch typecheck as Reactors, but reactors created from an existing object typecheck as the original object:

const baseReactor = new Reactor()
baseReactor instanceof Reactor // true
const mapReactor = new Reactor(new Map())
mapReactor instanceof Reactor // false
mapReactor instanceof Map // true

This also has implications for native objects or objects that use private properties. Since proxies can't access native or private properties, some methods will fail. To work around this, we provide the shuck function which returns a reactor's internal object:

// Native object example
const mapReactor = new Reactor(new Map())
Map.prototype.keys.apply(mapReactor) // throws an error
Map.prototype.keys.apply(shuck(mapReactor)) // works fine

Observers

An Observer is like a normal function that you can define and call. When an Observer reads from a Reactor, it automatically tracks that dependency, and when that reactor's property is updated, it automatically triggers the observer again.

Observer functions are created by passing a function to its constructor:

const observer = new Observer(() => {
  console.log("hello world")
})
observer() // prints "hello world" and starts the observer

For brevity, observers can also be created and instantly executed like this:

new Observer(() => {
  console.log("hello world")
})() // prints "hello world" and starts the observer

When an Observer reads a Reactor property, it gets saved as a dependent. When that property is updated, it notifies the observer which reruns its function. This happens automatically without any need to manually declare dependencies:

const reactor = new Reactor()
new Observer(() => {
  console.log("reactor.foo is ", reactor.foo)
})() // prints "reactor.foo is undefined"

reactor.foo = "bar" // prints "reactor.foo is bar"

An observer's dependencies are dynamically determined. Only the dependencies actually read in the last execution of an observer can trigger it again. This means that reactor reads that are only conditionally used will not trigger the observer unnecessarily:

const reactor = new Reactor({
  a: true,
  b: "bee",
  c: "cee"
})
new Observer(() => {
  if (reactor.a) {
    console.log("reactor.b is ", reactor.b)
  } else {
    console.log("reactor.c is ", reactor.c)
  }
})() // prints "reactor.b is bee"

reactor.b = "boop" // prints "reactor.b is boop"
reactor.c = "cat" // does not trigger the observer

reactor.a = false // prints "reactor.c is cat"
reactor.b = "blue" // does not trigger the observer
reactor.c = "cheese" // prints "reactor.c is cheese"

An observer's results are themselves observable via either the value property or by triggering the observer via observer() and using the return value. This allows you to chain observers together:

const reactor = new Reactor({ foo: 'bar' })
const capitalizer = new Observer(() => {
  return reactor.foo.toUpperCase()
})()
const printer = new Observer(() => {
  console.log(capitalizer.value)
})() // prints 'BAR'
reactor.foo = 'baz' // prints 'BAZ'

This also works:

const reactor = new Reactor({ foo: 'bar' })
const capitalizer = new Observer(() => {
  return reactor.foo.toUpperCase()
}) // Did not start the observer here
const printer = new Observer(() => {
  // Manually calls capitalizer like a function which activates it
  // As well as accesses its return value as a dependency
  console.log(capitalizer())
})() // starts printer which starts capitalizer
reactor.foo = 'baz' // prints 'BAZ'

You can stop an observer by calling stop() on the returned observer object. This clears any existing dependencies and prevents triggering. You can restart the observer by calling start(). Starting is idempotent, so calling start() on an already running observer will have no effect:

const reactor = new Reactor()
const observer = new Observer(() => {
  console.log(reactor.foo)
})() // prints "undefined"

reactor.foo = "bar" // prints "bar"

observer.stop()

reactor.foo = "cheese" // does not trigger the observer

observer.start() // prints "cheese"
observer.start() // No effect
observer.start() // No effect
observer.start() // No effect

reactor.foo = "moo" // prints "moo"

For convenience, you can call an observer to execute like a normal function. This works regardless of whether the observer is stopped. Doing so starts the observer up again:

const reactor = new Reactor({ foo: "hello" })
const observer = new Observer(() => {
  console.log(reactor.foo)
})() // prints "hello"
reactor.foo = "hi" // prints "hi"
observer() // prints "hi" again

observer.stop()
reactor.foo = "hola" // does not trigger the observer since it's stopped
observer() // prints "hola"

Like normal functions, observers can expect and be called with arguments. They remember the arguments from the last time they were called and reuse them when automatically triggered:

const parameterizedObserver = new Observer((arg1, arg2) => {
  console.log(reactor.foo + arg1 + arg2)
})
parameterizedObserver('beep', 'bop') // prints bazbeepbop
reactor.foo = 'bla' // prints blabeepbop

Observers can also use and remember the last this context. Note that just like normal functions, for the this context to be bound to the holding object, it needs to be defined with the traditional function keyword instead of ES6 arrow functions:

const holdingObject = {
  name: 'Mario',
  greet: new Observer(function () { // Need to use `function`
    console.log("Hello " + reactor.foo + " itsa me " + this.name)
  })
}
holdingObject.greet() // prints "Hello bla itsa me Mario"
reactor.foo = 'bonk' // prints "Hello bonk itsa me Mario"
holdingObject.name = 'Luigi' // prints nothing since holdingObject is not a Reactor

If you ever need to access the raw function the observer is wrapping, you do so by using shuck:

const myFunction = () => {}
const observer = new Observer(myFunction)
myFunction === shuck(observer) // true

Hide

Sometimes you might want to read from a reactor without becoming dependent on it. A common case for this is when using array modification methods. These often also read from the array in order to do the modification:

const taskList = new Reactor(["a", "b", "c", "d"])

// Creating the following observer will cause an infinite loop
// because it both reads from and modifies the length property of taskList
// As a result, it triggers itself in the middle of execution
// This loop is detected and creates an exception
new Observer(() => {
  // Even though we only want to modify the array
  // pop() also reads the length property of the array
  console.log(taskList.pop())
})()

In these cases, you can use "hide" to shield a block of code from creating dependencies. It takes a function and any reactor properties read inside that function will not be set as dependencies. hide also passes through the return value of its function for syntactic simplicity:

const taskList = new Reactor(["a", "b", "c", "d"])

new Observer(() => {
  console.log(
    // Because we wrap the pop() call in a hide block
    // it does not create a dependency on the length property
    // unlike our previous example
    hide(() => taskList.pop())
  )
})() // prints "d"

taskList.push("e") // does not trigger the observer

Note that only the reads inside the hide block are shielded from creating dependencies. The rest of the observe block still creates dependencies as normal.

Batching

One problem with automatic watchers is that you might end up with multiple repeated triggering when you're updating a lot of information all at once. The following code shows an example where you want to update multiple properties, but each property update prematurely triggers the observer since you are not done updating yet:

const person = new Reactor({
  firstName: "Anakin",
  lastName: "Skywalker",
  faction: "Jedi",
  rank: "Knight"
})

// This observer tracks multiple properties
// and so will be triggered when any of the properties get updated
const observer = new Observer(() => {
  console.log(
    "I am " +
    person.firstName +
    " " +
    person.lastName +
    ", " +
    person.faction +
    " " +
    person.rank
  )
})() // prints "I am Anakin Skywalker, Jedi Knight"

// The following updates will each trigger the observer even though we only
// want to trigger the observer once all the updates are complete
person.firstName = "Darth" // prints "I am Darth Skywalker, Jedi Knight"
person.lastName = "Vader" // prints "I am Darth Vader, Jedi Knight"
person.faction = "Sith" // prints "I am Darth Vader, Sith Knight"
person.rank = "Lord" // prints "I am Darth Vader, Sith Lord"

The batch function is provided to allow you to batch multiple updates together and only trigger the appropriate observers once at the end of the batch block. So the last part of the previous example can be turned into:

// batch postpones any observer triggers that originate from inside it
// Triggers are deduplicated so any observer is triggered at most once
batch(() => {
  // None of the following updates will trigger the observer yet
  person.firstName = "Darth"
  person.lastName = "Vader"
  person.faction = "Sith"
  person.rank = "Lord"
}) // prints "I am Darth Vader, Sith Lord"

This is useful when you are making multiple data updates and want to avoid showing an "incomplete" view of the data to observers.

Note that only the observer triggering is postponed until the end. The actual reactor properties are updated in place as expected. This means that you can have other logic with read-what-you-write semantics within the observer block working just fine.

Summary

import { 
  el, attr, on, bind, ob,
  Reactor, Observer, hide, batch, shuck 
} from '@fynyky/elemental'

// el(description, children...)
el('h1')          // Creates <h1>
el('h1.foo')      // Creates <h1 class="foo">
el('h1#bar.foo')  // Creates <h1 id="bar" class="foo">
el('.foo')        // Creates <div class="foo">
el('#foo')        // Creates <div id="foo">

let aDiv = document.createElement('div')
el(aDiv) // Uses the provided element instead of creating a new one


el('h1', 'foo') // Creates <h1>foo</h1>
                // Strings provided as children are inserted as text nodes

el('h1', aDiv)  // Creates <h1><div></div></h1>
                // Elements provided as children are just appended

el('h1', function(){this.id = 'foo'}) // Creates <h1 id="foo"></h1>
                                      // Functions provided as children are 
                                      // executed in the context of the parent

el('h1', x => { x.id = 'foo' }) // Also creates <h1 id="foo"></h1>
                                // The parent is also provided as an argument
                                // This allows arrow functions to work

el('h1', () => "return value") // Creates <h1>return value</h1>
                               // Return values are appended as children

let resolve
const aPromise = new Promise(r => { resolve = r })
el('h1', aPromise) // Creates <h1><!-- promisePlaceholder --></h1>
                   // Places a comment to be replaced when the promise resolves
resolve('resolved!') // Becomes <h1>resolved!</h1>


// Example of how el works with reactors and observers
// Full explanation of how Observers and Reactors work comes later on
// Attached observers use comments to bookmark their children 
let rx = new Reactor({ foo: 'foo' })
let reactiveEl = el('h1', ob(() => rx.foo)) 
// Creates
// <h1>
//   <!-- observerStart -->
//   foo
//   <!-- observerEnd -->
// </h1>

document.body.appendChild(reactiveEl) // Attached observers sleep when their 
                                      // parent is out of the DOM
                                      // Need to attach it for reactivity

// When updated anything between the bookmarks gets replaced
rx.foo = 'bar'  
// Updates to 
// <h1>
  // <!-- observerStart -->
  // bar
  // <!-- observerEnd -->
// </h1>


el('h1', ['foo', 'bar', 'qux']) // Creates <h1>foobarqux</h1>
                                // Any iterable (arrays, Sets, generators, etc.) are done recursively
                                
// attr is shorthand for setting attributes
// These 2 are equivalent
el('h1', attr('id', 'foo'))
el('h1', self => self.setAttribute('id', 'foo'))

// on is shorthand for addEventListener
// These 2 are equivalent
el('button', on('click', handler))
el('button', self => self.addEventListener('click', handler))

// bind is shorthand for 2 way binding with a reactor
// These 2 are equivalent
el('input', bind(rx, 'foo'))
el('input', self => {
  self.addEventListener('input', () => { rx['foo'] = self.value })
  return new Observer(() => { self.value = rx['foo'] })
})

// ob is shorthand for creating Observers
// These 2 are equivalent
ob(function(){})
new Observer(function(){})

// Reactors and Observers
const reactor = new Reactor({ foo: 'bar' })
const observer = new Observer(() => {
  const result = 'reactor.foo is ' + reactor.foo // Sets a dependency on foo
  console.log(result) 
  return result
})
observer() // Prints 'reactor.foo is bar' and starts the observer
reactor.foo = 'baz' // Prints 'reactor.foo is baz'

observer.stop()
reactor.foo = 'qux' // Prints nothing since observer is stopped

observer.start() // Prints 'reactor.foo is baz'
observer.start() // Prints nothing since observer is already started
observer() // Prints 'reactor.foo is baz' even if it is already running

// Observers return values are themselves observable
const trailingObserver = new Observer(() => {
  const result = 'Did you hear: ' + observer.value
  console.log(result)
})
trailingObserver() // Prints 'Did you hear: reactor.foo is baz'
reactor.foo = 'blorp' // Prints 'reactor.foo is blorp' from observer
                      // Also prints 'Did you hear: reactor.foo is blorp' from trailingObserver

// Observers can be given parameters and remember these parameters when triggered
const parameterizedObserver = new Observer((arg1, arg2) => {
  console.log(reactor.foo + arg1 + arg2)
})
parameterizedObserver('beep', 'bop') // Prints bazbeepbop
reactor.foo = 'bla' // Prints blabeepbop

// Observers can also access and remember the last `this` context
const holdingObject = {
  name: 'Mario',
  greet: new Observer(function () { // Need to use traditional functions instead of arrow functions
    console.log("Hello " + reactor.foo + " itsa me " + this.name)
  })
}
holdingObject.greet() // Prints "Hello bla itsa me Mario"
reactor.foo = 'bonk' // Prints "Hello bonk itsa me Mario"
holdingObject.name = 'Luigi' // Prints nothing since holdingObject is not a Reactor

// hide allows you to avoid particular dependencies in an observer
// This is useful especially when using Array methods that both read and write
reactor.ticker = 1
reactor.names = ["Alice", "Bob", "Charles", "David"]
const partialObserver = new Observer(() => {
  if (reactor.ticker) {
    // hide passes through the return value of its block
    const next = hide(() => reactor.names.pop())
    console.log("next ", next)
  }
})
partialObserver() // prints "next David"
reactor.ticker = 2 // prints "next Charles"
reactor.names.push("Elsie") // Will not trigger the observer

// batch postpones any observer triggers until it is complete
// This allows grouping updates together
const person = new Reactor({
  firstName: 'Clark',
  lastName: 'Kent'
})
new Observer(() => {
  console.log('Look its ' + person.firstName + ' ' + person.lastName)
})() // Prints 'Look its Clark Kent'
batch(() => {
  // None of the following updates will trigger the observer yet
  person.firstName = "Bruce" 
  person.lastName = "Wayne"
}) // prints 'Look its Bruce Wayne'

// shuck removes the Reactor layer and returns the base object
// This is necessary for some native objects which don't work with proxies
const mapReactor = new Reactor(new Map())
Map.prototype.keys.call(mapReactor) // throws an Error
Map.prototype.keys.call(shuck(mapReactor)) // works fine

Development & Testing

Tests are stored in test/test.js and run in real browsers using Web Test Runner.

Run npm install to install the dev dependencies.

To run the tests run npm test.