achilles 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +122 -0
- data/MAINTAINERS.md +53 -0
- data/README.md +248 -14
- data/SECURITY.md +50 -0
- data/app/javascript/achilles/application/application.js +40 -3
- data/app/javascript/achilles/application/dom-mutation-observer/observer.js +11 -1
- data/app/javascript/achilles/application/hooks-manager/turbo.js +28 -7
- data/app/javascript/achilles/components/component_base.js +19 -2
- data/app/javascript/achilles/components/component_parser.js +22 -2
- data/app/javascript/achilles/components/components_registry.js +34 -14
- data/docs/core-js-gaps.md +26 -0
- data/docs/release-checklist.md +42 -23
- data/docs/releases/v1.1.0.md +74 -0
- data/docs/releases/v1.1.1.md +46 -0
- data/docs/upgrading-to-1.1.0.md +144 -0
- data/docs/upgrading.md +31 -0
- data/lib/achilles/version.rb +1 -1
- data/lib/achilles.rb +1 -0
- metadata +10 -1
|
@@ -16,6 +16,7 @@ class Application {
|
|
|
16
16
|
_componentsClassMapper;
|
|
17
17
|
_hooksManager;
|
|
18
18
|
_domMutationObserver;
|
|
19
|
+
_started = false;
|
|
19
20
|
|
|
20
21
|
constructor() {
|
|
21
22
|
// Set the application while creating the object
|
|
@@ -53,12 +54,45 @@ class Application {
|
|
|
53
54
|
return this._page;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
get strictLifecycleErrors() {
|
|
58
|
+
return this.componentRegistry.strictLifecycleErrors;
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
// Setters
|
|
57
62
|
set engine(engine) {
|
|
58
63
|
this._engine = engine;
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
set strictLifecycleErrors(value) {
|
|
67
|
+
this.componentRegistry.strictLifecycleErrors = value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
start() {
|
|
71
|
+
if (this._started) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this._started = true;
|
|
76
|
+
this._hooksManager.start();
|
|
77
|
+
this.setup();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
stop() {
|
|
81
|
+
if (!this._started) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this._started = false;
|
|
86
|
+
this._hooksManager.stop();
|
|
87
|
+
this.teardown();
|
|
88
|
+
this._domMutationObserver.stop();
|
|
89
|
+
}
|
|
90
|
+
|
|
61
91
|
setup() {
|
|
92
|
+
if (!this._started) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
62
96
|
this._domMutationObserver.stop();
|
|
63
97
|
this.parseHtmlAndRegisterComponents();
|
|
64
98
|
this.componentRegistry.callSetupForComponent(AppConstants.PageComponentId);
|
|
@@ -71,7 +105,10 @@ class Application {
|
|
|
71
105
|
this.componentRegistry.callTeardownForComponent(AppConstants.PageComponentId);
|
|
72
106
|
// Remove/deregister all components from page except Page
|
|
73
107
|
this.deregisterAllComponentsExceptPage();
|
|
74
|
-
|
|
108
|
+
|
|
109
|
+
if (this._started) {
|
|
110
|
+
this._domMutationObserver.start();
|
|
111
|
+
}
|
|
75
112
|
}
|
|
76
113
|
|
|
77
114
|
parseHtmlAndRegisterComponents() {
|
|
@@ -80,8 +117,8 @@ class Application {
|
|
|
80
117
|
}
|
|
81
118
|
|
|
82
119
|
deregisterAllComponentsExceptPage() {
|
|
83
|
-
let pageComponent = this.componentRegistry.getRegisteredComponent(AppConstants.PageComponentId)
|
|
84
|
-
pageComponent.subComponents.forEach((subComponentId) => {
|
|
120
|
+
let pageComponent = this.componentRegistry.getRegisteredComponent(AppConstants.PageComponentId);
|
|
121
|
+
[...pageComponent.subComponents].forEach((subComponentId) => {
|
|
85
122
|
this.componentRegistry.deregisterComponent(subComponentId);
|
|
86
123
|
})
|
|
87
124
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
class Observer {
|
|
2
2
|
_mutationObserver;
|
|
3
3
|
_callback;
|
|
4
|
+
_callbackScheduled = false;
|
|
4
5
|
|
|
5
6
|
constructor(callback) {
|
|
6
7
|
this._callback = callback;
|
|
@@ -18,10 +19,19 @@ class Observer {
|
|
|
18
19
|
|
|
19
20
|
stop() {
|
|
20
21
|
this._mutationObserver.disconnect();
|
|
22
|
+
this._callbackScheduled = false;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
domChangedCallback(mutationsList, observer) {
|
|
24
|
-
this.
|
|
26
|
+
if (this._callbackScheduled) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this._callbackScheduled = true;
|
|
31
|
+
queueMicrotask(() => {
|
|
32
|
+
this._callbackScheduled = false;
|
|
33
|
+
this._callback();
|
|
34
|
+
});
|
|
25
35
|
}
|
|
26
36
|
}
|
|
27
37
|
|
|
@@ -2,25 +2,46 @@ class Turbo {
|
|
|
2
2
|
_application;
|
|
3
3
|
_setupCallback;
|
|
4
4
|
_teardownCallback;
|
|
5
|
+
_setupHandler;
|
|
6
|
+
_teardownHandler;
|
|
7
|
+
_started = false;
|
|
5
8
|
|
|
6
9
|
constructor(application, setupCallback, teardownCallback) {
|
|
7
10
|
this._application = application;
|
|
8
11
|
this._setupCallback = setupCallback;
|
|
9
12
|
this._teardownCallback = teardownCallback;
|
|
10
|
-
|
|
11
|
-
this.setupEvents();
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
// Setups relevant hooks to the page for component lifecycles. This depends on the framework being used.
|
|
15
16
|
// Here we are using turbo drive, so hooking into that.
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
start() {
|
|
18
|
+
if (this._started) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this._setupHandler = () => {
|
|
18
23
|
this._setupCallback();
|
|
19
|
-
}
|
|
24
|
+
};
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
this._teardownHandler = () => {
|
|
22
27
|
this._teardownCallback();
|
|
23
|
-
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
document.addEventListener("turbo:load", this._setupHandler);
|
|
31
|
+
document.addEventListener("turbo:before-render", this._teardownHandler);
|
|
32
|
+
this._started = true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
stop() {
|
|
36
|
+
if (!this._started) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
document.removeEventListener("turbo:load", this._setupHandler);
|
|
41
|
+
document.removeEventListener("turbo:before-render", this._teardownHandler);
|
|
42
|
+
this._setupHandler = null;
|
|
43
|
+
this._teardownHandler = null;
|
|
44
|
+
this._started = false;
|
|
24
45
|
}
|
|
25
46
|
}
|
|
26
47
|
|
|
@@ -2,8 +2,7 @@ class ComponentBase {
|
|
|
2
2
|
parentComponentId;
|
|
3
3
|
id;
|
|
4
4
|
defaultParams;
|
|
5
|
-
|
|
6
|
-
teardownExecuted = false;
|
|
5
|
+
mounted = false;
|
|
7
6
|
|
|
8
7
|
constructor(id, parentComponentId = 'Page', defaultParams = []) {
|
|
9
8
|
this.id = id;
|
|
@@ -17,6 +16,24 @@ class ComponentBase {
|
|
|
17
16
|
setup() {}
|
|
18
17
|
teardown() {}
|
|
19
18
|
|
|
19
|
+
get setupExecuted() {
|
|
20
|
+
return this.mounted;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
set setupExecuted(value) {
|
|
24
|
+
this.mounted = value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get teardownExecuted() {
|
|
28
|
+
return !this.mounted;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
set teardownExecuted(value) {
|
|
32
|
+
if(value === true) {
|
|
33
|
+
this.mounted = false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
20
37
|
rootElement() {
|
|
21
38
|
return this.rootNode();
|
|
22
39
|
}
|
|
@@ -11,6 +11,10 @@ class ComponentParser {
|
|
|
11
11
|
[...document.querySelectorAll('[data-component-class]')].forEach((elem) => {
|
|
12
12
|
let klassName = elem.dataset.componentClass;
|
|
13
13
|
if(klassName.trim() === '') { return; }
|
|
14
|
+
if(!this.hasValidId(elem)) {
|
|
15
|
+
console.error(`Component root element is missing an id. className: ${klassName} | Element:`, elem);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
14
18
|
|
|
15
19
|
let klass = this._componentsClassMapper.getComponentClass(klassName);
|
|
16
20
|
if(typeof klass === 'undefined' || klass === null) {
|
|
@@ -18,11 +22,11 @@ class ComponentParser {
|
|
|
18
22
|
console.error(elem);
|
|
19
23
|
return;
|
|
20
24
|
}
|
|
21
|
-
if(elem.dataset.componentRegistered === 'true') {
|
|
25
|
+
if(elem.dataset.componentRegistered === 'true' && this._componentRegistry.getRegisteredComponent(elem.id)) {
|
|
22
26
|
return;
|
|
23
27
|
}
|
|
24
28
|
try {
|
|
25
|
-
let obj = new klass(elem.id,
|
|
29
|
+
let obj = new klass(elem.id, this.parentComponentIdFor(elem))
|
|
26
30
|
this._componentRegistry.registerComponentByObj(obj);
|
|
27
31
|
} catch (e) {
|
|
28
32
|
console.error(`Error parsing component. className: ${klassName} | Element ID: ${elem.id}`);
|
|
@@ -31,6 +35,22 @@ class ComponentParser {
|
|
|
31
35
|
}
|
|
32
36
|
})
|
|
33
37
|
}
|
|
38
|
+
|
|
39
|
+
hasValidId(elem) {
|
|
40
|
+
return typeof elem.id === 'string' && elem.id.trim() !== '';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
parentComponentIdFor(elem) {
|
|
44
|
+
let parent = elem.parentElement;
|
|
45
|
+
while(parent) {
|
|
46
|
+
if(parent.dataset?.componentClass && this.hasValidId(parent)) {
|
|
47
|
+
return parent.id;
|
|
48
|
+
}
|
|
49
|
+
parent = parent.parentElement;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return AppConstants.PageComponentId;
|
|
53
|
+
}
|
|
34
54
|
}
|
|
35
55
|
|
|
36
56
|
export { ComponentParser };
|
|
@@ -3,6 +3,7 @@ import { AppConstants } from "achilles/application/app_constants";
|
|
|
3
3
|
// Contains all the registered components in a page at the current moment
|
|
4
4
|
class ComponentsRegistry {
|
|
5
5
|
_registeredComponents = {};
|
|
6
|
+
strictLifecycleErrors = false;
|
|
6
7
|
|
|
7
8
|
matchingElementsForId(id) {
|
|
8
9
|
return [...document.querySelectorAll('[id]')].filter((elem) => elem.id === id);
|
|
@@ -25,6 +26,14 @@ class ComponentsRegistry {
|
|
|
25
26
|
// Component is already registered. So have to call deregister and teardown
|
|
26
27
|
this.teardownAndDeregister(id);
|
|
27
28
|
}
|
|
29
|
+
let parentComponent = null;
|
|
30
|
+
if(parentComponentId != null) {
|
|
31
|
+
parentComponent = this.getRegisteredComponent(parentComponentId);
|
|
32
|
+
if(!parentComponent) {
|
|
33
|
+
console.error(`Parent component not found while registering component. id: ${id} | parentComponentId: ${parentComponentId}. Skipping registering component`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
28
37
|
|
|
29
38
|
this._registeredComponents[id] = {
|
|
30
39
|
id: id,
|
|
@@ -34,7 +43,6 @@ class ComponentsRegistry {
|
|
|
34
43
|
subComponents: []
|
|
35
44
|
};
|
|
36
45
|
if(parentComponentId != null) {
|
|
37
|
-
let parentComponent = this.getRegisteredComponent(parentComponentId);
|
|
38
46
|
parentComponent.subComponents.push(id);
|
|
39
47
|
}
|
|
40
48
|
this.elementForId(id)?.setAttribute('data-component-registered', 'true');
|
|
@@ -45,13 +53,17 @@ class ComponentsRegistry {
|
|
|
45
53
|
if(!component)
|
|
46
54
|
return;
|
|
47
55
|
|
|
56
|
+
[...component.subComponents].forEach((subComponentId) => {
|
|
57
|
+
this.deregisterComponent(subComponentId);
|
|
58
|
+
});
|
|
59
|
+
|
|
48
60
|
let parentComponent = this.getRegisteredComponent(component.parentComponentId);
|
|
49
61
|
|
|
50
62
|
if(parentComponent != null){
|
|
51
63
|
parentComponent.subComponents = parentComponent.subComponents.filter(item => item !== id);
|
|
52
64
|
}
|
|
53
65
|
|
|
54
|
-
this._registeredComponents[id]
|
|
66
|
+
delete this._registeredComponents[id];
|
|
55
67
|
this.elementForId(id)?.removeAttribute('data-component-registered');
|
|
56
68
|
}
|
|
57
69
|
|
|
@@ -68,13 +80,13 @@ class ComponentsRegistry {
|
|
|
68
80
|
return;
|
|
69
81
|
}
|
|
70
82
|
|
|
71
|
-
// Call the objs default setup if
|
|
72
|
-
if(component.obj.setup && component.obj.
|
|
83
|
+
// Call the objs default setup if it is not mounted already, then continue to children
|
|
84
|
+
if(component.obj.setup && component.obj.mounted === false) {
|
|
73
85
|
try{
|
|
74
86
|
component.obj.setup(...component.defaultParams);
|
|
75
|
-
component.obj.
|
|
87
|
+
component.obj.mounted = true;
|
|
76
88
|
} catch(e) {
|
|
77
|
-
|
|
89
|
+
this.handleLifecycleError(e);
|
|
78
90
|
}
|
|
79
91
|
}
|
|
80
92
|
|
|
@@ -89,20 +101,28 @@ class ComponentsRegistry {
|
|
|
89
101
|
if(!component || !component.obj)
|
|
90
102
|
return;
|
|
91
103
|
|
|
92
|
-
// Call
|
|
93
|
-
|
|
104
|
+
// Call teardown for all sub view_components before their parent
|
|
105
|
+
component.subComponents.forEach((subComponentId) => {
|
|
106
|
+
this.callTeardownForComponent(subComponentId);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Call the objs default teardown if it is currently mounted
|
|
110
|
+
if(component.obj.teardown && component.obj.mounted === true) {
|
|
94
111
|
try{
|
|
95
112
|
component.obj.teardown(...component.defaultParams);
|
|
96
|
-
component.obj.
|
|
113
|
+
component.obj.mounted = false;
|
|
97
114
|
} catch(e) {
|
|
98
|
-
|
|
115
|
+
this.handleLifecycleError(e);
|
|
99
116
|
}
|
|
100
117
|
}
|
|
118
|
+
}
|
|
101
119
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
120
|
+
handleLifecycleError(error) {
|
|
121
|
+
console.error(error);
|
|
122
|
+
|
|
123
|
+
if(this.strictLifecycleErrors) {
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
106
126
|
}
|
|
107
127
|
|
|
108
128
|
teardownAndDeregister(id) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Core JavaScript Gaps
|
|
2
|
+
|
|
3
|
+
This is the current improvement backlog for Achilles core JavaScript. API
|
|
4
|
+
reference docs should wait until the structure settles.
|
|
5
|
+
|
|
6
|
+
## Completed
|
|
7
|
+
|
|
8
|
+
- Added an explicit `Application#start` and `Application#stop` lifecycle.
|
|
9
|
+
- Made Turbo event listeners removable.
|
|
10
|
+
- Validated that component roots have non-empty ids before registration.
|
|
11
|
+
- Changed teardown order so child components tear down before parent components.
|
|
12
|
+
- Deleted deregistered registry entries instead of leaving `null` tombstones.
|
|
13
|
+
- Batched mutation observer setup work with a microtask debounce.
|
|
14
|
+
- Replaced one-way lifecycle flags with a `mounted` state while keeping
|
|
15
|
+
`setupExecuted` and `teardownExecuted` as compatibility aliases.
|
|
16
|
+
- Added tests for missing ids, duplicate starts, listener cleanup,
|
|
17
|
+
child-before-parent teardown, dynamic insertion batching, deregistration, and
|
|
18
|
+
remounting a reused component instance.
|
|
19
|
+
- Chose a DOM ancestry model for nested components under the single synthetic
|
|
20
|
+
`Page` root.
|
|
21
|
+
- Split JavaScript tests by parser, registry, application, Turbo hooks, and
|
|
22
|
+
component base responsibility.
|
|
23
|
+
- Added package/file-list regression coverage for the gemspec.
|
|
24
|
+
- Added opt-in strict lifecycle error handling for tests and development.
|
|
25
|
+
|
|
26
|
+
## Remaining Priority Gaps
|
data/docs/release-checklist.md
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
# Release Checklist
|
|
2
2
|
|
|
3
|
-
Use this checklist for
|
|
3
|
+
Use this checklist for every Achilles release. Replace `VERSION` with the
|
|
4
|
+
version being prepared, for example `1.1.0`.
|
|
4
5
|
|
|
5
|
-
## Before
|
|
6
|
+
## Before Finalizing The Version
|
|
6
7
|
|
|
8
|
+
- Confirm the release type: patch, minor, major, or prerelease.
|
|
7
9
|
- Confirm `lib/achilles/version.rb` has the intended version.
|
|
8
10
|
- Confirm `CHANGELOG.md` has an entry for the intended version.
|
|
11
|
+
- Confirm application-facing changes have an upgrade note in `docs/upgrading.md`.
|
|
12
|
+
- Confirm release notes exist in `docs/releases/` when the release needs a
|
|
13
|
+
GitHub release body.
|
|
14
|
+
- Confirm package file expectations are covered by `test/gemspec_files_test.rb`
|
|
15
|
+
when adding source or documentation files.
|
|
16
|
+
|
|
17
|
+
## Before Building
|
|
18
|
+
|
|
9
19
|
- Confirm CI is green on GitHub.
|
|
10
20
|
- Run the local verification commands:
|
|
11
21
|
|
|
12
22
|
```bash
|
|
13
23
|
bin/rails test
|
|
14
|
-
|
|
24
|
+
node --test test/javascript/*_test.mjs
|
|
25
|
+
for file in $(rg --files app/javascript/achilles test/dummy/app/javascript test/javascript | rg "\.(js|mjs)$"); do node --input-type=module --check < "$file" || exit 1; done
|
|
26
|
+
bin/rails test:system
|
|
15
27
|
RAILS_ENV=test bin/rails app:assets:precompile
|
|
16
28
|
```
|
|
17
29
|
|
|
@@ -24,13 +36,13 @@ gem build achilles.gemspec
|
|
|
24
36
|
Confirm the generated gem name matches the intended version:
|
|
25
37
|
|
|
26
38
|
```bash
|
|
27
|
-
ls achilles
|
|
39
|
+
ls achilles-VERSION.gem
|
|
28
40
|
```
|
|
29
41
|
|
|
30
42
|
## Test In A Real Application
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
Before a broad rollout, test the built gem in at least one real Rails + Turbo
|
|
45
|
+
application that already uses Achilles.
|
|
34
46
|
|
|
35
47
|
Check:
|
|
36
48
|
|
|
@@ -44,30 +56,37 @@ Check:
|
|
|
44
56
|
- components that attach window, document, timer, observer, or third-party
|
|
45
57
|
widget state
|
|
46
58
|
|
|
47
|
-
For
|
|
48
|
-
|
|
49
|
-
`$(this.rootElement())`.
|
|
59
|
+
For releases with breaking or compatibility-sensitive behavior, test one app
|
|
60
|
+
first, then roll out to the rest of the maintained apps gradually.
|
|
50
61
|
|
|
51
62
|
## Publish A Prerelease
|
|
52
63
|
|
|
53
|
-
|
|
64
|
+
Publish a prerelease only when the release needs real-app validation before a
|
|
65
|
+
final tag.
|
|
54
66
|
|
|
55
67
|
```bash
|
|
56
|
-
|
|
57
|
-
git
|
|
58
|
-
|
|
68
|
+
git tag vVERSION
|
|
69
|
+
git push origin vVERSION
|
|
70
|
+
gem push achilles-VERSION.gem
|
|
59
71
|
```
|
|
60
72
|
|
|
61
|
-
|
|
73
|
+
Mark the GitHub release as a prerelease and include the matching release notes.
|
|
62
74
|
|
|
63
|
-
Publish
|
|
64
|
-
|
|
75
|
+
## Publish A Final Release
|
|
76
|
+
|
|
77
|
+
Publish a final release after local checks, GitHub CI, packaging verification,
|
|
78
|
+
and real-app testing are complete.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
git tag vVERSION
|
|
82
|
+
git push origin vVERSION
|
|
83
|
+
gem push achilles-VERSION.gem
|
|
84
|
+
```
|
|
65
85
|
|
|
66
|
-
|
|
86
|
+
After pushing:
|
|
67
87
|
|
|
68
|
-
- update
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
- tag `v1.0.0`
|
|
88
|
+
- Create or update the GitHub release for `vVERSION`.
|
|
89
|
+
- Paste the release note from `docs/releases/vVERSION.md` when one exists, for
|
|
90
|
+
example `docs/releases/v1.1.0.md`.
|
|
91
|
+
- Link the release back to the upgrade guide for application-facing changes.
|
|
92
|
+
- Confirm the pushed gem is visible on RubyGems.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Achilles 1.1.0
|
|
2
|
+
|
|
3
|
+
Achilles `1.1.0` tightens the application lifecycle for Rails + Turbo apps and
|
|
4
|
+
adds stronger nested-component behavior. This release is intended for existing
|
|
5
|
+
Achilles users who want explicit startup, safer teardown ordering, and better
|
|
6
|
+
runtime behavior around Turbo navigation and dynamic markup.
|
|
7
|
+
|
|
8
|
+
## Upgrade First
|
|
9
|
+
|
|
10
|
+
This release includes application-facing changes. Before upgrading an existing
|
|
11
|
+
app, read the full guide:
|
|
12
|
+
|
|
13
|
+
- [Upgrading to 1.1.0](../upgrading-to-1.1.0.md)
|
|
14
|
+
|
|
15
|
+
The most important change is explicit application startup:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const achilles = new Application();
|
|
19
|
+
achilles.componentsClassMapper.addComponentClass("MenuComponent", MenuComponent);
|
|
20
|
+
achilles.start();
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Call `start()` after registering component classes. Achilles no longer starts
|
|
24
|
+
from the `Application` constructor.
|
|
25
|
+
|
|
26
|
+
## Highlights
|
|
27
|
+
|
|
28
|
+
- Added explicit `Application#start` and `Application#stop` lifecycle methods.
|
|
29
|
+
- Added `Application#strictLifecycleErrors` for tests and development.
|
|
30
|
+
- Component parentage now follows DOM ancestry under the synthetic `Page` root.
|
|
31
|
+
- Component teardown now runs from children to parents.
|
|
32
|
+
- Elements with `data-component-class` must have a non-empty `id`; invalid roots
|
|
33
|
+
are skipped with a browser-console error.
|
|
34
|
+
- Achilles now requires `turbo-rails`, so Turbo importmap assets are available
|
|
35
|
+
to host applications.
|
|
36
|
+
- Added browser system coverage for nested components inside Turbo form
|
|
37
|
+
replacement and Turbo Drive navigation.
|
|
38
|
+
|
|
39
|
+
## Breaking Or Compatibility-Sensitive Changes
|
|
40
|
+
|
|
41
|
+
Applications should check these items before updating:
|
|
42
|
+
|
|
43
|
+
- Search for `new Application` and make sure each Achilles instance calls
|
|
44
|
+
`start()` after all component classes are registered.
|
|
45
|
+
- Search for `data-component-class` and make sure every component root has a
|
|
46
|
+
non-empty unique `id`.
|
|
47
|
+
- Review parent components whose `teardown()` removes DOM nodes, shared event
|
|
48
|
+
targets, widgets, or state that child components also use during cleanup.
|
|
49
|
+
- If you have tests that should fail on lifecycle errors, enable strict mode:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
achilles.strictLifecycleErrors = true;
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Verification
|
|
56
|
+
|
|
57
|
+
This release line was verified with:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
bin/rails test
|
|
61
|
+
node --test test/javascript/*_test.mjs
|
|
62
|
+
bin/rails test:system
|
|
63
|
+
RAILS_ENV=test bin/rails app:assets:precompile
|
|
64
|
+
gem build achilles.gemspec
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
One real application has also been updated successfully. Continue testing in
|
|
68
|
+
your own Rails + Turbo apps before rolling the update through every project.
|
|
69
|
+
|
|
70
|
+
## Links
|
|
71
|
+
|
|
72
|
+
- [Changelog](../../CHANGELOG.md)
|
|
73
|
+
- [Upgrade index](../upgrading.md)
|
|
74
|
+
- [1.1.0 upgrade guide](../upgrading-to-1.1.0.md)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Achilles 1.1.1
|
|
2
|
+
|
|
3
|
+
Achilles `1.1.1` is a patch release for Rails + Turbo applications using
|
|
4
|
+
browser history restoration. It fixes restored component markup that appeared on
|
|
5
|
+
screen after browser back or forward navigation but no longer had active
|
|
6
|
+
Achilles event listeners.
|
|
7
|
+
|
|
8
|
+
## Who Should Upgrade
|
|
9
|
+
|
|
10
|
+
Upgrade from `1.1.0` if your application uses Turbo Drive navigation and users
|
|
11
|
+
can return to Achilles-managed pages with the browser back or forward buttons.
|
|
12
|
+
|
|
13
|
+
There are no new application setup requirements in this patch release. Apps
|
|
14
|
+
upgrading from versions before `1.1.0` should still read the
|
|
15
|
+
[1.1.0 upgrade guide](../upgrading-to-1.1.0.md).
|
|
16
|
+
|
|
17
|
+
## Fixed
|
|
18
|
+
|
|
19
|
+
- Restored Turbo-cached DOM with stale `data-component-registered="true"` flags
|
|
20
|
+
is now parsed correctly when Achilles no longer has a matching registry entry.
|
|
21
|
+
- Deregistering a parent component now deregisters its child component subtree,
|
|
22
|
+
preventing orphaned child registry entries from blocking future setup.
|
|
23
|
+
- Page-level deregistration now iterates over a stable copy of child component
|
|
24
|
+
ids while the registry is being mutated.
|
|
25
|
+
|
|
26
|
+
## Verification
|
|
27
|
+
|
|
28
|
+
This release was verified with:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bin/rails test
|
|
32
|
+
node --test test/javascript/*_test.mjs
|
|
33
|
+
bin/rails test:system
|
|
34
|
+
RAILS_ENV=test bin/rails app:assets:precompile
|
|
35
|
+
gem build achilles.gemspec
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The system suite includes browser back, browser forward, restored nested
|
|
39
|
+
components, restored form-field listeners, restored Turbo-replaced forms, and
|
|
40
|
+
duplicate listener prevention.
|
|
41
|
+
|
|
42
|
+
## Links
|
|
43
|
+
|
|
44
|
+
- [Changelog](../../CHANGELOG.md)
|
|
45
|
+
- [Upgrade index](../upgrading.md)
|
|
46
|
+
- [1.1.0 upgrade guide](../upgrading-to-1.1.0.md)
|