Skip to content

Commit

Permalink
improve and. simplify syncnodes introducing DoubleLinkedList
Browse files Browse the repository at this point in the history
  • Loading branch information
ECorreia45 committed Aug 8, 2024
1 parent bdcac65 commit 7c7f355
Show file tree
Hide file tree
Showing 17 changed files with 695 additions and 281 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@beforesemicolon/markup",
"version": "1.0.14-next",
"version": "1.0.16-next",
"description": "Reactive HTML Templating System",
"engines": {
"node": ">=18.16.0"
Expand Down
102 changes: 102 additions & 0 deletions src/DoubleLinkedList.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { DoubleLinkedList } from './DoubleLinkedList'

describe('DoubleLinkedList', () => {
it('should push, remove, and clear values correctly', () => {
let ll= new DoubleLinkedList();

ll.push(1);
ll.push(2);
ll.push(3);

expect([...ll]).toEqual([1, 2, 3])
expect(ll.head).toBe(1)
expect(ll.getNextValueOf(2)).toBe(3)
expect(ll.getPreviousValueOf(2)).toBe(1)
expect(ll.tail).toBe(3)
expect(ll.size).toBe(3)

ll.remove(2)

expect([...ll]).toEqual([1, 3])
expect(ll.size).toBe(2)
expect(ll.has(2)).toBeFalsy()

ll.remove(3)

expect([...ll]).toEqual([1])
expect(ll.size).toBe(1)
expect(ll.has(3)).toBeFalsy()

ll.remove(1)

expect([...ll]).toEqual([])
expect(ll.size).toBe(0)

ll = DoubleLinkedList.fromArray([1, 2, 3])

expect(ll.size).toBe(3)

ll.clear()

expect([...ll]).toEqual([])
expect(ll.size).toBe(0)
})

it('should insert value before', () => {
const ll= new DoubleLinkedList();

ll.push(5);

expect(ll.head).toBe(ll.tail)
expect([...ll]).toEqual([5])

ll.insertValueBefore(4, 5)

expect([...ll]).toEqual([4, 5])

expect(ll.head).toBe(4)
expect(ll.getNextValueOf(4)).toBe(5)
expect(ll.getPreviousValueOf(5)).toBe(4)
expect(ll.tail).toBe(5)
expect(ll.size).toBe(2)
})

it('should insert value after', () => {
const ll= new DoubleLinkedList();

ll.push(5);

expect(ll.head).toBe(ll.tail)
expect([...ll]).toEqual([5])

ll.insertValueAfter(6, 5)

expect([...ll]).toEqual([5, 6])

expect(ll.head).toBe(5)
expect(ll.getNextValueOf(5)).toBe(6)
expect(ll.getPreviousValueOf(6)).toBe(5)
expect(ll.tail).toBe(6)
expect(ll.size).toBe(2)
})

it('should reverse list', () => {
const ll= DoubleLinkedList.fromArray([1, 2, 3, 4, 5]);

expect([...ll]).toEqual([1,2,3,4,5])

let c = ll.head;

[5, 4, 3, 2, 1].forEach(n => {
c && n !== c && ll.insertValueBefore(n, c);
})

expect([...ll]).toEqual([
5,
4,
3,
2,
1
])
})
})
161 changes: 161 additions & 0 deletions src/DoubleLinkedList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
interface DLLElement<T> {
value: T
next: DLLElement<T> | null
prev: DLLElement<T> | null
}

export class DoubleLinkedList<T> {
#head: DLLElement<T> | null = null
#tail: DLLElement<T> | null = null
#map = new Map();

*[Symbol.iterator]() {
let current = this.#head

while (current) {
yield current.value
current = current.next
}
}

static fromArray<T>(arr: Array<T>) {
const list = new DoubleLinkedList<T>()

for (const value of arr) {
list.push(value)
}

return list
}

get size() {
return this.#map.size
}

get head(): T | null {
return this.#head?.value ?? null
}

get tail(): T | null {
return this.#tail?.value ?? null
}

push(value: T) {
if (!this.has(value)) {
const element = { value, next: null, prev: null } as DLLElement<T>

if (this.#map.size === 0) {
this.#head = element
} else {
;(this.#tail as DLLElement<T>).next = element
element.prev = this.#tail
}

this.#tail = element

this.#map.set(value, element)
}
}

remove(value: T) {
if (this.has(value)) {
const element = this.#map.get(value)

if (this.#map.size === 1) {
this.#head = null
this.#tail = null
} else if (element === this.#head) {
this.#head = element.next
element.next.prev = null
} else if (element === this.#tail) {
this.#tail = element.prev
element.prev.next = null
} else {
element.prev.next = element.next
element.next.prev = element.prev
}

this.#map.delete(value)
}
}

insertValueAfter(newValue: T, value: T) {
if (this.has(value)) {
const element = this.#map.get(value)
const existingValue = this.#map.has(newValue)
const newElement = this.#map.get(newValue) || {
value: newValue,
next: null,
prev: null,
}

if (element.next !== newElement) {
if (existingValue) {
this.remove(newValue)
}

newElement.prev = element
newElement.next = element.next

if (element === this.#tail) {
this.#tail = newElement
} else {
element.next.prev = newElement
}

element.next = newElement

this.#map.set(newValue, newElement)
}
}
}

insertValueBefore(newValue: T, value: T) {
if (this.has(value)) {
const element = this.#map.get(value)
const existingValue = this.#map.has(newValue)
const newElement = this.#map.get(newValue) || {
value: newValue,
next: null,
prev: null,
}

if (element.prev !== newElement) {
if (existingValue) {
this.remove(newValue)
}

newElement.next = element
newElement.prev = element.prev

if (element === this.#head) {
this.#head = newElement
} else {
element.prev.next = newElement
}

element.prev = newElement

this.#map.set(newValue, newElement)
}
}
}

clear() {
this.#head = null
this.#tail = null
this.#map.clear()
}

has(value: T) {
return this.#map.has(value)
}

getNextValueOf(value: T | null): T | null {
return this.#map.get(value)?.next?.value ?? null
}

getPreviousValueOf(value: T | null): T | null {
return this.#map.get(value)?.prev?.value ?? null
}
}
14 changes: 6 additions & 8 deletions src/ReactiveNode.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import '../test.common'
import { ReactiveNode } from './ReactiveNode'
import { html, HtmlTemplate } from './html'
import { state } from './state'
import { is, when, repeat } from './helpers'
import { syncNodes } from './utils/sync-nodes'
import { is, when } from './helpers'

