",
10 | "license": "Apache-2.0",
11 | "devDependencies": {
12 | "@testing-library/dom": "^9.3.3",
13 | "@types/jest": "^29.5.5",
14 | "dotenv": "^16.3.1",
15 | "jest": "^29.7.0",
16 | "jest-environment-jsdom": "^29.7.0",
17 | "mobx": "^5.15.4",
18 | "prettier": "^3.0.3",
19 | "ts-jest": "^29.1.1",
20 | "ts-node": "^10.9.1",
21 | "tslib": "^2.6.2",
22 | "typescript": "^5.2.2"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/lib/util/o-array.ts:
--------------------------------------------------------------------------------
1 | export interface OElement {
2 | order: string
3 | }
4 |
5 | export class OArray {
6 | static insert(array: OElement[], element: OElement): number {
7 | const {order} = element
8 |
9 | for (let i = 0; i < array.length; i++) {
10 | const current = array[i]
11 |
12 | if (order < current.order) {
13 | array.splice(i, 0, element)
14 | return i
15 | } else if (order === current.order) {
16 | array[i] = element
17 | return i
18 | }
19 | }
20 |
21 | array.push(element)
22 | return array.length - 1
23 | }
24 |
25 | static remove(array: OElement[], element: OElement): void {
26 | const {order} = element
27 |
28 | for (let i = 0; i < array.length; i++) {
29 | if (array[i].order === order) {
30 | array.splice(i, 1)
31 | }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/lib/util/ordered-array.test.ts:
--------------------------------------------------------------------------------
1 | import {OArray, OElement} from './o-array'
2 |
3 | describe('OArray operations', () => {
4 | const a = {order: 'a'}
5 | const b = {order: 'b'}
6 | const c = {order: 'c'}
7 | const z = {order: 'z'}
8 |
9 | test('inserts at correct location', () => {
10 | const array: OElement[] = []
11 |
12 | OArray.insert(array, c)
13 | expect(array).toEqual([c])
14 |
15 | OArray.insert(array, a)
16 | expect(array).toEqual([a, c])
17 | OArray.insert(array, a)
18 | expect(array).toEqual([a, c])
19 |
20 | OArray.insert(array, z)
21 | expect(array).toEqual([a, c, z])
22 |
23 | OArray.insert(array, b)
24 | expect(array).toEqual([a, b, c, z])
25 | })
26 |
27 | test('remove elements', () => {
28 | const array = [a, b, c, z]
29 |
30 | OArray.remove(array, b)
31 | expect(array).toEqual([a, c, z])
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export {$RunInAction, $Action, $AsyncAction} from './lib/observables/action'
2 | export * from './lib/component-types/host/dom-component-types'
3 | export {render} from './lib/render'
4 | export * from './lib/util/ref'
5 | export * from './lib/component-types/host/dom-components'
6 | export {PureComponent} from './lib/component-types/pure-component'
7 | export {$Component} from './lib/observables/$component'
8 | export * from './lib/observables/$set'
9 | export * from './lib/observables/$map'
10 | export {makeObservable, getObservableUntracked, $Model} from './lib/observables/$model'
11 | export {$AutoRun, $Reaction} from './lib/observables/responder'
12 | export * from './lib/util/style'
13 | export * from './lib/util/element'
14 | export * from './lib/component-types/host/svg-components'
15 | export {$Val} from './lib/observables/value-style'
16 | export {$Change} from './lib/observables/change/change'
17 |
--------------------------------------------------------------------------------
/src/lib/util/measure.ts:
--------------------------------------------------------------------------------
1 | //
2 |
3 | export function time(name: string): void {
4 | if (__DEV__ && typeof window !== 'undefined') {
5 | performance.mark(`${name} start`)
6 | }
7 | }
8 |
9 | export function timeEnd(name: string): void {
10 | if (__DEV__ && typeof window !== 'undefined') {
11 | performance.mark(`${name} end`)
12 | performance.measure(name, `${name} start`, `${name} end`)
13 | }
14 | }
15 |
16 | export function timeF(f: () => void, name: string) {
17 | time(name)
18 | f()
19 | timeEnd(name)
20 | }
21 |
22 | export function timeF2(f: () => void, name: string) {
23 | console.time(name)
24 | f()
25 | console.timeEnd(name)
26 | }
27 |
28 | /*
29 | performance.mark('Iteration Start')
30 | Iteration()
31 | performance.mark('Iteration End')
32 | performance.measure(
33 | 'Iteration',
34 | 'Iteration Start',
35 | 'Iteration End'
36 | )
37 | */
38 |
--------------------------------------------------------------------------------
/src/dom-tests/key-order-bug.test.ts:
--------------------------------------------------------------------------------
1 | import {mkRoot} from './test-helpers'
2 | import {ScaleElements} from './key-order-bug'
3 |
4 | describe('key order bug', () => {
5 | test('Scale at position 0', () => {
6 | const root = mkRoot()
7 |
8 | root.render(ScaleElements.$({position: 0}))
9 |
10 | expect(root.element.innerHTML).toEqual(
11 | 'element 0
element 1
element 2
'
12 | )
13 |
14 | root.render(ScaleElements.$({position: 1}))
15 |
16 | expect(root.element.innerHTML).toEqual(
17 | 'element 1
element 2
element 3
'
18 | )
19 |
20 | root.render(ScaleElements.$({position: 7}))
21 | root.render(ScaleElements.$({position: 1}))
22 | root.render(ScaleElements.$({position: 0}))
23 |
24 | expect(root.element.innerHTML).toEqual(
25 | 'element 0
element 1
element 2
'
26 | )
27 |
28 | // console.log(root.element.innerHTML)
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/src/lib/util/element.ts:
--------------------------------------------------------------------------------
1 | import type {CustomComponent} from '../component-types/pure-component'
2 |
3 | export interface StandardProps {
4 | children?: FiendNode[]
5 | key?: string
6 | }
7 |
8 | export enum ElementNamespace {
9 | html,
10 | svg,
11 | }
12 |
13 | export enum ElementType {
14 | dom,
15 | custom,
16 | }
17 |
18 | export type FiendElement =
19 | | HostElement
20 | | SvgElement
21 | | CustomElement
22 |
23 | export interface HostElement
{
24 | elementType: ElementType.dom
25 | _type: keyof HTMLElementTagNameMap
26 | namespace: ElementNamespace.html
27 | props: P
28 | }
29 | export interface SvgElement
{
30 | elementType: ElementType.dom
31 | _type: keyof SVGElementTagNameMap
32 | namespace: ElementNamespace.svg
33 | props: P
34 | }
35 | export interface CustomElement
{
36 | elementType: ElementType.custom
37 | _type: CustomComponent
38 | props: P
39 | }
40 |
41 | export type FiendNode = FiendElement | string | null
42 |
--------------------------------------------------------------------------------
/src/lib/observables/value-style.ts:
--------------------------------------------------------------------------------
1 | import {globalStack} from './global-stack'
2 | import {Computed} from './computed/computed'
3 | import {Atom} from './atom'
4 |
5 | export interface Calc {
6 | (): Readonly
7 |
8 | length: Symbol // This is to prevent accidental comparisons.
9 | }
10 |
11 | export function $Calc(f: () => T): Calc {
12 | const c = new Computed(f, f.name)
13 |
14 | return (() => {
15 | return c.get(globalStack.getCurrentResponder())
16 | }) as any
17 | }
18 |
19 | export interface Observable {
20 | (): Readonly
21 |
22 | (newValue: T): T
23 |
24 | length: Symbol // We override length to be symbol to prevent accidental length checks.
25 | }
26 |
27 | export function $Val(value: T): Observable {
28 | const a = new Atom(value, '$Val')
29 |
30 | function inner(): T
31 | function inner(newValue: T): T
32 | function inner(newValue?: T) {
33 | if (arguments.length === 0) {
34 | return a.get(globalStack.getCurrentResponder())
35 | }
36 |
37 | if (newValue !== undefined) a.set(newValue)
38 |
39 | return newValue
40 | }
41 |
42 | return inner as Observable
43 | }
44 |
--------------------------------------------------------------------------------
/src/lib/observables/$set.ts:
--------------------------------------------------------------------------------
1 | import {Atom} from './atom'
2 | import {globalStack} from './global-stack'
3 |
4 | export class $Set {
5 | #set: Set
6 | #size: Atom = new Atom(0, 'size')
7 |
8 | constructor(items: T[] = []) {
9 | this.#set = new Set(items)
10 | }
11 |
12 | add(item: T): this {
13 | this.#set.add(item)
14 | this.#size.set(this.#set.size)
15 |
16 | return this
17 | }
18 |
19 | delete(item: T): boolean {
20 | const deleted = this.#set.delete(item)
21 |
22 | if (deleted) {
23 | this.#size.set(this.#set.size)
24 | }
25 |
26 | return deleted
27 | }
28 |
29 | has(item: T): boolean {
30 | return this.#size.get(globalStack.getCurrentResponder()) > 0 && this.#set.has(item)
31 | }
32 |
33 | clear(): void {
34 | this.#set.clear()
35 | this.#size.set(0)
36 | }
37 |
38 | forEach(callbackfn: (value: T, value2: T, set: Set) => void, thisArg?: any): void {
39 | if (this.#size.get(globalStack.getCurrentResponder()) > 0) {
40 | this.#set.forEach(callbackfn, thisArg)
41 | }
42 | }
43 |
44 | get size(): number {
45 | return this.#size.get(globalStack.getCurrentResponder())
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/dom-tests/key-order-bug.ts:
--------------------------------------------------------------------------------
1 | import {Div} from '../lib/component-types/host/dom-components'
2 | import {FiendNode} from '../lib/util/element'
3 | import {PureComponent} from '../lib/component-types/pure-component'
4 |
5 | const numElements = 3
6 |
7 | export class ScaleElements extends PureComponent<{position: number}> {
8 | render() {
9 | const {position} = this.props
10 |
11 | const elements: FiendNode[] = []
12 |
13 | for (let i = position; i < position + numElements; i++) {
14 | const text = `element ${i}`
15 |
16 | elements.push(
17 | Element.$({
18 | text,
19 | key: text,
20 | }),
21 | )
22 | }
23 |
24 | return Div({children: elements})
25 | }
26 | }
27 |
28 | class Element extends PureComponent<{text: string}> {
29 | render() {
30 | const {text} = this.props
31 |
32 | return ElementInner.$({
33 | key: text,
34 | text,
35 | })
36 | }
37 | }
38 |
39 | interface Props {
40 | text: string
41 | }
42 |
43 | class ElementInner extends PureComponent {
44 | render() {
45 | const {text} = this.props
46 |
47 | return Div({
48 | key: text,
49 | children: [text],
50 | })
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/dom-tests/simple-switcher-z.test.ts:
--------------------------------------------------------------------------------
1 | import {render} from '..'
2 | import {screen} from '@testing-library/dom'
3 | import {$Component} from '..'
4 | import {Div} from '..'
5 | import {$Val} from '../lib/observables/value-style'
6 |
7 | export function sleep(ms: number) {
8 | return new Promise(resolve => {
9 | setTimeout(() => {
10 | resolve()
11 | }, ms)
12 | })
13 | }
14 |
15 | describe('simple switching with own observer library', () => {
16 | test('shows alternate div after state update', async () => {
17 | const store = new Store()
18 |
19 | const s = Switcher.$({store})
20 | render(s, document.body)
21 |
22 | expect(screen.queryByText('a')).toBeDefined()
23 | expect(screen.queryByText('b')).toBeNull()
24 |
25 | store.a(false)
26 |
27 | // await sleep(1)
28 |
29 | expect(screen.queryByText('b')).toBeDefined()
30 | expect(screen.queryByText('a')).toBeNull()
31 | })
32 | })
33 |
34 | class Store {
35 | a = $Val(true)
36 | }
37 |
38 | interface SwitcherProps {
39 | store: Store
40 | }
41 |
42 | class Switcher extends $Component {
43 | render() {
44 | const {store} = this.props
45 |
46 | if (store.a()) {
47 | return Div('a')
48 | }
49 |
50 | return Div('b')
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/lib/observables/computed/computed-run.test.ts:
--------------------------------------------------------------------------------
1 | import {$AutoRun, F0} from '../responder'
2 | import {makeObservable} from '../$model'
3 |
4 | describe('computed run behaviour', () => {
5 | class A {
6 | d: F0[] = []
7 | runs = 0
8 |
9 | $o = 0
10 |
11 | constructor() {
12 | makeObservable(this)
13 | }
14 |
15 | get $c() {
16 | this.runs++
17 | return this.$o
18 | }
19 |
20 | result = 0
21 |
22 | enableReactions() {
23 | this.d.push(
24 | $AutoRun(() => {
25 | this.result = this.$c
26 | })
27 | )
28 | }
29 |
30 | disableReactions() {
31 | this.d.forEach(d => d())
32 | this.d = []
33 | }
34 | }
35 |
36 | test('Runs at expected times when responder is off or on', () => {
37 | const a = new A()
38 | expect(a.runs).toBe(0)
39 | a.enableReactions()
40 | expect(a.runs).toBe(1)
41 | a.$o = 1
42 |
43 | expect(a.runs).toBe(2)
44 | expect(a.result).toBe(1)
45 |
46 | a.disableReactions()
47 | expect(a.runs).toBe(2)
48 |
49 | a.$o = 2
50 | expect(a.runs).toBe(2)
51 | expect(a.result).toBe(1)
52 |
53 | a.$o = 3
54 | expect(a.runs).toBe(2)
55 | expect(a.result).toBe(1)
56 |
57 | a.$o = 4
58 | a.enableReactions()
59 | expect(a.runs).toBe(3)
60 | expect(a.result).toBe(4)
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/src/lib/component-types/root-component.ts:
--------------------------------------------------------------------------------
1 | import {DomComponent} from './host/dom-component'
2 | import {TextComponent} from './text-component'
3 | import {Render} from '../render'
4 | import {Order} from '../util/order'
5 | import {AnyComponent} from './base-component'
6 | import {FiendElement} from '../..'
7 | import {RunStack} from '../observables/run-stack'
8 |
9 | export class RootComponent {
10 | component: AnyComponent | null = null
11 | order = '1'
12 | key = 'root'
13 |
14 | inserted: (DomComponent | TextComponent)[] = []
15 |
16 | // key is an element, value is the previous element
17 | siblings = new WeakMap()
18 |
19 | constructor(public element: HTMLElement) {}
20 |
21 | render(tree: FiendElement) {
22 | this.component = Render.component(tree, this.component, this, this, 0)
23 | RunStack.run()
24 | }
25 |
26 | insertChild(child: DomComponent | TextComponent) {
27 | Order.insert(this, child)
28 | }
29 |
30 | removeChild(child: DomComponent | TextComponent) {
31 | Order.remove(this, child)
32 | }
33 |
34 | moveChild(child: DomComponent | TextComponent) {
35 | Order.move(this, child)
36 | }
37 |
38 | remove(): void {
39 | this.component?.remove()
40 | this.component = null
41 | }
42 |
43 | get html(): string {
44 | return this.element.innerHTML
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/lib/observables/memory.test.ts:
--------------------------------------------------------------------------------
1 | import {$Model, makeObservable} from './$model'
2 | import {$AutoRun} from './responder'
3 | import {$Val} from './value-style'
4 |
5 | describe('memory leak test', () => {
6 | let n = 0
7 | const outer = $Val(0)
8 |
9 | class A {
10 | constructor() {
11 | makeObservable(this)
12 |
13 | $AutoRun(() => {
14 | this.$b
15 | })
16 | }
17 | get $b() {
18 | n++
19 | return outer()
20 | }
21 | }
22 |
23 | test('a', () => {
24 | new A()
25 | expect(n).toEqual(1)
26 | outer(outer() + 1)
27 | expect(n).toEqual(2)
28 | })
29 |
30 | test('a2', () => {
31 | new A()
32 | expect(n).toEqual(3)
33 | outer(outer() + 1)
34 | expect(n).toEqual(5)
35 | })
36 | })
37 |
38 | describe('memory leak test2', () => {
39 | let n = 0
40 | const outer = $Val(0)
41 |
42 | class A extends $Model {
43 | constructor() {
44 | super()
45 | super.connect()
46 |
47 | this.$AutoRun(() => {
48 | this.$b
49 | })
50 | }
51 | get $b() {
52 | n++
53 | return outer()
54 | }
55 | }
56 |
57 | test('a', () => {
58 | const a = new A()
59 | expect(n).toEqual(1)
60 | outer(outer() + 1)
61 | expect(n).toEqual(2)
62 |
63 | a.disposeReactions()
64 | })
65 |
66 | test('a2', () => {
67 | new A()
68 | expect(n).toEqual(3)
69 | outer(outer() + 1)
70 | expect(n).toEqual(4)
71 | })
72 | })
73 |
--------------------------------------------------------------------------------
/src/lib/observables/$component.ts:
--------------------------------------------------------------------------------
1 | import {$AutoRun, $Reaction, PureComponent} from '../..'
2 | import {globalStack} from './global-stack'
3 | import {F0, OrderedResponder, ResponderType} from './responder'
4 | import {makeObservable} from './$model'
5 | import {RunStack} from './run-stack'
6 |
7 | export abstract class $Component
8 | extends PureComponent
9 | implements OrderedResponder
10 | {
11 | responderType = ResponderType.component as const
12 |
13 | ordered = true as const
14 | disposers: F0[] = []
15 |
16 | mount() {
17 | makeObservable(this)
18 |
19 | this.update()
20 | RunStack.componentDidMountStack.push(this._ref)
21 | }
22 |
23 | run() {
24 | if (this._ref.current === null) {
25 | return
26 | }
27 |
28 | if (__FIEND_DEV__) {
29 | console.debug('run', this.constructor.name)
30 | }
31 |
32 | this.update()
33 |
34 | RunStack.componentDidUpdateStack.push(this._ref)
35 | }
36 |
37 | update() {
38 | globalStack.pushResponder(this)
39 | super.update()
40 | globalStack.popResponder()
41 | }
42 |
43 | remove(): void {
44 | this._ref.current = null
45 |
46 | for (const d of this.disposers) d()
47 | this.disposers = []
48 | super.remove()
49 | }
50 |
51 | $AutoRun(f: () => void) {
52 | this.disposers.push($AutoRun(f))
53 | }
54 |
55 | $Reaction(calc: () => T, f: (result: T) => void) {
56 | this.disposers.push($Reaction(calc, f))
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/lib/observables/computed/computed-get.test.ts:
--------------------------------------------------------------------------------
1 | import {makeObservable} from '../$model'
2 | import {Computed} from './computed'
3 | import {Atom} from '../atom'
4 |
5 | describe('Computed get behaviour', () => {
6 | test('1 computed, call get without a responder', () => {
7 | class A {
8 | declare __$o: Atom
9 | $o = 0
10 |
11 | runs = 0
12 |
13 | constructor() {
14 | makeObservable(this)
15 | }
16 |
17 | declare __$c: Computed
18 | get $c() {
19 | this.runs++
20 | return this.$o
21 | }
22 | }
23 |
24 | const a = new A()
25 | expect(a.runs).toBe(0)
26 |
27 | // call get(null)
28 | a.$c
29 | expect(a.runs).toBe(1)
30 |
31 | a.$o = 1
32 | expect(a.runs).toBe(1)
33 |
34 | expect(a.__$c._ref.current).toBe(null)
35 | expect(a.__$o.hasActiveResponders()).toBe(false)
36 | })
37 |
38 | test(`Computed shouldn't become active if the calling computed isn't active`, () => {
39 | class A {
40 | declare __$o: Atom
41 | $o = 0
42 |
43 | constructor() {
44 | makeObservable(this)
45 | }
46 |
47 | declare __$c: Computed
48 | get $c() {
49 | return this.$o
50 | }
51 |
52 | declare __$c2: Computed
53 | get $c2() {
54 | return this.$c
55 | }
56 | }
57 |
58 | const a = new A()
59 |
60 | expect(a.$c2).toBe(0)
61 | expect(a.__$c2._ref.current).toBe(null)
62 | expect(a.__$c._ref.current).toBe(null)
63 | })
64 | })
65 |
--------------------------------------------------------------------------------
/src/lib/observables/$map.ts:
--------------------------------------------------------------------------------
1 | import {Atom} from './atom'
2 | import {globalStack} from './global-stack'
3 |
4 | export class $Map {
5 | #map: Map
6 | #size: Atom = new Atom(0, 'size')
7 | #changes: Atom = new Atom(0, 'changes')
8 |
9 | constructor() {
10 | this.#map = new Map()
11 | }
12 |
13 | clear(): void {
14 | this.#map.clear()
15 | this.#size.set(0)
16 | }
17 |
18 | delete(key: K): boolean {
19 | const deleted = this.#map.delete(key)
20 |
21 | if (deleted) {
22 | this.#size.set(this.#map.size)
23 | }
24 |
25 | return deleted
26 | }
27 |
28 | forEach(callbackfn: (value: V, key: K, map: Map) => void, thisArg?: any): void {
29 | if (this.#size.get(globalStack.getCurrentResponder()) > 0) {
30 | this.#map.forEach(callbackfn, thisArg)
31 | }
32 | }
33 |
34 | get(key: K): V | undefined {
35 | if (this.#size.get(globalStack.getCurrentResponder()) > 0) {
36 | return this.#map.get(key)
37 | }
38 | return undefined
39 | }
40 |
41 | has(key: K): boolean {
42 | return this.#size.get(globalStack.getCurrentResponder()) > 0 && this.#map.has(key)
43 | }
44 |
45 | set(key: K, value: V): this {
46 | const prevValue = this.#map.get(key)
47 |
48 | if (value !== prevValue) {
49 | this.#map.set(key, value)
50 | this.#changes.set(this.#changes.get(globalStack.getCurrentResponder()) + 1)
51 | }
52 | this.#size.set(this.#map.size)
53 |
54 | return this
55 | }
56 |
57 | get size(): number {
58 | return this.#size.get(globalStack.getCurrentResponder())
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/lib/observables/atom.ts:
--------------------------------------------------------------------------------
1 | import {Responder, UnorderedResponder} from './responder'
2 | import {
3 | addCallingResponderToOurList,
4 | hasActiveResponders,
5 | Notifier,
6 | notify,
7 | } from './notifier'
8 | import {$Component} from './$component'
9 | import {RefObject} from '../util/ref'
10 | import {Computed} from './computed/computed'
11 |
12 | export class Atom implements Notifier {
13 | computeds = new Set>>()
14 | reactions = new Set>()
15 | components = new Map>()
16 |
17 | constructor(public value: T, public name: string) {}
18 |
19 | get(responder: Responder | null): T {
20 | if (responder !== null) {
21 | this.addCallingResponderToOurList(responder)
22 | }
23 |
24 | return this.value
25 | }
26 |
27 | set(value: T) {
28 | if (this.value !== value) {
29 | this.value = value
30 |
31 | if (this.hasActiveResponders()) {
32 | notify(this)
33 | } else {
34 | this.deactivateAndClear()
35 | }
36 | }
37 | }
38 |
39 | hasActiveResponders(): boolean {
40 | return hasActiveResponders(this)
41 | }
42 |
43 | deactivateAndClear(): void {
44 | const {computeds, reactions, components} = this
45 |
46 | reactions.clear()
47 | components.clear()
48 |
49 | for (const c of computeds) {
50 | if (c.current !== null) {
51 | c.current.deactivateAndClear()
52 | }
53 | }
54 | computeds.clear()
55 | }
56 |
57 | addCallingResponderToOurList(responder: Responder): void {
58 | addCallingResponderToOurList(this, responder)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/dom-tests/test-scheduling-z.test.tsx:
--------------------------------------------------------------------------------
1 | import {render} from '..'
2 | import {screen} from '@testing-library/dom'
3 | import {$Component} from '..'
4 | import {Div} from '..'
5 | import {$Val} from '../lib/observables/value-style'
6 |
7 | export function sleep(ms: number) {
8 | return new Promise(resolve => {
9 | setTimeout(() => {
10 | resolve()
11 | }, ms)
12 | })
13 | }
14 |
15 | // TODO: This test needs to be rewritten.
16 |
17 | xdescribe('test scheduling', () => {
18 | test('shows alternate div after state update', () => {
19 | const store = new Store()
20 |
21 | // const s =
22 | const s = A.$({store})
23 | render(s, document.body)
24 |
25 | console.log(document.body.innerHTML)
26 |
27 | expect(screen.queryByText('a')).toBeDefined()
28 | expect(screen.queryByText('b')).toBeNull()
29 |
30 | // store.a(false)
31 |
32 | // await sleep(1)
33 |
34 | expect(screen.queryByText('b')).toBeDefined()
35 | expect(screen.queryByText('a')).toBeNull()
36 | })
37 | })
38 |
39 | class Store {
40 | a = $Val(2)
41 | b = $Val(3)
42 | c = $Val(4)
43 | }
44 |
45 | interface SwitcherProps {
46 | store: Store
47 | }
48 | class A extends $Component {
49 | render() {
50 | const {store} = this.props
51 |
52 | return [Div(store.a().toString()), B.$({store})]
53 |
54 | // return (
55 | //
56 | // {store.a()}
57 | //
58 | //
59 | // )
60 | }
61 | }
62 | class B extends $Component {
63 | render() {
64 | const {store} = this.props
65 |
66 | return Div(store.b().toString())
67 | }
68 | }
69 | class C extends $Component {
70 | render() {
71 | const {store} = this.props
72 |
73 | return Div(store.c().toString())
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/lib/component-types/host/dom-components.ts:
--------------------------------------------------------------------------------
1 | import {makeHtmlElementConstructor} from './dom-component-types'
2 |
3 | export const H1 = makeHtmlElementConstructor('h1')
4 | export const H2 = makeHtmlElementConstructor('h2')
5 | export const H3 = makeHtmlElementConstructor('h3')
6 | export const Div = makeHtmlElementConstructor('div')
7 | export const Header = makeHtmlElementConstructor('header')
8 | export const Footer = makeHtmlElementConstructor('footer')
9 | export const Span = makeHtmlElementConstructor('span')
10 | export const A = makeHtmlElementConstructor('a')
11 | export const P = makeHtmlElementConstructor('p')
12 | export const B = makeHtmlElementConstructor('b')
13 | export const Br = makeHtmlElementConstructor('br')
14 | export const Img = makeHtmlElementConstructor('img')
15 | export const Ul = makeHtmlElementConstructor('ul')
16 | export const Li = makeHtmlElementConstructor('li')
17 | export const Ol = makeHtmlElementConstructor('ol')
18 | export const Video = makeHtmlElementConstructor('video')
19 | export const Source = makeHtmlElementConstructor('source')
20 | export const Idiomatic = makeHtmlElementConstructor('i')
21 | export const Button = makeHtmlElementConstructor('button')
22 | export const Canvas = makeHtmlElementConstructor('canvas')
23 | export const Form = makeHtmlElementConstructor('form')
24 | export const Input = makeHtmlElementConstructor('input')
25 | export const DataList = makeHtmlElementConstructor('datalist')
26 | export const Option = makeHtmlElementConstructor('option')
27 | export const Textarea = makeHtmlElementConstructor('textarea')
28 | export const Label = makeHtmlElementConstructor('label')
29 |
30 | export const Table = makeHtmlElementConstructor('table')
31 | export const Tr = makeHtmlElementConstructor('tr')
32 | export const Td = makeHtmlElementConstructor('td')
33 | export const Th = makeHtmlElementConstructor('th')
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FiendUI
2 |
3 | Small UI library (~15kb) heavily inspired by React and Mobx that I use in both GitFiend and the GitFiend website.
4 |
5 | After spending 100s of hours optimising React code I got tired of how difficult it was. This library makes it a lot
6 | simpler to make awesome dynamic UI.
7 |
8 | ### Features:
9 |
10 | - Familiar declarative component style
11 | - Fast and lightweight
12 | - Easy and efficient state sharing between components:
13 | Modify a variable anywhere and any component that is using it is automatically and efficiently updated.
14 |
15 | ```ts
16 | class Store {
17 | constructor() {
18 | // Converts any fields starting with '$' into observables.
19 | makeObservable(this)
20 | }
21 |
22 | // '$' as first character creates an observable.
23 | $num = 2
24 |
25 | // '$' as first character makes this getter a computed.
26 | get $square() {
27 | return this.$num ** 2
28 | }
29 | }
30 |
31 | // A $Component renders when any observable or computed it
32 | // uses from anywhere is updated.
33 | class MyComponent extends $Component<{
34 | store: Store
35 | }> {
36 | render() {
37 | const {$num, $square} = this.props.store
38 |
39 | return Div({
40 | className: 'MyComponent',
41 | children: [
42 | `Square of ${$num} equals ${$square}.`,
43 | Button({children: ['increment'], onclick: this.onClick}),
44 | ],
45 | })
46 | }
47 |
48 | onClick = () => {
49 | this.props.store.$num++
50 | }
51 | }
52 | ```
53 |
54 | #### Setup Root
55 | ```ts
56 | const store = new Store()
57 |
58 | render(MyComponent.$({store}), document.getElementById('root'))
59 | ```
60 |
61 | ### Usage
62 |
63 | I currently use this by cloning this repo and importing from "index.ts".
64 |
65 | ### Development
66 |
67 | Requires a recent Node.js and Npm installed.
68 |
69 | Install dependencies: `npm i`
70 | Run tests: `npm t`
71 |
--------------------------------------------------------------------------------
/src/lib/component-types/text-component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AnyComponent,
3 | ComponentBase,
4 | ComponentType,
5 | ParentComponent,
6 | } from './base-component'
7 | import {Order} from '../util/order'
8 | import {DomComponent} from './host/dom-component'
9 | import {RootComponent} from './root-component'
10 |
11 | export class TextComponent implements ComponentBase {
12 | _type = ComponentType.text as const
13 | element: Text
14 | order: string
15 | key: string
16 |
17 | constructor(
18 | public text: string,
19 | public domParent: DomComponent | RootComponent,
20 | directParent: ParentComponent,
21 | public index: number,
22 | ) {
23 | const order = Order.key(directParent.order, index)
24 |
25 | this.key = directParent.key + index
26 | this.order = order
27 | this.element = document.createTextNode(text)
28 |
29 | domParent.insertChild(this)
30 | }
31 |
32 | remove(): void {
33 | this.domParent.removeChild(this)
34 | }
35 | }
36 |
37 | export function renderTextComponent(
38 | text: string,
39 | prev: AnyComponent | null,
40 | domParent: DomComponent | RootComponent,
41 | directParent: ParentComponent,
42 | index: number,
43 | ): TextComponent {
44 | if (prev === null) {
45 | return new TextComponent(text, domParent, directParent, index)
46 | }
47 |
48 | if (prev._type === ComponentType.text) {
49 | const prevOrder = prev.order
50 | const newOrder = Order.key(directParent.order, index)
51 |
52 | if (prevOrder !== newOrder) {
53 | prev.index = index
54 | prev.order = newOrder
55 |
56 | domParent.moveChild(prev)
57 | }
58 |
59 | if (prev.text === text) {
60 | return prev
61 | } else {
62 | prev.element.nodeValue = text
63 | prev.text = text
64 | return prev
65 | }
66 | }
67 |
68 | prev.remove()
69 | return new TextComponent(text, domParent, directParent, index)
70 | }
71 |
--------------------------------------------------------------------------------
/src/dom-tests/host.test.ts:
--------------------------------------------------------------------------------
1 | import {Div} from '..'
2 | import {mkRoot} from './test-helpers'
3 |
4 | describe('simple div', () => {
5 | // xtest('render host', () => {
6 | // const root = mkRoot()
7 | //
8 | // renderHost('div', {}, null, root.order, 0)
9 | //
10 | // expect(root.element.innerHTML).toEqual('
')
11 | // })
12 | //
13 | // xtest('update div', () => {
14 | // const root = mkRoot()
15 | //
16 | // const host = renderHost('div', {}, null, root.order, 0)
17 | //
18 | // expect(root.element.innerHTML).toEqual('
')
19 | //
20 | // const host2 = renderHost>(
21 | // 'div',
22 | // {className: 'simple'},
23 | // host,
24 | // root.order,
25 | // 0
26 | // )
27 | //
28 | // expect(root.element.innerHTML).toEqual(`
`)
29 | //
30 | // renderHost('div', {}, host2, root.order, 0)
31 | //
32 | // expect(root.element.innerHTML).toEqual('
')
33 | // })
34 |
35 | test('remove child on next render', () => {
36 | const root = mkRoot()
37 |
38 | const h = Div({children: [Div('a'), Div('b')]})
39 |
40 | root.render(h)
41 |
42 | expect(root.element.innerHTML).toEqual(``)
43 |
44 | const h2 = Div({children: [Div('a')]})
45 |
46 | root.render(h2)
47 |
48 | expect(root.element.innerHTML).toEqual(``)
49 | })
50 |
51 | test('remove child on next render2', () => {
52 | const root = mkRoot()
53 |
54 | root.render(Div({children: [Div('a'), Div('b')]}))
55 | // const divs = renderTree(div({children: [div('a'), div('b')]}), null, root.order, 0)
56 |
57 | expect(root.element.innerHTML).toEqual(``)
58 |
59 | root.render(Div({children: [Div('a')]}))
60 | // renderTree(div({children: [div('a')]}), divs, root.order, 0)
61 |
62 | expect(root.element.innerHTML).toEqual(``)
63 | })
64 | })
65 |
--------------------------------------------------------------------------------
/src/lib/util/order.test.ts:
--------------------------------------------------------------------------------
1 | import {Order} from './order'
2 | import {DomComponent} from '../component-types/host/dom-component'
3 | import {ElementNamespace} from './element'
4 | import {ElementComponent} from '../component-types/base-component'
5 | import {RunStack} from '../observables/run-stack'
6 | import {mkRoot} from '../../dom-tests/test-helpers'
7 |
8 | describe('Order.add comparisons', () => {
9 | test('1.1 < 1.2', () => {
10 | expect(Order.key('1', 1) < Order.key('1', 2)).toBe(true)
11 | })
12 |
13 | test('1 3 < 1 20', () => {
14 | expect(Order.key('1', 3) < Order.key('1', 20)).toBe(true)
15 | })
16 | })
17 |
18 | describe('insert', () => {
19 | const root = mkRoot()
20 |
21 | const parent = new DomComponent('div', ElementNamespace.html, {}, root, root, 0)
22 | RunStack.run()
23 | const {inserted} = parent
24 |
25 | test('try different insert indices', () => {
26 | new DomComponent('div', ElementNamespace.html, {}, parent, parent, 3)
27 | RunStack.run()
28 | expect(inserted.map(i => i.order)).toEqual(['103'])
29 | checkOrder(inserted)
30 |
31 | new DomComponent('div', ElementNamespace.html, {}, parent, parent, 4)
32 | RunStack.run()
33 | expect(inserted.map(i => i.order)).toEqual(['103', '104'])
34 | checkOrder(inserted)
35 |
36 | new DomComponent('div', ElementNamespace.html, {}, parent, parent, 1)
37 | RunStack.run()
38 | expect(inserted.map(i => i.order)).toEqual(['101', '103', '104'])
39 | checkOrder(inserted)
40 |
41 | new DomComponent('div', ElementNamespace.html, {}, parent, parent, 2)
42 | RunStack.run()
43 | expect(inserted.map(i => i.order)).toEqual(['101', '102', '103', '104'])
44 | checkOrder(inserted)
45 | })
46 | })
47 |
48 | export function checkOrder(inserted: ElementComponent[]) {
49 | let prev: ElementComponent | null = null
50 |
51 | for (const c of inserted) {
52 | if (prev !== null) {
53 | expect(prev.element).toBe(c.element.previousElementSibling)
54 | }
55 | prev = c
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/lib/util/style.test.ts:
--------------------------------------------------------------------------------
1 | import {c, s} from './style'
2 |
3 | describe('s - tagged style string template function', () => {
4 | const text = 'width: 50px; height: 2px;'
5 | const style = s`width: 50px; height: 2px;`
6 |
7 | test(text, () => {
8 | expect(style).toEqual(text)
9 | })
10 |
11 | test('empty string', () => {
12 | expect(s``).toEqual('')
13 | })
14 |
15 | test('templates', () => {
16 | expect(s`height: ${50}px; width: ${30}px`).toEqual('height: 50px; width: 30px')
17 | })
18 | })
19 |
20 | describe('class merging', () => {
21 | test('generates expected class string when given bool flags', () => {
22 | expect(c`CloneDialog ${true}`).toEqual('CloneDialog')
23 | expect(c`CloneDialog ${false}`).toEqual('')
24 |
25 | expect(c`omg ${true} nah ${false}`).toEqual('omg')
26 | expect(c`omg ${false} nah ${true}`).toEqual('nah')
27 | expect(c`omg ${false} nah ${false}`).toEqual('')
28 | expect(c`omg`).toEqual('omg')
29 | expect(c`omg nah`).toEqual('omg nah')
30 | })
31 |
32 | xtest('compare speed', () => {
33 | const num = 1000000
34 |
35 | let s: string
36 |
37 | console.time('plain')
38 | for (let i = 0; i < num; i++) {
39 | s = `omg`
40 | s = `omg2`
41 | }
42 | console.timeEnd('plain')
43 |
44 | console.time('c')
45 | for (let i = 0; i < num; i++) {
46 | s = c`omg ${true} nah ${false} blah ${false}`
47 | s = c`omg2`
48 | }
49 | console.timeEnd('c')
50 |
51 | console.time('mc')
52 | for (let i = 0; i < num; i++) {
53 | s = mc('omg', {
54 | nah: false,
55 | blah: false,
56 | })
57 | s = mc('omg2', {})
58 | }
59 | console.timeEnd('mc')
60 | })
61 | })
62 |
63 | function mc(initialClass: string, mapping: Record): string {
64 | let classes = ''
65 |
66 | for (const key in mapping) {
67 | if (mapping.hasOwnProperty(key) && mapping[key] === true) {
68 | classes += ' ' + key
69 | }
70 | }
71 |
72 | return initialClass + classes
73 | }
74 |
--------------------------------------------------------------------------------
/src/lib/component-types/host/dom-component-types.ts:
--------------------------------------------------------------------------------
1 | import {RefObject} from '../../util/ref'
2 | import {FiendNode} from '../../..'
3 | import {ElementNamespace, ElementType, HostElement} from '../../util/element'
4 |
5 | type DataPropertyNames = {
6 | [K in keyof T]: T[K] extends Function ? never : K
7 | }[keyof T]
8 | type OptionalDataPropertiesOnly = {
9 | [P in DataPropertyNames]?: T[P]
10 | }
11 |
12 | export type ElementNameMap = SVGElementTagNameMap & HTMLElementTagNameMap
13 |
14 | export type HostAttributes = Omit<
15 | OptionalDataPropertiesOnly,
16 | 'style' | 'children'
17 | > & {
18 | key?: string
19 | style?: string
20 | ref?: RefObject
21 | 'aria-labelledby'?: string
22 | 'aria-describedby'?: string
23 | role?: 'tab' | 'dialog' | 'checkbox' | 'button'
24 | children?: FiendNode[]
25 | }
26 |
27 | export type SvgElementAttributes = Omit<
28 | OptionalDataPropertiesOnly,
29 | 'style' | 'children'
30 | > & {
31 | key?: string
32 | style?: string
33 | ref?: RefObject
34 | children?: FiendNode[]
35 | }
36 |
37 | export function makeHtmlElementConstructor(
38 | tagName: N,
39 | ): (props?: HostAttributes | string) => HostElement {
40 | return props => {
41 | if (props === undefined)
42 | return {
43 | elementType: ElementType.dom,
44 | _type: tagName,
45 | namespace: ElementNamespace.html,
46 | props: {},
47 | }
48 |
49 | if (typeof props === 'string')
50 | return {
51 | elementType: ElementType.dom,
52 | _type: tagName,
53 | namespace: ElementNamespace.html,
54 | props: {
55 | children: [props],
56 | },
57 | }
58 |
59 | return {
60 | elementType: ElementType.dom,
61 | _type: tagName,
62 | namespace: ElementNamespace.html,
63 | props,
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/demos/lots-of-elements.tsx:
--------------------------------------------------------------------------------
1 | // import {createElement} from '../lib/create-element'
2 | // import {render} from '../lib/render'
3 | //
4 | // export function lotsOfElements(root: HTMLElement): void {
5 | // console.time('render')
6 | //
7 | // render(
8 | //
14 | //
Hello
15 | // Some
Text
16 | //
17 | //
a
18 | //
b
19 | //
c
20 | //
a
21 | //
b
22 | //
c
23 | //
a
24 | //
b
25 | //
c
26 | //
32 | //
Hello
33 | // Some
Text
34 | //
35 | //
a
36 | //
b
37 | //
c
38 | //
a
39 | //
b
40 | //
c
41 | //
a
42 | //
b
43 | //
c
44 | //
50 | //
Hello
51 | // Some
Text
52 | //
53 | //
a
54 | //
b
55 | //
c
56 | //
a
57 | //
b
58 | //
c
59 | //
a
60 | //
b
61 | //
c
62 | //
63 | //
64 | //
65 | //
66 | //
67 | //
,
68 | // root
69 | // )
70 | //
71 | // console.timeEnd('render')
72 | // }
73 |
--------------------------------------------------------------------------------
/src/lib/render.test.ts:
--------------------------------------------------------------------------------
1 | import {Div} from './component-types/host/dom-components'
2 | import {render} from './render'
3 | import {$Component} from './observables/$component'
4 | import {$Model} from './observables/$model'
5 | import {PureComponent} from './component-types/pure-component'
6 | import {FiendNode} from './util/element'
7 |
8 | class F extends PureComponent {
9 | render() {
10 | return this.props.children ?? []
11 | }
12 | }
13 |
14 | function frag(...children: FiendNode[]) {
15 | return F.$({children})
16 | }
17 |
18 | describe('render', () => {
19 | test('repeat render', () => {
20 | const el = document.createElement('div')
21 |
22 | let t = frag(Div('a'), Div('b'), Div('c'))
23 |
24 | render(t, el)
25 |
26 | expect(el.innerHTML).toEqual('a
b
c
')
27 |
28 | t = frag(Div('a'), Div('b'), Div('c'), Div('d'))
29 |
30 | render(t, el)
31 |
32 | expect(el.innerHTML).toEqual('a
b
c
d
')
33 |
34 | t = frag(Div('d'), Div('a'), Div('b'), Div('c'))
35 |
36 | render(t, el)
37 |
38 | expect(el.innerHTML).toEqual('d
a
b
c
')
39 | })
40 |
41 | test('update observables', () => {
42 | class S extends $Model {
43 | $a = true
44 |
45 | constructor() {
46 | super()
47 | super.connect()
48 | }
49 | }
50 |
51 | class A extends PureComponent {
52 | render() {
53 | return Div('a')
54 | }
55 | }
56 | class B extends PureComponent {
57 | render() {
58 | return Div('b')
59 | }
60 | }
61 |
62 | class C extends $Component<{store: S}> {
63 | render() {
64 | if (this.props.store.$a) return A.$({})
65 | return B.$({})
66 | }
67 | }
68 |
69 | const el = document.createElement('div')
70 |
71 | const s = new S()
72 | render(C.$({store: s}), el)
73 | expect(el.innerHTML).toEqual('a
')
74 |
75 | s.$a = false
76 | expect(el.innerHTML).toEqual('b
')
77 | })
78 | })
79 |
--------------------------------------------------------------------------------
/src/demos/boxes.ts:
--------------------------------------------------------------------------------
1 | import {PureComponent} from '../lib/component-types/pure-component'
2 | import {render} from '../lib/render'
3 | import {Div} from '../lib/component-types/host/dom-components'
4 | import {s} from '../lib/util/style'
5 | import {FiendElement} from '../lib/util/element'
6 |
7 | const boxHeight = 14
8 |
9 | interface BoxesProps {
10 | width: number
11 | height: number
12 | }
13 |
14 | export class Boxes extends PureComponent {
15 | render() {
16 | const t = ((Date.now() - this.tick) / 20) % boxHeight
17 |
18 | const {width, height} = this.props
19 |
20 | return Div({children: this.drawBoxes(width, height, -t)})
21 | }
22 |
23 | drawBoxes(width: number, height: number, top: number) {
24 | const numBoxes = Math.ceil(height / boxHeight) + 1
25 | const left = width * 0.2
26 | const boxWidth = width * 0.6
27 |
28 | const boxes: FiendElement[] = Array(numBoxes)
29 |
30 | for (let i = 0; i < numBoxes; i++) {
31 | boxes[i] = this.drawBox(left, top + i * boxHeight, boxWidth, boxHeight)
32 | }
33 |
34 | return boxes
35 | }
36 |
37 | drawBox(x: number, y: number, w: number, h: number): FiendElement {
38 | return Div({
39 | style: s`
40 | position: absolute;
41 | left: ${x}px;
42 | top: ${y}px;
43 | width: ${w}px;
44 | height: ${h}px;
45 | border: 1px solid;
46 | display: flex;
47 | align-items: center;
48 | padding-left: 5px;
49 | font-size: 8px;
50 | `,
51 | children: [`I am a commit message. Here is some text.`],
52 | })
53 | }
54 |
55 | componentDidMount(): void {
56 | const {width, height} = this.props
57 |
58 | this.loopBoxes(width, height)
59 | }
60 |
61 | tick = Date.now()
62 |
63 | loopBoxes(width: number, height: number) {
64 | this.update()
65 |
66 | requestAnimationFrame(() => {
67 | this.loopBoxes(width, height)
68 | })
69 | }
70 | }
71 |
72 | export function boxesTest() {
73 | console.time('render')
74 |
75 | render(Boxes.$({width: window.innerWidth, height: window.innerHeight}), document.body)
76 |
77 | console.timeEnd('render')
78 | }
79 |
--------------------------------------------------------------------------------
/.idea/watcherTasks.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/lib/component-types/fragment.test.ts:
--------------------------------------------------------------------------------
1 | import {$F} from './fragment'
2 | import {Div} from './host/dom-components'
3 | import {$Component} from '../..'
4 | import {makeObservable} from '../..'
5 | import {mkRoot} from '../../dom-tests/test-helpers'
6 |
7 | describe('fragment', () => {
8 | test('one host child', () => {
9 | const root = mkRoot()
10 |
11 | const t = $F(Div('omg'))
12 |
13 | root.render(t)
14 |
15 | expect(root.element.innerHTML).toEqual('omg
')
16 | })
17 |
18 | test('custom component child', () => {
19 | class C extends $Component {
20 | render() {
21 | return Div('omg')
22 | }
23 | }
24 |
25 | const root = mkRoot()
26 | root.render(C.$({}))
27 |
28 | expect(root.element.innerHTML).toEqual('omg
')
29 | })
30 |
31 | test('multiple children', () => {
32 | const root = mkRoot()
33 |
34 | let t = $F(Div('a'), Div('b'), Div('c'))
35 |
36 | root.render(t)
37 |
38 | expect(root.element.innerHTML).toEqual('a
b
c
')
39 |
40 | t = $F(Div('a'), Div('b'), Div('c'), Div('d'))
41 |
42 | root.render(t)
43 |
44 | expect(root.element.innerHTML).toEqual(
45 | 'a
b
c
d
',
46 | )
47 |
48 | t = $F(Div('d'), Div('a'), Div('b'), Div('c'))
49 |
50 | root.render(t)
51 |
52 | expect(root.element.innerHTML).toEqual(
53 | 'd
a
b
c
',
54 | )
55 | })
56 |
57 | test('custom component with fragment', () => {
58 | class S {
59 | $text = 'text1'
60 |
61 | constructor() {
62 | makeObservable(this)
63 | }
64 | }
65 |
66 | class C extends $Component<{store: S}> {
67 | render() {
68 | return Div({children: [this.props.store.$text], key: 'container'})
69 | }
70 | }
71 | const root = mkRoot()
72 |
73 | const s = new S()
74 |
75 | root.render(C.$({store: s}))
76 | expect(root.element.innerHTML).toEqual('text1
')
77 |
78 | s.$text = 'text2'
79 | expect(root.element.innerHTML).toEqual('text2
')
80 |
81 | s.$text = 'text3'
82 | expect(root.element.innerHTML).toEqual('text3
')
83 | })
84 | })
85 |
--------------------------------------------------------------------------------
/src/lib/observables/notifier.ts:
--------------------------------------------------------------------------------
1 | import {globalStack} from './global-stack'
2 | import {Responder, ResponderType, UnorderedResponder} from './responder'
3 | import {$Component} from './$component'
4 | import {RunStack} from './run-stack'
5 | import {RefObject} from '../util/ref'
6 | import {Computed} from './computed/computed'
7 |
8 | /*
9 | A Notifier is something with observable-like behaviour.
10 |
11 | Could be a plain observable or a computed (Computeds are both Notifiers and Responders).
12 |
13 | */
14 | export interface Notifier {
15 | computeds: Set>>
16 | reactions: Set>
17 | components: Map>
18 | }
19 |
20 | export function addCallingResponderToOurList(
21 | notifier: Notifier,
22 | responder: Responder
23 | ): void {
24 | switch (responder.responderType) {
25 | case ResponderType.computed:
26 | notifier.computeds.add(responder._ref)
27 | break
28 | case ResponderType.autoRun:
29 | case ResponderType.reaction:
30 | notifier.reactions.add(responder._ref)
31 | break
32 | case ResponderType.component:
33 | // TODO: Improve types.
34 | const r = responder as $Component
35 | notifier.components.set(r.order, r._ref)
36 | break
37 | }
38 | }
39 |
40 | export function notify(notifier: Notifier): void {
41 | const {computeds, reactions, components} = notifier
42 |
43 | if (computeds.size > 0 || reactions.size > 0 || components.size > 0) {
44 | if (!globalStack.queueNotifierIfInAction(notifier)) {
45 | notifier.computeds = new Set()
46 | notifier.reactions = new Set()
47 | notifier.components = new Map()
48 |
49 | RunStack.runResponders(computeds, reactions, components)
50 | }
51 | }
52 | }
53 |
54 | export function hasActiveResponders({
55 | reactions,
56 | components,
57 | computeds,
58 | }: Notifier): boolean {
59 | for (const r of reactions) if (r.current !== null) return true
60 | for (const c of components.values()) if (c.current !== null) return true
61 | for (const r of computeds)
62 | if (r.current !== null) {
63 | if (hasActiveResponders(r.current)) {
64 | return true
65 | }
66 | }
67 |
68 | return false
69 | }
70 |
--------------------------------------------------------------------------------
/src/lib/component-types/host/svg-components.ts:
--------------------------------------------------------------------------------
1 | import {FiendElement, FiendNode, StandardProps, SvgElementAttributes} from '../../..'
2 | import {ElementNamespace, ElementType, SvgElement} from '../../util/element'
3 |
4 | /** @deprecated as this approach doesn't work well for Svg */
5 | export function makeSvgElementConstructor(
6 | tagName: N,
7 | ): (props: SvgElementAttributes) => SvgElement {
8 | return props => {
9 | return {
10 | elementType: ElementType.dom,
11 | _type: tagName,
12 | namespace: ElementNamespace.svg,
13 | props,
14 | }
15 | }
16 | }
17 |
18 | export function makeSvgElementConstructor2(
19 | tagName: keyof SVGElementTagNameMap,
20 | ): (props: Props) => SvgElement {
21 | return props => {
22 | return {
23 | elementType: ElementType.dom,
24 | _type: tagName,
25 | namespace: ElementNamespace.svg,
26 | props,
27 | }
28 | }
29 | }
30 |
31 | export type SvgAttributes = Omit, 'width' | 'height'> & {
32 | width?: number
33 | height?: number
34 | }
35 | export type PolyLineAttributes = Omit, 'points'> & {
36 | points?: string
37 | strokeLinejoin?: string
38 | stroke?: string
39 | strokeWidth?: string
40 | fill?: string
41 | }
42 | export type PolygonAttributes = Omit, 'points'> & {
43 | points?: string
44 | strokeLinejoin?: string
45 | stroke?: string
46 | strokeWidth?: string
47 | fill?: string
48 | }
49 |
50 | export const Svg = makeSvgElementConstructor('svg') as (
51 | ...args: [SvgAttributes] | FiendNode[]
52 | ) => FiendElement
53 | export const Polyline = makeSvgElementConstructor('polyline') as (
54 | ...args: [PolyLineAttributes] | FiendNode[]
55 | ) => FiendElement
56 | export const Polygon = makeSvgElementConstructor('polygon') as (
57 | ...args: [PolygonAttributes] | FiendNode[]
58 | ) => FiendElement
59 | export const Circle = makeSvgElementConstructor('circle') as any
60 | export const Title = makeSvgElementConstructor('title')
61 | export const G = makeSvgElementConstructor('g')
62 |
63 | export const Line = makeSvgElementConstructor2<{
64 | x1?: number
65 | x2?: number
66 | y1?: number
67 | y2?: number
68 | stroke?: string
69 | style?: string
70 | }>('line')
71 |
--------------------------------------------------------------------------------
/src/lib/observables/global-stack.ts:
--------------------------------------------------------------------------------
1 | import {Responder, ResponderType} from './responder'
2 | import {Notifier} from './notifier'
3 | import {ActionState} from './action'
4 | import {Computed} from './computed/computed'
5 | import {RefObject} from '../util/ref'
6 |
7 | export class GlobalStack {
8 | private responderStack: Responder[] = []
9 | private actionStack: ActionState | null = null
10 | private actionDepth = 0
11 |
12 | /*
13 | We put a responder on a stack so that notifiers can register themselves
14 | with the current responder.
15 | */
16 | pushResponder(responder: Responder): void {
17 | this.responderStack.push(responder)
18 | }
19 |
20 | popResponder(): void {
21 | this.responderStack.pop()
22 | }
23 |
24 | getCurrentResponder(): Responder | null {
25 | const len = this.responderStack.length
26 |
27 | if (len > 0) {
28 | return this.responderStack[len - 1]
29 | }
30 | return null
31 | }
32 |
33 | queueNotifierIfInAction(notifier: Notifier): boolean {
34 | if (this.actionStack !== null) {
35 | this.actionStack.add(notifier)
36 |
37 | return true
38 | }
39 | return false
40 | }
41 |
42 | // "Inside reactive context"
43 | insideNonComputedResponder(): boolean {
44 | return this.responderStack.some(r => r.responderType !== ResponderType.computed)
45 | }
46 |
47 | /*
48 | We don't wait till the end of an action before running a computed if accessed as
49 | computeds need to always return the correct value.
50 | */
51 | runComputedNowIfInActionStack(computed: RefObject>): boolean {
52 | if (this.actionStack?.computeds.delete(computed) === true) {
53 | computed.current?.run()
54 | return true
55 | }
56 | return false
57 | }
58 |
59 | startAction(): void {
60 | this.actionDepth++
61 |
62 | if (this.actionStack === null) {
63 | this.actionStack = new ActionState(this.getCurrentResponder())
64 | } else {
65 | this.actionStack.runningResponder = this.getCurrentResponder()
66 | }
67 | }
68 |
69 | endAction(): void {
70 | this.actionDepth--
71 |
72 | if (this.actionDepth === 0) {
73 | if (this.actionStack !== null) {
74 | const action = this.actionStack
75 | this.actionStack = null
76 | action.run()
77 | }
78 | }
79 | }
80 | }
81 |
82 | export const globalStack = new GlobalStack()
83 |
--------------------------------------------------------------------------------
/src/lib/observables/computed/computed-behaviour.md:
--------------------------------------------------------------------------------
1 | # Computed Behaviour
2 | #### TODO: Convert these scenarios into unit tests.
3 |
4 | A computed can be notified by (via run())
5 | - Observable
6 | - Another Computed
7 |
8 | A Computed can notify
9 | - AutoRun
10 | - Reaction
11 | - $Component
12 |
13 | Confusing: Called by Responder vs dependencies to notify.
14 |
15 | Imagine:
16 | - A computed contains an observable.
17 | - It was created inside a $Component
18 | - The observable is updated
19 | - This notifies the computed outside a Responder
20 | - The computed calls run:
21 | - Notifies the $Component
22 | - $Component calls its run. $Component requests latest value (get) from Computed.
23 | - Computed is given a responder this time.
24 | - The $Component is unmounted, setting its own _ref to null. The computed has the null ref in it's notify list
25 | - The computed no longer has non-null responders to notify.
26 | - We should clear our list next time we run and set our _ref to null if we have none.
27 |
28 | ## Called outside/without a Responder:
29 |
30 | ### run()
31 | `run()` can only be called if _ref is not null.
32 |
33 | If a Computed is called outside a responder it should run because the observable
34 | that called it is allowed to be outside a responder.
35 |
36 | Responders don't get added to this computed as part of run(). We could
37 | get added to the calling observable or computed. We shouldn't if we have no
38 | responders.
39 |
40 | ### get(null)
41 | get is typically called by another computed, a reaction or $Component.
42 |
43 | Example:
44 | We call a computed inside the body of a $Reaction.
45 |
46 | (TODO: Check this example, seems wrong)
47 | Imagine an onmouseover event; a computed is then called outside a responder via `get(null)`.
48 | E.g. onmouseover -> set $hoverIndex -> notify $hovering computed -> draw
49 |
50 | If we are dirty (run() queued in action), then rerun. Notify responders.
51 | if _ref is null we don't know whether we are dirty because `run()` can only be called
52 | (by a notifier) if _ref is not null! (we have been turned off after all)
53 | This means we also have to rerun. We should have no responders?
54 |
55 | If get is called outside a responder, then no responders will be added to our list.
56 | Do we have any responders? If not, then _ref should be null.
57 |
58 | ## Call Computed.get(responder) when computed._ref is null
59 |
60 | ### If the responder is active (reaction or a computed with a reaction dependency):
61 | We haven't been listening for changes, so must call f(). We have a new responder
62 | to add to our list.
63 | _ref is no longer null
64 |
65 | ### We are called by a computed with null _ref
66 | We must call f(). _ref is still null. Warn about full recompute?
67 |
--------------------------------------------------------------------------------
/src/lib/observables/action.test.ts:
--------------------------------------------------------------------------------
1 | import {$Action, $AsyncAction, $RunInAction} from './action'
2 | import {$AutoRun} from './responder'
3 | import {sleep} from '../../dom-tests/simple-switcher-z.test'
4 | import {$Calc, $Val} from './value-style'
5 |
6 | describe('action', () => {
7 | test('action batches updates', () => {
8 | const a = $Val(1)
9 | let count = 0
10 |
11 | $AutoRun(() => {
12 | count++
13 | a()
14 | })
15 |
16 | $RunInAction(() => {
17 | a(2)
18 | a(3)
19 | a(4)
20 | })
21 |
22 | expect(count).toEqual(2)
23 | })
24 |
25 | test('autorun supports setting', () => {
26 | const a = $Val(1)
27 | const b = $Val(2)
28 | let count = 0
29 |
30 | $AutoRun(() => {
31 | count++
32 | b(a() * 5)
33 | })
34 |
35 | expect(count).toEqual(1)
36 | })
37 |
38 | test('autoRun with computed inside behaves', () => {
39 | const a = $Val(2)
40 | const b = $Val(2)
41 | const c = $Calc(() => a() * b())
42 | let d: number = c()
43 |
44 | expect(c()).toEqual(4)
45 |
46 | let count = 0
47 |
48 | $AutoRun(() => {
49 | count++
50 | b(a() * 5)
51 |
52 | expect(b()).toEqual(10)
53 |
54 | // Computed needs to update now.
55 | expect(c()).toEqual(20)
56 |
57 | d = c()
58 | })
59 |
60 | expect(count).toEqual(1)
61 | })
62 | })
63 |
64 | describe('async action behaviour', () => {
65 | test('simple case', async () => {
66 | class Actions {
67 | num = $Val(1)
68 |
69 | updates = 0
70 |
71 | constructor() {
72 | $AutoRun(() => {
73 | this.num()
74 | this.updates++
75 | })
76 | }
77 |
78 | run = $Action(() => {
79 | this.num(2)
80 | this.num(3)
81 | this.num(4)
82 | })
83 |
84 | // @ts-expect-error
85 | runAsync = $Action(async () => {
86 | this.num(2)
87 | await sleep(1)
88 | this.num(3)
89 | await sleep(1)
90 | this.num(4)
91 | })
92 |
93 | runAsync2 = $AsyncAction(async () => {
94 | this.num(2)
95 | await sleep(1)
96 | this.num(3)
97 | await sleep(1)
98 | this.num(4)
99 | })
100 | }
101 |
102 | const a = new Actions()
103 |
104 | await a.runAsync()
105 |
106 | expect(a.num()).toEqual(4)
107 | expect(a.updates).toEqual(4)
108 |
109 | a.num(1)
110 | a.run()
111 |
112 | expect(a.num()).toEqual(4)
113 | expect(a.updates).toEqual(6)
114 |
115 | a.num(1)
116 | await a.runAsync2()
117 |
118 | expect(a.num()).toEqual(4)
119 | expect(a.updates).toEqual(8)
120 | })
121 | })
122 |
--------------------------------------------------------------------------------
/src/lib/observables/observable.test.ts:
--------------------------------------------------------------------------------
1 | import {$AutoRun} from './responder'
2 | import {$RunInAction} from './action'
3 | import {makeObservable} from './$model'
4 | import {$Calc, $Val} from './value-style'
5 |
6 | describe('observables', () => {
7 | test('autorun', () => {
8 | let count = 0
9 | const a = $Val(5)
10 |
11 | $AutoRun(() => {
12 | count++
13 | a()
14 | })
15 |
16 | a(6)
17 | expect(a()).toEqual(6)
18 | expect(count).toEqual(2)
19 |
20 | a(7)
21 | expect(a()).toEqual(7)
22 | expect(count).toEqual(3)
23 | })
24 |
25 | test('computed', () => {
26 | let count = 0
27 | const a = $Val(5)
28 |
29 | const c = $Calc(() => {
30 | count++
31 | return a() * 3
32 | })
33 |
34 | a(5)
35 | a(5)
36 |
37 | expect(c()).toEqual(15)
38 | expect(count).toEqual(1)
39 |
40 | a(6)
41 | expect(c()).toEqual(18)
42 | expect(count).toEqual(2)
43 | })
44 |
45 | test('nested computed', () => {
46 | let count1 = 0
47 | let count2 = 0
48 |
49 | const a = $Val(5)
50 |
51 | const c = $Calc(() => {
52 | count1++
53 | return a() * 3
54 | })
55 |
56 | const c2 = $Calc(() => {
57 | count2++
58 | return c() + 2
59 | })
60 |
61 | expect(c2()).toEqual(17)
62 |
63 | a(4)
64 |
65 | expect(c()).toEqual(12)
66 | expect(c2()).toEqual(14)
67 | expect(count2).toEqual(2)
68 |
69 | a(6)
70 | expect(c2()).toEqual(20)
71 | expect(count2).toEqual(3)
72 | })
73 |
74 | /*
75 |
76 | I think computeds need to keep their own queue and run when called upon?
77 |
78 | They are different from reactions.
79 |
80 | */
81 | test('computeds in actions', () => {
82 | let count = 0
83 |
84 | const a = $Val(2)
85 | const c = $Calc(() => {
86 | count++
87 | return a() + 1
88 | })
89 |
90 | expect(count).toEqual(0)
91 | c()
92 | expect(count).toEqual(1)
93 |
94 | $RunInAction(() => {
95 | a(3)
96 | expect(a()).toEqual(3)
97 | expect(c()).toEqual(4)
98 | })
99 |
100 | expect(count).toEqual(2)
101 | })
102 |
103 | test('computeds in actions 2', () => {
104 | class A {
105 | count = 0
106 |
107 | $a = 2
108 |
109 | get $c() {
110 | this.count++
111 | return this.$a + 1
112 | }
113 |
114 | constructor() {
115 | makeObservable(this)
116 | }
117 | }
118 |
119 | const a = new A()
120 |
121 | expect(a.count).toEqual(0)
122 |
123 | a.$c
124 |
125 | $RunInAction(() => {
126 | a.$a = 3
127 |
128 | expect(a.$a).toEqual(3)
129 | expect(a.count).toEqual(1)
130 |
131 | expect(a.$c).toEqual(4)
132 | expect(a.count).toEqual(2)
133 | })
134 | })
135 | })
136 |
--------------------------------------------------------------------------------
/src/lib/observables/responder.ts:
--------------------------------------------------------------------------------
1 | import {globalStack} from './global-stack'
2 | import {$Component} from './$component'
3 | import {RefObject} from '../util/ref'
4 | import {Computed} from './computed/computed'
5 |
6 | /*
7 | A Responder is an object that listens accesses to notifiers (observables). When these
8 | notifiers change, the responder is run.
9 |
10 | Could be a Computed or a reaction such as AutoRun.
11 |
12 | */
13 |
14 | export enum ResponderType {
15 | computed,
16 | autoRun,
17 | reaction,
18 | component,
19 | }
20 |
21 | export type Responder = $Component | Computed | AutoRun | Reaction
22 |
23 | export interface OrderedResponder {
24 | responderType: ResponderType
25 | ordered: true
26 | order: string
27 | run(): void
28 | _ref: RefObject<$Component>
29 | }
30 |
31 | export interface UnorderedResponder {
32 | responderType: ResponderType
33 | ordered: false
34 | run(): void
35 | _ref: RefObject
36 | }
37 |
38 | export type F0 = () => void
39 |
40 | class AutoRun implements UnorderedResponder {
41 | responderType = ResponderType.autoRun as const
42 | ordered = false as const
43 |
44 | _ref: RefObject = {
45 | current: this,
46 | }
47 |
48 | constructor(public f: () => void) {
49 | this.run()
50 | }
51 |
52 | run() {
53 | if (this._ref.current === null) return
54 |
55 | globalStack.pushResponder(this)
56 | this.f()
57 | globalStack.popResponder()
58 | }
59 |
60 | end: F0 = () => {
61 | this._ref.current = null
62 | }
63 | }
64 |
65 | export function $AutoRun(f: () => void): F0 {
66 | return new AutoRun(f).end
67 | }
68 |
69 | class Reaction implements UnorderedResponder {
70 | responderType = ResponderType.reaction as const
71 | ordered = false as const
72 | value: T
73 |
74 | _ref: RefObject = {
75 | current: this,
76 | }
77 |
78 | constructor(private calc: () => T, private f: (result: T) => void) {
79 | globalStack.pushResponder(this)
80 | this.value = this.calc()
81 | globalStack.popResponder()
82 | }
83 |
84 | run(): void {
85 | if (this._ref.current === null) return
86 |
87 | globalStack.pushResponder(this)
88 | const value = this.calc()
89 | globalStack.popResponder()
90 |
91 | if (this.value !== value) {
92 | this.value = value
93 |
94 | this.f(value)
95 | }
96 | }
97 |
98 | end: F0 = () => {
99 | this._ref.current = null
100 | // this._ref = {current: null}
101 | }
102 | }
103 |
104 | /*
105 | A Reaction will keep going so long as the observables inside it's function still exist.
106 |
107 | If a fiend class has a shorter lifetime than observables it depends on, they should be cleaned up.
108 | */
109 | export function $Reaction(calc: () => T, f: (result: T) => void): F0 {
110 | return new Reaction(calc, f).end
111 | }
112 |
--------------------------------------------------------------------------------
/src/lib/observables/computed/computed.ts:
--------------------------------------------------------------------------------
1 | import {
2 | addCallingResponderToOurList,
3 | hasActiveResponders,
4 | Notifier,
5 | notify,
6 | } from '../notifier'
7 | import {globalStack} from '../global-stack'
8 | import {Responder, ResponderType, UnorderedResponder} from '../responder'
9 | import {$Component} from '../$component'
10 | import {RefObject} from '../../util/ref'
11 |
12 | export class Computed implements UnorderedResponder, Notifier {
13 | responderType = ResponderType.computed as const
14 | ordered = false as const
15 |
16 | // Things we call when this computed updates.
17 | computeds = new Set>>()
18 | reactions = new Set>()
19 | components = new Map>()
20 |
21 | value: T | unknown
22 |
23 | _ref: RefObject = {
24 | current: null,
25 | }
26 |
27 | constructor(public f: () => T, public name: string) {}
28 |
29 | run(): void {
30 | const active = this.hasActiveResponders()
31 |
32 | if (active) {
33 | this.runFunction(true)
34 | } else if (this._ref.current !== null) {
35 | this.deactivateAndClear()
36 | }
37 | }
38 |
39 | get(responder: Responder | null): T | unknown {
40 | if (responder !== null) {
41 | this.addCallingResponderToOurList(responder)
42 | }
43 |
44 | const previouslyDeactivated = this._ref.current === null
45 | const active = this.hasActiveResponders()
46 |
47 | if (previouslyDeactivated) {
48 | if (active) {
49 | this._ref.current = this
50 | }
51 | this.runFunction(false)
52 | } else {
53 | if (active) {
54 | globalStack.runComputedNowIfInActionStack(this._ref)
55 | } else {
56 | this.deactivateAndClear()
57 | }
58 | }
59 |
60 | return this.value
61 | }
62 |
63 | runFunction(shouldNotify: boolean) {
64 | globalStack.pushResponder(this)
65 | const value = this.f()
66 | globalStack.popResponder()
67 |
68 | if (value !== this.value) {
69 | this.value = value
70 |
71 | if (shouldNotify) {
72 | notify(this)
73 | }
74 | }
75 | }
76 |
77 | addCallingResponderToOurList(responder: Responder) {
78 | addCallingResponderToOurList(this, responder)
79 | }
80 |
81 | hasActiveResponders(): boolean {
82 | return hasActiveResponders(this)
83 | }
84 |
85 | deactivateAndClear(): void {
86 | this._ref.current = null
87 |
88 | const {computeds, reactions, components} = this
89 |
90 | reactions.clear()
91 | components.clear()
92 |
93 | for (const c of computeds) {
94 | if (c.current !== null) {
95 | c.current.deactivateAndClear()
96 | }
97 | }
98 | computeds.clear()
99 | }
100 |
101 | isMarkedActive(): boolean {
102 | return this._ref.current !== null
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/demos/canvas.ts:
--------------------------------------------------------------------------------
1 | import {PureComponent} from '../lib/component-types/pure-component'
2 | import {createRef} from '../lib/util/ref'
3 | import {render} from '../lib/render'
4 | import {Canvas} from '../lib/component-types/host/dom-components'
5 | import {s} from '../lib/util/style'
6 |
7 | interface TextCanvasProps {
8 | width: number
9 | height: number
10 | }
11 |
12 | const boxHeight = 30
13 |
14 | export class TextCanvas extends PureComponent {
15 | ref = createRef()
16 |
17 | render() {
18 | const {width, height} = this.props
19 |
20 | return Canvas({ref: this.ref, style: s`width: ${width}px; height: ${height}px;`})
21 | }
22 |
23 | componentDidMount(): void {
24 | const el = this.ref.current
25 |
26 | if (el !== null) {
27 | const {width, height} = this.props
28 |
29 | const context = getCanvasContext(el, width, height)
30 |
31 | if (context !== null) this.drawStuff(context, width, height)
32 | }
33 | }
34 |
35 | tick = Date.now()
36 |
37 | drawStuff(ctx: CanvasRenderingContext2D, width: number, height: number) {
38 | const t = ((Date.now() - this.tick) / 6) % boxHeight
39 |
40 | drawBoxes(ctx, width, height, -t)
41 |
42 | requestAnimationFrame(() => {
43 | this.drawStuff(ctx, width, height)
44 | })
45 | }
46 | }
47 |
48 | function drawBoxes(
49 | ctx: CanvasRenderingContext2D,
50 | width: number,
51 | height: number,
52 | top: number,
53 | ) {
54 | // console.time('draw boxes')
55 | ctx.clearRect(0, 0, width, height)
56 | ctx.beginPath()
57 |
58 | const numBoxes = Math.ceil(height / boxHeight) + 1
59 | const left = width * 0.2
60 | const boxWidth = width * 0.6
61 |
62 | for (let i = 0; i < numBoxes; i++) {
63 | drawBox(ctx, left, top + i * boxHeight, boxWidth, boxHeight)
64 | }
65 |
66 | ctx.stroke()
67 | // console.timeEnd('draw boxes')
68 | }
69 |
70 | function drawBox(
71 | ctx: CanvasRenderingContext2D,
72 | x: number,
73 | y: number,
74 | w: number,
75 | h: number,
76 | ) {
77 | ctx.rect(x, y, w, h)
78 |
79 | ctx.font = '13px Arial'
80 | ctx.fillText('I am a commit message. Here is some text.', x + 5, y + h / 1.5)
81 | }
82 |
83 | export function canvasTest(root: HTMLElement) {
84 | console.time('render')
85 |
86 | render(
87 | TextCanvas.$({width: window.innerWidth, height: window.innerHeight}),
88 | document.body,
89 | )
90 |
91 | console.timeEnd('render')
92 | }
93 |
94 | export function getCanvasContext(
95 | canvas: HTMLCanvasElement,
96 | width: number,
97 | height: number,
98 | ): CanvasRenderingContext2D | null {
99 | const scale = window.devicePixelRatio
100 |
101 | canvas.width = width * scale
102 | canvas.height = height * scale
103 |
104 | const ctx = canvas.getContext('2d')
105 |
106 | if (ctx !== null) ctx.scale(scale, scale)
107 |
108 | return ctx
109 | }
110 |
--------------------------------------------------------------------------------
/src/lib/observables/computed/chained-computed.test.ts:
--------------------------------------------------------------------------------
1 | import {makeObservable} from '../$model'
2 | import {$AutoRun, F0} from '../responder'
3 | import {Computed} from './computed'
4 |
5 | describe('Computed', () => {
6 | class A {
7 | d: F0[] = []
8 | runs = 0
9 |
10 | $o = 1
11 |
12 | declare __$c1: Computed
13 | get $c1(): number {
14 | return this.$o + 1
15 | }
16 |
17 | declare __$c2: Computed
18 | get $c2(): number {
19 | return this.$c1 + 1
20 | }
21 |
22 | declare __$c3: Computed
23 | get $c3(): number {
24 | return this.$c2 + 1
25 | }
26 |
27 | constructor() {
28 | makeObservable(this)
29 | }
30 |
31 | result = 0
32 | enableReactions() {
33 | this.d.push(
34 | $AutoRun(() => {
35 | this.runs++
36 | this.result = this.$c3
37 | })
38 | )
39 | }
40 |
41 | disableReactions() {
42 | this.d.forEach(d => d())
43 | this.d = []
44 | }
45 | }
46 |
47 | test('Chain of computeds activates when called', () => {
48 | const a = new A()
49 | a.enableReactions()
50 |
51 | expect(a.__$c3.isMarkedActive()).toBe(true)
52 |
53 | expect(a.$c1).toBe(2)
54 | expect(a.$c2).toBe(3)
55 | expect(a.$c3).toBe(4)
56 |
57 | expect(a.runs).toBe(1)
58 |
59 | a.disableReactions()
60 | expect(a.__$c3.isMarkedActive()).toBe(true)
61 |
62 | a.$o = 2
63 |
64 | expect(a.__$c3.isMarkedActive()).toBe(false)
65 | expect(a.__$c2.isMarkedActive()).toBe(false)
66 | expect(a.__$c1.isMarkedActive()).toBe(false)
67 |
68 | // Call computeds outside reactive context.
69 | expect(a.$c3).toBe(5)
70 | expect(a.__$c3._ref.current).toBe(null)
71 |
72 | a.$o = 3
73 |
74 | expect(a.runs).toBe(1)
75 | a.enableReactions()
76 | expect(a.runs).toBe(2)
77 |
78 | expect(a.$c3).toBe(6)
79 | })
80 |
81 | test('Chained computeds called outside reactive context', () => {
82 | const a = new A()
83 | a.enableReactions()
84 |
85 | expect(a.runs).toBe(1)
86 |
87 | a.disableReactions()
88 |
89 | a.$o = 2
90 |
91 | // Call computeds outside reactive context.
92 | a.$c1
93 | expect(a.__$c1.isMarkedActive()).toBe(false)
94 | a.$c2
95 | expect(a.__$c2.isMarkedActive()).toBe(false)
96 | a.$c3
97 | expect(a.__$c3.isMarkedActive()).toBe(false)
98 |
99 | a.$o = 3
100 | expect(a.__$c1.isMarkedActive()).toBe(false)
101 | expect(a.__$c2.isMarkedActive()).toBe(false)
102 | expect(a.__$c3.isMarkedActive()).toBe(false)
103 |
104 | expect(a.runs).toBe(1)
105 | a.enableReactions()
106 |
107 | expect(a.__$c1.isMarkedActive()).toBe(true)
108 | expect(a.__$c2.isMarkedActive()).toBe(true)
109 | expect(a.__$c3.isMarkedActive()).toBe(true)
110 |
111 | expect(a.runs).toBe(2)
112 |
113 | expect(a.$c3).toBe(6)
114 | })
115 | })
116 |
--------------------------------------------------------------------------------
/src/lib/component-types/host/set-attributes.test.ts:
--------------------------------------------------------------------------------
1 | import {setAttributesFromProps, updateAttributes} from './set-attributes'
2 | import {Rec} from '../pure-component'
3 | import {Button} from './dom-components'
4 | import {render} from '../../render'
5 | import {ElementNamespace} from '../../util/element'
6 |
7 | describe('setAttributesFromProps', () => {
8 | test('add class', () => {
9 | const parent = document.createElement('div')
10 | const div = document.createElement('div')
11 |
12 | parent.appendChild(div)
13 |
14 | const props: Partial = {
15 | className: 'simple',
16 | }
17 |
18 | setAttributesFromProps(div, ElementNamespace.html, props as Rec)
19 |
20 | expect(parent.innerHTML).toEqual('
')
21 | })
22 | })
23 |
24 | describe('updateAttributes', () => {
25 | test('update props', () => {
26 | const parent = document.createElement('div')
27 | const div = document.createElement('div')
28 |
29 | parent.appendChild(div)
30 |
31 | const props: Partial = {
32 | className: 'simple',
33 | }
34 |
35 | setAttributesFromProps(div, ElementNamespace.html, props as Rec)
36 |
37 | expect(parent.innerHTML).toEqual('
')
38 |
39 | const newProps: Partial = {
40 | id: 'simple',
41 | }
42 |
43 | updateAttributes(div, ElementNamespace.html, newProps as Rec, props as Rec)
44 |
45 | expect(parent.innerHTML).toEqual('
')
46 | })
47 | })
48 |
49 | describe('handling of undefined props', () => {
50 | test(`expect undefined attributes don't show on element on first set`, () => {
51 | const parent = document.createElement('div')
52 | const div = document.createElement('div')
53 |
54 | parent.appendChild(div)
55 |
56 | const props: Partial = {
57 | id: undefined,
58 | }
59 |
60 | setAttributesFromProps(div, ElementNamespace.html, props as Rec)
61 |
62 | expect(parent.innerHTML).toEqual('
')
63 | })
64 |
65 | test(`expect undefined attributes don't show on element after previously defined`, () => {
66 | const parent = document.createElement('div')
67 | const div = document.createElement('div')
68 |
69 | parent.appendChild(div)
70 |
71 | const props: Partial = {
72 | id: 'simple',
73 | }
74 |
75 | setAttributesFromProps(div, ElementNamespace.html, props as Rec)
76 |
77 | expect(parent.innerHTML).toEqual('
')
78 |
79 | const newProps: Partial = {
80 | id: undefined,
81 | }
82 |
83 | updateAttributes(div, ElementNamespace.html, newProps as Rec, props as Rec)
84 |
85 | expect(parent.innerHTML).toEqual('
')
86 | })
87 | })
88 |
89 | describe('Applies expected attribute', () => {
90 | test('disabled', () => {
91 | render(Button({disabled: true}), document.body)
92 | const b = document.getElementsByTagName('button').item(0)
93 | expect(b?.disabled).toEqual(true)
94 |
95 | render(Button({disabled: false}), document.body)
96 |
97 | const b2 = document.getElementsByTagName('button').item(0)
98 | expect(b2?.disabled).toEqual(false)
99 | })
100 | })
101 |
--------------------------------------------------------------------------------
/src/lib/observables/$model.ts:
--------------------------------------------------------------------------------
1 | import {Atom} from './atom'
2 | import {Computed} from './computed/computed'
3 | import {globalStack} from './global-stack'
4 | import {$AutoRun, $Reaction, F0} from './responder'
5 |
6 | export type Constructor = {new (...args: any[]): {}}
7 |
8 | export function makeObservable(object: Object) {
9 | makeObservableInner(object, object.constructor as Constructor)
10 | }
11 |
12 | export function makeObservableInner(object: Object, Con: Constructor) {
13 | for (const key in object) {
14 | if (key.startsWith('$')) {
15 | const valueName = `__${key}`
16 | const value: unknown = object[key as keyof object]
17 |
18 | if (!(value instanceof Function)) {
19 | Object.defineProperties(object, {
20 | [valueName]: {
21 | value: new Atom(value, valueName),
22 | },
23 | [key]: {
24 | get() {
25 | return this[valueName].get(globalStack.getCurrentResponder())
26 | },
27 | set(value) {
28 | if (__FIEND_DEV__) console.debug(`${Con.name}.${key} <- `, value)
29 |
30 | this[valueName].set(value)
31 | },
32 | },
33 | })
34 | }
35 | }
36 | }
37 |
38 | const descriptors: [string, TypedPropertyDescriptor][] = Object.entries(
39 | Object.getOwnPropertyDescriptors(Con.prototype)
40 | )
41 |
42 | for (const [key, descriptor] of descriptors) {
43 | if (key.startsWith('$') && descriptor.get !== undefined) {
44 | const valueName = `__${key}`
45 |
46 | Object.defineProperties(object, {
47 | [valueName]: {
48 | value: new Computed(descriptor.get.bind(object), valueName),
49 | },
50 | [key]: {
51 | get() {
52 | return this[valueName].get(globalStack.getCurrentResponder())
53 | },
54 | },
55 | })
56 | }
57 | }
58 | }
59 |
60 | // This lets you get the value of an observable without setting up a dependency/notifying.
61 | // This can help avoid a dependency infinite loop.
62 | // Only use if you are sure that the observable will be up-to-date.
63 | export function getObservableUntracked(object: T, key: K): T[K] {
64 | const untrackedKey = `__${String(key)}` as typeof key
65 |
66 | // @ts-ignore
67 | return object[untrackedKey].value
68 | }
69 |
70 | export class $Model {
71 | protected disposers: F0[] = []
72 |
73 | constructor() {
74 | if (__DEV__) {
75 | setTimeout(() => {
76 | // @ts-ignore
77 | if (!Boolean(this.connected)) {
78 | console.error(
79 | `super.connect() wasn't called in the constructor of ${this.constructor.name}. This is require for $Model to work.`
80 | )
81 | }
82 | })
83 | }
84 | }
85 |
86 | connect() {
87 | makeObservableInner(this, this.constructor as Constructor)
88 |
89 | if (__DEV__) {
90 | // @ts-ignore
91 | this.connected = true
92 | }
93 | }
94 |
95 | $AutoRun(f: () => void) {
96 | this.disposers.push($AutoRun(f))
97 | }
98 |
99 | $Reaction(calc: () => T, f: (result: T) => void) {
100 | this.disposers.push($Reaction(calc, f))
101 | }
102 |
103 | disposeReactions(): void {
104 | for (const d of this.disposers) d()
105 | this.disposers = []
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/lib/misc/compare.test.ts:
--------------------------------------------------------------------------------
1 | import {autorun, computed, IReactionDisposer, observable, runInAction} from 'mobx'
2 | import {$AutoRun} from '../observables/responder'
3 | import {makeObservable} from '../observables/$model'
4 | import {$Calc, $Val} from '../observables/value-style'
5 |
6 | xdescribe('compare mbox computeds with fiend-ui', () => {
7 | const loops = 1000
8 |
9 | test('time b', () => {
10 | class A {
11 | @observable
12 | a = 5
13 |
14 | @observable
15 | b = 5
16 |
17 | @observable
18 | c = 5
19 |
20 | @observable.ref
21 | d = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
22 | }
23 |
24 | class RunA {
25 | a = new A()
26 |
27 | disposer: IReactionDisposer
28 |
29 | constructor() {
30 | this.disposer = autorun(() => {
31 | const {a, b, c, d} = this.a
32 |
33 | this.a.d = [...d, a, b, c]
34 | })
35 | }
36 |
37 | @computed
38 | get c(): number {
39 | return this.a.a + this.a.b + this.a.c
40 | }
41 | }
42 |
43 | console.time('mobx')
44 | let z = 0
45 |
46 | for (let i = 0; i < loops; i++) {
47 | const a = new RunA()
48 |
49 | runInAction(() => {
50 | a.a.a = 10
51 | })
52 | runInAction(() => {
53 | a.a.a = 12
54 | })
55 | runInAction(() => {
56 | a.a.a = 14
57 | })
58 |
59 | z = a.c
60 |
61 | a.disposer()
62 | }
63 | expect(z).toEqual(24)
64 |
65 | console.timeEnd('mobx')
66 | })
67 |
68 | test('fiend-ui', () => {
69 | class E {
70 | a = $Val(5)
71 |
72 | b = $Val(5)
73 |
74 | c = $Val(5)
75 |
76 | d = $Val([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
77 | }
78 |
79 | class Run {
80 | b = new E()
81 |
82 | constructor() {
83 | $AutoRun(() => {
84 | const {a, b, c, d} = this.b
85 |
86 | const array = [...d()]
87 |
88 | array.push(a(), b(), c())
89 |
90 | d(array)
91 | })
92 | }
93 | }
94 |
95 | console.time('fiend-ui')
96 | let z = 0
97 | for (let i = 0; i < loops; i++) {
98 | const r = new Run()
99 |
100 | const c = $Calc(() => {
101 | return r.b.a() + r.b.b() + r.b.c()
102 | })
103 |
104 | r.b.a(10)
105 | r.b.a(12)
106 | r.b.a(14)
107 | z = c()
108 | // console.log('fiend-ui', z)
109 | }
110 |
111 | expect(z).toEqual(24)
112 | console.timeEnd('fiend-ui')
113 | })
114 |
115 | test('fiend', () => {
116 | class E {
117 | $a = 5
118 |
119 | $b = 5
120 |
121 | $c = 5
122 |
123 | $d = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
124 |
125 | constructor() {
126 | makeObservable(this)
127 | }
128 | }
129 |
130 | class Run {
131 | b = new E()
132 |
133 | constructor() {
134 | $AutoRun(() => {
135 | const {$a, $b, $c, $d} = this.b
136 |
137 | this.b.$d = [...$d, $a, $b, $c]
138 | })
139 | }
140 | }
141 |
142 | console.time('fiend')
143 | let z = 0
144 | for (let i = 0; i < loops; i++) {
145 | const r = new Run()
146 |
147 | const c = $Calc(() => {
148 | return r.b.$a + r.b.$b + r.b.$c
149 | })
150 |
151 | r.b.$a = 10
152 | r.b.$a = 12
153 | r.b.$a = 14
154 | z = c()
155 | // console.log('fiend-ui', z)
156 | }
157 |
158 | expect(z).toEqual(24)
159 | console.timeEnd('fiend')
160 | })
161 | })
162 |
--------------------------------------------------------------------------------
/src/lib/util/order.ts:
--------------------------------------------------------------------------------
1 | import {DomComponent} from '../component-types/host/dom-component'
2 | import {RootComponent} from '../component-types/root-component'
3 | import {ElementComponent} from '../component-types/base-component'
4 | import {RunStack} from '../observables/run-stack'
5 |
6 | // export function checkOrder(inserted: ElementComponent[]) {
7 | // if (!__DEV__) return
8 | //
9 | // let prev: ElementComponent | null = null
10 | //
11 | // for (const c of inserted) {
12 | // console.log(c.key, prev?.key)
13 | // if (prev !== null) {
14 | // if (prev.element !== c.element.previousElementSibling) {
15 | // console.log('elements out of order')
16 | // // debugger
17 | // }
18 | // if (prev.order > c.order) {
19 | // console.log('keys out of order')
20 | // }
21 | // }
22 | // prev = c
23 | // }
24 | // }
25 |
26 | export function applyInserts(parent: RootComponent | DomComponent): void {
27 | const {inserted, siblings, element} = parent
28 |
29 | const len = inserted.length
30 |
31 | let next: ElementComponent | null = null
32 |
33 | for (let i = len - 1; i >= 0; i--) {
34 | const current = inserted[i]
35 |
36 | if (next === null) {
37 | if (!siblings.has(current.element)) {
38 | element.insertBefore(current.element, null)
39 | siblings.set(current.element, null)
40 | }
41 | } else if (siblings.has(next.element)) {
42 | const prevElement = siblings.get(next.element)
43 |
44 | if (prevElement !== current.element) {
45 | element.insertBefore(current.element, next.element)
46 | siblings.set(next.element, current.element)
47 | if (!siblings.has(current.element)) siblings.set(current.element, null)
48 | }
49 | }
50 |
51 | next = current
52 | }
53 | }
54 |
55 | export class Order {
56 | static key(parentOrder: string, index: number): string {
57 | return parentOrder + String.fromCharCode(index + 48)
58 | }
59 |
60 | static insert(parent: RootComponent | DomComponent, child: ElementComponent): void {
61 | const {inserted} = parent
62 | const {order, key} = child
63 |
64 | const len = inserted.length
65 |
66 | for (let i = len - 1; i >= 0; i--) {
67 | const current = inserted[i]
68 | const next: ElementComponent | undefined = inserted[i + 1]
69 |
70 | /*
71 | If order is the same we expect the keys to be different. This
72 | is expected for a virtual list.
73 | */
74 | if (order >= current.order) {
75 | if (key !== current.key) {
76 | if (next != null) {
77 | inserted.splice(i + 1, 0, child)
78 | RunStack.insertsStack.add(parent)
79 | } else {
80 | inserted.push(child)
81 | RunStack.insertsStack.add(parent)
82 | }
83 | }
84 |
85 | return
86 | }
87 | }
88 |
89 | inserted.unshift(child)
90 | RunStack.insertsStack.add(parent)
91 | }
92 |
93 | static move(parent: RootComponent | DomComponent, child: ElementComponent) {
94 | const {inserted} = parent
95 | const {key} = child
96 |
97 | const i = inserted.findIndex(ins => ins.key === key)
98 |
99 | if (i >= 0) {
100 | inserted.splice(i, 1)
101 | }
102 |
103 | this.insert(parent, child)
104 | }
105 |
106 | static remove(parent: RootComponent | DomComponent, child: ElementComponent): void {
107 | const {inserted, siblings} = parent
108 | const {key} = child
109 |
110 | const i = inserted.findIndex(i => i.key === key)
111 |
112 | if (i >= 0) {
113 | const [child] = inserted.splice(i, 1)
114 |
115 | siblings.delete(child.element)
116 | RunStack.removeStack.add(child.element)
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/lib/observables/$component.test.ts:
--------------------------------------------------------------------------------
1 | import {$Component} from './$component'
2 | import {Div, FiendNode, makeObservable, PureComponent} from '../..'
3 | import {mkRoot} from '../../dom-tests/test-helpers'
4 |
5 | describe('$Component', () => {
6 | test('order 1', () => {
7 | class Store {
8 | $num = 5
9 |
10 | constructor() {
11 | makeObservable(this)
12 | }
13 | }
14 |
15 | interface Props {
16 | store: Store
17 | depth: number
18 | }
19 |
20 | class A extends $Component {
21 | render(): FiendNode | null {
22 | const {store, depth} = this.props
23 |
24 | if (depth <= 0) return null
25 |
26 | return Div({
27 | children: [`${store.$num} - ${depth}`, A.$({store, depth: depth - 1})],
28 | })
29 | }
30 | }
31 |
32 | const root = mkRoot()
33 | const store = new Store()
34 |
35 | root.render(A.$({store, depth: 3}))
36 |
37 | expect(root.element.innerHTML).toEqual(
38 | ''
39 | )
40 |
41 | store.$num = 6
42 |
43 | expect(root.element.innerHTML).toEqual(
44 | ''
45 | )
46 | })
47 |
48 | test('order without keys', () => {
49 | const root = mkRoot()
50 |
51 | const a = Div({children: [Div('a')]})
52 |
53 | root.render(a)
54 |
55 | expect(root.element.innerHTML).toEqual('')
56 |
57 | const b = Div({children: [Div('b'), Div('a')]})
58 |
59 | root.render(b)
60 |
61 | expect(root.element.innerHTML).toEqual('')
62 | })
63 |
64 | test('order, no keys, custom component', () => {
65 | const root = mkRoot()
66 |
67 | class DivC extends PureComponent {
68 | render(): FiendNode {
69 | return Div({children: this.props.children})
70 | }
71 | }
72 |
73 | const a = DivC.$({children: [DivC.$({children: ['a']})]})
74 |
75 | root.render(a)
76 |
77 | expect(root.element.innerHTML).toEqual('')
78 |
79 | const b = DivC.$({children: [DivC.$({children: ['b']}), DivC.$({children: ['a']})]})
80 |
81 | root.render(b)
82 |
83 | expect(root.element.innerHTML).toEqual('')
84 | })
85 |
86 | test('order 2, without keys', () => {
87 | const root = mkRoot()
88 |
89 | const a = Div({children: [Div('a'), Div('b'), Div('c')]})
90 |
91 | root.render(a)
92 |
93 | expect(root.element.innerHTML).toEqual(
94 | ''
95 | )
96 |
97 | const b = Div({children: [Div('d'), Div('a'), Div('b'), Div('c')]})
98 |
99 | root.render(b)
100 |
101 | expect(root.element.innerHTML).toEqual(
102 | ''
103 | )
104 | })
105 |
106 | test('order 3, with keys', () => {
107 | const root = mkRoot()
108 |
109 | const a = Div({
110 | children: [
111 | Div({children: ['a'], key: 'a'}),
112 | Div({children: ['b'], key: 'b'}),
113 | Div({children: ['c'], key: 'c'}),
114 | ],
115 | })
116 |
117 | root.render(a)
118 |
119 | expect(root.element.innerHTML).toEqual(
120 | ''
121 | )
122 |
123 | const b = Div({
124 | children: [
125 | Div({children: ['d'], key: 'd'}),
126 | Div({children: ['a'], key: 'a'}),
127 | Div({children: ['b'], key: 'b'}),
128 | Div({children: ['c'], key: 'c'}),
129 | ],
130 | })
131 |
132 | root.render(b)
133 |
134 | expect(root.element.innerHTML).toEqual(
135 | ''
136 | )
137 | })
138 | })
139 |
--------------------------------------------------------------------------------
/src/lib/component-types/host/set-attributes.ts:
--------------------------------------------------------------------------------
1 | import {RefObject} from '../../util/ref'
2 | import {ElementNamespace, StandardProps} from '../../util/element'
3 |
4 | // This should only be called the first time or if previous props were null.
5 | export function setAttributesFromProps(
6 | element: Element,
7 | namespace: ElementNamespace,
8 | props: P,
9 | ): void {
10 | const attributes = Object.keys(props)
11 |
12 | for (const attr of attributes) {
13 | // @ts-ignore
14 | const value = props[attr]
15 |
16 | if (value !== undefined) {
17 | // @ts-ignore
18 | setAttribute(element, namespace, attr, props[attr])
19 | }
20 | }
21 | }
22 |
23 | export function updateAttributes(
24 | element: Element,
25 | namespace: ElementNamespace,
26 | newProps: object,
27 | oldProps: object | null,
28 | ): void {
29 | if (oldProps === null) {
30 | setAttributesFromProps(element, namespace, newProps)
31 | } else {
32 | updateAttrInner(element, namespace, newProps, oldProps)
33 | }
34 | }
35 |
36 | // TODO: Could we remove a loop by using Array.from?
37 | function updateAttrInner(
38 | element: Element,
39 | namespace: ElementNamespace,
40 | newProps: object,
41 | oldProps: object,
42 | ): void {
43 | const newKeys = Object.keys(newProps)
44 | const oldKeys = Object.keys(oldProps)
45 |
46 | for (const key of newKeys) {
47 | // @ts-ignore
48 | const oldValue = oldProps[key]
49 | // @ts-ignore
50 | const newValue = newProps[key]
51 |
52 | if (newValue === undefined) continue
53 |
54 | if (oldValue === undefined) {
55 | setAttribute(element, namespace, key, newValue)
56 | } //
57 | else if (oldValue !== newValue) {
58 | if (key.startsWith('on')) deleteAttribute(element, key, oldValue)
59 |
60 | setAttribute(element, namespace, key, newValue)
61 | }
62 | }
63 |
64 | for (const key of oldKeys) {
65 | // @ts-ignore
66 | const oldValue = oldProps[key]
67 | // @ts-ignore
68 | const newValue = newProps[key]
69 |
70 | if (newValue === undefined && oldValue !== undefined) {
71 | deleteAttribute(element, key, oldValue)
72 | }
73 | }
74 | }
75 |
76 | // TODO: Improve types.
77 | function setAttribute(
78 | element: Element,
79 | namespace: ElementNamespace,
80 | attr: string,
81 | value: any,
82 | ): void {
83 | switch (attr) {
84 | case 'key':
85 | case 'children':
86 | break
87 | case 'className':
88 | element.setAttribute('class', value)
89 | break
90 | case 'value':
91 | case 'style':
92 | ;(element as any)[attr] = value
93 | break
94 | case 'ref':
95 | ;(value as RefObject).current = element
96 | break
97 | default:
98 | if (namespace === ElementNamespace.svg && setSvgAttribute(element, attr, value))
99 | break
100 |
101 | if (attr.startsWith('on')) {
102 | element.addEventListener(attr.slice(2), value)
103 | } else if (typeof value === 'boolean') {
104 | if (value) element.setAttribute(attr, '')
105 | else element.removeAttribute(attr)
106 | } else {
107 | element.setAttribute(attr, value)
108 | }
109 | break
110 | }
111 | }
112 |
113 | function setSvgAttribute(element: Element, attr: string, value: any) {
114 | switch (attr) {
115 | case 'strokeWidth':
116 | element.setAttribute('stroke-width', value)
117 | return true
118 | case 'strokeLinejoin':
119 | element.setAttribute('stroke-linejoin', value)
120 | return true
121 | }
122 |
123 | return false
124 | }
125 |
126 | function deleteAttribute(element: Element, attr: string, oldValue: unknown): void {
127 | if (attr.startsWith('on')) {
128 | element.removeEventListener(attr.slice(2), oldValue as any)
129 | } else if (attr === 'className') {
130 | element.removeAttribute('class')
131 | } else if (attr === 'ref') {
132 | ;(oldValue as RefObject).current = null
133 | } else {
134 | element.removeAttribute(attr)
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### JetBrains template
3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
5 |
6 | # User-specific stuff
7 | .idea/**/workspace.xml
8 | .idea/**/tasks.xml
9 | .idea/**/usage.statistics.xml
10 | .idea/**/dictionaries
11 | .idea/**/shelf
12 |
13 | # Generated files
14 | .idea/**/contentModel.xml
15 |
16 | # Sensitive or high-churn files
17 | .idea/**/dataSources/
18 | .idea/**/dataSources.ids
19 | .idea/**/dataSources.local.xml
20 | .idea/**/sqlDataSources.xml
21 | .idea/**/dynamic.xml
22 | .idea/**/uiDesigner.xml
23 | .idea/**/dbnavigator.xml
24 |
25 | # Gradle
26 | .idea/**/gradle.xml
27 | .idea/**/libraries
28 |
29 | # Gradle and Maven with auto-import
30 | # When using Gradle or Maven with auto-import, you should exclude module files,
31 | # since they will be recreated, and may cause churn. Uncomment if using
32 | # auto-import.
33 | # .idea/artifacts
34 | # .idea/compiler.xml
35 | # .idea/modules.xml
36 | # .idea/*.iml
37 | # .idea/modules
38 | # *.iml
39 | # *.ipr
40 |
41 | # CMake
42 | cmake-build-*/
43 |
44 | # Mongo Explorer plugin
45 | .idea/**/mongoSettings.xml
46 |
47 | # File-based project format
48 | *.iws
49 |
50 | # IntelliJ
51 | out/
52 |
53 | # mpeltonen/sbt-idea plugin
54 | .idea_modules/
55 |
56 | # JIRA plugin
57 | atlassian-ide-plugin.xml
58 |
59 | # Cursive Clojure plugin
60 | .idea/replstate.xml
61 |
62 | # Crashlytics plugin (for Android Studio and IntelliJ)
63 | com_crashlytics_export_strings.xml
64 | crashlytics.properties
65 | crashlytics-build.properties
66 | fabric.properties
67 |
68 | # Editor-based Rest Client
69 | .idea/httpRequests
70 |
71 | # Android studio 3.1+ serialized cache file
72 | .idea/caches/build_file_checksums.ser
73 |
74 | ### Node template
75 | # Logs
76 | logs
77 | *.log
78 | npm-debug.log*
79 | yarn-debug.log*
80 | yarn-error.log*
81 | lerna-debug.log*
82 |
83 | # Diagnostic reports (https://nodejs.org/api/report.html)
84 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
85 |
86 | # Runtime data
87 | pids
88 | *.pid
89 | *.seed
90 | *.pid.lock
91 |
92 | # Directory for instrumented libs generated by jscoverage/JSCover
93 | lib-cov
94 |
95 | # Coverage directory used by tools like istanbul
96 | coverage
97 | *.lcov
98 |
99 | # nyc test coverage
100 | .nyc_output
101 |
102 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
103 | .grunt
104 |
105 | # Bower dependency directory (https://bower.io/)
106 | bower_components
107 |
108 | # node-waf configuration
109 | .lock-wscript
110 |
111 | # Compiled binary addons (https://nodejs.org/api/addons.html)
112 | build/Release
113 |
114 | # Dependency directories
115 | node_modules/
116 | jspm_packages/
117 |
118 | # TypeScript v1 declaration files
119 | typings/
120 |
121 | # TypeScript cache
122 | *.tsbuildinfo
123 |
124 | # Optional npm cache directory
125 | .npm
126 |
127 | # Optional eslint cache
128 | .eslintcache
129 |
130 | # Microbundle cache
131 | .rpt2_cache/
132 | .rts2_cache_cjs/
133 | .rts2_cache_es/
134 | .rts2_cache_umd/
135 |
136 | # Optional REPL history
137 | .node_repl_history
138 |
139 | # Output of 'npm pack'
140 | *.tgz
141 |
142 | # Yarn Integrity file
143 | .yarn-integrity
144 |
145 | # dotenv environment variables file
146 | .env
147 | .env.test
148 |
149 | # parcel-bundler cache (https://parceljs.org/)
150 | .cache
151 |
152 | # Next.js build output
153 | .next
154 |
155 | # Nuxt.js build / generate output
156 | .nuxt
157 | dist
158 |
159 | # Gatsby files
160 | .cache/
161 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
162 | # https://nextjs.org/blog/next-9-1#public-directory-support
163 | # public
164 |
165 | # vuepress build output
166 | .vuepress/dist
167 |
168 | # Serverless directories
169 | .serverless/
170 |
171 | # FuseBox cache
172 | .fusebox/
173 |
174 | # DynamoDB Local files
175 | .dynamodb/
176 |
177 | # TernJS port file
178 | .tern-port
179 |
180 | /output-code/
181 |
--------------------------------------------------------------------------------
/src/lib/component-types/host/dom-component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AnyComponent,
3 | ComponentType,
4 | ElementComponent,
5 | ParentComponent,
6 | } from '../base-component'
7 | import {Render} from '../../render'
8 | import {setAttributesFromProps, updateAttributes} from './set-attributes'
9 | import {ElementNameMap} from './dom-component-types'
10 | import {Order} from '../../util/order'
11 | import {TextComponent} from '../text-component'
12 | import {RootComponent} from '../root-component'
13 | import {FiendNode} from '../../..'
14 | import {
15 | ElementNamespace,
16 | HostElement,
17 | StandardProps,
18 | SvgElement,
19 | } from '../../util/element'
20 |
21 | export class DomComponent {
22 | _type = ComponentType.host as const
23 | element: ElementNameMap[this['tag']]
24 | order: string
25 | key: string
26 |
27 | subComponents = new Map()
28 | inserted: ElementComponent[] = []
29 |
30 | // key is an element, value is the previous element
31 | siblings = new WeakMap()
32 |
33 | constructor(
34 | public tag: keyof ElementNameMap,
35 | public namespace: ElementNamespace,
36 | public props: P,
37 | public domParent: DomComponent | RootComponent,
38 | directParent: ParentComponent,
39 | public index: number,
40 | ) {
41 | this.order = Order.key(directParent.order, index)
42 | this.key = this.props.key ?? directParent.key + index
43 |
44 | if (namespace === ElementNamespace.svg) {
45 | this.element = document.createElementNS(
46 | 'http://www.w3.org/2000/svg',
47 | tag,
48 | ) as ElementNameMap[this['tag']]
49 | } else {
50 | this.element = document.createElement(tag) as ElementNameMap[this['tag']]
51 | }
52 |
53 | setAttributesFromProps(this.element, namespace, props)
54 |
55 | this.renderSubtrees(props.children ?? [])
56 |
57 | domParent.insertChild(this)
58 | }
59 |
60 | renderSubtrees(children: FiendNode[]) {
61 | this.subComponents = Render.subComponents(this, this, children, this.subComponents)
62 | }
63 |
64 | // What if our sub component has lots of elements to insert?
65 | insertChild(child: DomComponent | TextComponent) {
66 | Order.insert(this, child)
67 | }
68 |
69 | moveChild(child: DomComponent | TextComponent) {
70 | Order.move(this, child)
71 | }
72 |
73 | removeChild(child: DomComponent | TextComponent): void {
74 | Order.remove(this, child)
75 | }
76 |
77 | remove(): void {
78 | this.domParent.removeChild(this)
79 |
80 | if (this.subComponents.size > 0) {
81 | // This is required so that observer components don't keep updating.
82 | for (const c of this.subComponents.values()) c.remove()
83 | this.subComponents.clear()
84 | }
85 |
86 | if (this.inserted.length > 0) this.inserted = []
87 | }
88 | }
89 |
90 | // TODO: prevTree.children? parent.children? Seems there might be a bug here.
91 | export function renderDom(
92 | tree: HostElement | SvgElement,
93 | prev: AnyComponent | null,
94 | domParent: RootComponent | DomComponent,
95 | directParent: ParentComponent,
96 | index: number,
97 | ): DomComponent {
98 | const {_type, namespace, props} = tree
99 |
100 | if (prev === null) {
101 | return new DomComponent(_type, namespace, props, domParent, directParent, index)
102 | }
103 |
104 | if (prev._type === ComponentType.host && prev.tag === _type) {
105 | const prevOrder = prev.order
106 | const newOrder = Order.key(directParent.order, index)
107 |
108 | if (prevOrder !== newOrder) {
109 | prev.index = index
110 | prev.order = newOrder
111 |
112 | domParent.moveChild(prev)
113 | }
114 |
115 | updateAttributes(prev.element, namespace, props, prev.props)
116 | prev.props = props
117 | prev.renderSubtrees(props.children ?? [])
118 |
119 | return prev
120 | } else {
121 | // Type has changed. Remove it.
122 | prev.remove()
123 |
124 | return new DomComponent(_type, namespace, props, domParent, directParent, index)
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/lib/component-types/component.test.ts:
--------------------------------------------------------------------------------
1 | import {PureComponent} from './pure-component'
2 | import {Div} from './host/dom-components'
3 | import {$Component, makeObservable} from '../..'
4 | import {mkRoot} from '../../dom-tests/test-helpers'
5 | // import {$Model} from '../..'
6 |
7 | describe('component', () => {
8 | test('null in render should remove previous elements', () => {
9 | class A extends PureComponent<{ok: boolean}> {
10 | render() {
11 | const {ok} = this.props
12 |
13 | if (!ok) return null
14 | return Div('OK')
15 | }
16 | }
17 |
18 | const root = mkRoot()
19 |
20 | root.render(A.$({ok: true}))
21 |
22 | expect(root.element.innerHTML).toEqual(`OK
`)
23 |
24 | root.render(A.$({ok: false}))
25 |
26 | expect(root.element.innerHTML).toEqual(``)
27 | })
28 | })
29 |
30 | describe('switch between custom and host component', () => {
31 | class M {
32 | constructor() {
33 | makeObservable(this)
34 | }
35 | $custom = false
36 | }
37 |
38 | class DivC extends PureComponent {
39 | render() {
40 | return Div('custom')
41 | }
42 | }
43 |
44 | test('switch to custom from host', () => {
45 | class A extends $Component<{model: M}> {
46 | render() {
47 | const {model} = this.props
48 |
49 | if (model.$custom) {
50 | return DivC.$({})
51 | }
52 | return Div('host')
53 | }
54 | }
55 |
56 | const root = mkRoot()
57 | const model = new M()
58 |
59 | root.render(A.$({model}))
60 | expect(root.element.innerHTML).toEqual('host
')
61 |
62 | model.$custom = true
63 | expect(root.element.innerHTML).toEqual('custom
')
64 |
65 | model.$custom = false
66 | expect(root.element.innerHTML).toEqual('host
')
67 | })
68 |
69 | test('switch one inside array', () => {
70 | class A extends $Component<{model: M}> {
71 | render() {
72 | const {model} = this.props
73 |
74 | return [Div('a'), model.$custom ? DivC.$({}) : Div('host')]
75 | }
76 | }
77 |
78 | const root = mkRoot()
79 | const model = new M()
80 |
81 | root.render(A.$({model}))
82 | expect(root.element.innerHTML).toEqual('a
host
')
83 |
84 | model.$custom = true
85 | expect(root.element.innerHTML).toEqual('a
custom
')
86 |
87 | model.$custom = false
88 | expect(root.element.innerHTML).toEqual('a
host
')
89 | })
90 |
91 | test('switch one to null', () => {
92 | class A extends $Component<{model: M}> {
93 | render() {
94 | const {model} = this.props
95 |
96 | return [model.$custom ? DivC.$({}) : null, Div('a')]
97 | }
98 | }
99 |
100 | const root = mkRoot()
101 | const model = new M()
102 |
103 | root.render(A.$({model}))
104 | expect(root.element.innerHTML).toEqual('a
')
105 |
106 | model.$custom = true
107 | expect(root.element.innerHTML).toEqual('custom
a
')
108 |
109 | model.$custom = false
110 | expect(root.element.innerHTML).toEqual('a
')
111 | })
112 |
113 | // test('scrolling', () => {
114 | // class Scroller extends PureComponent<{n: number}> {
115 | // render() {
116 | // const {n} = this.props
117 | //
118 | // return [
119 | // div({children: [n], key: `${n}`}),
120 | // div({children: [n + 1], key: `${n + 1}`}),
121 | // div({children: [n + 2], key: `${n + 2}`}),
122 | // ]
123 | // }
124 | // }
125 | //
126 | // const root = mkRoot()
127 | //
128 | // root.render(Scroller.$({n: 0}))
129 | // expect(root.element.innerHTML).toEqual('0
1
2
')
130 | //
131 | // root.render(Scroller.$({n: 1}))
132 | // expect(root.element.innerHTML).toEqual('1
2
3
')
133 | //
134 | // root.render(Scroller.$({n: 2}))
135 | // expect(root.element.innerHTML).toEqual('2
3
4
')
136 | // })
137 | })
138 |
--------------------------------------------------------------------------------
/src/lib/observables/action.ts:
--------------------------------------------------------------------------------
1 | import {globalStack} from './global-stack'
2 | import {Responder, UnorderedResponder} from './responder'
3 | import {Notifier} from './notifier'
4 | import {$Component} from './$component'
5 | import {RunStack} from './run-stack'
6 | import {RefObject} from '../util/ref'
7 |
8 | type FunctionWithoutPromise = T extends () => Promise ? never : T
9 |
10 | // Passed function needs to be synchronous otherwise it won't really be run inside action.
11 | export function $RunInAction(f: FunctionWithoutPromise<() => void | undefined>): void {
12 | globalStack.startAction()
13 | f()
14 | globalStack.endAction()
15 | }
16 |
17 | type ForbidPromise = T extends Promise ? never : T
18 |
19 | /**
20 | * Wraps passed in function in an action. Keeps the same type signature.
21 | *
22 | * E.g
23 | * const square = action((n: number) => n * n)
24 | *
25 | * @param f
26 | */
27 | export const $Action = (f: (...args: T) => ForbidPromise) => {
28 | return (...args: T): ForbidPromise => {
29 | globalStack.startAction()
30 | const result = f(...args)
31 | globalStack.endAction()
32 |
33 | return result
34 | }
35 | }
36 |
37 | /** @deprecated */
38 | export const $AsyncAction = (f: (...args: T) => Promise) => {
39 | return async (...args: T): Promise => {
40 | globalStack.startAction()
41 | const result = await f(...args)
42 | globalStack.endAction()
43 |
44 | return result
45 | }
46 | }
47 |
48 | /*
49 | An Action lets us batch our notifiers.
50 |
51 | */
52 | export class ActionState {
53 | computeds = new Set>()
54 | reactions = new Set>()
55 | components = new Map>()
56 |
57 | constructor(public runningResponder: Responder | null) {}
58 |
59 | add(notifier: Notifier) {
60 | for (const r of notifier.computeds) {
61 | if (r.current !== this.runningResponder) this.computeds.add(r)
62 | }
63 | for (const r of notifier.reactions) {
64 | if (r.current !== this.runningResponder) this.reactions.add(r)
65 | }
66 | // for (const [order, r] of notifier.components) {
67 | // if (r.current !== this.runningResponder) this.components.set(order, r)
68 | // }
69 | for (const c of notifier.components.values()) {
70 | if (c.current && c.current !== this.runningResponder) {
71 | this.components.set(c.current?.order, c)
72 | }
73 | }
74 |
75 | if (notifier.computeds.size > 0) {
76 | notifier.computeds.clear()
77 | }
78 | if (notifier.reactions.size > 0) {
79 | notifier.reactions.clear()
80 | }
81 | if (notifier.components.size > 0) {
82 | notifier.components.clear()
83 | }
84 | }
85 |
86 | // This whole object gets deleted after running, so I don't think cleanup is required.
87 | run() {
88 | RunStack.runResponders(this.computeds, this.reactions, this.components)
89 | }
90 | }
91 |
92 | /*
93 |
94 | setting an observable:
95 | -> computeds
96 | -> autorun/reaction
97 | -> components
98 |
99 | */
100 |
101 | /*
102 | LayoutSt.$scrollTop <- 1000
103 |
104 | RefVirtualPositions.$firstVisible <- 0
105 | RefVirtualPositions.$lastVisible <- 10
106 | RefVirtualPositions.$current <- (2000)[…]
107 |
108 | CommitVirtualPositions.$current <- (2000)[…]
109 | CommitVirtualPositions.$firstVisible <- 0
110 | CommitVirtualPositions.$lastVisible <- 66
111 |
112 | RefVirtualPositions.$firstVisible <- 8
113 | RefVirtualPositions.$lastVisible <- 10
114 | RefVirtualPositions.$current <- (2000)[…]
115 |
116 | $component.ts?49e1:20 run CommitCardList
117 | commit-card-list.ts?2833:84 CommitCardList render
118 | commit-card-list.ts?2833:34 CommitCardListInner render 1000
119 | $component.ts?49e1:20 run CommitCardBackground
120 | $component.ts?49e1:20 run CommitCardListInner
121 | commit-card-list.ts?2833:34 CommitCardListInner render 1000
122 |
123 | Explanation:
124 | scrollTop changes, so ref positions are recalculated.
125 | scrollTop also causes commit positions to change, which in turn causes a
126 | rerun of ref positions.
127 |
128 |
129 | */
130 |
--------------------------------------------------------------------------------
/src/lib/render.ts:
--------------------------------------------------------------------------------
1 | import {AnyComponent, ParentComponent} from './component-types/base-component'
2 | import {DomComponent, renderDom} from './component-types/host/dom-component'
3 | import {renderTextComponent} from './component-types/text-component'
4 | import {PureComponent, renderCustom} from './component-types/pure-component'
5 | import {RootComponent} from './component-types/root-component'
6 | import {ElementType, FiendElement, FiendNode} from './util/element'
7 |
8 | class RenderManager {
9 | rootNode: RootComponent | null = null
10 | target: HTMLElement | null = null
11 |
12 | render(tree: FiendElement, target: HTMLElement) {
13 | if (this.target !== target) {
14 | this.clear()
15 | this.rootNode = new RootComponent(target)
16 | } else if (this.rootNode === null) {
17 | this.rootNode = new RootComponent(target)
18 | }
19 |
20 | this.rootNode.render(tree)
21 | }
22 |
23 | clear() {
24 | this.rootNode?.remove()
25 | this.rootNode = null
26 | }
27 | }
28 |
29 | const renderManager = new RenderManager()
30 |
31 | export class Render {
32 | static component(
33 | tree: FiendElement,
34 | prevTree: AnyComponent | null,
35 | parentHost: DomComponent | RootComponent,
36 | directParent: ParentComponent,
37 | index: number,
38 | ): DomComponent | PureComponent {
39 | if (tree.elementType === ElementType.dom) {
40 | return renderDom(tree, prevTree, parentHost, directParent, index)
41 | } else {
42 | return renderCustom(tree, prevTree, parentHost, directParent, index)
43 | }
44 | }
45 |
46 | static subComponents(
47 | parentHost: DomComponent | RootComponent,
48 | directParent: ParentComponent,
49 | children: FiendNode[],
50 | prevComponents: Map,
51 | ): Map {
52 | const newComponents = new Map()
53 |
54 | const len = children.length - 1
55 |
56 | if (__DEV__) {
57 | checkChildrenKeys(children)
58 | }
59 |
60 | for (let i = len; i >= 0; i--) {
61 | const child = children[i]
62 |
63 | if (child !== null) {
64 | this.subComponent(
65 | parentHost,
66 | directParent,
67 | child,
68 | prevComponents,
69 | newComponents,
70 | i,
71 | )
72 | }
73 | }
74 |
75 | for (const c of prevComponents.values()) c.remove()
76 |
77 | return newComponents
78 | }
79 |
80 | private static subComponent(
81 | parentHost: DomComponent | RootComponent,
82 | directParent: ParentComponent,
83 | subtree: FiendElement | string,
84 | prevChildren: Map,
85 | newChildren: Map,
86 | index: number,
87 | ): AnyComponent {
88 | if (typeof subtree === 'string') {
89 | const key = index.toString()
90 |
91 | const s = renderTextComponent(
92 | subtree,
93 | prevChildren.get(key) ?? null,
94 | parentHost,
95 | directParent,
96 | index,
97 | )
98 | prevChildren.delete(key)
99 | newChildren.set(key, s)
100 | return s
101 | }
102 |
103 | const key: string = subtree.props.key ?? index.toString()
104 | const s = this.component(
105 | subtree,
106 | prevChildren.get(key) ?? null,
107 | parentHost,
108 | directParent,
109 | index,
110 | )
111 | prevChildren.delete(key)
112 | newChildren.set(key, s)
113 | return s
114 | }
115 | }
116 |
117 | export function render(tree: FiendElement | null, target: HTMLElement): void {
118 | if (tree === null) renderManager.clear()
119 | else renderManager.render(tree, target)
120 | }
121 |
122 | function checkChildrenKeys(children: FiendNode[]) {
123 | let numKeys = 0
124 | const set = new Set()
125 |
126 | for (const child of children) {
127 | if (child !== null && typeof child !== 'string') {
128 | if (typeof child.props.key === 'string') {
129 | numKeys++
130 | set.add(child.props.key)
131 | }
132 | }
133 | }
134 |
135 | if (numKeys !== set.size) {
136 | console.error(`Subtrees contain duplicate keys: `, children)
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/lib/observables/$model.test.ts:
--------------------------------------------------------------------------------
1 | import {makeObservable} from './$model'
2 | import {$AutoRun} from './responder'
3 | import {observable} from 'mobx'
4 | import {$Val} from './value-style'
5 |
6 | class Model {
7 | $a = 5
8 | $b = 6
9 |
10 | constructor() {
11 | makeObservable(this)
12 | }
13 | }
14 |
15 | class Val {
16 | a = $Val(5)
17 | b = $Val(6)
18 | }
19 |
20 | class Mobx {
21 | @observable
22 | a = 5
23 |
24 | @observable
25 | b = 6
26 | }
27 |
28 | xdescribe('new fiend-ui class construction speed', () => {
29 | const loops = 10_000
30 |
31 | timeConstructor(Val, 'plain', loops)
32 | timeConstructor(Model, 'decorator', loops)
33 | timeConstructor(Mobx, 'mobx', loops)
34 | timeConstructor2('static', loops)
35 |
36 | test('construct once time', () => {
37 | console.time('cold construction')
38 | new Model()
39 | console.timeEnd('cold construction')
40 | })
41 | })
42 |
43 | function timeConstructor(
44 | C: T,
45 | name: string,
46 | loops: number
47 | ) {
48 | test(name, () => {
49 | console.time(name)
50 | let c
51 | for (let i = 0; i < loops; i++) {
52 | c = new C()
53 | }
54 | console.timeEnd(name)
55 | })
56 | }
57 |
58 | function timeConstructor2(name: string, loops: number) {
59 | test(name, () => {
60 | console.time(name)
61 | const t = Date.now()
62 |
63 | let c
64 | for (let i = 0; i < loops; i++) {
65 | c = new Model()
66 | }
67 |
68 | const duration = Date.now() - t
69 | console.log(`${name} took ${duration}ms, ${duration / loops}ms each`)
70 | console.timeEnd(name)
71 | })
72 | }
73 |
74 | xdescribe('access and set observable speed', () => {
75 | const loops = 100_000
76 |
77 | test('plain', () => {
78 | console.time('plain')
79 | const c = new Val()
80 |
81 | for (let i = 0; i < loops; i++) {
82 | c.a(c.a() + 1)
83 | c.b(c.a())
84 | }
85 | console.timeEnd('plain')
86 | })
87 |
88 | test('mobx', () => {
89 | console.time('mobx')
90 | const c = new Mobx()
91 | for (let i = 0; i < loops; i++) {
92 | c.a = c.a + 1
93 | c.b = c.a
94 | }
95 | console.timeEnd('mobx')
96 | })
97 |
98 | test('decorator', () => {
99 | console.time('decorator')
100 | const c = new Model()
101 | for (let i = 0; i < loops; i++) {
102 | c.$a = c.$a + 1
103 | c.$b = c.$a
104 | }
105 | console.timeEnd('decorator')
106 | })
107 | })
108 |
109 | describe('makeObservable Alternative', () => {
110 | class Test {
111 | $a = 4
112 | $b = 3
113 |
114 | constructor() {
115 | makeObservable(this)
116 | }
117 |
118 | get $n(): number {
119 | return this.$a * this.$b
120 | }
121 | }
122 |
123 | test('some numbers', () => {
124 | const t = new Test()
125 |
126 | let result = 0
127 |
128 | $AutoRun(() => {
129 | result = t.$n
130 | })
131 |
132 | expect(t.$a).toEqual(4)
133 | expect(t.$b).toEqual(3)
134 | expect(t.$n).toEqual(12)
135 |
136 | t.$a = 2
137 |
138 | expect(t.$a).toEqual(2)
139 | expect(t.$n).toEqual(6)
140 | })
141 | })
142 |
143 | describe('check that only correct fields are modified', () => {
144 | let computedRuns = 0
145 |
146 | class Test2 {
147 | $a = 2
148 | e = 9
149 |
150 | b(): number {
151 | return this.$a
152 | }
153 |
154 | constructor() {
155 | makeObservable(this)
156 | }
157 |
158 | get $c(): number {
159 | computedRuns++
160 | return this.$a * this.$a
161 | }
162 |
163 | d(a: number): void {
164 | this.$a = a
165 | }
166 | }
167 |
168 | test('fields are as expected', () => {
169 | const t = new Test2()
170 |
171 | const entries = t as any
172 | expect(entries['__$a']).toBeDefined()
173 | expect(entries['__$c']).toBeDefined()
174 | expect(entries['__e']).toBeFalsy()
175 | expect(entries['__d']).toBeFalsy()
176 |
177 | expect(computedRuns).toEqual(0)
178 | expect(t.$c).toEqual(4)
179 | expect(computedRuns).toEqual(1)
180 |
181 | t.$a = 3
182 | expect(t.$c).toEqual(9)
183 |
184 | expect(computedRuns).toEqual(2)
185 | })
186 | })
187 |
--------------------------------------------------------------------------------
/src/lib/observables/run-stack.ts:
--------------------------------------------------------------------------------
1 | import {UnorderedResponder} from './responder'
2 | import {$Component} from './$component'
3 | import {OArray} from '../util/o-array'
4 | import {RootComponent} from '../component-types/root-component'
5 | import {DomComponent} from '../component-types/host/dom-component'
6 | import {applyInserts} from '../util/order'
7 | import {RefObject} from '../util/ref'
8 | import {PureComponent} from '../..'
9 | import {time, timeEnd} from '../util/measure'
10 |
11 | export class RunStack {
12 | static computeds = new Set>()
13 | static reactions = new Set>()
14 | static components: $Component[] = []
15 |
16 | private static running = false
17 |
18 | static insertsStack = new Set()
19 | static removeStack = new Set()
20 |
21 | static componentDidMountStack: RefObject[] = []
22 | static componentDidUpdateStack: RefObject[] = []
23 |
24 | // static depth = 0
25 |
26 | static runResponders(
27 | computeds: Set>,
28 | reactions: Set>,
29 | components: Map>,
30 | ) {
31 | // this.depth++
32 | // console.log('depth: ', this.depth)
33 |
34 | // for (const [, c] of components) {
35 | for (const c of components.values()) {
36 | if (c.current !== null) OArray.insert(this.components, c.current)
37 | }
38 | for (const o of computeds) {
39 | if (o.current !== null) this.computeds.add(o)
40 | }
41 | for (const o of reactions) {
42 | if (o.current !== null) this.reactions.add(o)
43 | }
44 |
45 | this.run()
46 |
47 | // this.depth--
48 | }
49 |
50 | static run() {
51 | if (!this.running) {
52 | this.running = true
53 |
54 | time('😈Reactions')
55 | while (this.computeds.size > 0 || this.reactions.size > 0) {
56 | while (this.computeds.size > 0) {
57 | const computed = this.computeds.values().next()
58 | .value as RefObject
59 | this.computeds.delete(computed)
60 | computed.current?.run()
61 | }
62 | while (this.reactions.size > 0) {
63 | const reaction = this.reactions.values().next()
64 | .value as RefObject
65 | this.reactions.delete(reaction)
66 | reaction.current?.run()
67 | }
68 | }
69 | timeEnd('😈Reactions')
70 |
71 | time('😈Render')
72 | while (this.components.length > 0) {
73 | const component = this.components.shift()
74 | component?.run()
75 | }
76 | timeEnd('😈Render')
77 |
78 | time('😈DOM')
79 | for (const e of this.removeStack) e.remove()
80 | this.removeStack.clear()
81 | for (const c of this.insertsStack) applyInserts(c)
82 | this.insertsStack.clear()
83 | timeEnd('😈DOM')
84 |
85 | time('😈Mount/Update')
86 | while (this.componentDidMountStack.length > 0) {
87 | const ref = this.componentDidMountStack.shift()
88 | ref?.current?.componentDidMount()
89 | }
90 | while (this.componentDidUpdateStack.length > 0) {
91 | const ref = this.componentDidUpdateStack.shift()
92 | ref?.current?.componentDidUpdate()
93 | }
94 | timeEnd('😈Mount/Update')
95 |
96 | this.running = false
97 |
98 | if (!this.empty()) this.run()
99 |
100 | // if (__DEV__) {
101 | // console.log(
102 | // this.computeds.size,
103 | // this.reactions.size,
104 | // this.components.length,
105 | // this.insertsStack.size,
106 | // this.removeStack.size,
107 | // this.componentDidMountStack.length,
108 | // this.componentDidUpdateStack.length
109 | // )
110 | // }
111 | }
112 | }
113 |
114 | static empty(): boolean {
115 | return (
116 | this.computeds.size === 0 &&
117 | this.reactions.size === 0 &&
118 | this.components.length === 0 &&
119 | this.insertsStack.size === 0 &&
120 | this.removeStack.size === 0 &&
121 | this.componentDidMountStack.length === 0 &&
122 | this.componentDidUpdateStack.length === 0
123 | )
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/lib/component-types/host/dom-component.test.ts:
--------------------------------------------------------------------------------
1 | import {Div} from './dom-components'
2 | import {PureComponent} from '../pure-component'
3 | import {FiendNode} from '../../..'
4 | import {ElementNamespace, ElementType} from '../../util/element'
5 | import {mkRoot} from '../../../dom-tests/test-helpers'
6 |
7 | describe('div renders', () => {
8 | test('no args', () => {
9 | const el = Div({children: []})
10 |
11 | expect(el).toEqual({
12 | elementType: ElementType.dom,
13 | _type: 'div',
14 | namespace: ElementNamespace.html,
15 | props: {children: []},
16 | })
17 | })
18 |
19 | test('single element arg', () => {
20 | const el = Div('hello')
21 |
22 | expect(el).toEqual({
23 | elementType: ElementType.dom,
24 | _type: 'div',
25 | namespace: ElementNamespace.html,
26 | props: {children: ['hello']},
27 | })
28 | })
29 |
30 | test('single attribute arg', () => {
31 | const el = Div({className: 'hi'})
32 |
33 | expect(el).toEqual({
34 | elementType: ElementType.dom,
35 | _type: 'div',
36 | namespace: ElementNamespace.html,
37 | props: {className: 'hi'},
38 | })
39 | })
40 |
41 | test('both attributes and elements', () => {
42 | const el = Div({className: 'hi', children: ['hello']})
43 |
44 | expect(el).toEqual({
45 | elementType: ElementType.dom,
46 | _type: 'div',
47 | namespace: ElementNamespace.html,
48 | props: {className: 'hi', children: ['hello']},
49 | })
50 | })
51 |
52 | test('two elements only', () => {
53 | const el = Div({children: ['hello', 'hello']})
54 |
55 | expect(el).toEqual({
56 | elementType: ElementType.dom,
57 | _type: 'div',
58 | namespace: ElementNamespace.html,
59 | props: {children: ['hello', 'hello']},
60 | })
61 | })
62 |
63 | test('render to dom', () => {
64 | const root = mkRoot()
65 |
66 | root.render(Div({children: ['omg']}))
67 |
68 | expect(root.element.innerHTML).toEqual(`omg
`)
69 | })
70 |
71 | test('custom component', () => {
72 | const root = mkRoot()
73 |
74 | root.render(A.$({}))
75 |
76 | expect(root.element.innerHTML).toEqual(`omg
`)
77 | })
78 | })
79 |
80 | class A extends PureComponent {
81 | render(): FiendNode {
82 | return Div('omg')
83 | }
84 | }
85 |
86 | // describe('isPropsObject', () => {
87 | // test('number', () => {
88 | // expect(isPropsObject(3)).toEqual(false)
89 | //
90 | // const n = 12
91 | //
92 | // expect(isPropsObject(n)).toEqual(false)
93 | // })
94 | //
95 | // test('strings', () => {
96 | // expect(isPropsObject('omg')).toEqual(false)
97 | // })
98 | //
99 | // test('null', () => {
100 | // expect(isPropsObject(null)).toEqual(false)
101 | // expect(isPropsObject(undefined)).toEqual(false)
102 | // })
103 | //
104 | // test('host components', () => {
105 | // expect(isPropsObject(div({}))).toEqual(false)
106 | // })
107 | //
108 | // test('custom components', () => {
109 | // class A extends Component {
110 | // render(): Subtree {
111 | // return div('omg')
112 | // }
113 | // }
114 | //
115 | // expect(isPropsObject(A)).toEqual(false)
116 | // expect(isPropsObject(A.$({}))).toEqual(false)
117 | // })
118 | //
119 | // test('possible objects', () => {
120 | // expect(isPropsObject({})).toEqual(true)
121 | // expect(isPropsObject({style: s`height: 10px`})).toEqual(true)
122 | // })
123 | // })
124 |
125 | // describe('test construction perf', () => {
126 | // const numLoops = 1000000
127 | // test('div2', () => {
128 | // timeF(() => {
129 | // let elements: Tree[] = []
130 | //
131 | // for (let i = 0; i < numLoops; i++) {
132 | // elements.push(div2({style: {width: '100px'}}, div2(), div2(), div2()))
133 | // }
134 | //
135 | // expect(elements.length).toEqual(numLoops)
136 | // }, 'div2')
137 | // })
138 | // test('div', () => {
139 | // timeF(() => {
140 | // let elements: Tree[] = []
141 | //
142 | // for (let i = 0; i < numLoops; i++) {
143 | // elements.push(div({style: {width: '100px'}}, div(), div(), div()))
144 | // }
145 | //
146 | // expect(elements.length).toEqual(numLoops)
147 | // }, 'div')
148 | // })
149 | // })
150 |
--------------------------------------------------------------------------------
/src/lib/create-element.test.ts:
--------------------------------------------------------------------------------
1 | import {Div, H1, P} from './component-types/host/dom-components'
2 | import {ElementNamespace, ElementType} from './util/element'
3 |
4 | // const numLoops = 300000
5 |
6 | describe('create element', () => {
7 | // test(`run createElement ${numLoops} times`, () => {
8 | // testFunc(createElement)
9 | //
10 | // expect(true).toBe(true)
11 | // })
12 |
13 | test('1 child', () => {
14 | const d = H1('Heading')
15 |
16 | expect(d).toEqual({
17 | _type: 'h1',
18 | elementType: ElementType.dom,
19 | namespace: ElementNamespace.html,
20 | props: {children: ['Heading']},
21 | })
22 | })
23 |
24 | test('2 children', () => {
25 | const d = Div({children: [H1('Heading'), P('paragraph')]})
26 |
27 | expect(d).toEqual({
28 | _type: 'div',
29 | elementType: ElementType.dom,
30 | namespace: ElementNamespace.html,
31 | props: {
32 | children: [
33 | {
34 | _type: 'h1',
35 | elementType: ElementType.dom,
36 | namespace: ElementNamespace.html,
37 | props: {
38 | children: ['Heading'],
39 | },
40 | },
41 | {
42 | _type: 'p',
43 | elementType: ElementType.dom,
44 | namespace: ElementNamespace.html,
45 | props: {
46 | children: ['paragraph'],
47 | },
48 | },
49 | ],
50 | },
51 | })
52 | })
53 |
54 | test('child array', () => {
55 | const d = Div({children: [...[1, 2, 3].map(n => Div(`${n}`))]})
56 |
57 | expect(d).toEqual({
58 | _type: 'div',
59 | elementType: ElementType.dom,
60 | namespace: ElementNamespace.html,
61 | props: {
62 | children: [
63 | {
64 | _type: 'div',
65 | elementType: ElementType.dom,
66 | namespace: ElementNamespace.html,
67 | props: {
68 | children: ['1'],
69 | },
70 | },
71 | {
72 | _type: 'div',
73 | elementType: ElementType.dom,
74 | namespace: ElementNamespace.html,
75 | props: {
76 | children: ['2'],
77 | },
78 | },
79 | {
80 | _type: 'div',
81 | elementType: ElementType.dom,
82 | namespace: ElementNamespace.html,
83 | props: {
84 | children: ['3'],
85 | },
86 | },
87 | ],
88 | },
89 | })
90 | })
91 |
92 | test('child array mixed', () => {
93 | const d = Div({children: [H1('Heading'), ...[1, 2, 3].map(n => Div(`${n}`))]})
94 |
95 | expect(d).toEqual({
96 | _type: 'div',
97 | elementType: ElementType.dom,
98 | namespace: ElementNamespace.html,
99 | props: {
100 | children: [
101 | {
102 | _type: 'h1',
103 | elementType: ElementType.dom,
104 | namespace: ElementNamespace.html,
105 | props: {
106 | children: ['Heading'],
107 | },
108 | },
109 | {
110 | _type: 'div',
111 | elementType: ElementType.dom,
112 | namespace: ElementNamespace.html,
113 | props: {
114 | children: ['1'],
115 | },
116 | },
117 | {
118 | _type: 'div',
119 | elementType: ElementType.dom,
120 | namespace: ElementNamespace.html,
121 | props: {
122 | children: ['2'],
123 | },
124 | },
125 | {
126 | _type: 'div',
127 | elementType: ElementType.dom,
128 | namespace: ElementNamespace.html,
129 | props: {
130 | children: ['3'],
131 | },
132 | },
133 | ],
134 | },
135 | })
136 | })
137 | })
138 |
139 | // function testFunc(f: Function) {
140 | // const a: any[] = []
141 | //
142 | // // noinspection JSUnusedLocalSymbols
143 | // const createElement = f
144 | //
145 | // console.time(f.name)
146 | // for (let i = 0; i < numLoops; i++) {
147 | // a.push(Hello
)
148 | // }
149 | // console.timeEnd(f.name)
150 | // console.log(a.slice(0, 1))
151 | // }
152 |
--------------------------------------------------------------------------------
/src/lib/component-types/keys.test.ts:
--------------------------------------------------------------------------------
1 | import {PureComponent} from './pure-component'
2 | import {Div} from './host/dom-components'
3 | import {FiendNode} from '../..'
4 | import {mkRoot} from '../../dom-tests/test-helpers'
5 |
6 | describe('test re-rendering keyed lists', () => {
7 | test('plain divs with keys', () => {
8 | const num = 20
9 |
10 | class Scroller extends PureComponent<{n: number}> {
11 | render() {
12 | const {n} = this.props
13 |
14 | const elements: FiendNode[] = []
15 |
16 | for (let i = 0; i < num; i++) {
17 | const s = `${n + i}`
18 |
19 | elements.push(
20 | Div({
21 | children: [s],
22 | key: s,
23 | }),
24 | )
25 | }
26 |
27 | return elements
28 | }
29 | }
30 |
31 | const root = mkRoot()
32 |
33 | let n = 0
34 | root.render(Scroller.$({n}))
35 | expect(root.inserted.length).toEqual(num)
36 | expect(root.html).toEqual(result(n, num))
37 |
38 | n = 10
39 | root.render(Scroller.$({n}))
40 | expect(root.inserted.length).toEqual(num)
41 | expect(root.html).toEqual(result(n, num))
42 |
43 | n = 5
44 | root.render(Scroller.$({n}))
45 | expect(root.inserted.length).toEqual(num)
46 | expect(root.html).toEqual(result(n, num))
47 | })
48 |
49 | test('divs without keys', () => {
50 | const num = 4
51 |
52 | class Scroller extends PureComponent<{n: number}> {
53 | render() {
54 | const {n} = this.props
55 |
56 | const elements: FiendNode[] = []
57 |
58 | for (let i = 0; i < num; i++) {
59 | const s = `${n + i}`
60 |
61 | elements.push(
62 | Div({
63 | children: [s],
64 | }),
65 | )
66 | }
67 |
68 | return elements
69 | }
70 | }
71 |
72 | const root = mkRoot()
73 |
74 | let n = 0
75 | root.render(Scroller.$({n}))
76 | expect(root.inserted.length).toEqual(num)
77 | expect(root.html).toEqual(result(n, num))
78 |
79 | // Scroll forward
80 | n = 6
81 | root.render(Scroller.$({n}))
82 | expect(root.inserted.length).toEqual(num)
83 | expect(root.html).toEqual(result(n, num))
84 |
85 | // console.log(result(n, num))
86 |
87 | // Scroll backward
88 | n = 5
89 | root.render(Scroller.$({n}))
90 | expect(root.inserted.length).toEqual(num)
91 | expect(root.html).toEqual(result(n, num))
92 |
93 | // Scroll backward
94 | n = 4
95 | root.render(Scroller.$({n}))
96 | expect(root.inserted.length).toEqual(num)
97 | expect(root.html).toEqual(result(n, num))
98 |
99 | // Scroll backward
100 | n = 3
101 | root.render(Scroller.$({n}))
102 | expect(root.inserted.length).toEqual(num)
103 | expect(root.html).toEqual(result(n, num))
104 | })
105 |
106 | test('wrapped divs', () => {
107 | const num = 3
108 |
109 | class DivC extends PureComponent<{text: string}> {
110 | render() {
111 | return Div({children: [this.props.text]})
112 | }
113 | }
114 |
115 | class Scroller extends PureComponent<{n: number}> {
116 | render() {
117 | const {n} = this.props
118 |
119 | const elements: FiendNode[] = []
120 |
121 | for (let i = 0; i < num; i++) {
122 | const s = `${n + i}`
123 |
124 | elements.push(
125 | DivC.$({
126 | text: s,
127 | key: s,
128 | }),
129 | )
130 | }
131 |
132 | return elements
133 | }
134 | }
135 |
136 | const root = mkRoot()
137 |
138 | let n = 0
139 | root.render(Scroller.$({n}))
140 | expect(root.inserted.length).toEqual(num)
141 | expect(root.html).toEqual(result(n, num))
142 |
143 | n = 9
144 | root.render(Scroller.$({n}))
145 | expect(root.inserted.length).toEqual(num)
146 | expect(root.html).toEqual(result(n, num))
147 |
148 | n = 8
149 | root.render(Scroller.$({n}))
150 | expect(root.inserted.length).toEqual(num)
151 | expect(root.html).toEqual(result(n, num))
152 | })
153 | })
154 |
155 | const result = (index: number, numDivs: number): string => {
156 | let r = ''
157 |
158 | for (let i = 0; i < numDivs; i++) {
159 | r += `${index + i}
`
160 | }
161 | return r
162 | }
163 |
--------------------------------------------------------------------------------
/src/dom-tests/deletion.test.ts:
--------------------------------------------------------------------------------
1 | import {PureComponent} from '..'
2 | import {render} from '..'
3 | import {Div, H1} from '..'
4 | import {FiendNode} from '..'
5 | import {mkRoot} from './test-helpers'
6 |
7 | describe('deletion of custom component', () => {
8 | test('host inside host', () => {
9 | const root = mkRoot()
10 |
11 | const c = Div({children: [Div('a'), Div('b')]})
12 |
13 | root.render(c)
14 |
15 | expect(root.element.innerHTML).toEqual(``)
16 |
17 | const c2 = Div({children: [Div('a')]})
18 |
19 | root.render(c2)
20 |
21 | expect(root.element.innerHTML).toEqual(``)
22 |
23 | const c3 = Div({children: [Div('a'), Div('b')]})
24 |
25 | root.render(c3)
26 |
27 | expect(root.element.innerHTML).toEqual(``)
28 | })
29 |
30 | test('host inside custom', () => {
31 | const root = mkRoot()
32 |
33 | const c = C.$({children: [Div('a'), Div('b')]})
34 |
35 | root.render(c)
36 |
37 | expect(root.element.innerHTML).toEqual(``)
38 |
39 | const c2 = C.$({children: [Div('a')]})
40 |
41 | root.render(c2)
42 |
43 | expect(root.element.innerHTML).toEqual(``)
44 |
45 | const c3 = C.$({children: [Div('a'), Div('b')]})
46 |
47 | root.render(c3)
48 |
49 | expect(root.element.innerHTML).toEqual(``)
50 | })
51 |
52 | test('custom inside custom', () => {
53 | const root = mkRoot()
54 |
55 | let c = C.$({children: [C.$({children: ['a']}), C.$({children: ['b']})]})
56 |
57 | root.render(c)
58 | // let divs = renderTree(c, null, root.order, 0)
59 |
60 | expect(root.element.innerHTML).toEqual(``)
61 |
62 | c = C.$({children: [C.$({children: ['a']})]})
63 |
64 | root.render(c)
65 | // divs = renderTree(c, divs, root.order, 0)
66 |
67 | expect(root.element.innerHTML).toEqual(``)
68 |
69 | c = C.$({children: [C.$({children: ['a']}), C.$({children: ['b']})]})
70 |
71 | root.render(c)
72 | // renderTree(c, divs, root.order, 0)
73 |
74 | expect(root.element.innerHTML).toEqual(``)
75 | })
76 |
77 | test('custom inside custom with keys', () => {
78 | const root = mkRoot()
79 |
80 | let c = C.$({
81 | children: [C.$({children: ['a'], key: 'a'}), C.$({children: ['b'], key: 'b'})],
82 | })
83 |
84 | root.render(c)
85 | // let divs = renderTree(c, null, root.order, 0)
86 |
87 | expect(root.element.innerHTML).toEqual(``)
88 |
89 | c = C.$({children: [C.$({children: ['a'], key: 'a'})]})
90 |
91 | root.render(c)
92 | // divs = renderTree(c, divs, root.order, 0)
93 |
94 | expect(root.element.innerHTML).toEqual(``)
95 |
96 | c = C.$({
97 | children: [C.$({children: ['a'], key: 'a'}), C.$({children: ['b'], key: 'b'})],
98 | })
99 |
100 | root.render(c)
101 | // renderTree(c, divs, root.order, 0)
102 |
103 | expect(root.element.innerHTML).toEqual(``)
104 | })
105 |
106 | // xtest('complex', () => {
107 | // const root = mkRoot()
108 | //
109 | // const c = (
110 | //
111 | // a
112 | // {pickComponent('a')}
113 | //
114 | // )
115 | //
116 | // const divs = renderTree(c, null, root, 0)
117 | //
118 | // expect(root.element.innerHTML).toEqual(`a A
`)
119 | //
120 | // const c2 = (
121 | //
122 | // a
123 | // {pickComponent('b')}
124 | //
125 | // )
126 | //
127 | // renderTree(c2, divs, root, 0)
128 | //
129 | // expect(root.element.innerHTML).toEqual(`a B
`)
130 | // })
131 |
132 | test('different order', () => {
133 | class Outer extends PureComponent {
134 | render() {
135 | return [A.$({}), H1('Heading')]
136 | }
137 | }
138 |
139 | const divElement = document.createElement('div')
140 | render(Outer.$({}), divElement)
141 |
142 | expect(divElement.innerHTML).toEqual(`A
Heading `)
143 | })
144 | })
145 |
146 | class C extends PureComponent {
147 | render(): FiendNode {
148 | const {children = []} = this.props
149 |
150 | return Div({children})
151 | }
152 | }
153 |
154 | class A extends PureComponent {
155 | render(): FiendNode | null {
156 | return Div('A')
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/src/lib/observables/decorator.test.ts:
--------------------------------------------------------------------------------
1 | import {autorun, computed, IReactionDisposer, observable} from 'mobx'
2 | import {$AutoRun} from './responder'
3 | import {globalStack} from './global-stack'
4 | import {$Val} from './value-style'
5 |
6 | function ob(value: T): {(): T; (newValue: T): undefined} {
7 | const obs = observable.box(value)
8 |
9 | function inner(): T
10 | function inner(newValue: T): undefined
11 | function inner(newValue?: T) {
12 | if (arguments.length === 0) return obs.get()
13 |
14 | if (newValue !== undefined) obs.set(newValue)
15 |
16 | return undefined
17 | }
18 |
19 | return inner
20 | }
21 |
22 | function computed2(f: () => T) {
23 | const c = computed(f)
24 |
25 | return () => {
26 | return c.get()
27 | }
28 | }
29 |
30 | xdescribe('o', () => {
31 | test('o', () => {
32 | const n = ob(5)
33 | let ok = false
34 |
35 | autorun(() => {
36 | console.log('n: ', n())
37 |
38 | if (n() === 6) {
39 | ok = true
40 | }
41 | })
42 |
43 | n(6)
44 |
45 | expect(ok).toBeTruthy()
46 | })
47 | })
48 |
49 | describe('mobx behaviour', () => {
50 | test('autorun supports setting', () => {
51 | const a = observable.box(2)
52 | const b = observable.box(2)
53 | const c = computed(() => a.get() * b.get())
54 | let d: number = c.get()
55 |
56 | expect(c.get()).toEqual(4)
57 | let count = 0
58 |
59 | autorun(() => {
60 | count++
61 | b.set(a.get() * 5)
62 |
63 | expect(b.get()).toEqual(10)
64 | expect(c.get()).toEqual(20)
65 |
66 | d = c.get()
67 | })
68 |
69 | expect(count).toEqual(1)
70 | })
71 | })
72 |
73 | xdescribe('test decorator perf', () => {
74 | const loops = 1000
75 |
76 | test('time a', () => {
77 | class A {
78 | @observable
79 | a = 5
80 |
81 | @observable
82 | b = 5
83 |
84 | @observable
85 | c = 5
86 |
87 | @observable
88 | d = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
89 | }
90 |
91 | class RunA {
92 | a = new A()
93 |
94 | disposer: IReactionDisposer
95 |
96 | constructor() {
97 | this.disposer = autorun(() => {
98 | const {a, b, c, d} = this.a
99 |
100 | d.push(a, b, c)
101 | })
102 | }
103 | }
104 |
105 | console.time('a')
106 | for (let i = 0; i < loops; i++) {
107 | const a = new RunA()
108 | a.disposer()
109 | }
110 | console.timeEnd('a')
111 | })
112 |
113 | test('time b', () => {
114 | class B {
115 | a = observable.box(5)
116 |
117 | b = observable.box(5)
118 |
119 | c = observable.box(5)
120 |
121 | d = observable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
122 | }
123 |
124 | class RunB {
125 | b = new B()
126 |
127 | disposer: IReactionDisposer
128 |
129 | constructor() {
130 | this.disposer = autorun(() => {
131 | const {a, b, c, d} = this.b
132 |
133 | d.push(a.get(), b.get(), c.get())
134 | })
135 | }
136 | }
137 |
138 | console.time('b')
139 | for (let i = 0; i < loops; i++) {
140 | const b = new RunB()
141 | b.disposer()
142 | }
143 | console.timeEnd('b')
144 | })
145 |
146 | test('C', () => {
147 | function mkC() {
148 | return observable({
149 | a: 5,
150 | b: 5,
151 | c: 5,
152 | d: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
153 | })
154 | }
155 |
156 | class RunC {
157 | c = mkC()
158 |
159 | disposer: IReactionDisposer
160 |
161 | constructor() {
162 | this.disposer = autorun(() => {
163 | const {a, b, c, d} = this.c
164 |
165 | d.push(a, b, c)
166 | })
167 | }
168 | }
169 |
170 | console.time('c')
171 | for (let i = 0; i < loops; i++) {
172 | const c = new RunC()
173 | c.disposer()
174 | }
175 | console.timeEnd('c')
176 | })
177 |
178 | test('d', () => {
179 | class D {
180 | a = ob(5)
181 |
182 | b = ob(5)
183 |
184 | c = ob(5)
185 |
186 | res = computed2(() => this.a() + this.b() + this.c())
187 |
188 | d = observable.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
189 | }
190 |
191 | class RunD {
192 | b = new D()
193 |
194 | disposer: IReactionDisposer
195 |
196 | constructor() {
197 | this.disposer = autorun(() => {
198 | const {a, b, c, d} = this.b
199 |
200 | d.push(a(), b(), c())
201 | })
202 | }
203 | }
204 |
205 | console.time('d')
206 | for (let i = 0; i < loops; i++) {
207 | const d = new RunD()
208 |
209 | console.log(d.b.res())
210 | d.disposer()
211 | }
212 | console.timeEnd('d')
213 | })
214 |
215 | test('e', () => {
216 | class E {
217 | a = $Val(5)
218 |
219 | b = $Val(5)
220 |
221 | c = $Val(5)
222 |
223 | d = $Val([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
224 | }
225 |
226 | class Run {
227 | b = new E()
228 |
229 | constructor() {
230 | $AutoRun(() => {
231 | const {a, b, c, d} = this.b
232 |
233 | const array = [...d()]
234 |
235 | array.push(a(), b(), c())
236 |
237 | d(array)
238 | })
239 | }
240 | }
241 |
242 | console.time('e')
243 | for (let i = 0; i < loops; i++) {
244 | new Run()
245 | }
246 |
247 | console.timeEnd('e')
248 | })
249 | })
250 |
--------------------------------------------------------------------------------
/src/lib/component-types/pure-component.ts:
--------------------------------------------------------------------------------
1 | import type {CustomElement, FiendNode, StandardProps} from '../util/element'
2 | import {ElementType} from '../util/element'
3 | import {
4 | AnyComponent,
5 | ComponentBase,
6 | ComponentType,
7 | ParentComponent,
8 | } from './base-component'
9 | import {Render} from '../render'
10 | import {time, timeEnd} from '../util/measure'
11 | import {Order} from '../util/order'
12 | import {DomComponent} from './host/dom-component'
13 | import {RootComponent} from './root-component'
14 | import {RefObject} from '../util/ref'
15 | import {RunStack} from '../observables/run-stack'
16 |
17 | export interface Rec {
18 | [prop: string]: unknown
19 | }
20 |
21 | export type PropsWithChildren = T & StandardProps
22 |
23 | // P = {} to simply prop type definitions.
24 | export abstract class PureComponent
25 | implements ComponentBase
26 | {
27 | _type = ComponentType.custom as const
28 | props: PropsWithChildren
29 | order: string
30 | key: string
31 |
32 | subComponents = new Map()
33 |
34 | _ref: RefObject = {
35 | current: this,
36 | }
37 |
38 | constructor(
39 | props: P,
40 | public domParent: DomComponent | RootComponent,
41 | directParent: ParentComponent,
42 | // TODO: Is this safe? It doesn't get updated? So could be reassigned accidentally?
43 | public index: number,
44 | ) {
45 | this.props = props
46 | this.order = Order.key(directParent.order, index)
47 | this.key = this.props.key ?? directParent.key + index
48 | }
49 |
50 | abstract render(): FiendNode | FiendNode[]
51 |
52 | update() {
53 | if (__DEV__ && this.removed) {
54 | throw 'Called update after removed!'
55 | }
56 | if (__DEV__) {
57 | time(this.constructor.name)
58 | }
59 | const res = this.render()
60 |
61 | this.subComponents = Render.subComponents(
62 | this.domParent,
63 | this,
64 | Array.isArray(res) ? res : [res],
65 | this.subComponents,
66 | )
67 |
68 | if (__DEV__) {
69 | timeEnd(this.constructor.name)
70 | }
71 | }
72 |
73 | // noinspection JSUnusedGlobalSymbols
74 | updateWithNewProps(props: PropsWithChildren): void {
75 | if (!equalProps(this.props, props)) {
76 | this.props = props
77 | this.update()
78 | RunStack.componentDidUpdateStack.push(this._ref)
79 | }
80 | }
81 |
82 | componentDidMount(): void {}
83 |
84 | componentDidUpdate(): void {}
85 |
86 | componentWillUnmount(): void {}
87 |
88 | forceUpdate = () => {
89 | // Sometimes we put a forceUpdate inside a setTimeout, we don't want it
90 | // to run if this element has been removed before it runs.
91 | if (this.removed) return
92 |
93 | // Doesn't componentDidUpdate get called after update?
94 | this.update()
95 | }
96 |
97 | mount() {
98 | this.update()
99 | RunStack.componentDidMountStack.push(this._ref)
100 | }
101 |
102 | removed = false
103 |
104 | remove(): void {
105 | if (__DEV__ && this.removed) {
106 | console.error('already removed')
107 | }
108 | this.componentWillUnmount()
109 |
110 | for (const c of this.subComponents.values()) c.remove()
111 | this.subComponents.clear()
112 | this.removed = true
113 | }
114 |
115 | static $(
116 | this: new (...args: never[]) => T,
117 | props: T['props'],
118 | ): CustomElement {
119 | return {
120 | _type: this as any,
121 | elementType: ElementType.custom,
122 | props,
123 | }
124 | }
125 | }
126 |
127 | export function renderCustom(
128 | tree: CustomElement,
129 | prev: AnyComponent | null,
130 | domParent: DomComponent | RootComponent,
131 | directParent: ParentComponent,
132 | index: number,
133 | ) {
134 | const {_type, props} = tree
135 |
136 | if (prev === null) {
137 | return makeCustomComponent(_type, props, domParent, directParent, index)
138 | }
139 |
140 | if (prev._type === ComponentType.custom && prev instanceof _type) {
141 | const newOrder = Order.key(directParent.order, index)
142 | const prevOrder = prev.order
143 |
144 | if (newOrder !== prevOrder) {
145 | prev.index = index
146 | prev.order = newOrder
147 |
148 | for (const c of prev.subComponents.values()) {
149 | const no = Order.key(prev.order, c.index)
150 |
151 | if (c.order !== no) {
152 | c.order = no
153 |
154 | switch (c._type) {
155 | case ComponentType.host:
156 | case ComponentType.text:
157 | domParent.moveChild(c)
158 | break
159 | case ComponentType.custom:
160 | c.update()
161 | break
162 | }
163 | }
164 | }
165 | }
166 |
167 | prev.updateWithNewProps(props)
168 |
169 | return prev
170 | }
171 |
172 | prev.remove()
173 |
174 | return makeCustomComponent(_type, props, domParent, directParent, index)
175 | }
176 |
177 | export type CustomComponent = new
(
178 | props: P,
179 | parentHost: DomComponent | RootComponent,
180 | directParent: ParentComponent,
181 | index: number,
182 | ) => PureComponent
183 |
184 | function makeCustomComponent
(
185 | cons: CustomComponent
,
186 | props: P,
187 | parentHost: DomComponent | RootComponent,
188 | directParent: ParentComponent,
189 | index: number,
190 | ) {
191 | const component = new cons
(props, parentHost, directParent, index)
192 | component.mount()
193 |
194 | return component
195 | }
196 |
197 | export function equalProps(a: object, b: object): boolean {
198 | const aKeys = Object.keys(a)
199 | const bKeys = Object.keys(b)
200 |
201 | if (aKeys.length !== bKeys.length) return false
202 |
203 | // We should only need to loop over aKeys since the length must be the same.
204 | for (const key of aKeys) {
205 | // @ts-ignore
206 | if (a[key] !== b[key]) return false
207 | }
208 |
209 | return true
210 | }
211 |
--------------------------------------------------------------------------------
/src/lib/misc/compare-speed.test.ts:
--------------------------------------------------------------------------------
1 | import {$AutoRun, $Reaction} from '../observables/responder'
2 | import {makeObservable} from '../observables/$model'
3 | import {autorun, computed, observable} from 'mobx'
4 | import {$Calc, $Val} from '../observables/value-style'
5 |
6 | describe('reaction tests', () => {
7 | test('basic reaction', () => {
8 | const n = $Val(4)
9 |
10 | const sqr = $Calc(() => n() * n())
11 |
12 | let out = 0
13 | $Reaction(
14 | () => sqr(),
15 | result => {
16 | out = result
17 | }
18 | )
19 |
20 | n(2)
21 | expect(out).toEqual(4)
22 |
23 | n(3)
24 | expect(out).toEqual(9)
25 | })
26 | })
27 |
28 | xdescribe('proxy test', () => {
29 | type Target = {message2: string; message1: string}
30 |
31 | const target: Target = {
32 | message1: 'hello',
33 | message2: 'everyone',
34 | }
35 |
36 | const handler2 = {
37 | get(target: Target, prop: keyof Target) {
38 | return target[prop]
39 | },
40 | }
41 |
42 | const proxy = new Proxy(target, handler2)
43 |
44 | const loops = 1000000
45 |
46 | test('proxy speed', () => {
47 | console.time('proxy')
48 |
49 | for (let i = 0; i < loops; i++) {
50 | const m1 = proxy.message1
51 | const m2 = proxy.message2
52 | }
53 |
54 | console.timeEnd('proxy')
55 | })
56 |
57 | test('normal access speed', () => {
58 | console.time('normy')
59 |
60 | for (let i = 0; i < loops; i++) {
61 | const m1 = target.message1
62 | const m2 = target.message2
63 | }
64 |
65 | console.timeEnd('normy')
66 | })
67 | })
68 |
69 | xdescribe('construction speed', () => {
70 | const num = 1_000_000
71 |
72 | test('time things', () => {
73 | console.time('array')
74 | let a
75 | for (let i = 0; i < num; i++) {
76 | a = [i]
77 | }
78 | console.timeEnd('array')
79 |
80 | console.time('array2')
81 | let d
82 | for (let i = 0; i < num; i++) {
83 | d = Array.from([])
84 | }
85 | console.timeEnd('array2')
86 |
87 | console.time('object')
88 | let b
89 | for (let i = 0; i < num; i++) {
90 | b = {a: i}
91 | }
92 | console.timeEnd('object')
93 |
94 | console.time('map')
95 | let c
96 | for (let i = 0; i < num; i++) {
97 | c = new Map()
98 | }
99 | console.timeEnd('map')
100 |
101 | expect(true).toBeTruthy()
102 | })
103 | })
104 |
105 | xdescribe('map vs object', () => {
106 | const o = {
107 | a: 1,
108 | b: 2,
109 | c: 3,
110 | d: 4,
111 | e: 5,
112 | f: 6,
113 | g: 7,
114 | h: 8,
115 | i: 9,
116 | j: 10,
117 | k: 11,
118 | l: 12,
119 | }
120 |
121 | const m = new Map()
122 |
123 | m.set('a', 1)
124 | m.set('b', 2)
125 | m.set('c', 3)
126 | m.set('d', 4)
127 | m.set('e', 5)
128 | m.set('f', 6)
129 | m.set('g', 7)
130 | m.set('h', 8)
131 | m.set('i', 9)
132 | m.set('j', 10)
133 | m.set('k', 11)
134 | m.set('l', 12)
135 |
136 | const num = 100_000
137 |
138 | test('object', () => {
139 | let n = 0
140 |
141 | console.time('object')
142 |
143 | for (let i = 0; i < num; i++) {
144 | // const keys = Object.keys(o) as (keyof typeof o)[]
145 | //
146 | // for (const key of keys) {
147 | // n = o[key]
148 | // }
149 |
150 | for (const key in o) {
151 | n = o[key as keyof typeof o]
152 | }
153 | }
154 |
155 | console.timeEnd('object')
156 |
157 | expect(n).toEqual(12)
158 | })
159 |
160 | test('map', () => {
161 | let n = 0
162 |
163 | console.time('map')
164 |
165 | for (let i = 0; i < num; i++) {
166 | for (const [, value] of m) {
167 | n = value
168 | }
169 | }
170 |
171 | console.timeEnd('map')
172 |
173 | expect(n).toEqual(12)
174 | })
175 | })
176 |
177 | describe('$AutoRun cleanup', () => {
178 | let count = 0
179 | const n = $Val(1)
180 | let dispose: () => void
181 |
182 | test('autorun', () => {
183 | dispose = $AutoRun(() => {
184 | n()
185 | count++
186 | })
187 |
188 | expect(count).toEqual(1)
189 | n(2)
190 | expect(count).toEqual(2)
191 | })
192 |
193 | test('out of scope, no cleanup', () => {
194 | // a.end()
195 | n(3)
196 | expect(count).toEqual(3)
197 | })
198 |
199 | test('out of scope, with cleanup', () => {
200 | dispose()
201 | n(4)
202 | expect(count).toEqual(3)
203 | })
204 | })
205 |
206 | describe('reaction scope', () => {
207 | let count = 0
208 | const n = $Val(1)
209 |
210 | test('init reaction', () => {
211 | let i = 0
212 |
213 | const disposer = $AutoRun(() => {
214 | i = n()
215 | count++
216 | })
217 |
218 | expect(count).toEqual(1)
219 | n(n() + 1)
220 | expect(count).toEqual(2)
221 |
222 | disposer()
223 | })
224 |
225 | test('out of scope', () => {
226 | const before = count
227 |
228 | n(n() + 1)
229 | n(n() + 1)
230 | n(n() + 1)
231 | n(n() + 1)
232 | expect(count).toEqual(before)
233 | })
234 | })
235 |
236 | /*
237 | When a class with a computed goes out of scope we want any computeds referencing
238 | external observables to stop running.
239 | */
240 | describe('computed scope', () => {
241 | let count = 0
242 |
243 | const n = $Val(1)
244 |
245 | /*
246 | The computed is inside n's responder list. The computed doesn't have any
247 | responders. If it doesn't have any responders, then it shouldn't be inside n's list?
248 |
249 | When n changes, it calls $num's run method. $num has no responders. It shouldn't run unless
250 | it's called via a get().
251 | */
252 |
253 | class A {
254 | constructor() {
255 | makeObservable(this)
256 | }
257 |
258 | get $num(): number {
259 | count++
260 | return n() * n()
261 | }
262 | }
263 |
264 | test('init computed', () => {
265 | const a = new A()
266 |
267 | const d = $AutoRun(() => {
268 | a.$num
269 | })
270 |
271 | expect(count).toEqual(1)
272 | n(n() + 1)
273 | expect(count).toEqual(2)
274 |
275 | d()
276 | })
277 |
278 | test('out of scope', () => {
279 | const before = count
280 |
281 | n(n() + 1)
282 | n(n() + 1)
283 | n(n() + 1)
284 | n(n() + 1)
285 |
286 | expect(count).toEqual(before)
287 | })
288 | })
289 |
290 | describe('mobx computed scope', () => {
291 | let count = 0
292 |
293 | const n = observable.box(1)
294 |
295 | class A {
296 | @computed
297 | get num(): number {
298 | count++
299 | return n.get() * n.get()
300 | }
301 |
302 | @computed
303 | get num2(): number {
304 | return this.num
305 | }
306 | }
307 |
308 | test('init computed', () => {
309 | const a = new A()
310 |
311 | let result = 0
312 |
313 | const d = autorun(() => {
314 | result = a.num2
315 | })
316 |
317 | expect(count).toEqual(1)
318 | n.set(n.get() + 1)
319 | expect(count).toEqual(2)
320 |
321 | d()
322 | })
323 |
324 | test('out of scope', () => {
325 | const before = count
326 |
327 | n.set(n.get() + 1)
328 | n.set(n.get() + 1)
329 | n.set(n.get() + 1)
330 | n.set(n.get() + 1)
331 |
332 | expect(count).toEqual(before)
333 | })
334 | })
335 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------