├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── example
├── complex
│ ├── index.js
│ └── mixed-list.js
└── simple
│ ├── index.js
│ └── tweet-list.js
├── index.js
├── package.json
├── screenshot.png
└── test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # nanomap 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.1 - 2017-09-11
6 | * Update dep floor
7 |
8 | ## 1.1.0 - 2017-08-24
9 | * **Added**: Use [`nanoassert`](https://github.com/emilbayes/nanoassert) in the browser
10 |
11 | ## 1.0.0 - 2017-08-20
12 | * Initial release. Beta API basically.
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Bret Comnes
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # nanomap [![stability][0]][1]
2 | [![npm version][2]][3] [![build status][4]][5]
3 | [![downloads][8]][9] [![js-standard-style][10]][11]
4 |
5 | Functionally map data into stateful [nanocomponents][nc].
6 |
7 | 
8 |
9 | ## Usage
10 |
11 | ```js
12 | var Nanomap = require('nanocomponent/map')
13 | var YoutubeComponent = require('youtube-component')
14 | var TwitterComponent = require('twitter-component')
15 | var OEmbedComponent = require('oembed-component')
16 | var simpleMapper = new Nanomap(opts, TwitterComponent)
17 | // OR
18 | var complexMapper = new Nanomap(opts, {
19 | 'video': YoutubeComponent,
20 | 'tweet': TwitterComponent,
21 | ...,
22 | default: OEmbedComponent
23 | })
24 |
25 | [{
26 | id: 'foo123',
27 | opts: { color: 'blue' },
28 | arguments: {an: 'arg'} // Non-array types passed in as the first argument
29 | }].map(simpleMapper) // Array of rendered DOM nodes from homogeneous components
30 |
31 | [{
32 | id: 'foo123',
33 | type: 'tweet',
34 | arguments: ['tweet-url'] // component.render.apply(component, arguments)
35 | }].map(complexMapper) // Array of rendered DOM nodes from a heterogeneous set of components
36 | ```
37 |
38 | ## Installation
39 | ```sh
40 | $ npm install nanomap
41 | ```
42 | ## API
43 | ### `Nanomap = require('nanomap`)
44 | Import `Nanomap` component class.
45 |
46 | ### `mapper = new Nanomap([opts], Component)`
47 | ### `mapper = Nanomap([opts], { type: Component, [default: Component]})`
48 | Create a new mapper instance that will render data into component instances.
49 |
50 | `opts` include:
51 |
52 | ```js
53 | {
54 | gc: true // clean up unused instances when mapped over
55 | }
56 | ```
57 |
58 | See examples for more details.
59 |
60 | ## License
61 | [MIT](https://tldrlegal.com/license/mit-license)
62 |
63 | [0]: https://img.shields.io/badge/stability-experimental-orange.svg?style=flat-square
64 | [1]: https://nodejs.org/api/documentation.html#documentation_stability_index
65 | [2]: https://img.shields.io/npm/v/nanomap.svg?style=flat-square
66 | [3]: https://npmjs.org/package/nanomap
67 | [4]: https://img.shields.io/travis/bcomnes/nanomap/master.svg?style=flat-square
68 | [5]: https://travis-ci.org/bcomnes/nanomap
69 | [8]: http://img.shields.io/npm/dm/nanomap.svg?style=flat-square
70 | [9]: https://npmjs.org/package/nanomap
71 | [10]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square
72 | [11]: https://github.com/feross/standard
73 | [bel]: https://github.com/shama/bel
74 | [yoyoify]: https://github.com/shama/yo-yoify
75 | [md]: https://github.com/patrick-steele-idem/morphdom
76 | [210]: https://github.com/patrick-steele-idem/morphdom/pull/81
77 | [nm]: https://github.com/yoshuawuyts/nanomorph
78 | [ce]: https://github.com/yoshuawuyts/cache-element
79 | [class]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
80 | [isSameNode]: https://github.com/choojs/nanomorph#caching-dom-elements
81 | [onload]: https://github.com/shama/on-load
82 | [choo]: https://github.com/choojs/choo
83 | [nca]: https://github.com/choojs/nanocomponent-adapters
84 | [nc]: https://github.com/choojs/nanocomponent
85 |
--------------------------------------------------------------------------------
/example/complex/index.js:
--------------------------------------------------------------------------------
1 | var html = require('choo/html')
2 | var choo = require('choo')
3 | var MixedList = require('./mixed-list')
4 | var shuffleArray = require('fy-shuffle')
5 |
6 | var app = choo()
7 | app.use(tweetStore)
8 | app.route('/', mainView)
9 | if (typeof window !== 'undefined') {
10 | var container = document.createElement('div')
11 | container.id = 'container'
12 | document.body.appendChild(container)
13 | app.mount('#container')
14 | }
15 |
16 | var list = new MixedList()
17 |
18 | function mainView (state, emit) {
19 | return html`
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
Embed some stuff with Choo
32 | ${list.render(state.tweets)}
33 |
34 |
`
35 | }
36 |
37 | var moreTweets = [
38 | 'https://twitter.com/uhhyeahbret/status/898315707254841344',
39 | 'https://www.youtube.com/watch?v=b8HO6hba9ZE',
40 | 'https://twitter.com/uhhyeahbret/status/898214560267608064',
41 | 'https://vimeo.com/229754542',
42 | 'https://twitter.com/uhhyeahbret/status/898196092189253632',
43 | 'https://www.youtube.com/watch?v=bYpKrA233vY'
44 | ]
45 |
46 | function tweetStore (state, emitter) {
47 | state.tweets = [
48 | 'https://www.youtube.com/watch?v=wGCoAFZiYMw',
49 | 'https://twitter.com/uhhyeahbret/status/897603426518876161',
50 | 'https://twitter.com/yoshuawuyts/status/895338700531535878'
51 | ]
52 |
53 | emitter.on('DOMContentLoaded', function () {
54 | emitter.on('shuffle-urls', function () {
55 | state.tweets = shuffleArray(state.tweets)
56 | emitter.emit('render')
57 | })
58 | emitter.on('reverse-urls', function () {
59 | state.tweets = state.tweets.reverse()
60 | emitter.emit('render')
61 | })
62 | emitter.on('add-url', function () {
63 | var a = moreTweets.pop()
64 | if (a) {
65 | state.tweets.unshift(a)
66 | emitter.emit('render')
67 | }
68 | })
69 | emitter.on('append-url', function () {
70 | var a = moreTweets.pop()
71 | if (a) {
72 | state.tweets.push(a)
73 | emitter.emit('render')
74 | }
75 | })
76 | emitter.on('pop-url', function () {
77 | var a = state.tweets.pop()
78 | if (a) {
79 | moreTweets.push(a)
80 | emitter.emit('render')
81 | }
82 | })
83 | emitter.on('shift-url', function () {
84 | var a = state.tweets.shift()
85 | console.log(a)
86 | if (a) {
87 | moreTweets.push(a)
88 | emitter.emit('render')
89 | }
90 | })
91 | emitter.on('re-render', function () {
92 | emitter.emit('render')
93 | })
94 | })
95 | }
96 |
--------------------------------------------------------------------------------
/example/complex/mixed-list.js:
--------------------------------------------------------------------------------
1 | var Tweet = require('twitter-component')
2 | var YoutubeComponent = require('youtube-component')
3 | var Nanocomponent = require('nanocomponent')
4 | var assert = require('assert')
5 | var html = require('bel')
6 | var compare = require('nanocomponent/compare')
7 | var Nanomap = require('../../')
8 | var isArray = Array.isArray
9 |
10 | function shapeData (url, i, list) {
11 | switch (true) {
12 | case /^https?:\/\/(www\.)?youtu\.be/i.test(url):
13 | case /^https?:\/\/(www\.)?youtube\.com/i.test(url):
14 | case /^https?:\/\/(www\.)?vimeo\.com/i.test(url):
15 | case /^https?:\/\/(www\.)?dailymotion\.com/i.test(url): {
16 | return {
17 | id: url,
18 | arguments: url,
19 | type: 'youtube-component',
20 | opts: {
21 | attr: {
22 | width: 480,
23 | height: 270
24 | }
25 | }
26 | }
27 | }
28 | case /^https?:\/\/(www\.)?twitter.com\/.*\/status\/\d*$/i.test(url): {
29 | return {
30 | id: url,
31 | arguments: url,
32 | type: 'twitter-component'
33 | }
34 | }
35 | }
36 | }
37 |
38 | class MixedList extends Nanocomponent {
39 | constructor () {
40 | super()
41 |
42 | this.urls = null
43 | this.mapper = new Nanomap({
44 | 'youtube-component': YoutubeComponent,
45 | 'twitter-component': Tweet
46 | })
47 | }
48 |
49 | createElement (urls) {
50 | assert(isArray(urls), 'MixedList: urls must be an array of tweet URLs')
51 | this.urls = urls.slice() // Have to slice since urls is an array
52 | return html`
53 |
54 | ${urls.map(shapeData).map(this.mapper)}
55 |
56 | `
57 | }
58 | update (urls) {
59 | assert(isArray(urls), 'tweetList must be an array of tweet URLs')
60 | return compare(this.urls, urls)
61 | }
62 | }
63 |
64 | module.exports = MixedList
65 |
--------------------------------------------------------------------------------
/example/simple/index.js:
--------------------------------------------------------------------------------
1 | var html = require('choo/html')
2 | var choo = require('choo')
3 | var TweetList = require('./tweet-list')
4 | var shuffleArray = require('fy-shuffle')
5 |
6 | var app = choo()
7 | app.use(tweetStore)
8 | app.route('/', mainView)
9 | if (typeof window !== 'undefined') {
10 | var container = document.createElement('div')
11 | container.id = 'container'
12 | document.body.appendChild(container)
13 | app.mount('#container')
14 | }
15 |
16 | var 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 | var 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 | var a = moreTweets.pop()
60 | if (a) {
61 | state.tweets.unshift(a)
62 | emitter.emit('render')
63 | }
64 | })
65 | emitter.on('append-tweet', function () {
66 | var a = moreTweets.pop()
67 | if (a) {
68 | state.tweets.push(a)
69 | emitter.emit('render')
70 | }
71 | })
72 | emitter.on('pop-tweet', function () {
73 | var a = state.tweets.pop()
74 | if (a) {
75 | moreTweets.push(a)
76 | emitter.emit('render')
77 | }
78 | })
79 | emitter.on('shift-tweet', function () {
80 | var 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/simple/tweet-list.js:
--------------------------------------------------------------------------------
1 | var Tweet = require('twitter-component')
2 | var Nanocomponent = require('nanocomponent')
3 | var assert = require('assert')
4 | var html = require('bel')
5 | var compare = require('nanocomponent/compare')
6 | var Nanomap = require('../../')
7 | var isArray = Array.isArray
8 |
9 | function shapeTweetList (tweetUrl, i, list) {
10 | return {
11 | id: tweetUrl,
12 | opts: {
13 | placeholder: false
14 | },
15 | arguments: tweetUrl
16 | }
17 | }
18 |
19 | class TweetList extends Nanocomponent {
20 | constructor () {
21 | super()
22 |
23 | this.tweetList = null
24 | this.mapper = new Nanomap({ gc: false }, Tweet)
25 | }
26 |
27 | createElement (tweetList) {
28 | assert(isArray(tweetList), 'tweetList must be an array of tweet URLs')
29 | this.tweetList = tweetList.slice() // Have to slice since tweetList is an array
30 | return html`
31 |
32 | ${tweetList.map(shapeTweetList).map(this.mapper)}
33 |
34 | `
35 | }
36 | update (tweetList) {
37 | assert(isArray(tweetList), 'tweetList must be an array of tweet URLs')
38 | return compare(this.tweetList, tweetList)
39 | }
40 | }
41 |
42 | module.exports = TweetList
43 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var isArray = Array.isArray
3 |
4 | function Nanomap (opts, Callback) {
5 | if (!(this instanceof Nanomap)) return new Nanomap(opts, Callback)
6 | assert(arguments.length > 0 && arguments.length <= 2, 'Provide the correct number of arguments')
7 | if (arguments.length < 2) {
8 | Callback = opts
9 | opts = {}
10 | }
11 |
12 | opts = Object.assign({
13 | gc: true
14 | }, opts)
15 |
16 | this.gc = opts.gc
17 |
18 | if (typeof Callback === 'function') {
19 | Callback = {
20 | default: Callback
21 | }
22 | }
23 |
24 | this.Callback = Callback
25 |
26 | this.mapping = false
27 |
28 | this.lastCache = {} // Instance cache
29 | this.nextCache = {}
30 | this.lastTypeCache = {} // Type instance cache
31 | this.nextTypeCache = {}
32 |
33 | return this.render.bind(this)
34 | }
35 |
36 | Nanomap.prototype.render = function (c, i, array) {
37 | assert(c.hasOwnProperty('id'),
38 | 'Nanomap: all map objects must have an \'id\' property')
39 | if (c.hasOwnProperty('type')) {
40 | assert(this.Callback.hasOwnProperty(c.type),
41 | `Nanomap: type ${c.type} not defined for this mapper`)
42 | } else {
43 | assert(this.Callback.hasOwnProperty('default'),
44 | `Nanomap: mappers processing map objects without a 'type' property must implement a 'default' Component`)
45 | }
46 | if (i === 0) {
47 | // Setup a a fresh cache
48 | this.mapping = true
49 | this.lastCache = this.nextCache
50 | this.lastTypeCache = this.nextTypeCache
51 | if (this.gc) {
52 | this.nextCache = {}
53 | this.nextTypeCache = {}
54 | }
55 | }
56 | var instance
57 | var Callback = this.Callback
58 | var args = isArray(c.arguments) ? c.arguments : [c.arguments]
59 | if (this.lastCache[c.id] && this.lastTypeCache[c.id] === (c.type || 'default')) {
60 | instance = this.nextCache[c.id] = this.lastCache[c.id]
61 | this.nextTypeCache[c.id] = (c.type || 'default')
62 | } else {
63 | instance = this.nextCache[c.id] = new Callback[c.type || 'default'](c.opts)
64 | this.nextTypeCache[c.id] = c.type || 'default'
65 | }
66 |
67 | if (i === (array.length - 1)) {
68 | // Clean up old cache references
69 | if (this.gc) {
70 | this.lastCache = null
71 | this.lastTypeCache = null
72 | }
73 | this.mapping = false
74 | }
75 |
76 | return instance.render.apply(instance, args)
77 | }
78 |
79 | module.exports = Nanomap
80 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nanomap",
3 | "description": "Functionally map data into DOM component instances",
4 | "version": "1.1.1",
5 | "author": "Bret Comnes",
6 | "bugs": {
7 | "url": "https://github.com/bcomnes/nanomap/issues"
8 | },
9 | "devDependencies": {
10 | "@tap-format/spec": "^0.2.0",
11 | "bankai": "^9.0.0-1",
12 | "bel": "^5.1.1",
13 | "budo": "^10.0.4",
14 | "choo": "^6.0.1",
15 | "dependency-check": "^2.6.0",
16 | "fy-shuffle": "^1.0.0",
17 | "lodash.isequal": "^4.5.0",
18 | "nanocomponent": "^6.4.1",
19 | "npm-run-all": "^4.0.2",
20 | "standard": "^10.0.0",
21 | "tape": "^4.7.0",
22 | "tape-run": "^3.0.0",
23 | "twitter-component": "^1.0.2",
24 | "youtube-component": "^1.1.1"
25 | },
26 | "homepage": "https://github.com/bcomnes/nanoap#readme",
27 | "keywords": [
28 | "bel",
29 | "cache-component",
30 | "choo",
31 | "element",
32 | "embed",
33 | "map",
34 | "nanocomponent",
35 | "nanomorph"
36 | ],
37 | "license": "MIT",
38 | "main": "index.js",
39 | "browser": {
40 | "assert": "nanoassert"
41 | },
42 | "repository": {
43 | "type": "git",
44 | "url": "git+https://github.com/bcomnes/nanomap.git"
45 | },
46 | "scripts": {
47 | "start": "budo example/complex/index.js --live --open",
48 | "start:simple": "budo example/simple/index.js --live --open",
49 | "start:test": "budo test.js --live --open",
50 | "test": "run-s test:*",
51 | "test:browser": "browserify test.js | tape-run | tap-format-spec",
52 | "test:deps": "dependency-check .",
53 | "test:lint": "standard",
54 | "build": "bankai build example/complex/index.js dist"
55 | },
56 | "dependencies": {
57 | "nanoassert": "^1.1.0"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bcomnes/nanomap/aeee81af63eb566d43f165ba2b2c78fdfd2a5ac8/screenshot.png
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | var test = require('tape')
2 | var YoutubeComponent = require('youtube-component')
3 | var TwitterComponent = require('twitter-component')
4 | var Nanomap = 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 | function renderAndMount (testEl, data, mapper) {
18 | var els = data.map(mapper)
19 | els.forEach(function (el) {
20 | testEl.appendChild(el)
21 | })
22 | }
23 |
24 | test('simple mapper', function (t) {
25 | var testRoot = createTestElement()
26 | var simpleMapper = new Nanomap(YoutubeComponent)
27 | var videos = [
28 | 'https://www.youtube.com/watch?v=b8HO6hba9ZE',
29 | 'https://vimeo.com/229754542',
30 | 'https://www.youtube.com/watch?v=bYpKrA233vY'
31 | ].map(function (url) {
32 | return { id: url, arguments: url }
33 | })
34 |
35 | t.doesNotThrow(renderAndMount.bind(null, testRoot, videos, simpleMapper), 'Able to render list of videos')
36 | t.true(testRoot.children[0].children[0].src.includes('youtube.com/embed/b8HO6hba9ZE'), 'videos are in page')
37 | t.end()
38 | })
39 |
40 | test('mixed mapper', function (t) {
41 | var testRoot = createTestElement()
42 | var mixedMapper = new Nanomap({
43 | video: YoutubeComponent,
44 | tweet: TwitterComponent,
45 | default: TwitterComponent
46 | })
47 | var videos = [
48 | 'https://www.youtube.com/watch?v=b8HO6hba9ZE',
49 | 'https://vimeo.com/229754542',
50 | 'https://www.youtube.com/watch?v=bYpKrA233vY'
51 | ].map(function (url) {
52 | return { id: url, arguments: url, type: 'video' }
53 | })
54 |
55 | var tweets = [
56 | 'https://twitter.com/uhhyeahbret/status/897603426518876161',
57 | 'https://twitter.com/yoshuawuyts/status/895338700531535878'
58 | ].map(function (url) {
59 | return { id: url, arguments: url, type: 'tweet' }
60 | })
61 |
62 | delete tweets[0].type
63 |
64 | var data = videos.concat(tweets)
65 |
66 | t.doesNotThrow(renderAndMount.bind(null, testRoot, data, mixedMapper), 'Able to render list of videos')
67 | t.true(testRoot.children[0].children[0].src.includes('youtube.com/embed/b8HO6hba9ZE'), 'videos are in page')
68 | t.end()
69 | })
70 |
--------------------------------------------------------------------------------