Skip to content

Commit

Permalink
Initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewp committed Aug 24, 2016
0 parents commit 3dff9ab
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
10 changes: 10 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
language: node_js
node_js: 6.3
before_script:
- npm install -g testee
script: npm test
before_install:
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"
addons:
firefox: "47.0"
118 changes: 118 additions & 0 deletions attr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
var forEach = Array.prototype.forEach;

class CustomAttributeRegistry {
constructor(ownerDocument){
if(!ownerDocument) {
throw new Error("Must be given a document");
}

this.ownerDocument = ownerDocument;
this._attrs = {};
this._elementMap = new WeakMap();
this._observe();
}

define(attrName, Constructor) {
this._attrs[attrName] = Constructor;
this._upgrade(attrName);
}

get(attrName){
return this._attrs[attrName];
}

_observe(){
var customAttributes = this;
var document = this.ownerDocument;
var root = document.documentElement;

this.attrMO = new MutationObserver(function(mutations){
forEach.call(mutations, function(m){
var attr = customAttributes.get(m.attributeName);
if(attr) {
customAttributes._found(m.attributeName, m.target, m.oldValue);
}
});
});

this.attrMO.observe(root, {
subtree: true,
attributes: true,
attributeOldValue: true
});

this.childMO = new MutationObserver(function(mutations){
var downgrade = customAttributes._downgrade.bind(customAttributes);
forEach.call(mutations, function(m){
forEach.call(m.removedNodes, downgrade);
});
});

this.childMO.observe(root, {
childList: true,
subtree: true
});
}

_upgrade(attrName) {
var document = this.ownerDocument;
var matches = document.querySelectorAll("[" + attrName + "]");
for(var match of matches) {
this._found(attrName, match);
}
}

_downgrade(element) {
var map = this._elementMap.get(element);
if(!map) return;

for(var inst of map.values()) {
if(inst.disconnectedCallback) {
inst.disconnectedCallback();
}
}

this._elementMap.delete(element);
}

_found(attrName, el, oldVal) {
var map = this._elementMap.get(el);
if(!map) {
map = new Map();
this._elementMap.set(el, map);
}

var inst = map.get(attrName);
var newVal = el.getAttribute(attrName);
if(!inst) {
var Constructor = this.get(attrName);
inst = new Constructor();
map.set(attrName, inst);
inst.ownerElement = el;
inst.name = attrName;
inst.value = newVal;
if(inst.connectedCallback) {
inst.connectedCallback();
}
}
// Attribute was removed
else if(newVal == null && !!inst.value) {
inst.value = newVal;
if(inst.disconnectedCallback) {
inst.disconnectedCallback();
}

map.delete(attrName);
}
// Attribute changed
else if(newVal !== inst.value) {
inst.value = newVal;
if(inst.changedCallback) {
inst.changedCallback(oldVal, newVal);
}
}

}
}

window.customAttributes = new CustomAttributeRegistry(document);
33 changes: 33 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "custom-attributes",
"version": "1.0.0",
"description": "Custom attributes, like custom elements, but for attributes",
"main": "attr.js",
"directories": {
"test": "test"
},
"scripts": {
"test": "testee test/connect.html test/disconnect.html --browsers firefox"
},
"repository": {
"type": "git",
"url": "git+https://github.com/matthewp/custom-attributes.git"
},
"keywords": [
"web",
"components"
],
"files": [
"attr.js"
],
"author": "Matthew Phillips",
"license": "BSD-2-Clause",
"bugs": {
"url": "https://github.com/matthewp/custom-attributes/issues"
},
"homepage": "https://github.com/matthewp/custom-attributes#readme",
"devDependencies": {
"mocha-test": "^1.0.3",
"webcomponents.js": "^0.7.22"
}
}
26 changes: 26 additions & 0 deletions test/all.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!doctype html>
<html>
<head>
<title>custom-attributes tests</title>
<style>
iframe {
border: 1px solid pink;
flex: 1;
margin-left: .5em;
}
.tests {
display: flex;
min-height: 300px;
}
.tests iframe {
flex-grow: 1;
}
</style>
</head>
<body>
<div class="tests">
<iframe src="./connect.html"></iframe>
<iframe src="./disconnect.html"></iframe>
</div>
</body>
</html>
73 changes: 73 additions & 0 deletions test/connect.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<!doctype html>
<html>
<head>
<title> simple test</title>

<script src="../node_modules/webcomponents.js/webcomponents-lite.min.js" defer></script>
<script src="../attr.js"></script>
<link rel="import" href="../node_modules/mocha-test/mocha-test.html">
</head>
<body>

<article bg-color="red">
<p>This article is static</p>
</article>

<div id="fixture"></div>

<script>
(function(){
class BgColorAttr {
connectedCallback() {
this.setColor();
}

changedCallback(oldValue, newValue) {
this.setColor();
}

setColor() {
var color = this.value || '';
this.ownerElement.style.backgroundColor = color;
}
}

customAttributes.define('bg-color', BgColorAttr);
})();
</script>

<mocha-test>
<template>
<script>
describe('connectedCallback()', function(){
function later(cb){
setTimeout(cb, 4);
}

afterEach(function(){
fixture.innerHTML = '';
});

it('Is called when element is already in the DOM', function(){
var article = document.querySelector('article');
assert.equal(article.style.backgroundColor, 'red');
});

it('Is called when an attribute is dynamically created', function(done){
var article = document.createElement('article');
article.textContent = 'hello world';
fixture.appendChild(article);

article.setAttribute('bg-color', 'blue');

later(function(){
assert.equal(article.style.backgroundColor, 'blue');
done();
});
});
});
</script>
</template>
</mocha-test>
</body>
</html>
77 changes: 77 additions & 0 deletions test/disconnect.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<!doctype html>
<html>
<head>
<title>disconnectedCallback() tests</title>

<script src="../node_modules/webcomponents.js/webcomponents-lite.min.js" defer></script>
<script src="../attr.js" defer></script>
<link rel="import" href="../node_modules/mocha-test/mocha-test.html" defer>
</head>
<body>
<div id="fixture"></div>

<mocha-test>
<template>
<script>
describe('disconnectedCallback()', function(){
function later(cb) {
setTimeout(cb, 0);
}

afterEach(function(){
fixture.innerHTML = '';
});

it('Is called when removeAttribute() is used', function(done){
class MyAttr {
connectedCallback() {
later(function(){
el.removeAttribute('my-attr');
});
}

changedCallback() {
assert.ok(false, 'changed shouldn\'t be called');
}

disconnectedCallback() {
assert.ok(true, 'was called');
done();
}
}

customAttributes.define('my-attr', MyAttr);

var el = document.createElement('span');
fixture.appendChild(el);

el.setAttribute('my-attr', 'test');
});

it('Is called when the element is removed', function(done){
class SomeAttr {
connectedCallback() {
later(function(){
fixture.removeChild(el);
});
}

disconnectedCallback() {
assert.ok(true, 'was called');
done();
}
}

customAttributes.define('some-attr', SomeAttr);

var el = document.createElement('span');
fixture.appendChild(el);

el.setAttribute('some-attr', 'test');
});
});
</script>
</template>
</mocha-test>
</body>
</html>

0 comments on commit 3dff9ab

Please sign in to comment.