Skip to content
This repository has been archived by the owner on Jan 2, 2024. It is now read-only.

Commit

Permalink
Update extension naming
Browse files Browse the repository at this point in the history
  • Loading branch information
Mark Greenwald authored and Mark Greenwald committed Mar 24, 2022
1 parent 01a14b1 commit 3161665
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 45 deletions.
23 changes: 15 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ new Application(document.body);

**View**
```xml
<div data-classes-active="value1"></div>
<div data-classes-active="value2"></div>
<div data-class-active="value1"></div>
<div data-class-active="value2"></div>
```
**Model**
```javascript
Expand All @@ -120,17 +120,17 @@ document.body.map(model);
```
**Result**
```xml
<div class="active" data-classes-active="value1"></div>
<div data-classes-active="value2"></div>
<div class="active" data-class-active="value1"></div>
<div data-class-active="value2"></div>
```

## Mapping an attribute on an element to a boolean on a model
*The `model.value1` and `model.value2` values will determine if the inputs have the `disabled` attribute or not*

**View**
```xml
<input type="text" data-settings-disabled="value1" />
<input type="text" data-settings-disabled="value2" />
<input type="text" data-attribute-disabled="value1" />
<input type="text" data-attribute-disabled="value2" />
```
**Model**
```javascript
Expand All @@ -145,8 +145,8 @@ document.body.map(model);
```
**Result**
```xml
<input type="text" disabled="disabled" data-settings-disabled="value1" />
<input type="text" data-settings-disabled="value2" />
<input type="text" disabled="disabled" data-attribute-disabled="value1" />
<input type="text" data-attribute-disabled="value2" />
```

