├── .gitignore
├── CHANGELOG.md
├── .travis.yml
├── index.js
├── example
├── cache-size.js
├── hella-buttons
│ ├── button.js
│ ├── index.js
│ └── button-zone.js
├── homogeneous
│ ├── tweet-list.js
│ └── index.js
├── shared
│ └── index.js
└── heterogeneous
│ ├── mixed-list.js
│ └── index.js
├── package.json
├── test.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .vscode/launch.json
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # nanocomponent-cache Change Log
2 | All notable changes to this project will be documented in this file.
3 | This project adheres to [Semantic Versioning](http://semver.org/).
4 |
5 | ## 1.1.0 - 2017-01-13
6 | * Update class-cache to add LRU gc
7 | * Add more examples
8 |
9 | ## 1.0.0 - 2017-01-13
10 | * Init
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: trusty
2 | language: node_js
3 | node_js:
4 | - 'node'
5 | sudo: false
6 | addons:
7 | apt:
8 | packages:
9 | - xvfb
10 | cache:
11 | directories:
12 | - ~/.npm
13 | install:
14 | - export DISPLAY=':99.0'
15 | - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
16 | - npm i
17 | script:
18 | - npm test
19 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const ClassCache = require('class-cache')
2 |
3 | class NanocomponentCache extends ClassCache {
4 | constructor (opts = {}) {
5 | // A sane default GC function for nanocomponents
6 | const { gc = nanocomponent => !nanocomponent.element } = opts
7 | super({ gc })
8 | }
9 | }
10 |
11 | module.exports = NanocomponentCache
12 |
--------------------------------------------------------------------------------
/example/cache-size.js:
--------------------------------------------------------------------------------
1 | const Nanocomponent = require('nanocomponent')
2 | const html = require('bel')
3 |
4 | class CacheSize extends Nanocomponent {
5 | createElement (size) {
6 | this.size = size || 0
7 | return html`
8 |
Cache size: ${this.size}
9 | `
10 | }
11 |
12 | update (size) {
13 | if (this.size !== size) return true
14 | }
15 | }
16 |
17 | module.exports = CacheSize
18 |
--------------------------------------------------------------------------------
/example/hella-buttons/button.js:
--------------------------------------------------------------------------------
1 | const html = require('bel')
2 | const Nanocomponent = require('nanocomponent')
3 | const compare = require('nanocomponent/compare')
4 |
5 | let buttonCounter = 0
6 |
7 | class Button extends Nanocomponent {
8 | constructor (name) {
9 | super()
10 | this.name = name || 'button-' + buttonCounter++
11 | }
12 |
13 | createElement (props, children) {
14 | var { className, disabled, onclick } = props
15 |
16 | this.args = [className, disabled, onclick, children]
17 |
18 | return html`
19 |
23 | ${children || this.name}
24 |
25 | `
26 | }
27 |
28 | update (props, children) {
29 | var { className, disabled, onclick } = props
30 | return compare(this.args, [className, disabled, onclick, children])
31 | }
32 | }
33 |
34 | module.exports = Button
35 |
--------------------------------------------------------------------------------
/example/hella-buttons/index.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html')
2 | const choo = require('choo')
3 | const ButtonZone = require('./button-zone')
4 |
5 | const app = choo()
6 | app.use(clickerStore)
7 | app.route('/', mainView)
8 | if (typeof window !== 'undefined') {
9 | const container = document.createElement('div')
10 | container.id = 'container'
11 | document.body.appendChild(container)
12 | app.mount('#container')
13 | }
14 |
15 | const buttonZone = new ButtonZone()
16 |
17 | function mainView (state, emit) {
18 | return html`
19 |
20 |
21 |
Lots and lots of buttons!
22 | ${buttonZone.render(state, emit)}
23 |
24 |
`
25 | }
26 |
27 | function clickerStore (state, emitter) {
28 | state.buttons = {}
29 | emitter.on('click', function (name) {
30 | console.log('clicked ' + name)
31 | if (!isNaN(state.buttons[name])) state.buttons[name]++
32 | else state.buttons[name] = 1
33 | console.log(state.buttons)
34 | emitter.emit('render')
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/example/homogeneous/tweet-list.js:
--------------------------------------------------------------------------------
1 | const Tweet = require('twitter-component')
2 | const Nanocomponent = require('nanocomponent')
3 | const assert = require('assert')
4 | const html = require('bel')
5 | const compare = require('nanocomponent/compare')
6 | const NanocomponentCache = require('../../')
7 | const isArray = Array.isArray
8 | const CacheSize = require('../cache-size')
9 |
10 | class TweetList extends Nanocomponent {
11 | constructor () {
12 | super()
13 | window.list = this
14 | this.tweetList = null
15 | this.size = new CacheSize()
16 | this.nc = new NanocomponentCache()
17 | this.nc.register(Tweet, [{ placeholder: false }])
18 | }
19 |
20 | createElement (tweetList) {
21 | assert(isArray(tweetList), 'tweetList must be an array of tweet URLs')
22 | this.tweetList = tweetList.slice() // Have to slice since tweetList is an array
23 | const nc = this.nc
24 | const tweets = tweetList.map(tweetURL => nc.get(tweetURL).render(tweetURL))
25 | return html`
26 |
27 | ${this.size.render(Object.keys(this.nc._cache).length)}
28 | ${tweets}
29 |
30 | `
31 | }
32 |
33 | update (tweetList) {
34 | assert(isArray(tweetList), 'tweetList must be an array of tweet URLs')
35 | return compare(this.tweetList, tweetList)
36 | }
37 |
38 | afterupdate (el) {
39 | this.nc.gc()
40 | this.size.render(Object.keys(this.nc._cache).length)
41 | }
42 | }
43 |
44 | module.exports = TweetList
45 |
--------------------------------------------------------------------------------
/example/shared/index.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html')
2 | const choo = require('choo')
3 | const NanocomponentCache = require('../../')
4 | const Button = require('../hella-buttons/button')
5 |
6 | const app = choo()
7 | app.use(clickerStore)
8 | app.route('/', view1)
9 | app.route('/view2', view2)
10 | if (typeof window !== 'undefined') {
11 | const container = document.createElement('div')
12 | container.id = 'container'
13 | document.body.appendChild(container)
14 | app.mount('#container')
15 | }
16 |
17 | function view1 (state, emit) {
18 | const c = state.cache
19 | return html`
20 |
21 |
22 |
Shared cache: view 1
23 | ${nav()}
24 | ${c.get('shared').render({}, 'lol')}
25 | ${c.get('view1-thing').render({}, 'hey')}
26 |
27 |
`
28 | }
29 |
30 | function view2 (state, emit) {
31 | const c = state.cache
32 | return html`
33 |
34 |
35 |
Shared cache: view 2
36 | ${nav()}
37 | ${c.get('shared').render({}, 'lol')}
38 | ${c.get('view2-thing').render({}, 'bye')}
39 |
40 |
`
41 | }
42 |
43 | function nav () {
44 | return html`
45 |
49 | `
50 | }
51 |
52 | function clickerStore (state, emitter) {
53 | state.cache = new NanocomponentCache()
54 | state.cache.register(Button)
55 | }
56 |
--------------------------------------------------------------------------------
/example/hella-buttons/button-zone.js:
--------------------------------------------------------------------------------
1 | const Nanocomponent = require('nanocomponent')
2 | const NanocomponentCache = require('../../')
3 | const Button = require('./button')
4 | const html = require('bel')
5 |
6 | class ButtonZone extends Nanocomponent {
7 | constructor () {
8 | super()
9 | this.handleClick = this.handleClick.bind(this)
10 | this.c = new NanocomponentCache()
11 | this.c.register(Button)
12 | }
13 |
14 | createElement (state, emit) {
15 | this.emit = emit
16 | const c = this.c
17 | const props = { onclick: this.handleClick }
18 |
19 | return html`
20 |
21 | ${c.get('button-0').render(props)}
22 | ${c.get('button-1').render(props, 'Button foo')}
23 | ${c.get('button-null').render({disabled: true})}
24 | ${c.get('button-2').render(props)}
25 | ${c.get('button-3').render(props)}
26 | ${c.get('button-4').render(props)}
27 | ${c.get('button-5', {args: ['button-whatever']}).render(props, 'Button 6')}
28 | ${c.get('button-6').render(props)}
29 | ${c.get('button-7').render(props)}
30 | ${c.get('button-8').render(props)}
31 | ${c.get('button-9').render(props)}
32 | ${c.get('button-10').render(props)}
33 | ${c.get('button-11').render(props)}
34 | ${c.get('button-12').render(props)}
35 | ${c.get('button-13').render(props)}
36 | ${c.get('button-14').render(props)}
37 | ${c.get('button-15').render(props)}
38 | ${c.get('button-16').render(props)}
39 | ${c.get('button-17').render(props)}
40 | ${c.get('button-18').render(props)}
41 | ${c.get('button-19').render(props)}
42 | ${c.get('button-20').render(props)}
43 | ${c.get('button-21').render(props)}
44 | ${c.get('button-22').render(props)}
45 | ${c.get('button-23').render(props)}
46 |
47 | `
48 | }
49 |
50 | update (state, emit) {
51 | return true
52 | }
53 |
54 | handleClick (ev) {
55 | this.emit('click', ev.currentTarget.dataset.name)
56 | }
57 | }
58 |
59 | module.exports = ButtonZone
60 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nanocomponent-cache",
3 | "description": "Create or cache nanocomponent instances by key",
4 | "version": "1.1.0",
5 | "author": "Bret Comnes",
6 | "bugs": {
7 | "url": "https://github.com/bcomnes/nanocomponent-cache/issues"
8 | },
9 | "dependencies": {
10 | "class-cache": "^1.1.0"
11 | },
12 | "devDependencies": {
13 | "@tap-format/spec": "^0.2.0",
14 | "bankai": "^9.1.0",
15 | "bel": "^5.1.5",
16 | "budo": "^11.1.0",
17 | "choo": "^6.6.1",
18 | "dependency-check": "^3.0.0",
19 | "existy": "^1.0.1",
20 | "fy-shuffle": "^1.0.0",
21 | "lodash.isequal": "^4.5.0",
22 | "nanocomponent": "^6.5.0",
23 | "npm-run-all": "^4.0.2",
24 | "standard": "^10.0.0",
25 | "tape": "^4.7.0",
26 | "tape-run": "^3.0.0",
27 | "twitter-component": "^1.0.2",
28 | "youtube-component": "^1.1.1"
29 | },
30 | "homepage": "https://github.com/bcomnes/nanocomponent-cache#readme",
31 | "keywords": [
32 | "cache",
33 | "choo",
34 | "components",
35 | "nanocomponent"
36 | ],
37 | "license": "MIT",
38 | "main": "index.js",
39 | "repository": {
40 | "type": "git",
41 | "url": "git+https://github.com/bcomnes/nanocomponent-cache.git"
42 | },
43 | "scripts": {
44 | "build": "run-p build:*",
45 | "build:heterogeneous": "bankai build example/heterogeneous/index.js",
46 | "build:buttons": "bankai build example/hella-buttons/index.js",
47 | "build:homogeneous": "bankai build example/homogeneous/index.js",
48 | "build:shared": "bankai build example/shared/index.js",
49 | "start": "run-s start:heterogeneous",
50 | "start:heterogeneous": "budo example/heterogeneous/index.js --live --open",
51 | "start:buttons": "budo example/hella-buttons/index.js --live --open",
52 | "start:homogenous": "budo example/homogeneous/index.js --live --open",
53 | "start:shared": "budo example/shared/index.js --live --open",
54 | "start:test": "budo test.js --live --open",
55 | "test": "run-s test:*",
56 | "test:browser": "browserify test.js | tape-run | tap-format-spec",
57 | "test:deps": "dependency-check .",
58 | "test:lint": "standard"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/example/heterogeneous/mixed-list.js:
--------------------------------------------------------------------------------
1 | const Tweet = require('twitter-component')
2 | const YoutubeComponent = require('youtube-component')
3 | const Nanocomponent = require('nanocomponent')
4 | const assert = require('assert')
5 | const html = require('bel')
6 | const compare = require('nanocomponent/compare')
7 | const NanocomponentCache = require('../../')
8 | const isArray = Array.isArray
9 | const CacheSize = require('../cache-size')
10 |
11 | const youtubeOpts = {
12 | attr: {
13 | width: 480,
14 | height: 270
15 | }
16 | }
17 |
18 | class MixedList extends Nanocomponent {
19 | constructor () {
20 | super()
21 |
22 | this.urls = null
23 | this.nc = new NanocomponentCache()
24 | this.size = new CacheSize()
25 | this.nc.register({
26 | 'youtube-component': {
27 | class: YoutubeComponent,
28 | args: [youtubeOpts]
29 | },
30 | 'twitter-component': Tweet
31 | })
32 |
33 | this.componentMap = this.componentMap.bind(this)
34 | }
35 |
36 | componentMap (url, i, list) {
37 | switch (true) {
38 | case /^https?:\/\/(www\.)?youtu\.be/i.test(url):
39 | case /^https?:\/\/(www\.)?youtube\.com/i.test(url):
40 | case /^https?:\/\/(www\.)?vimeo\.com/i.test(url):
41 | case /^https?:\/\/(www\.)?dailymotion\.com/i.test(url): {
42 | return this.nc.get(url, 'youtube-component').render(url)
43 | }
44 | case /^https?:\/\/(www\.)?twitter.com\/.*\/status\/\d*$/i.test(url): {
45 | return this.nc.get(url, 'twitter-component').render(url)
46 | }
47 | default: {
48 | return html`Unknown URL type: ${url}
`
49 | }
50 | }
51 | }
52 |
53 | createElement (urls) {
54 | assert(isArray(urls), 'MixedList: urls must be an array of tweet URLs')
55 | this.urls = urls.slice() // Have to slice since urls is an array
56 | const stream = urls.map(this.componentMap) // make sure the cache is warm before we inspect it in this example
57 | return html`
58 |
59 | ${this.size.render(Object.keys(this.nc._cache).length)}
60 | ${stream}
61 |
62 | `
63 | }
64 |
65 | update (urls) {
66 | assert(isArray(urls), 'tweetList must be an array of tweet URLs')
67 | return compare(this.urls, urls)
68 | }
69 |
70 | afterupdate (el) {
71 | this.nc.gc()
72 | this.size.render(Object.keys(this.nc._cache).length)
73 | }
74 | }
75 |
76 | module.exports = MixedList
77 |
--------------------------------------------------------------------------------
/example/homogeneous/index.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html')
2 | const choo = require('choo')
3 | const TweetList = require('./tweet-list')
4 | const shuffleArray = require('fy-shuffle')
5 |
6 | const app = choo()
7 | app.use(tweetStore)
8 | app.route('/', mainView)
9 | if (typeof window !== 'undefined') {
10 | const container = document.createElement('div')
11 | container.id = 'container'
12 | document.body.appendChild(container)
13 | app.mount('#container')
14 | }
15 |
16 | const list = new TweetList()
17 |
18 | function mainView (state, emit) {
19 | return html`
20 |
21 |
22 | emit('shuffle-tweets')}>shuffle tweets
23 | emit('reverse-tweets')}>reverse tweets
24 | emit('add-tweet')}>add tweet
25 | emit('append-tweet')}>append tweet
26 | emit('pop-tweet')}>pop tweet
27 | emit('shift-tweet')}>shift tweet
28 | emit('re-render')}>plain render
29 |
30 |
31 |
Embed some tweets in Choo
32 | ${list.render(state.tweets)}
33 |
34 |
`
35 | }
36 |
37 | const moreTweets = [
38 | 'https://twitter.com/uhhyeahbret/status/898315707254841344',
39 | 'https://twitter.com/uhhyeahbret/status/898214560267608064',
40 | 'https://twitter.com/uhhyeahbret/status/898196092189253632'
41 | ]
42 |
43 | function tweetStore (state, emitter) {
44 | state.tweets = [
45 | 'https://twitter.com/uhhyeahbret/status/897603426518876161',
46 | 'https://twitter.com/yoshuawuyts/status/895338700531535878'
47 | ]
48 |
49 | emitter.on('DOMContentLoaded', function () {
50 | emitter.on('shuffle-tweets', function () {
51 | state.tweets = shuffleArray(state.tweets)
52 | emitter.emit('render')
53 | })
54 | emitter.on('reverse-tweets', function () {
55 | state.tweets = state.tweets.reverse()
56 | emitter.emit('render')
57 | })
58 | emitter.on('add-tweet', function () {
59 | const a = moreTweets.pop()
60 | if (a) {
61 | state.tweets.unshift(a)
62 | emitter.emit('render')
63 | }
64 | })
65 | emitter.on('append-tweet', function () {
66 | const a = moreTweets.pop()
67 | if (a) {
68 | state.tweets.push(a)
69 | emitter.emit('render')
70 | }
71 | })
72 | emitter.on('pop-tweet', function () {
73 | const a = state.tweets.pop()
74 | if (a) {
75 | moreTweets.push(a)
76 | emitter.emit('render')
77 | }
78 | })
79 | emitter.on('shift-tweet', function () {
80 | const a = state.tweets.shift()
81 | console.log(a)
82 | if (a) {
83 | moreTweets.push(a)
84 | emitter.emit('render')
85 | }
86 | })
87 | emitter.on('re-render', function () {
88 | emitter.emit('render')
89 | })
90 | })
91 | }
92 |
--------------------------------------------------------------------------------
/example/heterogeneous/index.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html')
2 | const choo = require('choo')
3 | const MixedList = require('./mixed-list')
4 | const shuffleArray = require('fy-shuffle')
5 |
6 | const app = choo()
7 | app.use(tweetStore)
8 | app.route('/', mainView)
9 | if (typeof window !== 'undefined') {
10 | const container = document.createElement('div')
11 | container.id = 'container'
12 | document.body.appendChild(container)
13 | app.mount('#container')
14 | }
15 |
16 | const list = new MixedList()
17 |
18 | function mainView (state, emit) {
19 | return html`
20 |
21 |
22 | emit('shuffle-urls')}>shuffle urls
23 | emit('reverse-urls')}>reverse urls
24 | emit('add-url')}>add url
25 | emit('append-url')}>append url
26 | emit('pop-url')}>pop url
27 | emit('shift-url')}>shift url
28 | emit('re-render')}>plain render
29 | Cache Size: ${Object.keys(list.nc._cache).length}
30 |
31 |
32 |
Embed some stuff with Choo
33 | ${list.render(state.tweets)}
34 |
35 |
`
36 | }
37 |
38 | const moreTweets = [
39 | 'https://twitter.com/uhhyeahbret/status/898315707254841344',
40 | 'https://www.youtube.com/watch?v=b8HO6hba9ZE',
41 | 'https://twitter.com/uhhyeahbret/status/898214560267608064',
42 | 'https://vimeo.com/229754542',
43 | 'https://twitter.com/uhhyeahbret/status/898196092189253632',
44 | 'https://www.youtube.com/watch?v=bYpKrA233vY'
45 | ]
46 |
47 | function tweetStore (state, emitter) {
48 | state.tweets = [
49 | 'https://www.youtube.com/watch?v=wGCoAFZiYMw',
50 | 'https://twitter.com/uhhyeahbret/status/897603426518876161',
51 | 'https://twitter.com/yoshuawuyts/status/895338700531535878'
52 | ]
53 |
54 | emitter.on('DOMContentLoaded', function () {
55 | emitter.on('shuffle-urls', function () {
56 | state.tweets = shuffleArray(state.tweets)
57 | emitter.emit('render')
58 | })
59 | emitter.on('reverse-urls', function () {
60 | state.tweets = state.tweets.reverse()
61 | emitter.emit('render')
62 | })
63 | emitter.on('add-url', function () {
64 | const a = moreTweets.pop()
65 | if (a) {
66 | state.tweets.unshift(a)
67 | emitter.emit('render')
68 | }
69 | })
70 | emitter.on('append-url', function () {
71 | const a = moreTweets.pop()
72 | if (a) {
73 | state.tweets.push(a)
74 | emitter.emit('render')
75 | }
76 | })
77 | emitter.on('pop-url', function () {
78 | const a = state.tweets.pop()
79 | if (a) {
80 | moreTweets.push(a)
81 | emitter.emit('render')
82 | }
83 | })
84 | emitter.on('shift-url', function () {
85 | const a = state.tweets.shift()
86 | console.log(a)
87 | if (a) {
88 | moreTweets.push(a)
89 | emitter.emit('render')
90 | }
91 | })
92 | emitter.on('re-render', function () {
93 | emitter.emit('render')
94 | })
95 | })
96 | }
97 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | var test = require('tape')
2 | var YoutubeComponent = require('youtube-component')
3 | var TwitterComponent = require('twitter-component')
4 | var NanocomponentCache = require('./')
5 |
6 | function makeID () {
7 | return 'testid-' + Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1)
8 | }
9 |
10 | function createTestElement () {
11 | var testRoot = document.createElement('div')
12 | testRoot.id = makeID()
13 | document.body.appendChild(testRoot)
14 | return testRoot
15 | }
16 |
17 | test('simple mapper', function (t) {
18 | var testRoot = createTestElement()
19 | var c = new NanocomponentCache()
20 | c.register(YoutubeComponent)
21 | var videos = [
22 | 'https://www.youtube.com/watch?v=b8HO6hba9ZE',
23 | 'https://vimeo.com/229754542',
24 | 'https://www.youtube.com/watch?v=bYpKrA233vY'
25 | ]
26 |
27 | function renderAndMount (testEl, data, c) {
28 | var els = data.map(val => c.get('val').render(val))
29 | els.forEach(function (el) {
30 | testEl.appendChild(el)
31 | })
32 | }
33 |
34 | t.doesNotThrow(renderAndMount.bind(null, testRoot, videos, c), 'Able to render list of videos')
35 | t.true(testRoot.children[0].children[0].src.includes('youtube.com/embed/b8HO6hba9ZE'), 'videos are in page')
36 | t.end()
37 | })
38 |
39 | test('mixed mapper', function (t) {
40 | var testRoot = createTestElement()
41 | var c = new NanocomponentCache()
42 | c.register('video', YoutubeComponent)
43 | c.register('tweet', TwitterComponent)
44 | c.register('default', TwitterComponent)
45 |
46 | var videos = [
47 | 'https://www.youtube.com/watch?v=b8HO6hba9ZE',
48 | 'https://vimeo.com/229754542',
49 | 'https://www.youtube.com/watch?v=bYpKrA233vY'
50 | ].map(function (url) {
51 | return { url: url, type: 'video' }
52 | })
53 |
54 | var tweets = [
55 | 'https://twitter.com/uhhyeahbret/status/897603426518876161',
56 | 'https://twitter.com/yoshuawuyts/status/895338700531535878'
57 | ].map(function (url) {
58 | return { url: url, type: 'tweet' }
59 | })
60 |
61 | delete tweets[0].type // test the default type
62 |
63 | var data = videos.concat(tweets)
64 |
65 | function renderAndMount (testEl, data, c) {
66 | var els = data.map(({url, type}) => c.get('url', type).render(url))
67 | els.forEach(function (el) {
68 | testEl.appendChild(el)
69 | })
70 | }
71 |
72 | t.doesNotThrow(renderAndMount.bind(null, testRoot, data, c), 'Able to render list of videos')
73 | t.true(testRoot.children[0].children[0].src.includes('youtube.com/embed/b8HO6hba9ZE'), 'videos are in page')
74 | t.end()
75 | })
76 |
77 | test('gc function', function (t) {
78 | var testRoot = createTestElement()
79 | var c = new NanocomponentCache()
80 | c.register(YoutubeComponent)
81 | var videos = [
82 | 'https://www.youtube.com/watch?v=b8HO6hba9ZE',
83 | 'https://vimeo.com/229754542',
84 | 'https://www.youtube.com/watch?v=bYpKrA233vY'
85 | ]
86 |
87 | videos.map(url => c.get(url).render(url)).forEach(node => {
88 | testRoot.appendChild(node)
89 | })
90 |
91 | videos.map(url => c.get(url).render(url)).forEach((node, i) => {
92 | t.ok(node.isSameNode(testRoot.childNodes[i]), 'proxy nodes are generated and the same')
93 | })
94 |
95 | t.equal(Object.keys(c._cache).length, 3, 'cache has 3 nodes in it')
96 |
97 | testRoot.removeChild(testRoot.childNodes[0])
98 | c.gc()
99 |
100 | t.equal(Object.keys(c._cache).length, 2, 'gc purges unmounted components')
101 | t.end()
102 | })
103 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NanocomponentCache [![stability][0]][1]
2 | [![npm version][2]][3] [![build status][4]][5]
3 | [![downloads][8]][9] [![js-standard-style][10]][11]
4 |
5 | Cache a nanocomponent instance by key. Creates a new instance if the key doesn't exist, otherwise returns the cached instance. A subclass of [class-cache][cc] providing sane GC function defaults and a set of examples of intended usage. Optional LRU caching.
6 |
7 | ## Usage
8 |
9 | ```js
10 | const NanocomponentCache = require('nanocomponent-cache')
11 | const compare = require('nanocomponent/compare')
12 | const Nanocomponent = require('nanocomponent')
13 | const Tweet = require('twitter-component')
14 | const assert = require('assert')
15 | const html = require('bel')
16 |
17 | class TweetList extends Nanocomponent {
18 | constructor () {
19 | super()
20 | this.tweetList = null
21 | // auto-eject last used instances over 100 total cached
22 | this.nc = new NanocomponentCache({ lru: 100 })
23 | // Register the components you will be caching
24 | this.nc.register(Tweet, {args: [{ placeholder: false }]} )
25 | }
26 |
27 | createElement (tweetList) {
28 | assert(isArray(tweetList), 'tweetList must be an array of tweet URLs')
29 | this.tweetList = tweetList // Cache a reference to tweetList
30 | const nc = this.nc
31 | return html`
32 |
33 | ${tweetList.map(tweetURL => nc.get(tweetURL).render(tweetURL))}
34 |
35 | `
36 | }
37 |
38 | update (tweetList) {
39 | assert(isArray(tweetList), 'tweetList must be an array of tweet URLs')
40 | return compare(this.tweetList, tweetList)
41 | }
42 |
43 | afterupdate (el) {
44 | // Periodically run the GC function to clean up unused instances.
45 | this.nc.gc()
46 | }
47 | }
48 |
49 | module.exports = TweetList
50 | ```
51 |
52 | ## Examples
53 |
54 | - [homogeneous](example/homogeneous/) ([🌎](https://nanocomponent-cache-homogeneous.netlify.com))
55 | - [heterogeneous](example/heterogeneous/) ([🌎](https://nanocomponent-cache-heterogeneous.netlify.com))
56 | - [hella-buttons](example/hella-buttons/) ([🌎](https://nanocomponent-cache-buttons.netlify.com))
57 | - [shared](example/shared/) ([🌎](https://nanocomponent-cache-shared.netlify.com))
58 |
59 | ## Installation
60 | ```sh
61 | $ npm install nanocomponent-cache
62 | ```
63 | ## API
64 | ### `NanocomponentCache = require('nanocomponent-cache')`
65 | Require `NanocomponentCache` class.
66 |
67 | ### `c = new NanocomponentCache([opts])`
68 | Create a new cache instance.
69 |
70 | `opts` include:
71 |
72 | ```js
73 | {
74 | gc: (component) => !component.element // a default garbage collection function
75 | args: [] // Default args used for instantiating all classes,
76 | lru: 0 // Enable LRU gc by setting this to an integer greater than 0
77 | }
78 | ```
79 |
80 | ### `c.register([typeKey = 'default'], SomeNanocomponent, [opts])`
81 |
82 | Define a `Class` for the optional `typeKey`. The default `typeKey` is `default`, which is used whenever a `typeKey` is omitted during `get`s and `set`s. `opts` include:
83 |
84 | ```js
85 | {
86 | gc: undefined // a typeKey specific GC function.
87 | args: undefined // default arguments instance arguments for `typeKey`.
88 | // These options delegate to the top level options if left un-implemented
89 | }
90 | ```
91 |
92 | This is a shortcut for defining with a typeObject:
93 |
94 | ```js
95 | c.register({
96 | typeKey: { class: SomeNanocomponent, ...opts }
97 | })
98 | ```
99 |
100 | ### `c.register({ typeObject })`
101 |
102 | Define class 'type's using a `typeObject` definition. A typeObject is an object who's keys define the type name which are associated with a `Class` and optionally `args` and a type specific `gc` function.
103 |
104 | ```js
105 | c.register({
106 | default: SomeNanocomponent, // SomeNanocomponent with no args or gc. Uses instance gc function.
107 | baz: { class: SomeNanocomponent, ...opts }
108 | })
109 | ```
110 |
111 | Types are `Object.assign`ed over previously registered types. The `opts` keys are the same as above.
112 |
113 | ### `c.unregister(...types)`
114 |
115 | Pass typeKeys as arguments to un-register them. Instances are untouched during this process.
116 |
117 | ### `c.get(key, [Class || typeKey], [opts])`
118 |
119 | The primary method used to retrieve and create instances. Return instance of `Class` or defined `type` class at `key`. If an instance does not yet exist at `key`, it will be instantiated with `args` along with a `key` specific `gc` function. If `type` is not defined, this method will throw.
120 |
121 | Omitting optional method arguments delegates to the next most specific option.
122 |
123 | ```js
124 | c.get('some-key') // Return or create the 'default' Class
125 | c.get('some-key', {args: ['arg0', 'arg2']})
126 | c.get('some-key', null, {args: ['arg0', 'arg2']}) // Return the default registered class with specific args
127 | c.get('some-key', 'some-type', { args: ['arg0', 'arg2'] }) // Return the `some-type` class at `some-key`.
128 | c.get('some-key', SomeOtherNanocomponent, { args: ['arg0', 'arg2'], gc: instance => true })
129 | ```
130 |
131 | If `key` is already instantiated, `args` is ignored. Pass changing properties as subsequent calls to the returned instance. If `type` or `Class` changes, the `key` instance is re-instantiated.
132 |
133 | ### `c.set(key, [Class || type], [opts])`
134 |
135 | Force instantiate the class instance at `key`. Follows the same override behavior as `get`. If you must change `args` on a key, this is the safest way to do that.
136 |
137 | Returns the newly created instance.
138 |
139 | ### `c.gc()`
140 |
141 | Run the various `gc` functions defined. For each key, only the most specific `gc` function set is run. Return `true` from the `gc` functions to garbage collect that instance, and `false` to preserve.
142 |
143 | This is used to clean out instances you no longer need. Because this iterates over all keys with instances, run this often enough so that the key set doesn't grow too large but not too often to create unnecessary delays in render loops.
144 |
145 | ### `c.clear()`
146 |
147 | Clear all `key` instances. The `gc` functions for each instance will be run receiving the following signature: `(instance, key, true) => {}`. If your instance needs to let go of resources, watch for the second argument to equal true, indicating tht the instance will be deleted.
148 |
149 | ### `c.delete(key)`
150 |
151 | Delete specific `key` instance. Will run the `gc` function passing `true` as the second argument (`(instance, key, true) => {}`).
152 |
153 | ### `c.has(key)`
154 |
155 | Return true if `key` exists.
156 |
157 | See examples for more details.
158 |
159 | ## Examples
160 |
161 | See the `examples` folder for various ideas on how to use this library.
162 |
163 | ## See Also
164 |
165 | - [nanocomponent][nc]
166 | - [choo][choo]
167 | - [choo component thread](https://github.com/choojs/choo/issues/593#issuecomment-364555843)
168 | - [class-cache](https://github.com/bcomnes/class-cache)
169 |
170 | ## License
171 | [MIT](https://tldrlegal.com/license/mit-license)
172 |
173 | [0]: https://img.shields.io/badge/stability-experimental-orange.svg?style=flat-square
174 | [1]: https://nodejs.org/api/documentation.html#documentation_stability_index
175 | [2]: https://img.shields.io/npm/v/nanocomponent-cache.svg?style=flat-square
176 | [3]: https://npmjs.org/package/nanocomponent-cache
177 | [4]: https://img.shields.io/travis/bcomnes/nanocomponent-cache/master.svg?style=flat-square
178 | [5]: https://travis-ci.org/bcomnes/nanocomponent-cache
179 | [8]: http://img.shields.io/npm/dm/nanocomponent-cache.svg?style=flat-square
180 | [9]: https://npmjs.org/package/nanocomponent-cache
181 | [10]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square
182 | [11]: https://github.com/feross/standard
183 | [bel]: https://github.com/shama/bel
184 | [yoyoify]: https://github.com/shama/yo-yoify
185 | [md]: https://github.com/patrick-steele-idem/morphdom
186 | [210]: https://github.com/patrick-steele-idem/morphdom/pull/81
187 | [nm]: https://github.com/yoshuawuyts/nanomorph
188 | [ce]: https://github.com/yoshuawuyts/cache-element
189 | [class]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
190 | [isSameNode]: https://github.com/choojs/nanomorph#caching-dom-elements
191 | [onload]: https://github.com/shama/on-load
192 | [choo]: https://github.com/choojs/choo
193 | [nca]: https://github.com/choojs/nanocomponent-adapters
194 | [nc]: https://github.com/choojs/nanocomponent
195 | [cc]: https://github.com/bcomnes/class-cache
196 |
--------------------------------------------------------------------------------