describe('ReactiveNode', () => {
beforeEach(() => {
document.body.innerHTML = ''
})

it('should render text', () => {
const node = new ReactiveNode(() => 'sample', document.body)

Expand Down Expand Up @@ -218,8 +214,9 @@ describe('ReactiveNode', () => {
it('should swap second and before last items', () => {
const unmountMock = jest.fn();
const mountMock = jest.fn(() => unmountMock);
const moveMock = jest.fn();
const nodeTemplates = Array.from({ length: 10 }, (_, i) => {
return html`<li>item ${i + 1}</li>`.onMount(mountMock)
return html`<li>item ${i + 1}</li>`.onMount(mountMock).onMove(moveMock)
});
const [list, updateList] = state(nodeTemplates);

Expand Down Expand Up @@ -252,7 +249,8 @@ describe('ReactiveNode', () => {
nodeTemplates[9],
])

expect(mountMock).toHaveBeenCalledTimes(3)
expect(mountMock).toHaveBeenCalledTimes(0)
expect(moveMock).toHaveBeenCalledTimes(2)

expect(document.body.innerHTML).toBe('<li>item 1</li>' +
'<li>item 9</li>' +
Expand Down
28 changes: 17 additions & 11 deletions src/ReactiveNode.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { HtmlTemplate } from './html'
import { effect } from './state'
import { syncNodes } from './utils/sync-nodes'
import { EffectUnSubscriber } from './types'
import { EffectUnSubscriber, RenderType } from './types'
import { renderContent } from './utils/render-content'
import { DoubleLinkedList } from './DoubleLinkedList'

export class ReactiveNode {
#result: Array<Node | HtmlTemplate> = []
#result = new DoubleLinkedList<Node | HtmlTemplate>()
#unsubEffect: EffectUnSubscriber | null = null
#parent: HTMLElement | Element | null = null
#updateSub: (() => void) | undefined = undefined
Expand All @@ -20,19 +21,21 @@ export class ReactiveNode {
}

get refs(): Record<string, Array<Element>> {
return this.#result.reduce((acc, item) => {
let acc = {}

for (const item of this.#result) {
if (item instanceof HtmlTemplate) {
return {
acc = {
...acc,
...item.refs,
}
}
}

return acc
}, {})
return acc
}

constructor(action: () => unknown, parentNode: HTMLElement) {
constructor(action: (anchor: Node) => unknown, parentNode: HTMLElement) {
let init = false

if (parentNode) {
Expand All @@ -41,18 +44,21 @@ export class ReactiveNode {
parentNode.appendChild(this.#anchor)

this.#unsubEffect = effect(() => {
const res = action()
const res = action(this.#anchor)

if (res === RenderType.Skip) return

if (init) {
this.#result = syncNodes(
this.#result,
Array.isArray(res) ? res : [res],
parentNode,
this.#anchor
)
this.#updateSub?.()
} else {
this.#result = renderContent(res, parentNode)
renderContent(res, parentNode, (item) =>
this.#result.push(item)
)
init = true
}
})
Expand All @@ -70,7 +76,7 @@ export class ReactiveNode {
}
this.#anchor.parentNode?.removeChild(this.#anchor)
this.#parent = null
this.#result = []
this.#result.clear()
}

onUpdate(cb: () => void) {
Expand Down
Loading

0 comments on commit 7c7f355

Please sign in to comment.