Skip to content

Commit

Permalink
streamline repeat with more data type support and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
ECorreia45 committed Oct 20, 2024
1 parent 8a1501d commit 4b2abde
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 117 deletions.
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ playground
docs
docs-src
playground.html
website
104 changes: 104 additions & 0 deletions docs/documentation/utilities/repeat.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,107 @@ layout: document
---

## Repeat Utility

The `repeat` utility is Markup recommended way to render iterable data or repeating content. It handles things like caching and tracking for the template ensuring list changes only happen to the items that need them.

```javascript
html`${repeat(todos, renderTodo)}`
```

### Why use repeat?

Markup templates already handle arrays but like any other injected value, Markup track the whole thing and not its individual items.

```javascript
const [todos, updateTodos] = state([])

html`${() => todos().map(renderTodo)}`.render(document.body)

updateTodos((prev) => [
...prev,
{
name: 'Go to gym',
status: 'pending',
},
])
```

The above code will work just fine as far as rendering a list. The issue becomes evident when a single item changes, is added or removed. This is because the function will get called again generating a brand new list of content to render. If Markup sees a new list, it will re-render everything.

The better way is to not dynamically map the list on render but on creation so Markup always have the things you want to render so it can check if its different from before or not.

```javascript
const [todos, updateTodos] = state([])

html`${todos}`.render(document.body)

updateTodos((prev) => [
...prev,
renderTodo({
name: 'Go to gym',
status: 'pending',
}),
])
```

This is exactly what `repeat` utility does for you by just taking the data and the render function.

### Numbers

The `repeat` helper accepts a number among many things. This number represents the number of times you want the callback render function needs to be called.

```javascript
html`${repeat(3, html`<spa></span>`)}`
```

The callback function will be called with numbers from 1 to the number you provided along with the indexes.

```javascript
html`${repeat(3, (n, index) => html`<spa>${n} - ${index}</span>`)}`.render(
document.body
)
// <span>1 - 0</span><span>2 - 1</span><span>3 - 2</span>
```

### Iterables and Object literals

Additionally, `repeat` can consume any Object literal or [iterable object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) and this includes:

- Array
- Set
- Map
- String
- any object with [Symbol.iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator)

```javascript
const iterable = {}

iterable[Symbol.iterator] = function* () {
yield 1
yield 2
yield 3
}

html`${repeat(iterable, renderItem)}`
```

Since Markup template do not handle rendering such objects (except Array), this is an additional advantage in using `repeat`. You dont have to worry about converting any data into Array just so you can iterate and render.

The callback function will always get called with the entries and the index.

```javascript
const employeesSalary = {
'john doe': 30000,
'jane doe': 54000,
}

html`${repeat(employeesSalary, ([name, salary], index) => html`...`)}`
```

### Empty state

The `repeat` also consumes an optional third argument which is a function that will get called to return what to render when the data is empty.

