Whats Up is a front-end framework based on the ideas of streams and fractals. It is very easy to learn and powerful to work with. It has only a few components, but enough to build complex applications.
npm i whatsup
it's like a observable
import { conse } from 'whatsup'
const name = conse('Natali')
it's like a computed
import { cause } from 'whatsup'
const user = cause(function* () {
while (true) {
yield {
name: yield* name,
}
}
})
This is the cherry on the cake. We'll tell you about it below :)
yield
- send to stream
yield*
- read from stream
import { whatsUp } from 'whatsup'
const onData = (data) => console.log(data)
const onError = (err) => console.error(err)
const dispose = whatsUp(user, onData, onError)
//> {name: 'Natali'}
name.set('Aria')
//> {name: 'Aria'}
dispose() // to stop whatsUping
You can extend base classes, but you need to implement the whatsUp
method
import { conse, cause, whatsUp } from 'whatsup'
interface UserData {
name: string
}
class Name extends Conse<string> {}
class User extends Cause<UserData> {
readonly name: Name
constructor(name: string) {
super()
this.name = new Name(name)
}
*whatsUp() {
while (true) {
yield {
name: yield* this.name,
}
}
}
}
const user = new User('Natali')
whatsUp(user, (data) => console.log(data))
//> {name: 'Natali'}
user.name.set('Aria')
//> {name: 'Aria'}
The context has several useful methods for controlling flow, such as update
import { cause, whatsUp } from 'whatsup'
class Timer extends Cause<number> {
constructor(readonly delay: number) {
super()
}
*whatsUp(ctx: Context) {
let i = 0
while (true) {
setTimeout(() => ctx.update(), this.delay)
// kickstart ^^^^^^^^^^^^ updating loop
yield i++
}
}
}
const timer = new Timer(1000)
whatsUp(timer, (data) => console.log(data))
//> 0
//> 1
//> 2 ...
You can store ancillary data available from calculation to calculation directly in the generator body and you can react to disposing with the native language capabilities
import { conse, cause, whatsUp } from 'whatsup'
class Timer extends Cause<number> {
constructor(readonly delay: number) {
super()
}
*whatsUp(ctx: Context) {
let i = 0
let timeoutId: number
try {
while (true) {
timeoutId = setTimeout(() => ctx.update(), this.delay)
yield i++
}
} finally {
// This block will always be executed when unsubscribing from a stream.
clearTimeout(timeoutId)
console.log('Timer destroed')
}
}
}
class App extends Cause<number> {
readonly showTimer = conse(true);
*whatsUp() {
// local scoped Timer instance, she is alive while the stream App is alive
const timer = new Timer()
while (true) {
if (yield* this.showTimer) {
const time = yield* timer
yield time
} else {
yield 'App timer is hidden'
}
}
}
}
const app = new App()
whatsUp(app, (data) => console.log(data))
//> 0
//> 1
//> 2
app.showTimer.set(false)
//> Timer destroed
//> App timer is hidden
app.showTimer.set(true) // the timer starts from the beginning
//> 0
//> 1
//> ...
Allows you to create new data based on previous. You need just to implement the mutate method.
import { cause, Mutator } from 'whatsup'
class Increment extends Mutator<number> {
mutate(prev: number | undefined = 0) {
return prev + 1
}
}
class Timer extends Cause<number> {
constructor(readonly delay: number) {
super()
}
*whatsUp(ctx: Context) {
while (true) {
setTimeout(() => ctx.update(), this.delay)
yield new Increment()
// we no longer need to store a local counter "i"
}
}
}
You can create a mutator using shorthand
import { cause, mutator } from 'whatsup'
const increment = mutator<number>((n = 0) => n + 1)
class Timer extends Cause<number> {
constructor(readonly delay: number) {
super()
}
*whatsUp(ctx: Context) {
while (true) {
setTimeout(() => ctx.update(), this.delay)
yield increment
}
}
}
Mutators can be used to write filters.
import { whatsUp, cause, Mutator } from 'whatsup'
class EvenOnly extends Mutator<number> {
readonly next: number
constructor(next: number) {
super()
this.next = next
}
mutate(prev = 0) {
return this.next % 2 === 0 ? this.next : prev
// We allow the new value only if it is even,
// otherwise we return the old value
// if the new value is strictly equal (===) to the old value,
// the App will stop updates propagation
}
}
class App extends Cause<number> {
*whatsUp() {
const timer = new Timer()
while (true) {
yield new EvenOnly(yield* timer)
}
}
}
const app = new App()
whatsUp(app, (data) => console.log(data))
//> 0
//> 2
//> 4
//> ...
You can create custom equal filters
import { whatsUp, cause, Mutator } from 'whatsup'
class EqualArr<T> extends Mutator<T[]> {
readonly arr: T[]
constructor(arr: T[]) {
super()
this.arr = arr
}
mutate(prev) {
const { arr } = this
if (Array.isArray(prev) && prev.lenght === arr.length && prev.every((item, i) => item === arr[i])) {
return prev
}
return arr
}
}
/*
then in whatsUp generator - yield new EqualArr([...])
*/
Fractal has its own plugin that converts jsx-tags into mutators calls. You can read the installation details here whatsup/babel-plugin-transform-jsx and whatsup/jsx
import { conse, Cause } from 'whatsup'
import { render } from '@whatsup-js/jsx'
class User extends Cause<JSX.Element> {
readonly name = conse('John')
readonly age = conse(33);
*whatsUp() {
while (true) {
yield (
<Container>
<Name>{yield* this.name}</Name>
<Age>{yield* this.age}</Age>
</Container>
)
}
}
}
const user = new User()
render(user)
// Yes, we can render without a container, directly to the body
The mutator gets the old DOMNode and mutates it into a new DOMNode the shortest way.
It looks like a cause, but for each consumer, the fractal creates a new iterator and context. Contexts bind to the consumer context like a parent-child relation and form a context tree. This allows you to organize the transfer of factors down the tree, as well as the bubbling of events up. A cause, unlike a fractal, creates one iterator for all consumers, and one context without a reference to the parent (root context).
import { Fractal, Event, Context, factor } from 'whatsup'
import { render } from '@whatsup-js/jsx'
const Theme = factor<'light' | 'dark'>('light')
// factor determining color scheme
// сustom event for remove Todo
class RemoveEvent extends Event {
constructor(readonly todo: Todo) {
super()
}
}
class Todo extends Fractal<JSX.Element> {
readonly name: Conse<string>
constructor(name: string) {
this.name = conse(name)
}
*whatsUp(ctx: Context) {
const theme = ctx.find(Theme)
// get value of ^^^ Theme factor
const onClick = () => ctx.dispatch(new RemoveEvent(this))
// start event bubbling ^^^^^^^^
while (true) {
yield (
<Container theme={theme}>
<Name>{yield* this.name}</Name>
<RemoveButton onClick={onClick} />
</Container>
)
}
}
}
class Todos extends Fractal<JSX.Element> {
readonly list: List<Todo> = list()
create(name: string) {
const todo = new Todo(name)
this.list.insert(todo)
}
remove(todo: Todo) {
this.list.delete(todo)
}
*whatsUp(ctx: Context) {
ctx.share(Theme, 'dark')
// ^^^ define value of Theme factor for children contexts
ctx.on(RemoveEvent, (e) => this.remove(e.todo))
// ^^ start event listening
while (true) {
const acc = []
for (const todo of yield* this.list) {
acc.push(yield* todo)
}
yield <Container>{acc}</Container>
}
}
}
const todos = new Todos()
render(todos)
This mechanism allows you to make any object publicly available to all children.
import { Fractal, whatsUp } from 'whatsup'
class Session {
constructor(readonly token: string) {}
}
class App extends Fractal<string> {
*whatsUp(ctx: Context) {
const session = new Session('Secret token')
const page = new Page()
ctx.share(this.session) // share Session instance
while (true) {
yield `App ${yield* page}`
}
}
}
class Page extends Fractal<string> {
*whatsUp(ctx: Context) {
const session = ctx.find(Session) // get Session instance
while (true) {
yield `Page ${session.token}`
}
}
}
whatsUp(new App(), (d) => console.log(d))
//> App Page Secret token
The share
method can take a factor as a share-key.
import { Fractal, whatsUp, factor } from 'whatsup'
const Theme = factor<string>('light')
// default value ^^^^^^^
class App extends Fractal<string> {
*whatsUp(ctx: Context) {
const page = new Page()
ctx.share(Theme, 'dark') // share theme value for children contexts
while (true) {
yield `App ${yield* page}`
}
}
}
class Page extends Fractal<string> {
*whatsUp(ctx: Context) {
const theme = ctx.find(Theme) // get Theme shared value
// when no factor is found (not shared in parents)
// the default value is returned - 'light'
while (true) {
yield `Page. Theme is ${theme}.`
}
}
}
whatsUp(new App(), (d) => console.log(d))
//> Page. Theme is dark.
The context has a defer
method. This method allows you to start the execution of asynchronous code, after the execution of which ctx.update()
will be automatically called. Defer returns an object like {done: boolean, value: T}
.
import { Cause, whatsUp } from 'whatsup'
// welcome.ts
export class Welcome extends Cause<string> {
*whatsUp() {
while (true) {
yield 'Hello world'
}
}
}
// app.ts
class App extends Cause<string> {
*whatsUp(ctx) {
const deferred = ctx.defer(async function () {
const { Welcome } = await import('./welcome')
return new Welcome()
})
// deferred is {done: false}
yield 'Loading...'
// deferred is {done: true, value: Welcome}
const welcome = deferred.value
while (true) {
yield `App: ${yield* welcome}`
}
}
}
whatsUp(new App(), (d) => console.log(d))
//> Loading...
//> App: Hello world
A useful mechanism by which a stream can delegate its work to another stream.
import { fractal, conse, whatsUp, delegate } from 'whatsup'
const Name = conse('John')
const User = fractal(function* () {
while (true) {
yield `User ${yield* Name}`
}
})
const Guest = fractal(function* () {
while (true) {
yield delegate(User) // delegate work to User
}
})
const guest = new Guest()
whatsUp(guest, (data) => console.log(data))
//> User John
In the following example, you can see what happens if a delegation is passed to the conse.
import { cause, conse, whatsUp, delegate } from 'whatsup'
const BarryName = cause(function* () {
while (true) yield 'Barry'
})
const Name = conse('John')
whatsUp(Name, (data) => console.log(data))
//> John
Name.set(delegate(BarryName))
//> Barry
All dependencies are updated synchronously in a topological sequence without unnecessary calculations.
import { conse, cause, whatsUp } from 'whatsup'
const num = conse(1)
const evenOrOdd = cause(function* () {
while (true) {
yield (yield* num) % 2 === 0 ? 'even' : 'odd'
}
})
const isEven = cause(function* () {
while (true) {
yield `${yield* num} is ${yield* evenOrOdd}`
}
})
const isZero = cause(function* () {
while (true) {
yield (yield* num) === 0 ? 'zero' : 'not zero'
}
})
whatsUp(isEven, (data) => console.log(data))
whatsUp(isZero, (data) => console.log(data))
//> 1 is odd
//> not zero
num.set(2)
//> 2 is even
num.set(3)
//> 3 is odd
num.set(0)
//> 0 is even
//> zero
Inside the generator, the keyword yield
push data to stream, and yield*
pull data from stream.
The life cycle consists of three steps:
- create a new iterator using a generator
- the iterator starts executing and stops after
yield
orreturn
, during this operation, all calls toyield*
automatically establish the observed dependencies; as soon as a new data is generated, the stream reports this to the parent and goes into standby mode for updates - having received the update message, the stream clears the list of dependencies and, if in the previous step the data was obtained using
yield
, the stream continues its work from the second step; if there was areturn
, the work continues from the first step
The return
statement does the same as the yield
statement, but it does the iterator reset, and the stream starts its life anew.
- Sierpinski - perfomance test like React sierpinski triangle. [source]
- Todos - WhatsUp realization of TodoMVC. [source]
- Loadable - an example showing how you can organize the display of loaders during background loading. I specifically added small delays there in order to slow down the processes. [source]
- Factors - work in different conditions. One and the same fractal, depending on the factor set in the context, gives three different results, and also maintains their relevance. Try editing the name and age. [source]
- Antistress - just a toy, click the balls, paint them in different colors and get cool pictures. In fact, this is a fractal that shows a circle inside itself, or three of the same fractals inscribed in the perimeter of the circle. Click - paint, long click - crush, long click in the center of the crushed circle - return to its original state. If you crush the circles to a sufficiently deep level, you can see the Sierpinski triangle. [source]