## Mapping a template to an array of data
Expand Down Expand Up @@ -214,6 +214,13 @@ document.body.map(model);
```

# Version Notes
* 1.0.1
* Improved naming
* HTMLElement.prototype.classes -> Element.prototype.class
* Changed to match the non-plural name change of settings to attribute
* Less likely to become a future conflict (see (Element.prototype.className)[https://developer.mozilla.org/en-US/docs/Web/API/Element/className])
* HTMLElement.prototype.settings -> Element.prototype.attribute
* Changed to better reflect the extensions purpose.
* 1.0.0
* Hello World
* Extensions
Expand Down
206 changes: 206 additions & 0 deletions release/mvw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
(() => {
/*
* Node.prototype.map:
* Extends Node with a map method accepting an object that will
* map values from the model to properties on the elements based
* on attribute configurations on the elements
*
* Usage:
* <span data-property="model.value"></span>
*
* Example:
* var element = document.createElement('span');
* element.setAttribute('data-textContent', 'value'); // equiv to element.textContent = model.value
* element.map({value: 'test'});
*
* Result:
* <span data-textContent="value">test</span>
*/
(() => {
const Prefix = /^data\-/i;
const Scope = Symbol(' _mapping scope_ ');
Node.prototype.map = function(model) {
var view = this;
var scope = arguments[1] || view[Scope] || (view[Scope] = {});
if (scope !== (view[Scope] || (view[Scope] = scope))) { return; }
Array.from(view.attributes || [])
.filter(attribute => Prefix.test(attribute.name))
.forEach(attribute => {
var modelQuery = new ObjectQuery(model, attribute.value, false);
var value = typeof(modelQuery.value) === 'function'
? modelQuery.value.bind(modelQuery.binding)
: modelQuery.value;
var elementSelector = attribute.name.replace(Prefix, '').replace(/\-+/g, '.');
var elementQuery = new ObjectQuery(view, elementSelector, true);
elementQuery.value = value;
});
Array.from(view.childNodes)
.forEach(node => node.map(model, scope));
return view;
}
})();

/*
* ObjectQuery:
* Executes a selector query on an object and provides a
* value getter/setter along with additional details about
* the result of the query.
*
* Usage:
* new ObjectQuery((object)object, (string)selector[, (bool)matchExistingCase])
*
* Example:
* var query = new ObjectQuery({member1: {member2: 123}}, 'member1.member2');
* JSON.stringify(query);
*
* Result:
* {
* "complete": true,
* "binding": {"member2": 123},
* "property": "member2",
* "value": 123
* }
*/
class ObjectQuery {
complete;
binding;
property;
get value() { if (this.complete) { return this.binding[this.property]; } }
set value(value) { if (this.complete) { this.binding[this.property] = value; } }
constructor(object, selector, matchExistingCase = false) {
var query = Array.from(selector.matchAll(/[^\.]+/g)).map(match => match[0]);
while (object != null && query.length) {
this.property = matchExistingCase
? ObjectQuery.#matchMemberCase(object, query.shift())
: query.shift();
object = (this.binding = object)[this.property];
}
this.complete = !query.length && this.binding != null;
}
static #matchMemberCase(object, name) {
if (name in object) { return name; }
var lower = name.toLowerCase();
while (object != null) {
var match = Object.getOwnPropertyNames(object)
.find(m => m.toLowerCase() === lower);
if (match != null) { return match; }
object = Object.getPrototypeOf(object);
}
return name;
}
}

/*
* Element.prototype.attribute:
* Exposes the current attributes of an element as
* members on an object that can be bound/mapped to
*
* Usage:
* <div data-attribute-disabled="model.boolean.value"></div>
*
* Example:
* var element = document.createElement('div');
* element.attribute.test = true; // equiv to element.setAttribute('test', 'test');
* element.attribute['hyphenated-attribute'] = 'value'; // equiv to element.setAttribute('hyphenated-attribute', 'value')
* element.attribute.removed = false; // equiv to element.removeAttribute('removed')
*
* Result:
* <div test="test" hyphenated-attribute="value"></div>
*/
Object.defineProperty(Element.prototype, 'attribute', {
configurable: false, enumerable: true,
get: function() {
var element = this;
return new Proxy({}, {
has: (_, name) => element.hasAttribute(name),
get: (_, name) => element.hasAttribute(name) ? element.getAttribute(name) : false,
set: (_, name, value) => (value != null && value !== false)
? element.setAttribute(name, value === true ? name : value) || true
: element.removeAttribute(name) || true,
ownKeys: () => Array.from(element.attributes).map(a => a.name)
});
}
});

/*
* Element.prototype.class:
* Exposes the current classList of an element as boolean
* members on an object that can be bound/mapped to
*
* Usage:
* <div data-class-test="model.boolean.value"></div>
*
* Example:
* var element = document.createElement('div');
* element.class.test = true; // equiv to element.classList.add('test')
* element.class['hyphenated-class'] = true; // equiv to element.classList.add('hyphenated-class')
* element.class.removed = false; // equiv to element.classList.remove('removed')
*
* Result:
* <div class="test hyphenated-class"></div>
*/
Object.defineProperty(Element.prototype, 'class', {
configurable: false, enumerable: true,
get: function() {
var element = this;
return new Proxy({}, {
has: (_, name) => element.classList.contains(name),
get: (_, name) => element.classList.contains(name),
set: (_, name, value) => (element.classList.remove(name) || !!value && element.classList.add(name)) || true,
ownKeys: () => Array.from(element.classList),
});
}
});

/*
* HTMLTemplateElement.prototype.template:
* Exposes a binding/mapping member on HTMLTemplateElements
* that will render objects & arrays of objects to the DOM
* immediately after itself
*
* Usage:
* <template data-template="model.array.value"></template>
*
* Example:
* var parent = document.createElement('div');
* var template = parent.appendChild(document.createElement('template'));
* template.innerHTML = '<span data-textContent="value"></span>';
* template.template = [
* {value: 'a'},
* {value: 'b'},
* {value: 'c'}
* ];
*
* Result:
* <div>
* <template><span data-textContent="value"></span></template>
* <span data-textContent="value">a</span>
* <span data-textContent="value">b</span>
* <span data-textContent="value">c</span>
* </div>
*/
(() => {
const Cleanup = Symbol(' _template cleanup_ ');
Object.defineProperty(HTMLTemplateElement.prototype, 'template', {
configurable: false, enumerable: false,
get: function() { return this.content.cloneNode(true); },
set: function(value) {
var element = this;
if (Cleanup in element) { element[Cleanup](); }
if (!element.parentNode) { return; }
var content = (Array.isArray(value) ? value : [value])
.filter(v => v != null)
.map(model => element.template.map(model))
.flatMap(fragment => Array.from(fragment.childNodes));
element[Cleanup] = () => {
content.forEach(e => e.parentNode && e.parentNode.removeChild(e));
delete element[Cleanup];
};
element.parentNode.insertBefore(
content.reduce((frag,node) => frag.appendChild(node)&&frag, document.createDocumentFragment()),
element.nextSibling
)
}
});
})();
})();
1 change: 1 addition & 0 deletions release/mvw.minified.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions samples/CalendarDesigner.html
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@
}
</style>
</head>
<body data-classes-leapyear="year.leapyear" data-style-backgroundImage="background" data-ondrop="setBackgroundImage">
<body data-class-leapyear="year.leapyear" data-style-backgroundImage="background" data-ondrop="setBackgroundImage">
<header>
<div class="no-print">
<span data-onclick="setBackgroundUrl">drop an image or click here to set background</span>
Expand Down Expand Up @@ -296,7 +296,7 @@ <h1>
<template data-template="calendar.weeks">
<ul class="week">
<template data-template="days">
<li class="day" data-settings-disabled="inactive">
<li class="day" data-attribute-disabled="inactive">
<aside class="date">
<span data-textContent="date"></span>
<button class="tiny safe" data-onclick="createEvent">+</button>
Expand All @@ -323,15 +323,15 @@ <h1>
<span>Begins:</span>
<select name="begin" data-value="data.begin">
<template data-template="data.beginTimes">
<option data-textContent="value" data-settings-selected="active"></option>
<option data-textContent="value" data-attribute-selected="active"></option>
</template>
</select>
</label>
<label>
<span>Ends:</span>
<select name="end" data-value="data.end">
<template data-template="data.endTimes">
<option data-textContent="value" data-settings-selected="active"></option>
<option data-textContent="value" data-attribute-selected="active"></option>
</template>
</select>
</label>
Expand Down Expand Up @@ -360,8 +360,8 @@ <h1>
</template>
<script id="js-mvw">
// https://github.com/DataDink/mvw
// version 1.0.0
(() => {(()=>{const e=/^data\-/i,t=Symbol(" _mapping scope_ ");Node.prototype.map=function(r){var n=this,a=arguments[1]||n[t]||(n[t]={});if(a===(n[t]||(n[t]=a)))return Array.from(n.attributes||[]).filter((t=>e.test(t.name))).forEach((t=>{var a=new ObjectQuery(r,t.value,!1),i="function"==typeof a.value?a.value.bind(a.binding):a.value,o=t.name.replace(e,"").replace(/\-+/g,".");new ObjectQuery(n,o,!0).value=i})),Array.from(n.childNodes).forEach((e=>e.map(r,a))),n}})();class ObjectQuery{complete;binding;property;get value(){if(this.complete)return this.binding[this.property]}set value(e){this.complete&&(this.binding[this.property]=e)}constructor(e,t,r=!1){for(var n=Array.from(t.matchAll(/[^\.]+/g)).map((e=>e[0]));null!=e&&n.length;)this.property=r?ObjectQuery.#e(e,n.shift()):n.shift(),e=(this.binding=e)[this.property];this.complete=!n.length&&null!=this.binding}static#e(e,t){if(t in e)return t;for(var r=t.toLowerCase();null!=e;){var n=Object.getOwnPropertyNames(e).find((e=>e.toLowerCase()===r));if(null!=n)return n;e=Object.getPrototypeOf(e)}return t}}Object.defineProperty(HTMLElement.prototype,"classes",{configurable:!1,enumerable:!0,get:function(){var e=this;return new Proxy({},{has:(t,r)=>e.classList.contains(r),get:(t,r)=>e.classList.contains(r),set:(t,r,n)=>e.classList.remove(r)||!!n&&e.classList.add(r)||!0,ownKeys:()=>Array.from(e.classList)})}}),Object.defineProperty(HTMLElement.prototype,"settings",{configurable:!1,enumerable:!0,get:function(){var e=this;return new Proxy({},{has:(t,r)=>e.hasAttribute(r),get:(t,r)=>!!e.hasAttribute(r)&&e.getAttribute(r),set:(t,r,n)=>null!=n&&!1!==n?e.setAttribute(r,!0===n?r:n)||!0:e.removeAttribute(r)||!0,ownKeys:()=>Array.from(e.attributes).map((e=>e.name))})}}),(()=>{const e=Symbol(" _template cleanup_ ");Object.defineProperty(HTMLTemplateElement.prototype,"template",{configurable:!1,enumerable:!1,get:function(){return this.content.cloneNode(!0)},set:function(t){var r=this;if(e in r&&r[e](),r.parentNode){var n=(Array.isArray(t)?t:[t]).filter((e=>null!=e)).map((e=>r.template.map(e))).flatMap((e=>Array.from(e.childNodes)));r[e]=()=>{n.forEach((e=>e.parentNode&&e.parentNode.removeChild(e))),delete r[e]},r.parentNode.insertBefore(n.reduce(((e,t)=>e.appendChild(t)&&e),document.createDocumentFragment()),r.nextSibling)}}})})();})();
// version 1.0.1
(() => {(()=>{const e=/^data\-/i,t=Symbol(" _mapping scope_ ");Node.prototype.map=function(r){var n=this,a=arguments[1]||n[t]||(n[t]={});if(a===(n[t]||(n[t]=a)))return Array.from(n.attributes||[]).filter((t=>e.test(t.name))).forEach((t=>{var a=new ObjectQuery(r,t.value,!1),i="function"==typeof a.value?a.value.bind(a.binding):a.value,o=t.name.replace(e,"").replace(/\-+/g,".");new ObjectQuery(n,o,!0).value=i})),Array.from(n.childNodes).forEach((e=>e.map(r,a))),n}})();class ObjectQuery{complete;binding;property;get value(){if(this.complete)return this.binding[this.property]}set value(e){this.complete&&(this.binding[this.property]=e)}constructor(e,t,r=!1){for(var n=Array.from(t.matchAll(/[^\.]+/g)).map((e=>e[0]));null!=e&&n.length;)this.property=r?ObjectQuery.#e(e,n.shift()):n.shift(),e=(this.binding=e)[this.property];this.complete=!n.length&&null!=this.binding}static#e(e,t){if(t in e)return t;for(var r=t.toLowerCase();null!=e;){var n=Object.getOwnPropertyNames(e).find((e=>e.toLowerCase()===r));if(null!=n)return n;e=Object.getPrototypeOf(e)}return t}}Object.defineProperty(HTMLElement.prototype,"attribute",{configurable:!1,enumerable:!0,get:function(){var e=this;return new Proxy({},{has:(t,r)=>e.hasAttribute(r),get:(t,r)=>!!e.hasAttribute(r)&&e.getAttribute(r),set:(t,r,n)=>null!=n&&!1!==n?e.setAttribute(r,!0===n?r:n)||!0:e.removeAttribute(r)||!0,ownKeys:()=>Array.from(e.attributes).map((e=>e.name))})}}),Object.defineProperty(HTMLElement.prototype,"class",{configurable:!1,enumerable:!0,get:function(){var e=this;return new Proxy({},{has:(t,r)=>e.classList.contains(r),get:(t,r)=>e.classList.contains(r),set:(t,r,n)=>e.classList.remove(r)||!!n&&e.classList.add(r)||!0,ownKeys:()=>Array.from(e.classList)})}}),(()=>{const e=Symbol(" _template cleanup_ ");Object.defineProperty(HTMLTemplateElement.prototype,"template",{configurable:!1,enumerable:!1,get:function(){return this.content.cloneNode(!0)},set:function(t){var r=this;if(e in r&&r[e](),r.parentNode){var n=(Array.isArray(t)?t:[t]).filter((e=>null!=e)).map((e=>r.template.map(e))).flatMap((e=>Array.from(e.childNodes)));r[e]=()=>{n.forEach((e=>e.parentNode&&e.parentNode.removeChild(e))),delete r[e]},r.parentNode.insertBefore(n.reduce(((e,t)=>e.appendChild(t)&&e),document.createDocumentFragment()),r.nextSibling)}}})})();})();
</script>
<script id="js-application">
class Application {
Expand Down Expand Up @@ -647,7 +647,7 @@ <h1>
</script>
<script id="js-bootstrap">
new Application(document);
(() => {
(() => { // Page sizing information dumped to console to assist with asset creation
function formatNumber(number) {
return (Math.round(number*1000)/1000).toString().replace(/(\.\d{1,3})\d*$/, '$1');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
/*
* HTMLElement.prototype.settings:
* Element.prototype.attribute:
* Exposes the current attributes of an element as
* members on an object that can be bound/mapped to
*
* Usage:
* <div data-settings-disabled="model.boolean.value"></div>
* <div data-attribute-disabled="model.boolean.value"></div>
*
* Example:
* var element = document.createElement('div');
* element.settings.test = true; // equiv to element.setAttribute('test', 'test');
* element.settings['hyphenated-attribute'] = 'value'; // equiv to element.setAttribute('hyphenated-attribute', 'value')
* element.settings.removed = false; // equiv to element.removeAttribute('removed')
* element.attribute.test = true; // equiv to element.setAttribute('test', 'test');
* element.attribute['hyphenated-attribute'] = 'value'; // equiv to element.setAttribute('hyphenated-attribute', 'value')
* element.attribute.removed = false; // equiv to element.removeAttribute('removed')
*
* Result:
* <div test="test" hyphenated-attribute="value"></div>
*/
Object.defineProperty(HTMLElement.prototype, 'settings', {
Object.defineProperty(Element.prototype, 'attribute', {
configurable: false, enumerable: true,
get: function() {
var element = this;
Expand Down
Loading

0 comments on commit 3161665

Please sign in to comment.