Skip to content
This repository has been archived by the owner on Apr 4, 2019. It is now read-only.

Commit

Permalink
detect props that aren't settable so we fallback to setAttribute, fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
jayphelps committed Jun 10, 2015
1 parent 01d0461 commit 665b64a
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 16 deletions.
55 changes: 49 additions & 6 deletions packages/dom-helper/lib/prop.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,16 @@ export var propertyCaches = {};

export function normalizeProperty(element, attrName) {
var tagName = element.tagName;
var key, cachedAttrName;
var cache = propertyCaches[tagName];
if (!cache) {
// TODO should this be an o_create kind of thing?
cache = {};
for (cachedAttrName in element) {
key = cachedAttrName.toLowerCase();
if (isSettable(element, cachedAttrName)) {
cache[key] = cachedAttrName;
for (let key in element) {
let lowerKey = key.toLowerCase();
if (isSettable(element, key)) {
cache[lowerKey] = key;
} else {
cache[key] = UNDEFINED;
cache[lowerKey] = UNDEFINED;
}
}
propertyCaches[tagName] = cache;
Expand Down Expand Up @@ -59,5 +58,49 @@ function isSettable(element, attrName) {
}
}

// Properties can be effectively read-only two ways.
// If actually marked as writable = false, an exception is thrown if you attempt
// to assign. If it's simply missing a setter, it silently just doesn't
// assign anything. Both cases we will defer to setAttribute instead
var desc = getPropertyDescriptor(element, attrName);
if (!desc) { return true; }
if (desc.writable === false || !desc.hasOwnProperty('value') && typeof desc.set !== 'function') {
return false;
}

return true;
}

// Polyfill :(
const getPrototypeOf = (function() {
let fn = Object.getPrototypeOf;

if (!fn) {
/* jshint ignore:start */
if (typeof 'test'.__proto__ === 'object') {
fn = function getPrototypeOf(obj) {
return obj.__proto__;
};
} else {
// IE8
fn = function getPrototypeOf(obj) {
return obj.constructor.prototype;
};
}
/* jshint ignore:end */
}

return fn;
})();

const { getOwnPropertyDescriptor } = Object;

// Walks up the chain to find the desc by name
function getPropertyDescriptor(obj, key) {
let proto = obj, desc;
while (proto && !(desc = getOwnPropertyDescriptor(proto, key))) {
proto = getPrototypeOf(proto);
}

return desc;
}
16 changes: 16 additions & 0 deletions packages/dom-helper/tests/dom-helper-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,22 @@ test('#setProperty', function(){
node = dom.createElement('div');
dom.setProperty(node, 'style', 'color: red;');
equalHTML(node, '<div style="color: red;"></div>');

// Tests for browser quirk corrections

[
{ tagName: 'button', key: 'type' },
{ tagName: 'input', key: 'type' },
{ tagName: 'input', key: 'list' }
]
.forEach(function (item) {
node = dom.createElement(item.tagName);
dom.setProperty(node, item.key, 'x-foo-bar');
// The property may or may not be set depending
// on the browser. We only care that the attribute
// is actually set, in this test
equal(node.getAttribute(item.key), 'x-foo-bar');
});
});

test('#setProperty removes attr with undefined', function(){
Expand Down
91 changes: 81 additions & 10 deletions packages/dom-helper/tests/prop-test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,55 @@
import { normalizeProperty } from 'dom-helper/prop';
import { normalizeProperty, propertyCaches } from 'dom-helper/prop';

QUnit.module('dom-helper prop');
function createMockElement(tagName, props = {}) {
props.tagName = {
configurable: true,
enumerable: true,
get() {
return tagName.toUpperCase();
}
};

function MockElement() {}
Object.defineProperties(MockElement.prototype, props);
return new MockElement();
}

QUnit.module('dom-helper prop', {
teardown() {
for (let key in propertyCaches) {
delete propertyCaches[key];
}
}
});

test('returns normalized property name for the typical cases', function() {
expect(3);

var element1 = createMockElement('element1');
element1.form = null;
var element2 = createMockElement('element2', {
form: {
enumerable: true,
get() {
return null;
},
set() {
return null;
}
}
});
var element3 = createMockElement('element3', {
form: {
enumerable: true,
writable: true,
value: null
}
});

[element1, element2, element3].forEach(function (el) {
equal(normalizeProperty(el, 'form'), 'form');
});
});

test('returns `undefined` for special element properties that are non-compliant in certain browsers', function() {
expect(3);
Expand All @@ -12,17 +61,39 @@ test('returns `undefined` for special element properties that are non-compliant
];

badPairs.forEach(function(pair) {
var element = {
tagName: pair.tagName
};

Object.defineProperty(element, pair.key, {
set: function() {
throw new Error('I am a bad browser!');
var proto = {};
proto[pair.key] = {
enumerable: true,
set() {
throw new Error('I am a bad browser! ');
}
});
};
var element = createMockElement(pair.tagName, proto);

var actual = normalizeProperty(element, pair.key);
equal(actual, undefined);
});
});

test('returns `undefined` for properties that are effectively read-only (writable=false or no setter)', function() {
expect(2);

var element1 = createMockElement('element1', {
form: {
enumerable: true,
get() {
return null;
}
}
});
var element2 = createMockElement('element2', {
form: {
enumerable: true,
writable: false,
value: null
}
});

equal(normalizeProperty(element1, 'form'), undefined);
equal(normalizeProperty(element2, 'form'), undefined);
});

0 comments on commit 665b64a

Please sign in to comment.