```javascript
html`${repeat(todos, renderTodo, () => html`<p>No todos yet!</p>`)}`
```
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.1.5-next",
"version": "1.1.6-next",
"description": "Reactive HTML Templating System",
"engines": {
"node": ">=18.16.0"
Expand Down
5 changes: 0 additions & 5 deletions src/ReactiveNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,6 @@ export class ReactiveNode {
this.#unsubEffect = effect(() => {
const res = action(this.#anchor, template)

if (res instanceof DoubleLinkedList) {
this.#result = res
return
}

if (init) {
this.#result = syncNodes(
this.#result,
Expand Down
79 changes: 36 additions & 43 deletions src/helpers/repeat.helper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,69 +3,49 @@ import { repeat } from './repeat.helper'
import { html } from '../html'

describe('repeat', () => {
const temp = html``;
const anchor = document.createTextNode('');

beforeEach(() => {
document.body.appendChild(anchor)
})

it('should handle number', () => {
repeat(3, (n: number) => n)(anchor, temp);

expect(document.body.innerHTML).toBe('123')
expect(repeat(3, (n: number) => n)()).toEqual([1, 2, 3])
})

it('should handle updates', () => {
let count = 3
const r = repeat<number>(
() => count,
(n: number) => html`sample-${n}`
)
r(anchor, temp)
expect(document.body.innerHTML).toBe('sample-1sample-2sample-3')

r()

expect(r()).toHaveLength(3)

count = 4

r(anchor, temp)

expect(document.body.innerHTML).toBe('sample-1sample-2sample-3sample-4')
expect(r()).toHaveLength(4)
})

it('should handle empty', () => {
repeat([], (n) => n)(anchor, temp)
repeat([], (n) => n, () => 'no items')(anchor, temp)

expect(document.body.innerHTML).toBe('no items')
expect(repeat([], (n) => n, () => 'no items')()).toEqual('no items')
})

it('should handle array with unique primitives', () => {
const list = Array.from({ length: 3 }, (_, i) => i + 1)

repeat(list, (n: number) => n + 1)(anchor, temp)

expect(document.body.innerHTML).toBe('234')

expect(repeat(list, (n: number) => n + 1)()).toEqual([2, 3, 4])
})

it('should handle array with unique non-primitives', () => {
const list = Array.from({ length: 3 }, (_, i) => i + 1)

const r = repeat(
() => list,
(n: number) => html`sample-${n}`
)

r(anchor, temp)

expect(document.body.innerHTML).toBe('sample-1sample-2sample-3')

expect(r()).toHaveLength(3)

list.push(4)

r(anchor, temp)

expect(document.body.innerHTML).toBe('sample-1sample-2sample-3sample-4')
expect(r()).toHaveLength(4)
})

it('should handle array with repeated values', () => {
Expand All @@ -75,15 +55,28 @@ describe('repeat', () => {
() => list,
(n) => html`sample-${n}`
)

r(anchor, temp)

expect(document.body.innerHTML).toBe('sample-1')
expect(r()).toHaveLength(1)

list.push(2)

r(anchor, temp)

expect(document.body.innerHTML).toBe('sample-1sample-2')
expect(r()).toHaveLength(2)
})

it('should handle iterables', () => {
const iterable = {};

// @ts-ignore
iterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};

expect(repeat(new Set([1, 2, 3]), (n) => n)()).toEqual([1, 2, 3])
expect(repeat(new Map([['a', 'b']]), (n) => n)()).toEqual([['a', 'b']])
expect(repeat({sample: 12}, (n) => n)()).toEqual([['sample', 12]])
expect(repeat('sample', (n) => n)()).toEqual(['s', 'a', 'm', 'p', 'l', 'e'])
expect(repeat(iterable, (n) => n)()).toEqual([1, 2, 3])
})
})
93 changes: 27 additions & 66 deletions src/helpers/repeat.helper.ts
Original file line number Diff line number Diff line change
@@ -1,94 +1,55 @@
import { val } from './val'
import { DoubleLinkedList } from '../DoubleLinkedList'
import { HtmlTemplate } from '../html'
import { syncNodes } from '../utils/sync-nodes'
import { getNodeOrTemplate } from '../utils/get-node-or-template'
import { ObjectLiteral, StateGetter } from '../types'

type DataGetter<T> = () => number | Array<T>
type repeatData<T> = number | ObjectLiteral<T> | Iterable<T>

const getList = (data: unknown) => {
data = val(data)
if (data) {
if (typeof data === 'number') {
return Array.from({ length: data }, (_, i) => i + 1)
}

if (Array.isArray(data)) {
return data
}
if (
typeof (data as Iterable<unknown>)[Symbol.iterator] === 'function'
) {
return Array.from(data as Iterable<unknown>)
}

if (typeof data === 'number') {
return Array.from({ length: data }, (_, i) => i + 1)
if (data instanceof Object) {
return Object.entries(data)
}
}

return []
}

/**
* renders things repeatedly based on first argument list or number
* renders things repeatedly based on first argument iterable or number
* @param data
* @param cb
* @param whenEmpty
*/
export const repeat = <T>(
data: number | Array<T> | DataGetter<T>,
data: repeatData<T> | StateGetter<repeatData<T>>,
cb: (data: T, index: number) => unknown,
whenEmpty?: () => unknown
) => {
const cache: Map<T, Node | HtmlTemplate> = new Map()
let currentRenderedNodes = new DoubleLinkedList<Node | HtmlTemplate>()
let prevList: T[] = []

const each = (d: T, i: number) => {
if (!cache.has(d)) {
cache.set(d, getNodeOrTemplate(cb(d, i)))
}

return cache.get(d) as Node | HtmlTemplate
}
let map = new Map()

return (anchor: Node, temp: HtmlTemplate) => {
const list = getList(data) as T[]
return () => {
const list = getList(val(data)) as T[]

if (list.length === 0) {
const res = whenEmpty?.() ?? []
map = new Map()

currentRenderedNodes = syncNodes(
currentRenderedNodes,
Array.isArray(res) ? res : [res],
anchor,
temp
)

prevList = []
cache.clear()
} else {
const prevListSet = DoubleLinkedList.fromArray(prevList)

currentRenderedNodes = syncNodes(
currentRenderedNodes,
new Proxy(list, {
get(_, prop) {
if (typeof prop === 'string') {
const idx = Number(prop)

if (!isNaN(idx)) {
const item = list[idx]
prevListSet.remove(item)
return each(item, idx)
}
}

return Reflect.get(_, prop)
},
}) as Array<Node | HtmlTemplate>,
anchor,
temp
)

for (const d of prevListSet) {
cache.delete(d)
}

prevList = list
return whenEmpty?.() ?? []
}

return currentRenderedNodes
map = list.reduce((acc, item, idx) => {
acc.set(item, map.get(item) ?? cb(item, idx))
return acc
}, new Map())

return Array.from(map.values())
}
}
2 changes: 1 addition & 1 deletion src/html.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1098,7 +1098,7 @@ describe('html', () => {

items.render(document.body)

expect(document.body.innerHTML).toBe('item 1item 5item 3')
expect(document.body.innerHTML).toBe('item 1item 3item 5')
})

it('with array containing repeated values as html instance', () => {
Expand Down

0 comments on commit 4b2abde

Please sign in to comment.