├── .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 | 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 |
46 | View 1 47 | View 2 48 |
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 | 23 | 24 | 25 | 26 | 27 | 28 | 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 | 23 | 24 | 25 | 26 | 27 | 28 | 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 | --------------------------------------------------------------------------------