14 |
15 |
16 |
17 |
18 |
38 |
39 |
40 |
41 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/matthewp/custom-attributes)
2 | [](http://badge.fury.io/js/custom-attributes)
3 |
4 | # custom-attributes
5 |
6 | Define custom attributes in the same way you can define custom elements, which allows for rich mixin types of behaviors on elements.
7 |
8 | ## Install
9 |
10 | ```shell
11 | npm install custom-attributes --save
12 | ```
13 |
14 | Add as a script tag:
15 |
16 | ```html
17 |
18 | ```
19 |
20 | Or import as an ES module:
21 |
22 | ```js
23 | import customAttributes from 'custom-attributes';
24 | ```
25 |
26 | Or you can just import the CustomAttributeRegistry and create your own instance:
27 |
28 | ```js
29 | import {CustomAttributeRegistry} from 'custom-attributes';
30 |
31 | const customAttributes = new CustomAttributeRegistry(document);
32 | ```
33 |
34 | ## Example
35 |
36 | ```html
37 |
38 |
This will be shown in a green background!
39 |
40 | ```
41 |
42 | ```js
43 | class BgColor {
44 | connectedCallback() {
45 | this.setColor();
46 | }
47 |
48 | disconnectedCallback() {
49 | // cleanup here!
50 | }
51 |
52 | // Called whenever the attribute's value changes
53 | changedCallback() {
54 | this.setColor();
55 | }
56 |
57 | setColor() {
58 | this.ownerElement.style.backgroundColor = this.value;
59 | }
60 | }
61 |
62 | customAttributes.default.define('bg-color', BgColor);
63 | ```
64 |
65 | ## API
66 |
67 | custom-attributes follows a very similar API as v1 custom elements, but rather than a class instance representing the host element, the class instance is meant to represent the *attribute*.
68 |
69 | ### Lifecycle callbacks
70 |
71 | #### connectedCallback
72 |
73 | This is called when the attribute is first connected to the `document`. If the host element is already in the DOM, and the attribute is set, connectedCallback will be called as all registered attributes are upgraded.
74 |
75 | If the host element is already in the DOM and the attribute is programmatically added via `setAttribute`, then connectedCallback will be called asynchronously after.
76 |
77 | If the host element is being programmatically created and the attribute is set before the element is inserted into the DOM, connectedCallback will only call when the host element is inserted.
78 |
79 | #### disconnectedCallback
80 |
81 | Called when the attribute is no longer part of the host element, or the host document. This callback should be used if any cleanup is needed.
82 |
83 | If the attribute is removed via `removeAttribute`, then disconnectedCallback will be called asynchronously after this change. If the host element is removed from the DOM, disconnectedCallback will be called asynchronously after as well.
84 |
85 | #### changedCallback
86 |
87 | Called any time the attribute's `value` changes, after connected. Useful if you need to perform work based on the attribute value such as the example given in this readme.
88 |
89 | ### Properties
90 |
91 | #### ownerElement
92 |
93 | `this.ownerElement` refers to the element which the attribute is attached.
94 |
95 | #### name
96 |
97 | The attribute's name. Since multiple class definitions can be used for multiple attribute names, `this.name` is useful if you need to know what the attribute is being referred to as.
98 |
99 | #### value
100 |
101 | The current value of the attribute (the string value) is available as `this.value`.
102 |
103 | ## License
104 |
105 | BSD 2 Clause
106 |
--------------------------------------------------------------------------------
/src/registry.js:
--------------------------------------------------------------------------------
1 | var forEach = Array.prototype.forEach;
2 |
3 | class CustomAttributeRegistry {
4 | constructor(ownerDocument){
5 | if(!ownerDocument) {
6 | throw new Error("Must be given a document");
7 | }
8 |
9 | this.ownerDocument = ownerDocument;
10 | this._attrMap = new Map();
11 | this._elementMap = new WeakMap();
12 | this._observe();
13 | }
14 |
15 | define(attrName, Constructor) {
16 | this._attrMap.set(attrName, Constructor);
17 | this._upgradeAttr(attrName);
18 | }
19 |
20 | get(element, attrName) {
21 | var map = this._elementMap.get(element);
22 | if(!map) return;
23 | return map.get(attrName);
24 | }
25 |
26 | _getConstructor(attrName){
27 | return this._attrMap.get(attrName);
28 | }
29 |
30 | _observe(){
31 | var customAttributes = this;
32 | var root = this.ownerDocument;
33 | var downgrade = this._downgrade.bind(this);
34 | var upgrade = this._upgradeElement.bind(this);
35 |
36 | this.observer = new MutationObserver(function(mutations){
37 | forEach.call(mutations, function(m){
38 | if(m.type === 'attributes') {
39 | var attr = customAttributes._getConstructor(m.attributeName);
40 | if(attr) {
41 | customAttributes._found(m.attributeName, m.target, m.oldValue);
42 | }
43 | }
44 | // chlidList
45 | else {
46 | forEach.call(m.removedNodes, downgrade);
47 | forEach.call(m.addedNodes, upgrade);
48 | }
49 | });
50 | });
51 |
52 | this.observer.observe(root, {
53 | childList: true,
54 | subtree: true,
55 | attributes: true,
56 | attributeOldValue: true
57 | });
58 | }
59 |
60 | _upgradeAttr(attrName, document) {
61 | document = document || this.ownerDocument;
62 |
63 | var matches = document.querySelectorAll("[" + attrName + "]");
64 |
65 | // Use a forEach as Edge doesn't support for...of on a NodeList
66 | forEach.call(matches, function(match) {
67 | this._found(attrName, match);
68 | }, this);
69 | }
70 |
71 | _upgradeElement(element) {
72 | if(element.nodeType !== 1) return;
73 |
74 | // Use a forEach as Safari 10 doesn't support for...of on NamedNodeMap (attributes)
75 | forEach.call(element.attributes, function(attr) {
76 | if(this._getConstructor(attr.name)) {
77 | this._found(attr.name, element);
78 | }
79 | }, this);
80 |
81 | this._attrMap.forEach(function(constructor, attr) {
82 | this._upgradeAttr(attr, element);
83 | }, this);
84 | }
85 |
86 | _downgrade(element) {
87 | var map = this._elementMap.get(element);
88 | if(!map) return;
89 |
90 | map.forEach(function(inst) {
91 | if (inst.disconnectedCallback) {
92 | inst.disconnectedCallback();
93 | }
94 | }, this);
95 |
96 | this._elementMap.delete(element);
97 | }
98 |
99 | _found(attrName, el, oldVal) {
100 | var map = this._elementMap.get(el);
101 | if(!map) {
102 | map = new Map();
103 | this._elementMap.set(el, map);
104 | }
105 |
106 | var inst = map.get(attrName);
107 | var newVal = el.getAttribute(attrName);
108 | if(!inst) {
109 | var Constructor = this._getConstructor(attrName);
110 | inst = new Constructor();
111 | map.set(attrName, inst);
112 | inst.ownerElement = el;
113 | inst.name = attrName;
114 | inst.value = newVal;
115 | if(inst.connectedCallback) {
116 | inst.connectedCallback();
117 | }
118 | }
119 | // Attribute was removed
120 | else if(newVal == null && !!inst.value) {
121 | inst.value = newVal;
122 | if(inst.disconnectedCallback) {
123 | inst.disconnectedCallback();
124 | }
125 |
126 | map.delete(attrName);
127 | }
128 | // Attribute changed
129 | else if(newVal !== inst.value) {
130 | inst.value = newVal;
131 | if(inst.changedCallback) {
132 | inst.changedCallback(oldVal, newVal);
133 | }
134 | }
135 |
136 | }
137 | }
138 |
139 | export default CustomAttributeRegistry;
140 |
--------------------------------------------------------------------------------