83 | )
84 | }
85 | mount(render)
86 | await browser.find('div h1').shouldHave({text: 'Blue'})
87 | await browser.find('.orange').shouldHave({text: 'Orange'})
88 | await browser.find('.green').shouldHave({text: 'Green'})
89 | })
90 | })
91 |
--------------------------------------------------------------------------------
/prepareAttributes.js:
--------------------------------------------------------------------------------
1 | var render = require('./render')
2 | var bindModel = require('./bindModel')
3 |
4 | module.exports = function (tag, attributes, childElements) {
5 | var dataset
6 | var currentRender = render.currentRender()
7 |
8 | if (attributes.binding) {
9 | bindModel(tag, attributes, childElements)
10 | delete attributes.binding
11 | }
12 |
13 | var keys = Object.keys(attributes)
14 | for (var k = 0; k < keys.length; k++) {
15 | var key = keys[k]
16 | var attribute = attributes[key]
17 |
18 | if (typeof (attribute) === 'function' && currentRender) {
19 | attributes[key] = currentRender.transformFunctionAttribute(key, attribute)
20 | }
21 |
22 | var rename = renames[key]
23 | if (rename) {
24 | attributes[rename] = attribute
25 | delete attributes[key]
26 | continue
27 | }
28 |
29 | if (dataAttributeRegex.test(key)) {
30 | if (!dataset) {
31 | dataset = attributes.dataset
32 |
33 | if (!dataset) {
34 | dataset = attributes.dataset = {}
35 | }
36 | }
37 |
38 | var datakey = key
39 | .replace(dataAttributeRegex, '')
40 | .replace(/-([a-z])/ig, function (_, x) { return x.toUpperCase() })
41 |
42 | dataset[datakey] = attribute
43 | delete attributes[key]
44 | continue
45 | }
46 | }
47 |
48 | if (process.env.NODE_ENV !== 'production' && attributes.__source) {
49 | if (!dataset) {
50 | dataset = attributes.dataset
51 |
52 | if (!dataset) {
53 | dataset = attributes.dataset = {}
54 | }
55 | }
56 |
57 | dataset.fileName = attributes.__source.fileName
58 | dataset.lineNumber = attributes.__source.lineNumber
59 | }
60 |
61 | if (attributes.className) {
62 | attributes.className = generateClassName(attributes.className)
63 | }
64 |
65 | if (attributes.innerHTML === false) {
66 | delete attributes.innerHTML
67 | }
68 |
69 | return attributes
70 | }
71 |
72 | var renames = {
73 | for: 'htmlFor',
74 | class: 'className',
75 | contenteditable: 'contentEditable',
76 | tabindex: 'tabIndex',
77 | colspan: 'colSpan'
78 | }
79 |
80 | var dataAttributeRegex = /^data-/
81 |
82 | function generateClassName (obj) {
83 | if (typeof (obj) === 'object') {
84 | if (obj instanceof Array) {
85 | var names = obj.map(function (item) {
86 | return generateClassName(item)
87 | })
88 | return names.join(' ') || undefined
89 | } else {
90 | return generateConditionalClassNames(obj)
91 | }
92 | } else {
93 | return obj
94 | }
95 | }
96 |
97 | function generateConditionalClassNames (obj) {
98 | return Object.keys(obj).filter(function (key) {
99 | return obj[key]
100 | }).join(' ') || undefined
101 | }
102 |
--------------------------------------------------------------------------------
/mapBinding.js:
--------------------------------------------------------------------------------
1 | var bindingMeta = require('./meta')
2 |
3 | function makeConverter (converter) {
4 | if (typeof converter === 'function') {
5 | return {
6 | view: function (model) {
7 | return model
8 | },
9 | model: function (view) {
10 | return converter(view)
11 | }
12 | }
13 | } else {
14 | return converter
15 | }
16 | }
17 |
18 | function chainConverters (startIndex, converters) {
19 | function makeConverters () {
20 | if (!_converters) {
21 | _converters = new Array(converters.length - startIndex)
22 |
23 | for (var n = startIndex; n < converters.length; n++) {
24 | _converters[n - startIndex] = makeConverter(converters[n])
25 | }
26 | }
27 | }
28 |
29 | if ((converters.length - startIndex) === 1) {
30 | return makeConverter(converters[startIndex])
31 | } else {
32 | var _converters
33 | return {
34 | view: function (model) {
35 | makeConverters()
36 | var intermediateValue = model
37 | for (var n = 0; n < _converters.length; n++) {
38 | intermediateValue = _converters[n].view(intermediateValue)
39 | }
40 | return intermediateValue
41 | },
42 |
43 | model: function (view) {
44 | makeConverters()
45 | var intermediateValue = view
46 | for (var n = _converters.length - 1; n >= 0; n--) {
47 | intermediateValue = _converters[n].model(intermediateValue)
48 | }
49 | return intermediateValue
50 | }
51 | }
52 | }
53 | }
54 |
55 | module.exports = function (model, property) {
56 | var _meta
57 | function hyperdomMeta () {
58 | return _meta || (_meta = bindingMeta(model, property))
59 | }
60 |
61 | var converter = chainConverters(2, arguments)
62 |
63 | return {
64 | get: function () {
65 | var meta = hyperdomMeta()
66 | var modelValue = model[property]
67 | var modelText
68 |
69 | if (meta.error) {
70 | return meta.view
71 | } else if (meta.view === undefined) {
72 | modelText = converter.view(modelValue)
73 | meta.view = modelText
74 | return modelText
75 | } else {
76 | var previousValue
77 | try {
78 | previousValue = converter.model(meta.view)
79 | } catch (e) {
80 | meta.error = e
81 | return meta.view
82 | }
83 | modelText = converter.view(modelValue)
84 | var normalisedPreviousText = converter.view(previousValue)
85 |
86 | if (modelText === normalisedPreviousText) {
87 | return meta.view
88 | } else {
89 | meta.view = modelText
90 | return modelText
91 | }
92 | }
93 | },
94 |
95 | set: function (view) {
96 | var meta = hyperdomMeta()
97 | meta.view = view
98 |
99 | try {
100 | model[property] = converter.model(view, model[property])
101 | delete meta.error
102 | } catch (e) {
103 | meta.error = e
104 | }
105 | },
106 |
107 | meta: function () {
108 | return hyperdomMeta()
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/xml.js:
--------------------------------------------------------------------------------
1 | var AttributeHook = require('virtual-dom/virtual-hyperscript/hooks/attribute-hook')
2 |
3 | var namespaceRegex = /^([a-z0-9_-]+)(--|:)([a-z0-9_-]+)$/i
4 | var xmlnsRegex = /^xmlns(--|:)([a-z0-9_-]+)$/i
5 | var SVG_NAMESPACE = 'http://www.w3.org/2000/svg'
6 |
7 | function transformTanName (vnode, namespaces) {
8 | var tagNamespace = namespaceRegex.exec(vnode.tagName)
9 | if (tagNamespace) {
10 | var namespaceKey = tagNamespace[1]
11 | var namespace = namespaces[namespaceKey]
12 | if (namespace) {
13 | vnode.tagName = tagNamespace[1] + ':' + tagNamespace[3]
14 | vnode.namespace = namespace
15 | }
16 | } else if (!vnode.namespace) {
17 | vnode.namespace = namespaces['']
18 | }
19 | }
20 |
21 | function transformProperties (vnode, namespaces) {
22 | var properties = vnode.properties
23 |
24 | if (properties) {
25 | var attributes = properties.attributes || (properties.attributes = {})
26 |
27 | var keys = Object.keys(properties)
28 | for (var k = 0, l = keys.length; k < l; k++) {
29 | var key = keys[k]
30 | if (key !== 'style' && key !== 'attributes') {
31 | var match = namespaceRegex.exec(key)
32 | if (match) {
33 | properties[match[1] + ':' + match[3]] = new AttributeHook(namespaces[match[1]], properties[key])
34 | delete properties[key]
35 | } else {
36 | if (vnode.namespace === SVG_NAMESPACE && key === 'className') {
37 | attributes['class'] = properties.className
38 | delete properties.className
39 | } else {
40 | var property = properties[key]
41 | var type = typeof property
42 | if (type === 'string' || type === 'number' || type === 'boolean') {
43 | attributes[key] = property
44 | }
45 | }
46 | }
47 | }
48 | }
49 | }
50 | }
51 |
52 | function declaredNamespaces (vnode) {
53 | var namespaces = {
54 | '': vnode.properties.xmlns,
55 | xmlns: 'http://www.w3.org/2000/xmlns/'
56 | }
57 |
58 | var keys = Object.keys(vnode.properties)
59 |
60 | for (var k = 0, l = keys.length; k < l; k++) {
61 | var key = keys[k]
62 | var value = vnode.properties[key]
63 |
64 | if (key === 'xmlns') {
65 | namespaces[''] = value
66 | } else {
67 | var match = xmlnsRegex.exec(key)
68 |
69 | if (match) {
70 | namespaces[match[2]] = value
71 | }
72 | }
73 | }
74 |
75 | return namespaces
76 | }
77 |
78 | function transform (vnode) {
79 | var namespaces = declaredNamespaces(vnode)
80 |
81 | function transformChildren (vnode, namespaces) {
82 | transformTanName(vnode, namespaces)
83 | transformProperties(vnode, namespaces)
84 |
85 | if (vnode.children) {
86 | for (var c = 0, l = vnode.children.length; c < l; c++) {
87 | var child = vnode.children[c]
88 | if (!(child.properties && child.properties.xmlns)) {
89 | transformChildren(child, namespaces)
90 | }
91 | }
92 | }
93 | }
94 |
95 | transformChildren(vnode, namespaces)
96 |
97 | return vnode
98 | }
99 |
100 | module.exports.transform = transform
101 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at enquiries@featurist.co.uk. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hyperdom",
3 | "version": "2.1.0",
4 | "description": "A fast, feature rich and simple framework for building dynamic browser applications.",
5 | "main": "index.js",
6 | "dependencies": {
7 | "vdom-parser": "1.3.4",
8 | "vdom-to-html": "2.3.1",
9 | "virtual-dom": "2.1.1"
10 | },
11 | "devDependencies": {
12 | "@types/chai": "^4.1.4",
13 | "@types/jquery": "^3.3.6",
14 | "@types/jsdom": "^11.12.0",
15 | "@types/mocha": "^5.2.5",
16 | "babel-preset-hyperdom": "^1.0.0",
17 | "browser-monkey": "2.4.1",
18 | "chai": "3.5.0",
19 | "codesandbox-example-links": "^1.0.0",
20 | "detect-browser": "1.6.2",
21 | "docsify-cli": "^4.3.0",
22 | "electron": "1.8.2",
23 | "electron-mocha": "3.4.0",
24 | "eslint": "7.22.0",
25 | "eslint-config-standard": "16.0.2",
26 | "eslint-plugin-es5": "1.5.0",
27 | "eslint-plugin-import": "2.22.1",
28 | "eslint-plugin-node": "11.1.0",
29 | "eslint-plugin-promise": "4.3.1",
30 | "eslint-plugin-standard": "5.0.0",
31 | "gh-pages": "^2.0.1",
32 | "hyperx": "2.3.0",
33 | "jquery": "3.3.1",
34 | "jquery-sendkeys": "4.0.0",
35 | "jsdom": "12.1.0",
36 | "karma": "3.0.0",
37 | "karma-browserstack-launcher": "1.5.1",
38 | "karma-chrome-launcher": "2.2.0",
39 | "karma-cli": "1.0.1",
40 | "karma-electron-launcher": "0.2.0",
41 | "karma-firefox-launcher": "1.1.0",
42 | "karma-ievms": "0.1.0",
43 | "karma-mocha": "1.3.0",
44 | "karma-mocha-reporter": "2.2.5",
45 | "karma-safari-launcher": "1.0.0",
46 | "karma-sourcemap-loader": "^0.3.7",
47 | "karma-webpack": "^3.0.5",
48 | "lie": "3.1.1",
49 | "lowscore": "1.12.1",
50 | "mocha": "3.2.0",
51 | "trytryagain": "1.2.0",
52 | "ts-loader": "^5.2.2",
53 | "ts-node": "^7.0.1",
54 | "tslint": "^5.11.0",
55 | "typescript": "^3.2.1",
56 | "typescript-tslint-plugin": "^0.1.2",
57 | "uglify-js": "3.6.0",
58 | "watchify": "3.9.0",
59 | "webpack": "4.35.0",
60 | "webpack-cli": "^3.3.5"
61 | },
62 | "scripts": {
63 | "test": "./node_modules/.bin/tsc && npm run karma && npm run mocha && eslint . && npm run tslint",
64 | "test-all": "BROWSERS=all npm test",
65 | "tslint": "tslint --project . *.d.ts test/**/*.ts{,x}",
66 | "karma": "karma start --single-run",
67 | "mocha": "TS_NODE_FILES=true mocha -r ts-node/register test/server/*Spec.ts",
68 | "build": "webpack index.js && uglifyjs --compress --mangle -o dist/hyperdom.min.js dist/main.js",
69 | "size": "npm run build && gzip < dist/hyperdom.min.js > dist/hyperdom.min.js.gz && ls -lh dist/hyperdom.*",
70 | "dev-website": "yarn build-website && docsify serve ./website-dist",
71 | "watch-docs": "ls docs/*.md | entr yarn codesandbox-example-links --output-dir=./website-dist ./docs/*.md",
72 | "build-website": "rm -rf ./website-dist && cp -r ./docs ./website-dist && codesandbox-example-links --output-dir=./website-dist ./docs/*.md",
73 | "publish-website": "yarn build-website && gh-pages -t -d website-dist"
74 | },
75 | "keywords": [
76 | "virtual-dom",
77 | "front-end",
78 | "mvc",
79 | "framework",
80 | "html",
81 | "plastiq",
82 | "hyperdom"
83 | ],
84 | "author": "Tim Macfarlane ",
85 | "license": "MIT",
86 | "files": [
87 | "*.js",
88 | "*.d.ts",
89 | "*.ts"
90 | ],
91 | "repository": {
92 | "type": "git",
93 | "url": "https://github.com/featurist/hyperdom.git"
94 | },
95 | "bugs": {
96 | "url": "https://github.com/featurist/hyperdom/issues"
97 | },
98 | "homepage": "https://github.com/featurist/hyperdom"
99 | }
100 |
--------------------------------------------------------------------------------
/componentWidget.js:
--------------------------------------------------------------------------------
1 | var VText = require('virtual-dom/vnode/vtext.js')
2 | var domComponent = require('./domComponent')
3 | var render = require('./render')
4 | var deprecations = require('./deprecations')
5 |
6 | function ComponentWidget (state, vdom) {
7 | if (!vdom) {
8 | throw new Error('hyperdom.html.component([options], vdom) expects a vdom argument')
9 | }
10 |
11 | this.state = state
12 | this.key = state.key
13 | var currentRender = render.currentRender()
14 |
15 | if (typeof vdom === 'function') {
16 | this.render = function () {
17 | if (currentRender && state.on) {
18 | currentRender.transformFunctionAttribute = function (key, value) {
19 | return state.on(key.replace(/^on/, ''), value)
20 | }
21 | }
22 | return vdom.apply(this.state, arguments)
23 | }
24 | this.canRefresh = true
25 | } else {
26 | vdom = vdom || new VText('')
27 | this.render = function () {
28 | return vdom
29 | }
30 | }
31 | this.cacheKey = state.cacheKey
32 | this.component = domComponent.create()
33 |
34 | var renderFinished = currentRender && currentRender.finished
35 | if (renderFinished) {
36 | this.afterRender = function (fn) {
37 | renderFinished.then(fn)
38 | }
39 | } else {
40 | this.afterRender = function () {}
41 | }
42 | }
43 |
44 | ComponentWidget.prototype.type = 'Widget'
45 |
46 | ComponentWidget.prototype.init = function () {
47 | var self = this
48 |
49 | if (self.state.onbeforeadd) {
50 | self.state.onbeforeadd()
51 | }
52 |
53 | var vdom = this.render(this)
54 | if (vdom instanceof Array) {
55 | throw new Error('vdom returned from component cannot be an array')
56 | }
57 |
58 | var element = this.component.create(vdom)
59 |
60 | if (self.state.onadd) {
61 | this.afterRender(function () {
62 | self.state.onadd(element)
63 | })
64 | }
65 |
66 | if (self.state.detached) {
67 | return document.createTextNode('')
68 | } else {
69 | return element
70 | }
71 | }
72 |
73 | ComponentWidget.prototype.update = function (previous) {
74 | var self = this
75 |
76 | var refresh = !this.cacheKey || this.cacheKey !== previous.cacheKey
77 |
78 | if (refresh) {
79 | if (self.state.onupdate) {
80 | this.afterRender(function () {
81 | self.state.onupdate(self.component.element)
82 | })
83 | }
84 | }
85 |
86 | this.component = previous.component
87 |
88 | if (previous.state && this.state) {
89 | var keys = Object.keys(this.state)
90 | for (var n = 0; n < keys.length; n++) {
91 | var key = keys[n]
92 | previous.state[key] = self.state[key]
93 | }
94 | this.state = previous.state
95 | }
96 |
97 | if (refresh) {
98 | var element = this.component.update(this.render(this))
99 |
100 | if (self.state.detached) {
101 | return document.createTextNode('')
102 | } else {
103 | return element
104 | }
105 | }
106 | }
107 |
108 | ComponentWidget.prototype.refresh = function () {
109 | this.component.update(this.render(this))
110 | if (this.state.onupdate) {
111 | this.state.onupdate(this.component.element)
112 | }
113 | }
114 |
115 | ComponentWidget.prototype.destroy = function (element) {
116 | var self = this
117 |
118 | if (self.state.onremove) {
119 | this.afterRender(function () {
120 | self.state.onremove(element)
121 | })
122 | }
123 |
124 | this.component.destroy()
125 | }
126 |
127 | module.exports = function (state, vdom) {
128 | deprecations.component('hyperdom.html.component is deprecated, please use hyperdom.viewComponent')
129 | if (typeof state === 'function') {
130 | return new ComponentWidget({}, state)
131 | } else if (state.constructor === Object) {
132 | return new ComponentWidget(state, vdom)
133 | } else {
134 | return new ComponentWidget({}, state)
135 | }
136 | }
137 |
138 | module.exports.ComponentWidget = ComponentWidget
139 |
--------------------------------------------------------------------------------
/mount.js:
--------------------------------------------------------------------------------
1 | var hyperdomMeta = require('./meta')
2 | var runRender = require('./render')
3 | var domComponent = require('./domComponent')
4 | var Set = require('./set')
5 | var refreshEventResult = require('./refreshEventResult')
6 |
7 | var lastId = 0
8 |
9 | function Mount (model, options) {
10 | var win = (options && options.window) || window
11 | var router = typeof options === 'object' && options.hasOwnProperty('router') ? options.router : undefined
12 | this.requestRender = (options && options.requestRender) || win.requestAnimationFrame || win.setTimeout
13 |
14 | this.document = (options && options.document) || document
15 | this.model = model
16 |
17 | this.renderQueued = false
18 | this.mountRenderRequested = false
19 | this.componentRendersRequested = undefined
20 | this.id = ++lastId
21 | this.renderId = 0
22 | this.mounted = true
23 | this.router = router
24 | }
25 |
26 | Mount.prototype.refreshify = function (fn, options) {
27 | if (!fn) {
28 | return fn
29 | }
30 |
31 | if (options && (options.norefresh === true || options.refresh === false)) {
32 | return fn
33 | }
34 |
35 | var self = this
36 |
37 | return function () {
38 | var result = fn.apply(this, arguments)
39 | return refreshEventResult(result, self, options)
40 | }
41 | }
42 |
43 | Mount.prototype.transformFunctionAttribute = function (key, value) {
44 | return this.refreshify(value)
45 | }
46 |
47 | Mount.prototype.queueRender = function () {
48 | if (!this.renderQueued) {
49 | var self = this
50 |
51 | var requestRender = this.requestRender
52 | this.renderQueued = true
53 |
54 | requestRender(function () {
55 | self.renderQueued = false
56 |
57 | if (self.mounted) {
58 | if (self.mountRenderRequested) {
59 | self.refreshImmediately()
60 | } else if (self.componentRendersRequested) {
61 | self.refreshComponentsImmediately()
62 | }
63 | }
64 | })
65 | }
66 | }
67 |
68 | Mount.prototype.createDomComponent = function () {
69 | return domComponent.create({ document: this.document })
70 | }
71 |
72 | Mount.prototype.render = function () {
73 | if (this.router) {
74 | return this.router.render(this.model)
75 | } else {
76 | return this.model
77 | }
78 | }
79 |
80 | Mount.prototype.refresh = function () {
81 | this.mountRenderRequested = true
82 | this.queueRender()
83 | }
84 |
85 | Mount.prototype.refreshImmediately = function () {
86 | var self = this
87 |
88 | runRender(self, function () {
89 | self.renderId++
90 | var vdom = self.render()
91 | self.component.update(vdom)
92 | self.mountRenderRequested = false
93 | })
94 | }
95 |
96 | Mount.prototype.refreshComponentsImmediately = function () {
97 | var self = this
98 |
99 | runRender(self, function () {
100 | for (var i = 0, l = self.componentRendersRequested.length; i < l; i++) {
101 | var w = self.componentRendersRequested[i]
102 | w.refresh()
103 | }
104 | self.componentRendersRequested = undefined
105 | })
106 | }
107 |
108 | Mount.prototype.refreshComponent = function (component) {
109 | if (!this.componentRendersRequested) {
110 | this.componentRendersRequested = []
111 | }
112 |
113 | this.componentRendersRequested.push(component)
114 | this.queueRender()
115 | }
116 |
117 | Mount.prototype.isComponentInDom = function (component) {
118 | var meta = hyperdomMeta(component)
119 | return meta.lastRenderId === this.renderId
120 | }
121 |
122 | Mount.prototype.setupModelComponent = function (model) {
123 | var self = this
124 |
125 | var meta = hyperdomMeta(model)
126 |
127 | if (!meta.mount) {
128 | meta.mount = this
129 | meta.components = new Set()
130 |
131 | model.refresh = function () {
132 | self.refresh()
133 | }
134 |
135 | model.refreshImmediately = function () {
136 | self.refreshImmediately()
137 | }
138 |
139 | model.refreshComponent = function () {
140 | var meta = hyperdomMeta(this)
141 | meta.components.forEach(function (w) {
142 | self.refreshComponent(w)
143 | })
144 | }
145 |
146 | if (typeof model.onload === 'function') {
147 | this.refreshify(function () { return model.onload() }, {refresh: 'promise'})()
148 | }
149 | }
150 | }
151 |
152 | Mount.prototype.detach = function () {
153 | this.mounted = false
154 | }
155 |
156 | Mount.prototype.remove = function () {
157 | if (this.router) {
158 | this.router.reset()
159 | }
160 | this.component.destroy({removeElement: true})
161 | this.mounted = false
162 | }
163 |
164 | module.exports = Mount
165 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // Generated on Sat Dec 27 2014 08:06:04 GMT+0100 (CET)
3 |
4 | module.exports = function (config) {
5 | config.set({
6 |
7 | // base path that will be used to resolve all patterns (eg. files, exclude)
8 | basePath: '',
9 |
10 | // frameworks to use
11 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
12 | frameworks: ['mocha'],
13 |
14 | // list of files / patterns to load in the browser
15 | files: [
16 | 'test/browser/karma.index.ts'
17 | ],
18 |
19 | // list of files to exclude
20 | exclude: [
21 | '**/.*.sw?'
22 | ],
23 |
24 | // preprocess matching files before serving them to the browser
25 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
26 | preprocessors: {
27 | 'test/browser/karma.index.ts': ['webpack']
28 | },
29 |
30 | webpack: {
31 | mode: 'development',
32 | optimization: {
33 | nodeEnv: false
34 | },
35 | devtool: 'inline-source-map',
36 | resolve: {
37 | extensions: ['.js', '.ts', '.tsx']
38 | },
39 | module: {
40 | rules: [
41 | {
42 | test: /\.tsx?$/,
43 | loader: 'ts-loader',
44 | options: {
45 | // karma does not fail on compilation errors - so get rid of typechecking to save few seconds.
46 | transpileOnly: true,
47 | compilerOptions: {
48 | noEmit: false,
49 | target: 'es5'
50 | }
51 | },
52 | exclude: process.cwd() + '/node_modules'
53 | }
54 | ]
55 | }
56 | },
57 |
58 | // test results reporter to use
59 | // possible values: 'dots', 'progress'
60 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
61 | reporters: process.env.BROWSERS ? ['dots'] : ['mocha'],
62 |
63 | electronOpts: {
64 | show: false
65 | },
66 |
67 | mochaReporter: {
68 | showDiff: true
69 | },
70 |
71 | client: {
72 | mocha: {
73 | timeout: process.env.CI ? 61000 : 2000
74 | }
75 | },
76 |
77 | // web server port
78 | port: 9876,
79 |
80 | // enable / disable colors in the output (reporters and logs)
81 | colors: true,
82 |
83 | // level of logging
84 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
85 | logLevel: config.LOG_WARN,
86 | concurrency: process.env.BROWSERS === 'all' ? 2 : Infinity,
87 |
88 | // enable / disable watching file and executing tests whenever any file changes
89 | autoWatch: true,
90 |
91 | // start these browsers
92 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
93 | browsers: process.env.BROWSERS === 'all' ? Object.keys(browsers) : ['Chrome'],
94 |
95 | browserStack: {
96 | username: process.env.BROWSERSTACK_USER,
97 | accessKey: process.env.BROWSERSTACK_PASSWORD,
98 | captureTimeout: 300
99 | },
100 |
101 | // Continuous Integration mode
102 | // if true, Karma captures browsers, runs the tests and exits
103 | singleRun: false,
104 |
105 | customLaunchers: browsers,
106 |
107 | browserNoActivityTimeout: 60000
108 | })
109 | }
110 |
111 | var browsers = {
112 | 'browserstack-windows-firefox': {
113 | base: 'BrowserStack',
114 | browser: 'Firefox',
115 | os: 'Windows',
116 | os_version: '10',
117 | resolution: '1280x1024'
118 | },
119 | // 'browserstack-osx-firefox': {
120 | // base: 'BrowserStack',
121 | // browser: 'Firefox',
122 | // os: 'OS X',
123 | // os_version: 'Mojave',
124 | // resolution: '1280x1024'
125 | // },
126 | 'browserstack-safari': {
127 | base: 'BrowserStack',
128 | browser: 'Safari',
129 | os: 'OS X',
130 | os_version: 'Mojave',
131 | resolution: '1280x1024'
132 | },
133 | 'browserstack-windows-chrome': {
134 | base: 'BrowserStack',
135 | browser: 'Chrome',
136 | os: 'Windows',
137 | os_version: '10',
138 | resolution: '1280x1024'
139 | },
140 | 'browserstack-osx-chrome': {
141 | base: 'BrowserStack',
142 | browser: 'Chrome',
143 | os: 'OS X',
144 | os_version: 'Mojave',
145 | resolution: '1280x1024'
146 | },
147 | 'browserstack-ie11': {
148 | base: 'BrowserStack',
149 | browser: 'IE',
150 | os: 'Windows',
151 | os_version: '10',
152 | resolution: '1280x1024'
153 | },
154 | 'browserstack-edge': {
155 | base: 'BrowserStack',
156 | browser: 'Edge',
157 | os: 'Windows',
158 | os_version: '10',
159 | resolution: '1280x1024'
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/component.js:
--------------------------------------------------------------------------------
1 | var hyperdomMeta = require('./meta')
2 | var render = require('./render')
3 | var Vtext = require('virtual-dom/vnode/vtext.js')
4 | var debuggingProperties = require('./debuggingProperties')
5 |
6 | function Component (model, options) {
7 | this.isViewComponent = options && options.hasOwnProperty('viewComponent') && options.viewComponent
8 | this.model = model
9 | this.key = model.renderKey
10 | this.component = undefined
11 | }
12 |
13 | Component.prototype.type = 'Widget'
14 |
15 | Component.prototype.init = function () {
16 | var self = this
17 |
18 | var vdom = this.render()
19 |
20 | var meta = hyperdomMeta(this.model)
21 | meta.components.add(this)
22 |
23 | var currentRender = render.currentRender()
24 | this.component = currentRender.mount.createDomComponent()
25 | var element = this.component.create(vdom)
26 |
27 | if (self.model.detached) {
28 | return document.createTextNode('')
29 | } else {
30 | return element
31 | }
32 | }
33 |
34 | function beforeUpdate (model, element) {
35 | if (model.onbeforeupdate) {
36 | model.onbeforeupdate(element)
37 | }
38 |
39 | if (model.onbeforerender) {
40 | model.onbeforerender(element)
41 | }
42 | }
43 |
44 | function afterUpdate (model, element, oldElement) {
45 | if (model.onupdate) {
46 | model.onupdate(element, oldElement)
47 | }
48 |
49 | if (model.onrender) {
50 | model.onrender(element, oldElement)
51 | }
52 | }
53 |
54 | Component.prototype.update = function (previous) {
55 | if (previous.key !== this.key || this.model.constructor !== previous.model.constructor) {
56 | previous.destroy()
57 | return this.init()
58 | } else {
59 | var self = this
60 |
61 | if (this.isViewComponent) {
62 | var keys = Object.keys(this.model)
63 | for (var n = 0; n < keys.length; n++) {
64 | var key = keys[n]
65 | previous.model[key] = self.model[key]
66 | }
67 | this.model = previous.model
68 | }
69 |
70 | this.component = previous.component
71 | var oldElement = this.component.element
72 |
73 | var element = this.component.update(this.render(oldElement))
74 |
75 | if (self.model.detached) {
76 | return document.createTextNode('')
77 | } else {
78 | return element
79 | }
80 | }
81 | }
82 |
83 | Component.prototype.renderModel = function (oldElement) {
84 | var self = this
85 | var model = this.model
86 | var currentRender = render.currentRender()
87 | currentRender.mount.setupModelComponent(model)
88 |
89 | if (!oldElement) {
90 | if (self.model.onbeforeadd) {
91 | self.model.onbeforeadd()
92 | }
93 | if (self.model.onbeforerender) {
94 | self.model.onbeforerender()
95 | }
96 |
97 | if (self.model.onadd || self.model.onrender) {
98 | currentRender.finished.then(function () {
99 | if (self.model.onadd) {
100 | self.model.onadd(self.component.element)
101 | }
102 | if (self.model.onrender) {
103 | self.model.onrender(self.component.element)
104 | }
105 | })
106 | }
107 | } else {
108 | beforeUpdate(model, oldElement)
109 |
110 | if (model.onupdate || model.onrender) {
111 | currentRender.finished.then(function () {
112 | afterUpdate(model, self.component.element, oldElement)
113 | })
114 | }
115 | }
116 |
117 | var vdom = typeof model.render === 'function' ? model.render() : new Vtext(JSON.stringify(model))
118 |
119 | if (vdom instanceof Array) {
120 | throw new Error('vdom returned from component cannot be an array')
121 | }
122 |
123 | return debuggingProperties(vdom, model)
124 | }
125 |
126 | Component.prototype.render = function (oldElement) {
127 | var model = this.model
128 |
129 | var meta = hyperdomMeta(model)
130 | meta.lastRenderId = render.currentRender().mount.renderId
131 |
132 | if (typeof model.renderCacheKey === 'function') {
133 | var key = model.renderCacheKey()
134 | if (key !== undefined && meta.cacheKey === key && meta.cachedVdom) {
135 | return meta.cachedVdom
136 | } else {
137 | meta.cacheKey = key
138 | return (meta.cachedVdom = this.renderModel(oldElement))
139 | }
140 | } else {
141 | return this.renderModel(oldElement)
142 | }
143 | }
144 |
145 | Component.prototype.refresh = function () {
146 | var currentRender = render.currentRender()
147 | if (currentRender.mount.isComponentInDom(this.model)) {
148 | var oldElement = this.component.element
149 | beforeUpdate(this.model, oldElement)
150 | this.component.update(this.render())
151 | afterUpdate(this.model, this.component.element, oldElement)
152 | }
153 | }
154 |
155 | Component.prototype.destroy = function (element) {
156 | var self = this
157 |
158 | var meta = hyperdomMeta(this.model)
159 | meta.components.delete(this)
160 |
161 | if (self.model.onbeforeremove) {
162 | self.model.onbeforeremove(element)
163 | }
164 |
165 | if (self.model.onremove) {
166 | var currentRender = render.currentRender()
167 | currentRender.finished.then(function () {
168 | self.model.onremove(element)
169 | })
170 | }
171 |
172 | this.component.destroy()
173 | }
174 |
175 | module.exports = Component
176 |
--------------------------------------------------------------------------------
/bindModel.js:
--------------------------------------------------------------------------------
1 | var listener = require('./listener')
2 | var binding = require('./binding')
3 | var RefreshHook = require('./render').RefreshHook
4 |
5 | module.exports = function (tag, attributes, children) {
6 | var type = inputType(tag, attributes)
7 | var bind = inputTypeBindings[type] || bindTextInput
8 |
9 | bind(attributes, children, binding(attributes.binding))
10 | }
11 |
12 | var inputTypeBindings = {
13 | text: bindTextInput,
14 |
15 | textarea: bindTextInput,
16 |
17 | checkbox: function (attributes, children, binding) {
18 | attributes.checked = binding.get()
19 |
20 | attachEventHandler(attributes, 'onclick', function (ev) {
21 | attributes.checked = ev.target.checked
22 | return binding.set(ev.target.checked)
23 | }, binding)
24 | },
25 |
26 | radio: function (attributes, children, binding) {
27 | var value = attributes.value
28 | attributes.checked = binding.get() === attributes.value
29 | attributes.on_hyperdomsyncchecked = listener(function (event) {
30 | attributes.checked = event.target.checked
31 | })
32 |
33 | attachEventHandler(attributes, 'onclick', function (event) {
34 | var name = event.target.name
35 | if (name) {
36 | var inputs = document.getElementsByName(name)
37 | for (var i = 0, l = inputs.length; i < l; i++) {
38 | inputs[i].dispatchEvent(customEvent('_hyperdomsyncchecked'))
39 | }
40 | }
41 | return binding.set(value)
42 | }, binding)
43 | },
44 |
45 | select: function (attributes, children, binding) {
46 | var currentValue = binding.get()
47 |
48 | var options = []
49 | var tagName
50 | children.forEach(function (child) {
51 | tagName = child.tagName && child.tagName.toLowerCase()
52 |
53 | switch (tagName) {
54 | case 'optgroup':
55 | child.children.forEach(function (optChild) {
56 | if (optChild.tagName && optChild.tagName.toLowerCase() === 'option') {
57 | options.push(optChild)
58 | }
59 | })
60 | break
61 | case 'option':
62 | options.push(child)
63 | break
64 | }
65 | })
66 |
67 | var values = []
68 |
69 | var valueSelected = attributes.multiple
70 | ? function (value) { return currentValue instanceof Array && currentValue.indexOf(value) >= 0 }
71 | : function (value) { return currentValue === value }
72 |
73 | for (var n = 0; n < options.length; n++) {
74 | var option = options[n]
75 | var hasValue = option.properties.hasOwnProperty('value')
76 | var value = option.properties.value
77 | var text = option.children.map(function (x) { return x.text }).join('')
78 |
79 | values.push(hasValue ? value : text)
80 |
81 | var selected = valueSelected(hasValue ? value : text)
82 |
83 | option.properties.selected = selected
84 | }
85 |
86 | attachEventHandler(attributes, 'onchange', function (ev) {
87 | if (ev.target.multiple) {
88 | var options = ev.target.options
89 |
90 | var selectedValues = []
91 |
92 | for (var n = 0; n < options.length; n++) {
93 | var op = options[n]
94 | if (op.selected) {
95 | selectedValues.push(values[n])
96 | }
97 | }
98 | return binding.set(selectedValues)
99 | } else {
100 | attributes.selectedIndex = ev.target.selectedIndex
101 | return binding.set(values[ev.target.selectedIndex])
102 | }
103 | }, binding)
104 | },
105 |
106 | file: function (attributes, children, binding) {
107 | var multiple = attributes.multiple
108 |
109 | attachEventHandler(attributes, 'onchange', function (ev) {
110 | if (multiple) {
111 | return binding.set(ev.target.files)
112 | } else {
113 | return binding.set(ev.target.files[0])
114 | }
115 | }, binding)
116 | }
117 | }
118 |
119 | function inputType (selector, attributes) {
120 | if (/^textarea\b/i.test(selector)) {
121 | return 'textarea'
122 | } else if (/^select\b/i.test(selector)) {
123 | return 'select'
124 | } else {
125 | return attributes.type || 'text'
126 | }
127 | }
128 |
129 | function bindTextInput (attributes, children, binding) {
130 | var textEventNames = ['onkeyup', 'oninput', 'onpaste', 'textInput']
131 |
132 | var bindingValue = binding.get()
133 | if (!(bindingValue instanceof Error)) {
134 | attributes.value = bindingValue !== undefined ? bindingValue : ''
135 | }
136 |
137 | attachEventHandler(attributes, textEventNames, function (ev) {
138 | if (binding.get() !== ev.target.value) {
139 | return binding.set(ev.target.value)
140 | }
141 | }, binding)
142 | }
143 |
144 | function attachEventHandler (attributes, eventNames, handler) {
145 | if (eventNames instanceof Array) {
146 | for (var n = 0; n < eventNames.length; n++) {
147 | insertEventHandler(attributes, eventNames[n], handler)
148 | }
149 | } else {
150 | insertEventHandler(attributes, eventNames, handler)
151 | }
152 | }
153 |
154 | function insertEventHandler (attributes, eventName, handler) {
155 | var previousHandler = attributes[eventName]
156 | if (previousHandler) {
157 | attributes[eventName] = sequenceFunctions(handler, previousHandler)
158 | } else {
159 | attributes[eventName] = handler
160 | }
161 | }
162 |
163 | function sequenceFunctions (handler1, handler2) {
164 | return function (ev) {
165 | handler1(ev)
166 | if (handler2 instanceof RefreshHook) {
167 | return handler2.handler(ev)
168 | } else {
169 | return handler2(ev)
170 | }
171 | }
172 | }
173 |
174 | function customEvent (name) {
175 | if (typeof Event === 'function') {
176 | return new window.Event(name)
177 | } else {
178 | var event = document.createEvent('Event')
179 | event.initEvent(name, false, false)
180 | return event
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/rendering.js:
--------------------------------------------------------------------------------
1 | var vhtml = require('./vhtml')
2 | var domComponent = require('./domComponent')
3 | var bindingMeta = require('./meta')
4 | var toVdom = require('./toVdom')
5 | var parseTag = require('virtual-dom/virtual-hyperscript/parse-tag')
6 | var Mount = require('./mount')
7 | var Component = require('./component')
8 | var render = require('./render')
9 | var deprecations = require('./deprecations')
10 | var prepareAttributes = require('./prepareAttributes')
11 | var binding = require('./binding')
12 | var refreshAfter = require('./refreshAfter')
13 | var refreshEventResult = require('./refreshEventResult')
14 |
15 | exports.append = function (element, render, model, options) {
16 | return startAttachment(render, model, options, function (mount, domComponentOptions) {
17 | var component = domComponent.create(domComponentOptions)
18 | var vdom = mount.render()
19 | element.appendChild(component.create(vdom))
20 | return component
21 | })
22 | }
23 |
24 | exports.replace = function (element, render, model, options) {
25 | return startAttachment(render, model, options, function (mount, domComponentOptions) {
26 | var component = domComponent.create(domComponentOptions)
27 | var vdom = mount.render()
28 | element.parentNode.replaceChild(component.create(vdom), element)
29 | return component
30 | })
31 | }
32 |
33 | exports.appendVDom = function (vdom, render, model, options) {
34 | return startAttachment(render, model, options, function (mount) {
35 | var component = {
36 | create: function (newVDom) {
37 | vdom.children = []
38 | if (newVDom) {
39 | vdom.children.push(toVdom(newVDom))
40 | }
41 | },
42 | update: function (newVDom) {
43 | vdom.children = []
44 | if (newVDom) {
45 | vdom.children.push(toVdom(newVDom))
46 | }
47 | }
48 | }
49 | component.create(mount.render())
50 | return component
51 | })
52 | }
53 |
54 | function startAttachment (render, model, options, attachToDom) {
55 | if (typeof render === 'object') {
56 | return start(render, attachToDom, model)
57 | } else {
58 | deprecations.renderFunction('hyperdom.append and hyperdom.replace with render functions are deprecated, please pass a component')
59 | return start({render: function () { return render(model) }}, attachToDom, options)
60 | }
61 | }
62 |
63 | function start (model, attachToDom, options) {
64 | var mount = new Mount(model, options)
65 | render(mount, function () {
66 | if (options) {
67 | var domComponentOptions = {document: options.document}
68 | }
69 | try {
70 | mount.component = attachToDom(mount, domComponentOptions)
71 | } catch (e) {
72 | mount.component = {
73 | update: function () {},
74 | destroy: function () {}
75 | }
76 | throw e
77 | }
78 | })
79 | return mount
80 | }
81 |
82 | /**
83 | * this function is quite ugly and you may be very tempted
84 | * to refactor it into smaller functions, I certainly am.
85 | * however, it was written like this for performance
86 | * so think of that before refactoring! :)
87 | */
88 | exports.html = function (hierarchySelector) {
89 | var hasHierarchy = hierarchySelector.indexOf(' ') >= 0
90 | var selector, selectorElements
91 |
92 | if (hasHierarchy) {
93 | selectorElements = hierarchySelector.match(/\S+/g)
94 | selector = selectorElements[selectorElements.length - 1]
95 | } else {
96 | selector = hierarchySelector
97 | }
98 |
99 | var childElements
100 | var vdom
101 | var tag
102 | var attributes = arguments[1]
103 |
104 | if (attributes && attributes.constructor === Object && typeof attributes.render !== 'function') {
105 | childElements = toVdom.recursive(Array.prototype.slice.call(arguments, 2))
106 | prepareAttributes(selector, attributes, childElements)
107 | tag = parseTag(selector, attributes)
108 | vdom = vhtml(tag, attributes, childElements)
109 | } else {
110 | attributes = {}
111 | childElements = toVdom.recursive(Array.prototype.slice.call(arguments, 1))
112 | tag = parseTag(selector, attributes)
113 | vdom = vhtml(tag, attributes, childElements)
114 | }
115 |
116 | if (hasHierarchy) {
117 | for (var n = selectorElements.length - 2; n >= 0; n--) {
118 | vdom = vhtml(selectorElements[n], {}, [vdom])
119 | }
120 | }
121 |
122 | return vdom
123 | }
124 |
125 | exports.jsx = function (tag, attributes) {
126 | var childElements = toVdom.recursive(Array.prototype.slice.call(arguments, 2))
127 | if (typeof tag === 'string') {
128 | if (attributes) {
129 | prepareAttributes(tag, attributes, childElements)
130 | }
131 | return vhtml(tag, attributes || {}, childElements)
132 | } else {
133 | return new Component(new tag(attributes || {}, childElements), {viewComponent: true}) // eslint-disable-line new-cap
134 | }
135 | }
136 |
137 | Object.defineProperty(exports.html, 'currentRender', {get: function () {
138 | deprecations.currentRender('hyperdom.html.currentRender is deprecated, please use hyperdom.currentRender() instead')
139 | return render._currentRender
140 | }})
141 |
142 | Object.defineProperty(exports.html, 'refresh', {get: function () {
143 | deprecations.refresh('hyperdom.html.refresh is deprecated, please use component.refresh() instead')
144 | if (render._currentRender) {
145 | var currentRender = render._currentRender
146 | return function (result) {
147 | refreshEventResult(result, currentRender.mount)
148 | }
149 | } else {
150 | throw new Error('Please assign hyperdom.html.refresh during a render cycle if you want to use it in event handlers. See https://github.com/featurist/hyperdom#refresh-outside-render-cycle')
151 | }
152 | }})
153 |
154 | Object.defineProperty(exports.html, 'norefresh', {get: function () {
155 | deprecations.norefresh('hyperdom.html.norefresh is deprecated, please use hyperdom.norefresh() instead')
156 | return refreshEventResult.norefresh
157 | }})
158 |
159 | Object.defineProperty(exports.html, 'binding', {get: function () {
160 | deprecations.htmlBinding('hyperdom.html.binding() is deprecated, please use hyperdom.binding() instead')
161 | return binding
162 | }})
163 |
164 | Object.defineProperty(exports.html, 'refreshAfter', {get: function () {
165 | deprecations.refreshAfter("hyperdom.html.refreshAfter() is deprecated, please use require('hyperdom/refreshAfter')() instead")
166 | return refreshAfter
167 | }})
168 |
169 | exports.html.meta = bindingMeta
170 |
171 | function rawHtml () {
172 | var selector
173 | var html
174 | var options
175 |
176 | if (arguments.length === 2) {
177 | selector = arguments[0]
178 | html = arguments[1]
179 | options = {innerHTML: html}
180 | return exports.html(selector, options)
181 | } else {
182 | selector = arguments[0]
183 | options = arguments[1]
184 | html = arguments[2]
185 | options.innerHTML = html
186 | return exports.html(selector, options)
187 | }
188 | }
189 |
190 | exports.html.rawHtml = function () {
191 | deprecations.htmlRawHtml('hyperdom.html.rawHtml() is deprecated, please use hyperdom.rawHtml() instead')
192 | return rawHtml.apply(undefined, arguments)
193 | }
194 |
195 | exports.rawHtml = rawHtml
196 |
--------------------------------------------------------------------------------
/gh-md-toc:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | #
4 | # Steps:
5 | #
6 | # 1. Download corresponding html file for some README.md:
7 | # curl -s $1
8 | #
9 | # 2. Discard rows where no substring 'user-content-' (github's markup):
10 | # awk '/user-content-/ { ...
11 | #
12 | # 3.1 Get last number in each row like ' ... sitemap.js.*<\/h/)+2, RLENGTH-5)
21 | #
22 | # 5. Find anchor and insert it inside "(...)":
23 | # substr($0, match($0, "href=\"[^\"]+?\" ")+6, RLENGTH-8)
24 | #
25 |
26 | gh_toc_version="0.5.0"
27 |
28 | gh_user_agent="gh-md-toc v$gh_toc_version"
29 |
30 | #
31 | # Download rendered into html README.md by its url.
32 | #
33 | #
34 | gh_toc_load() {
35 | local gh_url=$1
36 |
37 | if type curl &>/dev/null; then
38 | curl --user-agent "$gh_user_agent" -s "$gh_url"
39 | elif type wget &>/dev/null; then
40 | wget --user-agent="$gh_user_agent" -qO- "$gh_url"
41 | else
42 | echo "Please, install 'curl' or 'wget' and try again."
43 | exit 1
44 | fi
45 | }
46 |
47 | #
48 | # Converts local md file into html by GitHub
49 | #
50 | # ➥ curl -X POST --data '{"text": "Hello world github/linguist#1 **cool**, and #1!"}' https://api.github.com/markdown
51 | #
Hello world github/linguist#1 cool, and #1!
'"
52 | gh_toc_md2html() {
53 | local gh_file_md=$1
54 | URL=https://api.github.com/markdown/raw
55 | TOKEN="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt"
56 | if [ -f "$TOKEN" ]; then
57 | URL="$URL?access_token=$(cat $TOKEN)"
58 | fi
59 | OUTPUT="$(curl -s --user-agent "$gh_user_agent" \
60 | --data-binary @"$gh_file_md" -H "Content-Type:text/plain" \
61 | $URL)"
62 |
63 | if [ "$?" != "0" ]; then
64 | echo "XXNetworkErrorXX"
65 | fi
66 | if [ "$(echo "${OUTPUT}" | awk '/API rate limit exceeded/')" != "" ]; then
67 | echo "XXRateLimitXX"
68 | else
69 | echo "${OUTPUT}"
70 | fi
71 | }
72 |
73 |
74 | #
75 | # Is passed string url
76 | #
77 | gh_is_url() {
78 | case $1 in
79 | https* | http*)
80 | echo "yes";;
81 | *)
82 | echo "no";;
83 | esac
84 | }
85 |
86 | #
87 | # TOC generator
88 | #
89 | gh_toc(){
90 | local gh_src=$1
91 | local gh_src_copy=$1
92 | local gh_ttl_docs=$2
93 | local need_replace=$3
94 |
95 | if [ "$gh_src" = "" ]; then
96 | echo "Please, enter URL or local path for a README.md"
97 | exit 1
98 | fi
99 |
100 |
101 | # Show "TOC" string only if working with one document
102 | if [ "$gh_ttl_docs" = "1" ]; then
103 |
104 | echo "Table of Contents"
105 | echo "================="
106 | echo ""
107 | gh_src_copy=""
108 |
109 | fi
110 |
111 | if [ "$(gh_is_url "$gh_src")" == "yes" ]; then
112 | gh_toc_load "$gh_src" | gh_toc_grab "$gh_src_copy"
113 | if [ "${PIPESTATUS[0]}" != "0" ]; then
114 | echo "Could not load remote document."
115 | echo "Please check your url or network connectivity"
116 | exit 1
117 | fi
118 | if [ "$need_replace" = "yes" ]; then
119 | echo
120 | echo "!! '$gh_src' is not a local file"
121 | echo "!! Can't insert the TOC into it."
122 | echo
123 | fi
124 | else
125 | local rawhtml=$(gh_toc_md2html "$gh_src")
126 | if [ "$rawhtml" == "XXNetworkErrorXX" ]; then
127 | echo "Parsing local markdown file requires access to github API"
128 | echo "Please make sure curl is installed and check your network connectivity"
129 | exit 1
130 | fi
131 | if [ "$rawhtml" == "XXRateLimitXX" ]; then
132 | echo "Parsing local markdown file requires access to github API"
133 | echo "Error: You exceeded the hourly limit. See: https://developer.github.com/v3/#rate-limiting"
134 | TOKEN="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt"
135 | echo "or place github auth token here: $TOKEN"
136 | exit 1
137 | fi
138 | local toc=`echo "$rawhtml" | gh_toc_grab "$gh_src_copy"`
139 | echo "$toc"
140 | if [ "$need_replace" = "yes" ]; then
141 | local ts="<\!--ts-->"
142 | local te="<\!--te-->"
143 | local dt=`date +'%F_%H%M%S'`
144 | local ext=".orig.${dt}"
145 | local toc_path="${gh_src}.toc.${dt}"
146 | local toc_footer=""
147 | # http://fahdshariff.blogspot.ru/2012/12/sed-mutli-line-replacement-between-two.html
148 | # clear old TOC
149 | sed -i${ext} "/${ts}/,/${te}/{//!d;}" "$gh_src"
150 | # create toc file
151 | echo "${toc}" > "${toc_path}"
152 | echo -e "\n${toc_footer}\n" >> "$toc_path"
153 | # insert toc file
154 | if [[ "`uname`" == "Darwin" ]]; then
155 | sed -i "" "/${ts}/r ${toc_path}" "$gh_src"
156 | else
157 | sed -i "/${ts}/r ${toc_path}" "$gh_src"
158 | fi
159 | echo
160 | echo "!! TOC was added into: '$gh_src'"
161 | echo "!! Origin version of the file: '${gh_src}${ext}'"
162 | echo "!! TOC added into a separate file: '${toc_path}'"
163 | echo
164 | fi
165 | fi
166 | }
167 |
168 | #
169 | # Grabber of the TOC from rendered html
170 | #
171 | # $1 — a source url of document.
172 | # It's need if TOC is generated for multiple documents.
173 | #
174 | gh_toc_grab() {
175 | # if closed is on the new line, then move it on the prev line
176 | # for example:
177 | # was: The command foo1
178 | #
179 | # became: The command foo1
180 | sed -e ':a' -e 'N' -e '$!ba' -e 's/\n<\/h/<\/h/g' |
181 | # find strings that corresponds to template
182 | grep -E -o '//' | sed 's/<\/code>//' |
185 | # now all rows are like:
186 | # ... .*<\/h/)+2, RLENGTH-5)"](" gh_url substr($0, match($0, "href=\"[^\"]+?\" ")+6, RLENGTH-8) ")"}' | sed 'y/+/ /; s/%/\\x/g')"
191 | }
192 |
193 | #
194 | # Returns filename only from full path or url
195 | #
196 | gh_toc_get_filename() {
197 | echo "${1##*/}"
198 | }
199 |
200 | #
201 | # Options hendlers
202 | #
203 | gh_toc_app() {
204 | local app_name=$(basename $0)
205 | local need_replace="no"
206 |
207 | if [ "$1" = '--help' ] || [ $# -eq 0 ] ; then
208 | echo "GitHub TOC generator ($app_name): $gh_toc_version"
209 | echo ""
210 | echo "Usage:"
211 | echo " $app_name [--insert] src [src] Create TOC for a README file (url or local path)"
212 | echo " $app_name - Create TOC for markdown from STDIN"
213 | echo " $app_name --help Show help"
214 | echo " $app_name --version Show version"
215 | return
216 | fi
217 |
218 | if [ "$1" = '--version' ]; then
219 | echo "$gh_toc_version"
220 | return
221 | fi
222 |
223 | if [ "$1" = "-" ]; then
224 | if [ -z "$TMPDIR" ]; then
225 | TMPDIR="/tmp"
226 | elif [ -n "$TMPDIR" -a ! -d "$TMPDIR" ]; then
227 | mkdir -p "$TMPDIR"
228 | fi
229 | local gh_tmp_md
230 | gh_tmp_md=$(mktemp $TMPDIR/tmp.XXXXXX)
231 | while read input; do
232 | echo "$input" >> "$gh_tmp_md"
233 | done
234 | gh_toc_md2html "$gh_tmp_md" | gh_toc_grab ""
235 | return
236 | fi
237 |
238 | if [ "$1" = '--insert' ]; then
239 | need_replace="yes"
240 | shift
241 | fi
242 |
243 | for md in "$@"
244 | do
245 | echo ""
246 | gh_toc "$md" "$#" "$need_replace"
247 | done
248 |
249 | echo ""
250 | echo "Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)"
251 | }
252 |
253 | #
254 | # Entry point
255 | #
256 | gh_toc_app "$@"
257 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | ## Install
2 |
3 | ```sh
4 | yarn create hyperdom-app --jsx myapp # or npx create-hyperdom-app --jsx myapp
5 | cd myapp
6 | yarn install
7 | ```
8 |
9 | Run dev server:
10 |
11 | ```sh
12 | yarn dev
13 | ```
14 |
15 | Open `http://localhost:5000`
16 |
17 | ## Hyperdom App
18 |
19 | An object with a `render()` method is a valid hyperdom component (we call top level component an app). The one in your project looks like this:
20 |
21 |
22 |
23 | #### ** Javascript **
24 |
25 | _./browser/app.jsx_
26 |
27 | [view code](/docs/codesandbox/get-started-init/browser/app.jsx)
28 |
29 | It's mounted into the DOM in _./browser/index.js_:
30 |
31 | [view code](/docs/codesandbox/get-started-init/browser/index.js)
32 |
33 | [codesandbox](/docs/codesandbox/get-started-init)
34 |
35 | #### ** Typescript **
36 |
37 | _./browser/app.tsx_
38 |
39 | [view code](/docs/codesandbox/get-started-init-ts/browser/app.tsx)
40 |
41 | It's mounted into the DOM in _./browser/index.ts_:
42 |
43 | [view code](/docs/codesandbox/get-started-init-ts/browser/index.ts)
44 |
45 | [codesandbox](/docs/codesandbox/get-started-init-ts)
46 |
47 |
48 |
49 | ## State Management
50 |
51 | It's rare to have to think about state management in Hyperdom. Just like React app, a Hyperdom app is often composed of multiple components. Unlike React though, Hyperdom does not _recreate_ them on each render - your app has total control over how long those components live. Another crucial difference is that Hyperdom always re-renders the whole app, no matter which component triggered an update.
52 |
53 | This means you can use normal JavaScript objects to store state and simply refer to those objects in jsx.
54 |
55 | ## Events and Bindings
56 |
57 | Hyperdom rerenders immediately after each UI event your app handles. There are two ways of handling UI events in hyperdom, event handlers (for things like mouse clicks) and input bindings (for things like text boxes).
58 |
59 | ## Event Handlers
60 |
61 | Event handlers run some code when a user clicks on something. Let's modify our `App` class:
62 |
63 |
64 |
65 | #### ** Javascript **
66 |
67 | _./browser/app.jsx_
68 |
69 | [view code](/docs/codesandbox/get-started-events/browser/app.jsx#L4)
70 |
71 | [codesandbox](/docs/codesandbox/get-started-events)
72 |
73 | #### ** Typescript **
74 |
75 | _./browser/app.tsx_
76 |
77 | [view code](/docs/codesandbox/get-started-events-ts/browser/app.tsx#L4)
78 |
79 | [codesandbox](/docs/codesandbox/get-started-events-ts)
80 |
81 |
82 |
83 | When "Next" link is clicked, the `onclick` handler is executed. After that, hyperdom re-renders (that is, calls the `render()` method, compares the result with the current DOM and updates it if needed).
84 |
85 | Read more about Events [here](api#event-handler-on-attributes)
86 |
87 | ## Input Bindings
88 |
89 | This is how we bind html inputs onto the state. Let's see it in action:
90 |
91 |
92 |
93 | #### ** Javascript **
94 |
95 | _./browser/app.jsx_
96 |
97 | [view code](/docs/codesandbox/get-started-bindings/browser/app.jsx#L13)
98 |
99 | [codesandbox](/docs/codesandbox/get-started-bindings)
100 |
101 | #### ** Typescript **
102 |
103 | _./browser/app.tsx_
104 |
105 | [view code](/docs/codesandbox/get-started-bindings-ts/browser/app.tsx#L19)
106 |
107 | [codesandbox](/docs/codesandbox/get-started-bindings-ts)
108 |
109 |
110 |
111 | Each time user types into the input, hyperdom re-renders.
112 |
113 | Read more about Bindings [here](api#the-binding-attribute)
114 |
115 | ## Calling Ajax
116 |
117 | The above examples represent _synchronous_ state change. Where it gets interesting though is how much trouble it would be to keep the page in sync with the _asynchronous_ changes. Calling an http endpoint is a prime example. Let's make one:
118 |
119 |
120 |
121 | #### ** Javascript **
122 |
123 | _./browser/app.jsx_
124 |
125 | [view code](/docs/codesandbox/get-started-ajax/browser/app.jsx#L15)
126 |
127 | [codesandbox](/docs/codesandbox/get-started-ajax)
128 |
129 | #### ** Typescript **
130 |
131 | _./browser/app.tsx_
132 |
133 | [view code](/docs/codesandbox/get-started-ajax-ts/browser/app.tsx#L25)
134 |
135 | [codesandbox](/docs/codesandbox/get-started-ajax-ts)
136 |
137 |
138 |
139 | When "Have a beer" button is clicked hyperdom executes the `onclick` handler and re-renders - just like in the "Events" example above. Unlike that previous example though, hyperdom spots that the handler returned a promise and schedules _another_ render to be executed when that promise resolves/rejects.
140 |
141 | Note how we take advantage of the two renders rule to toggle "Loading...".
142 |
143 | ## Composing Components
144 |
145 | Our `App` class is getting pretty hairy - why not to extact a component out of it? Like that beer table:
146 |
147 |
148 |
149 | #### ** Javascript **
150 |
151 | _./browser/BeerList.jsx_
152 |
153 | [view code](/docs/codesandbox/get-started-compose/browser/BeerList.jsx#L4)
154 |
155 | And use it in the main app:
156 |
157 | _./browser/app.jsx_
158 |
159 | [view code](/docs/codesandbox/get-started-compose/browser/app.jsx#L3)
160 |
161 | [codesandbox](/docs/codesandbox/get-started-compose)
162 |
163 | #### ** Typescript **
164 |
165 | _./browser/BeerList.tsx_
166 |
167 | [view code](/docs/codesandbox/get-started-compose-ts/browser/BeerList.tsx#L4)
168 |
169 | And use it in the main app:
170 |
171 | _./browser/app.tsx_
172 |
173 | [view code](/docs/codesandbox/get-started-compose-ts/browser/app.tsx#L4)
174 |
175 | [codesandbox](/docs/codesandbox/get-started-compose-ts)
176 |
177 |
178 |
179 | ?> Since `this.beerList` is a component, we can specify it in place in the jsx. Hyperdom will implicitly call its `render()` method.
180 |
181 | ## Routes
182 |
183 | Routing is essential in most non-trivial applications. That's why hyperdom has routing built in - so you don't have to spend time choosing and implementing one.
184 |
185 | We are going to add new routes to the beer site: `/beers` - to show the beers table - and `/beer/:id` to show an individual beer. And, of course, there is still a `/` route.
186 |
187 | First we need to tell hyperdom that this the routing is involved. We do this by mounting the app with a router:
188 |
189 |
190 |
191 | #### ** Javascript **
192 |
193 | _./browser/index.js_
194 |
195 | [view code](/docs/codesandbox/get-started-routing/browser/index.js)
196 |
197 | #### ** Typescript **
198 |
199 | _./browser/index.ts_
200 |
201 | [view code](/docs/codesandbox/get-started-routing-ts/browser/index.ts)
202 |
203 |
204 |
205 | Next, let's define what the top level path `/` is going to render:
206 |
207 |
208 |
209 | #### ** Javascript **
210 |
211 | _./browser/app.jsx_
212 |
213 | [view code](/docs/codesandbox/get-started-routing/browser/app.jsx#L4-L20)
214 |
215 | #### ** Typescript **
216 |
217 | _./browser/app.tsx_
218 |
219 | [view code](/docs/codesandbox/get-started-routing-ts/browser/app.tsx#L4-L20)
220 |
221 |
222 |
223 | The original `render()` method is gone. Two other special methods - `routes()` and (optional) `renderLayout()` - took its place. The former one is where the magic happens, so let's take a closer look. In a nutshell, `routes()` returns a "url -> render function" mapping. A particular render is invoked only if the current url matches. The "key" in that mapping is not actually a string url but a route definition. `routes.home()` in the above example is a route definition.
224 |
225 | We declare those route definitions separately so that they can be used anywhere in the project:
226 |
227 |
228 |
229 | #### ** Javascript **
230 |
231 | _./browser/routes.js_
232 |
233 | [view code](/docs/codesandbox/get-started-routing/browser/routes.js)
234 |
235 | #### ** Typescript **
236 |
237 | _./browser/routes.ts_
238 |
239 | [view code](/docs/codesandbox/get-started-routing-ts/browser/routes.ts)
240 |
241 |
242 |
243 | Route definition can also generate URLs strings. E.g. `routes.beer.href({id: 23})` returns `/beers/23`. This is how a link to `/beers` page looks in our example:
244 |
245 |
246 |
247 | #### ** Javascript **
248 |
249 | _./browser/app.jsx_
250 |
251 | [view code](/docs/codesandbox/get-started-routing/browser/app.jsx#L35-L35)
252 |
253 | #### ** Typescript **
254 |
255 | _./browser/app.tsx_
256 |
257 | [view code](/docs/codesandbox/get-started-routing-ts/browser/app.tsx#L48-L48)
258 |
259 |
260 |
261 | Apart from route definitions, there is one other thing that can be a part of the array returned from `routes()`. If you look closely at the above example, you'll notice `this.beerList` is also there.
262 |
263 | This works because `this.beerList` itself a has a `routes()` and that's where our second path - `/beers` - is mapped onto a render. The pattern then repeats itself with `this.showBeer` plugging in the final `/beers/:id` path.
264 |
265 |
266 |
267 | #### ** Javascript **
268 |
269 | _./browser/BeerList.jsx_
270 |
271 | [view code](/docs/codesandbox/get-started-routing/browser/BeerList.jsx#L3-L26)
272 |
273 | _./browser/Beer.jsx_
274 |
275 | [view code](/docs/codesandbox/get-started-routing/browser/Beer.jsx#L2-L40)
276 |
277 | Here is the entire example on codesandbox:
278 |
279 | [codesandbox](/docs/codesandbox/get-started-routing)
280 |
281 | #### ** Typescript **
282 |
283 | _./browser/BeerList.tsx_
284 |
285 | [view code](/docs/codesandbox/get-started-routing-ts/browser/BeerList.tsx#L3-L26)
286 |
287 | _./browser/Beer.tsx_
288 |
289 | [view code](/docs/codesandbox/get-started-routing-ts/browser/Beer.tsx#L2-L43)
290 |
291 | Here is the entire example on codesandbox:
292 |
293 | [codesandbox](/docs/codesandbox/get-started-routing-ts)
294 |
295 |
296 |
297 | When user navigates to the `/beers` page for the _first_ time, an `onload()` method is called by hyperdom (if provided). In our example, it performs an ajax request to fetch the data. Since the `onload` returns a promise, the UI will render again once the promise is resolved/rejected.
298 |
299 | A similar `onload()` method is implemented on the `/beers/:id` page. Except, if we happen to navigate from the `/beers` page, it won't perform an ajax call, but instead draw from the list of beers fetched previously.
300 |
301 | This is a pretty advanced setup as the app will only call the api once, no matter which page user happens to land on.
302 |
303 | Speaking of `/beers/:id`, note how the `:id` parameter is bound onto a component property using `bindings` property. This is very similar to the input bindings we saw earlier.
304 |
305 | ?> Note how we use a custom `beerId` getter to coerce `:id` param into a number. That's because all url bindings produce string values.
306 |
307 | Learn more about routing [here](api#routing)
308 |
309 | ## Testing
310 |
311 | We've touched all hyperdom bases - there aren't that many! - and this is definitely enough to get you started. To help you keep going past that, `create-hyperdom-app` contains a fast, _full stack_ browser tests powered by [electron-mocha](https://github.com/jprichardson/electron-mocha) runner and [browser-monkey](https://github.com/featurist/browser-monkey) for dom assertions/manipulations. It's only a `yarn test` away.
312 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | ## Rendering the Virtual DOM
2 |
3 | ```js
4 | const vdomFragment = hyperdom.html(selector, [attributes], children, ...);
5 | ```
6 |
7 | * `vdomFragment` - a virtual DOM fragment. This will be compared with the previous virtual DOM fragment, and the differences applied to the real DOM.
8 | * `selector` - (almost) any selector, containing element names, classes and ids: `tag.class#id`, or small hierarchies `pre code`.
9 | * `attributes` - (optional) the attributes of the HTML element, may contain `style`, event handlers, etc.
10 | * `children` - any number of children, which can be arrays of children, strings, or other vdomFragments.
11 |
12 | ### The `binding` Attribute
13 |
14 | Form input elements can be passed a `binding` attribute, which is expected to be either:
15 |
16 | * An array with two items, the first being the model and second the field name, the third being an optional function that is called when the binding is set, for examle, you can initiate some further processing when the value changes.
17 |
18 | ```js
19 | [object, fieldName, setter(value)]
20 | ```
21 |
22 | * `object` - an object
23 | * `fieldName` - the name of a field on `object`
24 | * `setter(value)` (optional) - a function called with the value when setting the model.
25 |
26 | * An object with two methods, `get` and `set`, to get and set the new value, respectively.
27 |
28 | ```js
29 | {
30 | get: function () {
31 | return model.property;
32 | },
33 | set: function (value) {
34 | model.property = value;
35 | },
36 | options: {
37 | // options passed directly to `hyperdom.binding()`
38 | }
39 | }
40 | ```
41 |
42 | ### Event Handler `on*` Attributes
43 |
44 | Event handlers follow the same semantics as normal HTML event handlers. They have the same names, e.g. `onclick`, `onchange`, `onmousedown` etc. They are passed an `Event` object as the first argument.
45 |
46 | When event handlers complete, the entire page's virtual DOM is re-rendered. Of course only the differences will by applied to the real DOM.
47 |
48 | ### Promises
49 |
50 | If the event handler returns a [Promise](https://promisesaplus.com/), then the view is re-rendered after the promise is fulfilled or rejected.
51 |
52 | ## Virtual Dom API
53 |
54 | ### Selectors (`hyperdom.html` only)
55 |
56 | Use `tagname`, with any number of `.class` and `#id`.
57 |
58 | ```js
59 | h('div.class#id', 'hi ', model.name);
60 | ```
61 |
62 | Spaces are taken to be small hierarchies of HTML elements, this will produce `
...
`:
63 |
64 | ```js
65 | h('pre code', 'hi ', model.name);
66 | ```
67 |
68 | ### Add HTML Attributes
69 |
70 | JS
71 |
72 | ```js
73 | h('span', { style: { color: 'red' } }, 'name: ', this.name);
74 | ```
75 |
76 | JSX
77 |
78 | ```jsx
79 | name: {this.name}
80 | name: {this.name}
81 | ```
82 |
83 | [virtual-dom](https://github.com/Matt-Esch/virtual-dom) uses JavaScript names for HTML attributes like `className`, `htmlFor` and `tabIndex`. Hyperdom supports these, but also allows regular HTML names so you can use `class`, `for` and `tabindex`. These are much more familiar to people and you don't have to learn anything new.
84 |
85 | Non-standard HTML attribtes can be placed in the `attributes` key:
86 |
87 | ```js
88 | h('span', {attributes: {'my-html-attribute': 'stuff'}}, 'name: ', model.name);
89 | ```
90 |
91 | ### Keys
92 |
93 | Hyperdom (or rather [virtual-dom](https://github.com/Matt-Esch/virtual-dom)) is not clever enough to be able to compare lists of elements. For example, say you render the following:
94 |
95 | ```jsx
96 |
97 |
one
98 |
two
99 |
three
100 |
101 | ```
102 |
103 | And then, followed by:
104 |
105 | ```jsx
106 |
107 |
zero
108 |
one
109 |
two
110 |
three
111 |
112 | ```
113 |
114 | The lists will be compared like this, and lots of work will be done to change the DOM:
115 |
116 | ```html
117 |
one
=>
zero
(change)
118 |
two
=>
one
(change)
119 |
three
=>
two
(change)
120 |
three
(new)
121 | ```
122 |
123 | If we put a unique `key` (String or Number) into the attributes, then we can avoid all that extra work, and just insert the `
142 | ```
143 |
144 | It will be compared like this, and is much faster:
145 |
146 | ```html
147 |
zero
(new)
148 |
one
=>
one
149 |
two
=>
two
150 |
three
=>
three
151 | ```
152 |
153 | Its not all about performance, there are other things that can be affected by this too, including CSS transitions when CSS classes or style is changed.
154 |
155 | ### Raw HTML
156 |
157 | Insert raw unescaped HTML. Be careful! Make sure there's no chance of script injection.
158 |
159 | ```js
160 | hyperdom.rawHtml('div',
161 | {style: { color: 'red' } },
162 | 'some dangerous HTML'
163 | )
164 | ```
165 |
166 | This can be useful for rendering HTML entities too. For example, to put ` ` in a table cell use `hyperdom.rawHtml('td', ' ')`.
167 |
168 | ### Classes
169 |
170 | Classes have some additional features:
171 |
172 | * a string, e.g. `'item selected'`.
173 | * an array - the classes will be all the items space delimited, e.g. `['item', 'selected']`.
174 | * an object - the classes will be all the keys with truthy values, space delimited, e.g. `{item: true, selected: item.selected}`.
175 |
176 | JS
177 |
178 | ```js
179 | this.items.map(item => {
180 | return h('span', { class: { selected: item == this.selectedItem } }, item.name)
181 | })
182 | ```
183 |
184 | JSX
185 |
186 | ```jsx
187 | this.items.map(item => {
188 | return
{item.name}
189 | })
190 | ```
191 |
192 | ### Joining VDOM Arrays
193 |
194 | You may have an array of vdom elements that you want to join together with a separator, something very much like `Array.prototype.join()`, but for vdom.
195 |
196 | ```jsx
197 | const items = ['one', 'two', 'three']
198 | hyperdom.join(items.map(i => {i}), ', ')
199 | ```
200 |
201 | Will produce this HTML:
202 |
203 | ```html
204 | one, two, three
205 | ```
206 |
207 | ### Data Attributes
208 |
209 | You can use either `data-*` attributes or set the `data` attribute to an object:
210 |
211 | ```jsx
212 | h('div', {'data-stuff': 'something'})
213 | h('div', {dataset: {stuff: 'something'}})
214 |
215 |
216 | ```
217 |
218 | ### Responding to Events
219 |
220 | Pass a function to any regular HTML `on*` event handler in, such as `onclick`. That event handler can modify the state of the application, and once finished, the HTML will be re-rendered to reflect the new state.
221 |
222 | If you return a promise from your event handler then the HTML will be re-rendered twice: once when the event handler initially returns, and again when the promise resolves.
223 |
224 | ```jsx
225 | class App {
226 | constructor() {
227 | this.people = []
228 | }
229 |
230 | addPerson() {
231 | this.people.push({name: 'Person ' + (this.people.length + 1)})
232 | }
233 |
234 | render() {
235 | return
236 |
237 | {
238 | this.people.map(person =>
{person.name}
)
239 | }
240 |
241 |
242 |
243 | }
244 | }
245 |
246 | hyperdom.append(document.body, new App())
247 | ```
248 |
249 | ### Binding the Inputs
250 |
251 | This applies to `textarea` and input types `text`, `url`, `date`, `email`, `color`, `range`, `checkbox`, `number`, and a few more obscure ones. Most of them.
252 |
253 | The `binding` attribute can be used to bind an input to a model field. You can pass either an array `[model, 'fieldName']`, or an object containing `get` and `set` methods: `{get(), set(value)}`. See [bindings](#the_binding_attribute) for more details.
254 |
255 | ```jsx
256 | class App {
257 | render() {
258 | return
259 |
260 |
261 |
hi {this.name}
262 |
263 | }
264 | }
265 | ```
266 |
267 | ### Radio Buttons
268 |
269 | Bind the model to each radio button. The buttons can be bound to complex (non-string) values, such as the `blue` object below.
270 |
271 | ```jsx
272 | const blue = { name: 'blue' };
273 |
274 | class App {
275 | constructor() {
276 | this.colour: blue
277 | }
278 |
279 | render() {
280 | return
281 |
282 |
283 |
284 | colour: {JSON.stringify(this.colour)}
285 |
286 |
287 | }
288 | }
289 |
290 | hyperdom.append(document.body, new App());
291 | ```
292 |
293 | ### Select Dropdowns
294 |
295 | Bind the model onto the `select` element. The `option`s can have complex (non-string) values.
296 |
297 | ```jsx
298 | const blue = { name: 'blue' };
299 |
300 | class App {
301 | constructor() {
302 | this.colour = blue
303 | }
304 |
305 | render() {
306 | return
307 |
311 | {JSON.stringify(this.colour)}
312 |
313 | }
314 | }
315 |
316 | hyperdom.append(document.body, new App());
317 | ```
318 |
319 | ### File Inputs
320 |
321 | The file input is much like any other binding, except that only the binding's `set` method ever called, never the `get` method - the file input can only be set by a user selecting a file.
322 |
323 | ```js
324 | class App {
325 | constructor () {
326 | this.filename = '(no file selected)'
327 | this.contents = ''
328 | }
329 |
330 | render() {
331 | return
332 | this.loadFile(file) } }>
333 |
{this.filename}
334 |
335 | {this.contents}
336 |
337 |
338 | }
339 |
340 | loadFile(file) {
341 | return new Promise((resolve) => {
342 | const reader = new FileReader();
343 | reader.readAsText(file);
344 |
345 | reader.onloadend = () => {
346 | this.filename = file.name;
347 | this.contents = reader.result;
348 | resolve();
349 | };
350 | });
351 | }
352 | }
353 |
354 | hyperdom.append(document.body, new App())
355 | ```
356 |
357 | ### Window Events
358 |
359 | You can attach event handlers to `window`, such as `window.onscroll` and `window.onresize`. Return a `windowEvents()` from your render function passing an object containing the event handlers to attach. When the window vdom is shown, the event handlers are added to `window`, when the window vdom is not shown, the event handlers are removed from `window`.
360 |
361 | E.g. to add an `onresize` handler:
362 |
363 | ```js
364 | const windowEvents = require('hyperdom/windowEvents');
365 |
366 | class App {
367 | render() {
368 | return
369 | width = {window.innerWidth}, height = {window.innerHeight}
370 | {
371 | windowEvents({
372 | onresize: () => console.log('resizing')
373 | })
374 | }
375 | )
376 | }
377 | }
378 | ```
379 |
380 | ### Mapping the model to the view
381 |
382 | Sometimes you have an input that doesn't map cleanly to a view, this is often just because the HTML input element represents a string value, while the model represents something else like a number or a date.
383 |
384 | For this you can use a `mapBinding`, found in `hyperdom/mapBinding`.
385 |
386 | ```jsx
387 | const mapBinding = require('hyperdom/mapBinding')
388 |
389 | const integer = {
390 | view (model) {
391 | // convert the model value to a string for the view
392 | return model.toString()
393 | },
394 |
395 | model (view) {
396 | // convert the input value to an integer for the model
397 | return Number(view)
398 | }
399 | }
400 |
401 |
402 | ```
403 |
404 | As is often the case, it's possible that the user enters an invalid value for the model, for example they type `xyz` into a field that should be a number. When this happens, you can throw an exception on the `model(value)` method. When this happens, the model is not modified, and so keeps the old value, but also, crucially, the view continues to be rendered with the invalid value. This way, the user can go from a valid value, they can pass through some invalid values as they type in finally a valid value. For example, when typing the date `2020-02-04`, it's not until the date is fully typed that it becomes valid.
405 |
406 | ```js
407 | const mapBinding = require('hyperdom/mapBinding')
408 |
409 | const date = {
410 | view (date) {
411 | // convert the model value into the user input value
412 | return `${date.getFullYear()}-${date.getUTCMonth() + 1}-${date.getUTCDate()}`
413 | },
414 | model (view) {
415 | // test the date format
416 | if (!/^\d{4}-\d{2}-\d{2}$/.test(view)) {
417 | // not correct, keep typing
418 | throw new Error('Must be a date of the format YYYY-MM-DD');
419 | } else {
420 | // correct format, set the model
421 | return new Date(view);
422 | }
423 | }
424 | }
425 |
426 |
427 | ```
428 |
429 | Under the hood, hyperdom stores the intermediate value and the exception in the model's [meta](#meta) area. You can get the exception by calling `hyperdom.meta(model, field).error`.
430 |
431 | ## Raw HTML
432 |
433 | **Careful of script injection attacks!** Make sure the HTML is trusted or free of `