@webhandle/backbone-view 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,167 @@
1
+ # Webhandle Backbone View
2
+
3
+ A replacement for Backbone's View.
4
+
5
+ I really like Backbone's View. It's a perfect level of abstraction and container for when a framework
6
+ is too big and managing a few different object types becomes messy.
7
+
8
+ However, it requires jQuery and Underscore, which add a staggering 600k to the bundle making it unsuitable for the
9
+ creation of little modules. Additionally, its code was written to run on older IE browsers and still
10
+ uses that packaging and those techniques.
11
+
12
+ While that level of backward compatibility can be useful, I want to write code that uses classes,
13
+ modules, etc, and want my tools to support that.
14
+
15
+
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install @webhandle/backbone-view
21
+ ```
22
+
23
+ It's about 1027 bytes zipped as part of a webpack bundle, so about 1% of the size of Backbone and its
24
+ dependencies.
25
+
26
+
27
+ ## Usage
28
+
29
+
30
+ Import like
31
+
32
+ ```js
33
+ const View = require('@webhandle/backbone-view').View
34
+ ```
35
+
36
+ or
37
+
38
+ ```js
39
+ import { View } from '@webhandle/backbone-view'
40
+ ```
41
+
42
+ [Backbone's actual documentation](https://backbonejs.org/#View) is still pretty descriptive of what this does.
43
+ But in brief, the basic process is to extend View and define how the data will be rendered and how events will be handled.
44
+
45
+
46
+ Setting callbacks to handle events is done by creating an events object, where `this.events` is a hash like:
47
+
48
+ ```json
49
+ {"event selector": "callback"}
50
+ ```
51
+
52
+ Events are things like `click` and `mousedown`. Selectors are standard css selectors like `button`, `footer .ok`, or `.ok`. Each
53
+ selector is assumed to target only elements within the view. A special selector value, `.`, is used to indicate the
54
+ root element of the view.
55
+
56
+ Events are handled by event listeners on the view's root element. That means that while you have to have the
57
+ events defined when the view is created, you can change the content of the view's dom section however you want and
58
+ whenever you want. Events on those new elements will still get handled.
59
+
60
+ Callbacks are either a string, which refers to a method of the view, or functions. In either case the method/function
61
+ will be called with the view as the `this` object. These functions will receive the dom event and the element matching
62
+ the selector which the event bubbled through. This may not be the same as the `evt.target` since a child element of the matching
63
+ element may have been the actual thing clicked (or whatever). Essentially, the second argument is what the `evt.target` would
64
+ have been if the event listener had been added directly to that element instead of the view root.
65
+
66
+ Rendering (via the `render` method) is the most free from. It requires that the contents of `this.el` (the view's root element)
67
+ get modified to show content appropriate to `this.model`. You can use any templating library or no templating library at all.
68
+
69
+ Include the view into the page by adding the view object's `el` (the root element of the view) to any place in the dom. There
70
+ are two convience functions `appendTo` and `replaceContentsOf`.
71
+
72
+ And don't forget to call `render`, either before or after the element is added to the dom, or you won't see any content.
73
+
74
+
75
+ ```js
76
+ import { View } from '@webhandle/backbone-view'
77
+
78
+ class DollarView extends View {
79
+ preinitialize() {
80
+ this.events = {
81
+ 'click .one': 'oneClicked',
82
+ 'click .': function (evt) {
83
+ console.log(`${this.model} it got clicked`)
84
+ }
85
+ }
86
+ }
87
+
88
+ render() {
89
+ this.el.innerHTML = "$" + `<span id="${this.id}one" class="one"><span id="${this.id}two" class="two">${parseFloat(this.model)}</span></span>`
90
+ return this
91
+ }
92
+
93
+ oneClicked(evt, selected) {
94
+ console.log(`one clicked ${selected.id}`)
95
+ }
96
+ }
97
+
98
+ let one = new DollarView({
99
+ model: 123.45
100
+ })
101
+
102
+ one.render()
103
+ one.appendTo(document.body)
104
+ ```
105
+
106
+ A view will accept the options 'model', 'el', 'id', 'attributes', 'className', 'tagName', and 'events'.
107
+ It's possible to pass these in as options to an instance or as options via the subclass's constructor.
108
+ However, I think it looks cleaner to implement the `preinitialize` function, which is called before
109
+ the view creates any elements.
110
+
111
+
112
+ ## Differences from Backbone
113
+
114
+ As mentioned above, there are no dependencies. The View also lacks some of the functions used by Backbone
115
+ for interaction with its Model and Collections components. Since there is no jQuery, some of the members
116
+ like `this.$el` don't exist and events will be plain HTML dom events.
117
+
118
+ Backbone also limits which members of the options will be added to the View instance object to those listed
119
+ above. That seems unnecessarily limiting so this View adds all members.
120
+
121
+ While broadly compatible, I wouldn't expect the behaviors to be identical.
122
+
123
+ ## An Only Slightly More Complicated Example
124
+
125
+ ```js
126
+ import { View } from '@webhandle/backbone-view'
127
+
128
+ class DollarView extends View {
129
+ preinitialize() {
130
+ this.events = {
131
+ 'click .one': 'oneClicked'
132
+ , 'click .math-button': 'doMath'
133
+ , 'click .': function (evt) {
134
+ console.log(`${this.model} it got clicked`)
135
+ }
136
+ }
137
+ }
138
+
139
+ render() {
140
+ this.el.innerHTML = "$" + `<span id="${this.id}one" class="one"><span id="${this.id}two" class="two">${parseFloat(this.model)}</span></span><button class="add-one math-button">Add One</button><button class="substract-one math-button">Subtract One</button>`
141
+ return this
142
+ }
143
+
144
+ oneClicked(evt, selected) {
145
+ console.log(`one clicked ${selected.id}`)
146
+ }
147
+
148
+ doMath(evt, selected) {
149
+ if(selected.classList.contains('add-one')) {
150
+ this.model = this.model + 1
151
+ }
152
+ else if(selected.classList.contains('subtract-one')) {
153
+ this.model = this.model - 1
154
+ }
155
+ this.el.querySelector('.two').innerHTML = parseFloat(this.model)
156
+ console.log(this.randomInfo)
157
+ }
158
+ }
159
+
160
+ let one = new DollarView({
161
+ model: 1.45
162
+ , randomInfo: 20
163
+ })
164
+
165
+ one.render()
166
+ one.appendTo(document.body)
167
+ ```
@@ -0,0 +1,16 @@
1
+ export default function eventEntryMapper([key, value]) {
2
+ key = key.trim()
3
+ let parts = key.split(' ')
4
+ let event = parts.shift().trim()
5
+ let selector = parts.join(' ').trim()
6
+
7
+ if(typeof value === 'string') {
8
+ value = value.trim()
9
+ }
10
+
11
+ return {
12
+ event: event,
13
+ selector: selector,
14
+ handler: value
15
+ }
16
+ }
@@ -0,0 +1,7 @@
1
+ export default function extractEventNames(eventTriggers) {
2
+ let eventNames = Array.from(eventTriggers.reduce((acc, trigger) => {
3
+ acc.add(trigger.event)
4
+ return acc
5
+ }, new Set()))
6
+ return eventNames
7
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Generates a random string id in the browser. Will probably not work
3
+ * on the server.
4
+ * @returns A base64 web url safe string
5
+ */
6
+ export default function generateId() {
7
+ let array = new Uint8Array(32)
8
+ window.crypto.getRandomValues(array)
9
+ let value = btoa(array)
10
+ value = value.replace(/\//g, "_").replace(/\+/g, "-").replace(/=+$/, "")
11
+ return value
12
+ }
@@ -0,0 +1,5 @@
1
+
2
+ import {View} from "./view.js"
3
+ export {
4
+ View
5
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Duplicates and returns an object with only the speicified keys.
3
+ * @param {object} foucs
4
+ * @param {string[]} keys
5
+ */
6
+ export default function pick(focus = {}, keys = []) {
7
+ let clone = {}
8
+ for(let key of keys) {
9
+ if(typeof focus[key] !== 'undefined'){
10
+ clone[key] = focus[key]
11
+ }
12
+ }
13
+ return clone
14
+ }
@@ -0,0 +1,199 @@
1
+ import generateId from "./generate-id.js"
2
+ // import pick from "./pick.js"
3
+ import eventEntryMapper from "./event-entry-mapper.js"
4
+ import extractEventNames from "./extract-event-names.js"
5
+
6
+ let defaultOptions = {
7
+ // The default `tagName` of a View's element is `"div"`.
8
+ tagName: 'div'
9
+
10
+ , events: {}
11
+
12
+ }
13
+ let viewOptions = ['model', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
14
+
15
+ /**
16
+ * A way to connect data to be displayed, a way to display it, and an organization
17
+ * of functions to handle events.
18
+ */
19
+ export class View {
20
+ constructor(options) {
21
+ this.id = generateId()
22
+ Object.assign(this, defaultOptions)
23
+ this.preinitialize.apply(this, arguments);
24
+ Object.assign(this, options)
25
+ this._ensureElement()
26
+ this.initialize.apply(this, arguments);
27
+ }
28
+
29
+
30
+ /**
31
+ * preinitialize is an empty function by default. You can override it with a function
32
+ * or object. preinitialize will run before any instantiation logic is run in the View
33
+ */
34
+ preinitialize() { }
35
+
36
+ /**
37
+ * Initialize is an empty function by default. Override it with your own
38
+ * initialization logic.
39
+ */
40
+ initialize() { }
41
+
42
+ /**
43
+ * **render** is the core function that your view should override, in order
44
+ * to populate its element (`this.el`), with the appropriate HTML. The
45
+ * convention is for **render** to always return `this`.
46
+ * @returns this
47
+ */
48
+ render() {
49
+ return this
50
+ }
51
+
52
+ /**
53
+ * Removes the element from the dom. Does not disable event listeners
54
+ */
55
+ remove() {
56
+ this.el.parentElement.removeChild(this.el)
57
+ }
58
+
59
+ /**
60
+ * Adds this view as a child to a containing element. Nothing special is going on here.
61
+ * This is just a shortcut for container.appendChild
62
+ * @param {Element} container
63
+ */
64
+ appendTo(container) {
65
+ container.appendChild(this.el)
66
+ }
67
+
68
+ /**
69
+ * Clears the contents of the container and adds this view.
70
+ * @param {Element} container
71
+ */
72
+ replaceContentsOf(container) {
73
+ container.innerHTML = ''
74
+ this.appendTo(container)
75
+ }
76
+
77
+ /**
78
+ * Set the element for this view, and if new, adds listeners to it in accordance
79
+ * with the "events" member.
80
+ * @param {Element} el The dom element which will be the root of this view
81
+ * @returns this
82
+ */
83
+ setElement(el) {
84
+ if (this.el !== el) {
85
+ this.el = el
86
+ this._addListeners()
87
+ }
88
+ return this
89
+ }
90
+
91
+ /**
92
+ * Produces a DOM element to be assigned to your view. Exposed for
93
+ * subclasses using an alternative DOM manipulation API.
94
+ * @param {string} name The element tag name
95
+ * @returns The dom element
96
+ */
97
+ _createElement(name) {
98
+ let el = document.createElement(name)
99
+ el.view = this
100
+ return el
101
+ }
102
+
103
+ /**
104
+ * Ensures that the element exists. Applies attributes and className
105
+ * to it regardless
106
+ */
107
+ _ensureElement() {
108
+ if (!this.el) {
109
+ this.setElement(this._createElement(this.tagName))
110
+ }
111
+ this._setAttributes()
112
+ if (this.className) {
113
+ this.el.classList.add(this.className)
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Set attributes from a hash on this view's element. Exposed for
119
+ * subclasses using an alternative DOM manipulation API.
120
+ * @param {object} attributes
121
+ */
122
+ _setAttributes(attributes) {
123
+ if (this.attributes) {
124
+ for (let [key, value] of Object.entries(this.attributes)) {
125
+ this.el.setAttribute(key, value)
126
+ }
127
+ }
128
+ }
129
+
130
+ /**
131
+ *
132
+ * Set callbacks, where `this.events` is a hash of
133
+ * *{"event selector": "callback"}*
134
+ *
135
+ * {
136
+ * 'mousedown .title': 'edit',
137
+ * 'click .button': 'save',
138
+ * 'click .open': function(e) { ... },
139
+ * 'keydown .': 'handleKey'
140
+ * }
141
+ * pairs. Callbacks will be bound to the view, with `this` set properly.
142
+ *
143
+ *
144
+ * Note that the selector `.` will match the root element and can be used
145
+ * as a final chance to handle events or for events like an escape key
146
+ * which are essentially global to the widget.
147
+ *
148
+ */
149
+ _addListeners() {
150
+ this.eventTriggers = Object.entries(this.events).map(eventEntryMapper)
151
+ let eventNames = extractEventNames(this.eventTriggers)
152
+
153
+ for(let eventName of eventNames) {
154
+ this.el.addEventListener(eventName, this._eventHandler.bind(this))
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Get the elements from the view which match the selector
160
+ * @param {string} selector A css selector. `.` will select the root element
161
+ * @returns An array of elements
162
+ */
163
+ _getCandidates(selector) {
164
+ if(selector === '.') {
165
+ return [this.el]
166
+ }
167
+ return Array.from(this.el.querySelectorAll(selector))
168
+ }
169
+
170
+ /**
171
+ * Handles all events for all elements within the view. It attempts to find a
172
+ * trigger matching the event and then process it. It will match and invoke
173
+ * only one trigger.
174
+ * @param {Event} evt
175
+ */
176
+ _eventHandler(evt) {
177
+ for(let trigger of this.eventTriggers) {
178
+ if(evt.type == trigger.event) {
179
+ let candidates = this._getCandidates(trigger.selector)
180
+ let found = null
181
+ for(let candidate of candidates) {
182
+ if(candidate === evt.target || candidate.contains(evt.target)) {
183
+ found = candidate
184
+ break
185
+ }
186
+ }
187
+ if(found) {
188
+ if(typeof trigger.handler === 'string') {
189
+ this[trigger.handler].call(this, evt, found)
190
+ }
191
+ else if(typeof trigger.handler === 'function') {
192
+ trigger.handler.call(this, evt, found)
193
+ }
194
+ break
195
+ }
196
+ }
197
+ }
198
+ }
199
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@webhandle/backbone-view",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "client-js/index.js",
6
+ "scripts": {
7
+ "test": "node_modules/mocha/bin/mocha",
8
+ "less-build": "npx lessc --source-map --source-map-include-source less/pages.less public/css/pages.css",
9
+ "less-compress": "npx uglifycss public/css/pages.css > public/css/pages.min.css",
10
+ "client-js-build": "npx webpack --config pages.webpack.cjs",
11
+ "client-js-compress": "npx uglifyjs public/js/pages.cjs -o public/js/pages.min.cjs -c --source-map url=public/js/pages.min.cjs.map",
12
+ "client-js-browserify-build": "npm run client-js-pages-browserify-build",
13
+ "client-js-pages-browserify build": "npx browserify client-js/pages.cjs --debug | npx exorcist public/js/pages.cjs.map > public/js/pages.cjs",
14
+ "dev-less-watch": "onchange 'less/**/*.less' -- npm run less-build",
15
+ "dev-client-js-watch": "onchange 'client-js/**/*js' 'client-js-test/**/*js' -- npm run client-js-build",
16
+ "dev-server-js-watch": "onchange 'server-js/**/*js' -- pm2 restart $npm_package_name-web",
17
+ "start": "node ./web-server.js",
18
+ "testDebug": "node --inspect-brk node_modules/mocha/bin/mocha",
19
+ "bg": "parallelshell 'npm run dev-less-watch' 'npm run dev-client-js-watch'",
20
+ "pm2-bg": "parallelshell 'npm run dev-less-watch' 'npm run dev-client-js-watch' 'npm run dev-server-js-watch'",
21
+ "dev": "parallelshell 'npm run start' 'npm run dev-less-watch' 'npm run dev-client-js-watch'"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/EmergentIdeas/webhandle-backbone-view.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/EmergentIdeas/webhandle-backbone-view/issues"
29
+ },
30
+ "homepage": "https://github.com/EmergentIdeas/webhandle-backbone-view#readme",
31
+ "keywords": [
32
+ "Backbone",
33
+ "View"
34
+ ],
35
+ "author": "Dan Kolz",
36
+ "license": "ISC",
37
+ "devDependencies": {
38
+ "browserify": "^14.4.0",
39
+ "chai": "^4.3.4",
40
+ "exorcist": "^2.0.0",
41
+ "express": "^4.17.1",
42
+ "file-sink": "^1.0.4",
43
+ "filter-log": "0.0.5",
44
+ "input-value-injector": "^1.0.8",
45
+ "less": "^3.10.3",
46
+ "mocha": "^9.1.3",
47
+ "node-polyfill-webpack-plugin": "^2.0.1",
48
+ "onchange": "^3.2.1",
49
+ "parallelshell": "3.0.1",
50
+ "tripartite": "^1.1.1",
51
+ "uglify-js": "^3.17.4",
52
+ "webhandle": "^1.0.32",
53
+ "webhandle-js-widget-setup": "^1.0.5",
54
+ "webpack-cli": "^5.1.4"
55
+ },
56
+ "browserify": {
57
+ "transform": [
58
+ "tripartite/browserify-transform"
59
+ ]
60
+ },
61
+ "files": [
62
+ "/client-js",
63
+ "README.md"
64
+ ],
65
+ "type": "module"
66
+ }