index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<div id="app" sd-controller="TodoList" sd-on="click:changeMessage | delegate .button">
<p sd-text="msg | capitalize"></p>
<p sd-text="msg | uppercase"></p>
<p sd-on="click:remove">bye</p>
<p sd-text="total | money"></p>
<p class="button">Change Message</p>
<p sd-class="red:error" sd-show="error">Error</p>
<ul sd-show="todos">
<li class="todo"
sd-controller="Todo"
sd-each="todo:todos"
sd-class="done:todo.done"
sd-on="click:changeMessage, click:todo.toggle"
sd-text="todo.title"
></li>
</ul>
</div>
<script>

var Seed = require('seed')

Seed.filter('money', function (value) {
return value
? '$' + value.toFixed(2)
: ''
})

Seed.controller('TodoList', function (scope, seed) {
scope.changeMessage = function () {
scope.msg = 'It works!'
}
scope.remove = function () {
seed.destroy()
}
})

Seed.controller('Todo', function (scope) {
scope.toggle = function () {
scope.done = !scope.done
}
})

var s = Date.now()

var data = {
msg: 'hello!',
total: 9999,
error: true,
todos: [
{
title: 'hello!',
done: true
},
{
title: 'hello!!',
done: false
},
{
title: 'hello!!!',
done: false
}
]
}

var app = Seed.bootstrap({
el: '#app',
data: data
})

console.log(Date.now() - s + 'ms')

</script>

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
var config      = require('./config'),
Seed = require('./seed'),
directives = require('./directives'),
filters = require('./filters'),
controllers = require('./controllers')

Seed.config = config

Seed.extend = function (opts) {
var Spore = function () {
Seed.apply(this, arguments)
for (var prop in this.extensions) {
var ext = this.extensions[prop]
this.scope[prop] = (typeof ext === 'function')
? ext.bind(this)
: ext
}
}
Spore.prototype = Object.create(Seed.prototype)
Spore.prototype.extensions = {}
for (var prop in opts) {
Spore.prototype.extensions[prop] = opts[prop]
}
return Spore
}

Seed.controller = function (id, extensions) {
if (controllers[id]) {
console.warn('controller "' + id + '" was already registered and has been overwritten.')
}
controllers[id] = extensions
}

Seed.bootstrap = function (seeds) {
if (!Array.isArray(seeds)) seeds = [seeds]
var instances = []
seeds.forEach(function (seed) {
var el = seed.el
if (typeof el === 'string') {
el = document.querySelector(el)
}
if (!el) console.warn('invalid element or selector: ' + seed.el)
instances.push(new Seed(el, seed.data, seed.options))
})
return instances.length > 1
? instances
: instances[0]
}

Seed.directive = function (name, fn) {
directives[name] = fn
}

Seed.filter = function (name, fn) {
filters[name] = fn
}

// alias for an alternative API
Seed.evolve = Seed.controller
Seed.plant = Seed.bootstrap

module.exports = Seed

config.js

1
2
3
module.exports = {
prefix: 'sd'
}

seed.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
var config        = require('./config'),
controllers = require('./controllers'),
bindingParser = require('./binding')

var map = Array.prototype.map,
each = Array.prototype.forEach

// lazy init
var ctrlAttr,
eachAttr

function Seed (el, data, options) {

// refresh
ctrlAttr = config.prefix + '-controller'
eachAttr = config.prefix + '-each'

if (typeof el === 'string') {
el = document.querySelector(el)
}

this.el = el
this.scope = data
this._bindings = {}
this._options = options || {}

var key, dataCopy = {}
for (key in data) {
dataCopy[key] = data[key]
}

// if has controller
var ctrlID = el.getAttribute(ctrlAttr),
controller = null
if (ctrlID) {
controller = controllers[ctrlID]
el.removeAttribute(ctrlAttr)
if (!controller) throw new Error('controller ' + ctrlID + ' is not defined.')
}

// process nodes for directives
this._compileNode(el, true)

// copy in methods from controller
if (controller) {
controller.call(null, this.scope, this)
}

// initialize all variables by invoking setters
for (key in dataCopy) {
this.scope[key] = dataCopy[key]
}

}

Seed.prototype._compileNode = function (node, root) {
var self = this

if (node.nodeType === 3) {
// text node
self._compileTextNode(node)
} else if (node.attributes && node.attributes.length) {
var eachExp = node.getAttribute(eachAttr),
ctrlExp = node.getAttribute(ctrlAttr)
if (eachExp) {
// each block
var binding = bindingParser.parse(eachAttr, eachExp)
if (binding) {
self._bind(node, binding)
}
} else if (!ctrlExp || root) { // skip nested controllers
// normal node
// clone attributes because the list can change
var attrs = map.call(node.attributes, function (attr) {
return {
name: attr.name,
expressions: attr.value.split(',')
}
})
attrs.forEach(function (attr) {
var valid = false
attr.expressions.forEach(function (exp) {
var binding = bindingParser.parse(attr.name, exp)
if (binding) {
valid = true
self._bind(node, binding)
}
})
if (valid) node.removeAttribute(attr.name)
})
if (node.childNodes.length) {
each.call(node.childNodes, function (child) {
self._compileNode(child)
})
}
}
}

}

Seed.prototype._compileTextNode = function (node) {
return node
}

Seed.prototype._bind = function (node, bindingInstance) {

bindingInstance.seed = this
bindingInstance.el = node

var key = bindingInstance.key,
epr = this._options.eachPrefixRE,
isEachKey = epr && epr.test(key),
seed = this
// TODO make scope chain work on nested controllers
if (isEachKey) {
key = key.replace(epr, '')
} else if (epr) {
seed = this._options.parentSeed
}

var binding = seed._bindings[key] || seed._createBinding(key)

// add directive to this binding
binding.instances.push(bindingInstance)

// invoke bind hook if exists
if (bindingInstance.bind) {
bindingInstance.bind(binding.value)
}

}

