| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 1 | <!DOCTYPE html> |
| 2 | <html> |
| 3 | <head> |
| 4 | <title>Custom Elements: attributeChangedCallback</title> |
| 5 | <meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> |
| 6 | <meta name="assert" content="attributeChangedCallback must be enqueued whenever custom element's attribute is added, changed or removed"> |
| 7 | <link rel="help" href="https://w3c.github.io/webcomponents/spec/custom/#dfn-attribute-changed-callback"> |
| 8 | <script src="/resources/testharness.js"></script> |
| 9 | <script src="/resources/testharnessreport.js"></script> |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 10 | <script src="resources/custom-elements-helpers.js"></script> |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 11 | </head> |
| 12 | <body> |
| 13 | <div id="log"></div> |
| Edgar Chen | d4888ba | 2018-01-23 15:27:55 | [diff] [blame] | 14 | <parser-created-element title></parser-created-element> |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 15 | <script> |
| 16 | |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 17 | var customElement = define_new_custom_element(['title', 'id', 'r']); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 18 | |
| 19 | test(function () { |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 20 | const instance = document.createElement(customElement.name); |
| 21 | assert_array_equals(customElement.takeLog().types(), ['constructed']); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 22 | |
| 23 | instance.setAttribute('title', 'foo'); |
| 24 | assert_equals(instance.getAttribute('title'), 'foo'); |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 25 | var logEntries = customElement.takeLog(); |
| 26 | assert_array_equals(logEntries.types(), ['attributeChanged']); |
| 27 | assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: null, newValue: 'foo', namespace: null}); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 28 | |
| 29 | instance.removeAttribute('title'); |
| 30 | assert_equals(instance.getAttribute('title'), null); |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 31 | var logEntries = customElement.takeLog(); |
| 32 | assert_array_equals(logEntries.types(), ['attributeChanged']); |
| 33 | assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: 'foo', newValue: null, namespace: null}); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 34 | }, 'setAttribute and removeAttribute must enqueue and invoke attributeChangedCallback'); |
| 35 | |
| 36 | test(function () { |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 37 | var instance = document.createElement(customElement.name); |
| 38 | assert_array_equals(customElement.takeLog().types(), ['constructed']); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 39 | |
| 40 | instance.setAttributeNS('http://www.w3.org/2000/svg', 'title', 'hello'); |
| 41 | assert_equals(instance.getAttribute('title'), 'hello'); |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 42 | var logEntries = customElement.takeLog(); |
| 43 | assert_array_equals(logEntries.types(), ['attributeChanged']); |
| 44 | assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: null, newValue: 'hello', namespace: 'http://www.w3.org/2000/svg'}); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 45 | |
| 46 | instance.removeAttributeNS('http://www.w3.org/2000/svg', 'title'); |
| 47 | assert_equals(instance.getAttribute('title'), null); |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 48 | var logEntries = customElement.takeLog(); |
| 49 | assert_array_equals(logEntries.types(), ['attributeChanged']); |
| 50 | assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: 'hello', newValue: null, namespace: 'http://www.w3.org/2000/svg'}); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 51 | }, 'setAttributeNS and removeAttributeNS must enqueue and invoke attributeChangedCallback'); |
| 52 | |
| 53 | test(function () { |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 54 | var instance = document.createElement(customElement.name); |
| 55 | assert_array_equals(customElement.takeLog().types(), ['constructed']); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 56 | |
| 57 | var attr = document.createAttribute('id'); |
| 58 | attr.value = 'bar'; |
| 59 | instance.setAttributeNode(attr); |
| 60 | |
| 61 | assert_equals(instance.getAttribute('id'), 'bar'); |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 62 | var logEntries = customElement.takeLog(); |
| 63 | assert_array_equals(logEntries.types(), ['attributeChanged']); |
| 64 | assert_attribute_log_entry(logEntries.last(), {name: 'id', oldValue: null, newValue: 'bar', namespace: null}); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 65 | |
| 66 | instance.removeAttributeNode(attr); |
| 67 | assert_equals(instance.getAttribute('id'), null); |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 68 | var logEntries = customElement.takeLog(); |
| 69 | assert_array_equals(logEntries.types(), ['attributeChanged']); |
| 70 | assert_attribute_log_entry(logEntries.last(), {name: 'id', oldValue: 'bar', newValue: null, namespace: null}); |
| 71 | }, 'setAttributeNode and removeAttributeNode must enqueue and invoke attributeChangedCallback for an HTML attribute'); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 72 | |
| 73 | test(function () { |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 74 | const instance = document.createElement(customElement.name); |
| 75 | assert_array_equals(customElement.takeLog().types(), ['constructed']); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 76 | |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 77 | const attr = document.createAttributeNS('http://www.w3.org/2000/svg', 'r'); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 78 | attr.value = '100'; |
| 79 | instance.setAttributeNode(attr); |
| 80 | |
| 81 | assert_equals(instance.getAttribute('r'), '100'); |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 82 | var logEntries = customElement.takeLog(); |
| 83 | assert_array_equals(logEntries.types(), ['attributeChanged']); |
| 84 | assert_attribute_log_entry(logEntries.last(), {name: 'r', oldValue: null, newValue: '100', namespace: 'http://www.w3.org/2000/svg'}); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 85 | |
| 86 | instance.removeAttributeNode(attr); |
| 87 | assert_equals(instance.getAttribute('r'), null); |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 88 | var logEntries = customElement.takeLog(); |
| 89 | assert_array_equals(logEntries.types(), ['attributeChanged']); |
| 90 | assert_attribute_log_entry(logEntries.last(), {name: 'r', oldValue: '100', newValue: null, namespace: 'http://www.w3.org/2000/svg'}); |
| 91 | }, 'setAttributeNode and removeAttributeNS must enqueue and invoke attributeChangedCallback for an SVG attribute'); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 92 | |
| 93 | test(function () { |
| Rakina Zata Amni | f6881fb | 2019-06-18 13:11:46 | [diff] [blame] | 94 | const instance = document.createElement(customElement.name); |
| 95 | assert_array_equals(customElement.takeLog().types(), ['constructed']); |
| 96 | |
| 97 | instance.toggleAttribute('title', true); |
| 98 | assert_equals(instance.hasAttribute('title'), true); |
| 99 | var logEntries = customElement.takeLog(); |
| 100 | assert_array_equals(logEntries.types(), ['attributeChanged']); |
| 101 | assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: null, newValue: '', namespace: null}); |
| 102 | |
| 103 | instance.toggleAttribute('title'); |
| 104 | assert_equals(instance.hasAttribute('title'), false); |
| 105 | var logEntries = customElement.takeLog(); |
| 106 | assert_array_equals(logEntries.types(), ['attributeChanged']); |
| 107 | assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: '', newValue: null, namespace: null}); |
| 108 | }, 'toggleAttribute must enqueue and invoke attributeChangedCallback'); |
| 109 | |
| 110 | test(function () { |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 111 | const callsToOld = []; |
| 112 | const callsToNew = []; |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 113 | class CustomElement extends HTMLElement { } |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 114 | CustomElement.prototype.attributeChangedCallback = function (...args) { |
| 115 | callsToOld.push(create_attribute_changed_callback_log(this, ...args)); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 116 | } |
| 117 | CustomElement.observedAttributes = ['title']; |
| 118 | customElements.define('element-with-mutated-attribute-changed-callback', CustomElement); |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 119 | CustomElement.prototype.attributeChangedCallback = function (...args) { |
| 120 | callsToNew.push(create_attribute_changed_callback_log(this, ...args)); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 121 | } |
| 122 | |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 123 | const instance = document.createElement('element-with-mutated-attribute-changed-callback'); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 124 | instance.setAttribute('title', 'hi'); |
| 125 | assert_equals(instance.getAttribute('title'), 'hi'); |
| 126 | assert_array_equals(callsToNew, []); |
| 127 | assert_equals(callsToOld.length, 1); |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 128 | assert_attribute_log_entry(callsToOld[0], {name: 'title', oldValue: null, newValue: 'hi', namespace: null}); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 129 | }, 'Mutating attributeChangedCallback after calling customElements.define must not affect the callback being invoked'); |
| 130 | |
| 131 | test(function () { |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 132 | const calls = []; |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 133 | class CustomElement extends HTMLElement { |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 134 | attributeChangedCallback(...args) { |
| 135 | calls.push(create_attribute_changed_callback_log(this, ...args)); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 136 | } |
| 137 | } |
| 138 | CustomElement.observedAttributes = ['title']; |
| 139 | customElements.define('element-not-observing-id-attribute', CustomElement); |
| 140 | |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 141 | const instance = document.createElement('element-not-observing-id-attribute'); |
| 142 | assert_equals(calls.length, 0); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 143 | instance.setAttribute('title', 'hi'); |
| 144 | assert_equals(calls.length, 1); |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 145 | assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: 'hi', namespace: null}); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 146 | instance.setAttribute('id', 'some'); |
| 147 | assert_equals(calls.length, 1); |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 148 | assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: 'hi', namespace: null}); |
| 149 | }, 'attributedChangedCallback must not be invoked when the observed attributes does not contain the attribute'); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 150 | |
| 151 | test(function () { |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 152 | const calls = []; |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 153 | class CustomElement extends HTMLElement { } |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 154 | CustomElement.prototype.attributeChangedCallback = function (...args) { |
| 155 | calls.push(create_attribute_changed_callback_log(this, ...args)); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 156 | } |
| 157 | CustomElement.observedAttributes = ['title', 'lang']; |
| 158 | customElements.define('element-with-mutated-observed-attributes', CustomElement); |
| 159 | CustomElement.observedAttributes = ['title', 'id']; |
| 160 | |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 161 | const instance = document.createElement('element-with-mutated-observed-attributes'); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 162 | instance.setAttribute('title', 'hi'); |
| 163 | assert_equals(calls.length, 1); |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 164 | assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: 'hi', namespace: null}); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 165 | |
| 166 | instance.setAttribute('id', 'some'); |
| 167 | assert_equals(calls.length, 1); |
| 168 | |
| 169 | instance.setAttribute('lang', 'en'); |
| 170 | assert_equals(calls.length, 2); |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 171 | assert_attribute_log_entry(calls[1], {name: 'lang', oldValue: null, newValue: 'en', namespace: null}); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 172 | }, 'Mutating observedAttributes after calling customElements.define must not affect the set of attributes for which attributedChangedCallback is invoked'); |
| 173 | |
| 174 | test(function () { |
| 175 | var calls = []; |
| 176 | class CustomElement extends HTMLElement { } |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 177 | CustomElement.prototype.attributeChangedCallback = function (...args) { |
| 178 | calls.push(create_attribute_changed_callback_log(this, ...args)); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 179 | } |
| 180 | CustomElement.observedAttributes = { [Symbol.iterator]: function *() { yield 'lang'; yield 'style'; } }; |
| 181 | customElements.define('element-with-generator-observed-attributes', CustomElement); |
| 182 | |
| 183 | var instance = document.createElement('element-with-generator-observed-attributes'); |
| 184 | instance.setAttribute('lang', 'en'); |
| 185 | assert_equals(calls.length, 1); |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 186 | assert_attribute_log_entry(calls[0], {name: 'lang', oldValue: null, newValue: 'en', namespace: null}); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 187 | |
| 188 | instance.setAttribute('lang', 'ja'); |
| 189 | assert_equals(calls.length, 2); |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 190 | assert_attribute_log_entry(calls[1], {name: 'lang', oldValue: 'en', newValue: 'ja', namespace: null}); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 191 | |
| 192 | instance.setAttribute('title', 'hello'); |
| 193 | assert_equals(calls.length, 2); |
| 194 | |
| 195 | instance.setAttribute('style', 'font-size: 2rem'); |
| 196 | assert_equals(calls.length, 3); |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 197 | assert_attribute_log_entry(calls[2], {name: 'style', oldValue: null, newValue: 'font-size: 2rem', namespace: null}); |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 198 | }, 'attributedChangedCallback must be enqueued for attributes specified in a non-Array iterable observedAttributes'); |
| 199 | |
| Ryosuke Niwa | c7fbe2d | 2016-10-06 06:19:47 | [diff] [blame] | 200 | test(function () { |
| 201 | var calls = []; |
| 202 | class CustomElement extends HTMLElement { } |
| 203 | CustomElement.prototype.attributeChangedCallback = function (...args) { |
| 204 | calls.push(create_attribute_changed_callback_log(this, ...args)); |
| 205 | } |
| 206 | CustomElement.observedAttributes = ['style']; |
| 207 | customElements.define('element-with-style-attribute-observation', CustomElement); |
| 208 | |
| 209 | var instance = document.createElement('element-with-style-attribute-observation'); |
| 210 | assert_equals(calls.length, 0); |
| 211 | |
| 212 | instance.style.fontSize = '10px'; |
| 213 | assert_equals(calls.length, 1); |
| 214 | assert_attribute_log_entry(calls[0], {name: 'style', oldValue: null, newValue: 'font-size: 10px;', namespace: null}); |
| 215 | |
| 216 | instance.style.fontSize = '20px'; |
| 217 | assert_equals(calls.length, 2); |
| 218 | assert_attribute_log_entry(calls[1], {name: 'style', oldValue: 'font-size: 10px;', newValue: 'font-size: 20px;', namespace: null}); |
| 219 | |
| 220 | }, 'attributedChangedCallback must be enqueued for style attribute change by mutating inline style declaration'); |
| 221 | |
| 222 | test(function () { |
| 223 | var calls = []; |
| 224 | class CustomElement extends HTMLElement { } |
| 225 | CustomElement.prototype.attributeChangedCallback = function (...args) { |
| 226 | calls.push(create_attribute_changed_callback_log(this, ...args)); |
| 227 | } |
| 228 | CustomElement.observedAttributes = ['title']; |
| 229 | customElements.define('element-with-no-style-attribute-observation', CustomElement); |
| 230 | |
| 231 | var instance = document.createElement('element-with-no-style-attribute-observation'); |
| 232 | assert_equals(calls.length, 0); |
| 233 | instance.style.fontSize = '10px'; |
| 234 | assert_equals(calls.length, 0); |
| 235 | instance.title = 'hello'; |
| 236 | assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: 'hello', namespace: null}); |
| 237 | }, 'attributedChangedCallback must not be enqueued when mutating inline style declaration if the style attribute is not observed'); |
| 238 | |
| Edgar Chen | d4888ba | 2018-01-23 15:27:55 | [diff] [blame] | 239 | test(function () { |
| 240 | var calls = []; |
| 241 | class CustomElement extends HTMLElement { } |
| 242 | CustomElement.prototype.attributeChangedCallback = function (...args) { |
| 243 | calls.push(create_attribute_changed_callback_log(this, ...args)); |
| 244 | } |
| 245 | CustomElement.observedAttributes = ['title']; |
| 246 | customElements.define('parser-created-element', CustomElement); |
| 247 | assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: '', namespace: null}); |
| 248 | }, 'Upgrading a parser created element must enqueue and invoke attributeChangedCallback for an HTML attribute'); |
| 249 | |
| 250 | test(function () { |
| 251 | var calls = []; |
| 252 | class CustomElement extends HTMLElement { } |
| 253 | CustomElement.prototype.attributeChangedCallback = function (...args) { |
| 254 | calls.push(create_attribute_changed_callback_log(this, ...args)); |
| 255 | } |
| 256 | CustomElement.observedAttributes = ['title']; |
| 257 | customElements.define('cloned-element-with-attribute', CustomElement); |
| 258 | |
| 259 | var instance = document.createElement('cloned-element-with-attribute'); |
| 260 | assert_equals(calls.length, 0); |
| 261 | instance.title = ''; |
| 262 | assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: '', namespace: null}); |
| 263 | |
| 264 | calls = []; |
| 265 | var clone = instance.cloneNode(false); |
| 266 | assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: '', namespace: null}); |
| 267 | }, 'Upgrading a cloned element must enqueue and invoke attributeChangedCallback for an HTML attribute'); |
| 268 | |
| Ryosuke Niwa | e09776a | 2016-09-29 07:14:40 | [diff] [blame] | 269 | </script> |
| 270 | </body> |
| 271 | </html> |