Seed.prototype._createBinding = function (key) {

var binding = {
value: null,
instances: []
}

this._bindings[key] = binding

// bind accessor triggers to scope
Object.defineProperty(this.scope, key, {
get: function () {
return binding.value
},
set: function (value) {
binding.value = value
binding.instances.forEach(function (instance) {
instance.update(value)
})
}
})

return binding
}

Seed.prototype.dump = function () {
var data = {}
for (var key in this._bindings) {
data[key] = this._bindings[key].value
}
return data
}

Seed.prototype.destroy = function () {
for (var key in this._bindings) {
this._bindings[key].instances.forEach(unbind)
;delete this._bindings[key]
}
this.el.parentNode.removeChild(this.el)
function unbind (instance) {
if (instance.unbind) {
instance.unbind()
}
}
}

module.exports = Seed

filters.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
module.exports = {

capitalize: function (value) {
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
},

uppercase: function (value) {
return value.toString().toUpperCase()
},

delegate: function (handler, args) {
var selector = args[0]
return function (e) {
if (delegateCheck(e.target, e.currentTarget, selector)) {
handler.apply(this, arguments)
}
}
}

}

function delegateCheck (current, top, selector) {
if (current.webkitMatchesSelector(selector)) {
return true
} else if (current === top) {
return false
} else {
return delegateCheck(current.parentNode, top, selector)
}
}

directives.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
var config = require('./config'),
watchArray = require('./watchArray')

module.exports = {

text: function (value) {
this.el.textContent = value || ''
},

show: function (value) {
this.el.style.display = value ? '' : 'none'
},

class: function (value) {
this.el.classList[value ? 'add' : 'remove'](this.arg)
},

on: {
update: function (handler) {
var event = this.arg
if (this.handler) {
this.el.removeEventListener(event, this.handler)
}
if (handler) {
this.el.addEventListener(event, handler)
this.handler = handler
}
},
unbind: function () {
var event = this.arg
if (this.handlers) {
this.el.removeEventListener(event, this.handlers[event])
}
}
},

each: {
bind: function () {
this.el.removeAttribute(config.prefix + '-each')
this.prefixRE = new RegExp('^' + this.arg + '.')
var ctn = this.container = this.el.parentNode
this.marker = document.createComment('sd-each-' + this.arg + '-marker')
ctn.insertBefore(this.marker, this.el)
ctn.removeChild(this.el)
this.childSeeds = []
},
update: function (collection) {
if (this.childSeeds.length) {
this.childSeeds.forEach(function (child) {
child.destroy()
})
this.childSeeds = []
}
watchArray(collection, this.mutate.bind(this))
var self = this
collection.forEach(function (item, i) {
self.childSeeds.push(self.buildItem(item, i, collection))
})
},
mutate: function (mutation) {
console.log(mutation)
},
buildItem: function (data, index, collection) {
var Seed = require('./seed'),
node = this.el.cloneNode(true)
var spore = new Seed(node, data, {
eachPrefixRE: this.prefixRE,
parentSeed: this.seed
})
this.container.insertBefore(node, this.marker)
collection[index] = spore.scope
return spore
}
}

}

binding.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
var config     = require('./config'),
directives = require('./directives'),
filters = require('./filters')

var KEY_RE = /^[^\|]+/,
ARG_RE = /([^:]+):(.+)$/,
FILTERS_RE = /\|[^\|]+/g,
FILTER_TOKEN_RE = /[^\s']+|'[^']+'/g,
QUOTE_RE = /'/g

function Binding (directiveName, expression) {

var directive = directives[directiveName]
if (typeof directive === 'function') {
this._update = directive
} else {
for (var prop in directive) {
if (prop === 'update') {
this['_update'] = directive.update
} else {
this[prop] = directive[prop]
}
}
}

var rawKey = expression.match(KEY_RE)[0], // guarded in parse
argMatch = rawKey.match(ARG_RE)

this.key = argMatch
? argMatch[2].trim()
: rawKey.trim()

this.arg = argMatch
? argMatch[1].trim()
: null

var filterExpressions = expression.match(FILTERS_RE)
if (filterExpressions) {
this.filters = filterExpressions.map(function (filter) {
var tokens = filter.slice(1)
.match(FILTER_TOKEN_RE)
.map(function (token) {
return token.replace(QUOTE_RE, '').trim()
})
return {
name : tokens[0],
apply : filters[tokens[0]],
args : tokens.length > 1
? tokens.slice(1)
: null
}
})
} else {
this.filters = null
}
}

Binding.prototype.update = function (value) {
// apply filters
if (this.filters) {
value = this.applyFilters(value)
}
this._update(value)
}

Binding.prototype.applyFilters = function (value) {
var filtered = value
this.filters.forEach(function (filter) {
if (!filter.apply) throw new Error('Unknown filter: ' + filter.name)
filtered = filter.apply(filtered, filter.args)
})
return filtered
}

module.exports = {

// make sure the directive and value is valid
parse: function (dirname, expression) {

var prefix = config.prefix
if (dirname.indexOf(prefix) === -1) return null
dirname = dirname.slice(prefix.length + 1)

var dir = directives[dirname],
valid = KEY_RE.test(expression)

if (!dir) console.warn('unknown directive: ' + dirname)
if (!valid) console.warn('invalid directive expression: ' + expression)

return dir && valid
? new Binding(dirname, expression)
: null
}
}

controllers.js

1
module.exports = {}