turbo-rails 0.5.12 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 27a0cac6803a0e1e410fb4fce4aaa60a0998aee4dec7d1b4b4cbb674533ce5cf
4
- data.tar.gz: a4a67e8cff6989445d669cd6aa3b64b39dfc7de4ca7afdfddfd41c1e9334558e
3
+ metadata.gz: 42d7a658511c698c83b4e41853f96f5b2534a9ee81b89500044a22d6695a5150
4
+ data.tar.gz: 46e9abb9a01e4c9b50d2c2b704ff0e522a7231496b58d7acbc78303cdef67486
5
5
  SHA512:
6
- metadata.gz: a2de3cde68a13beffda32366c8b209666b1bae8a8dc3423ed9f5645dcae00fe88ba9f8ff08582eb60c9212a209d24683adf14f220a670923eac6806268198583
7
- data.tar.gz: be9f2251ca7bf7684d71ecfba9cbaaefbc64423d1b7f86428fc733facc9e1867ff6b54df1c756cc96ce0367ffe2cbb090af2bbf3319d432d88fbae005a3edd7b
6
+ metadata.gz: be906021b12ba905c7ce316900cb28db1ec22cbec561c2e009758a58cb059c82984862bae1f045c2f60b1db9fd396ebb2c88ba07c08a2871d1f39556f67cd2b1
7
+ data.tar.gz: de3c17c9f07971421add4c6038f385b609d465eb939f1860900c635b35b72b65ebbbea030a2d45722bb6bda86b2b38e5d794c005e063698c3ec63478fd3dd083
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Turbo
2
2
 
3
- [Turbo](https://turbo.hotwire.dev) gives you the speed of a single-page web application without having to write any JavaScript. Turbo accelerates links and form submissions without requiring you to change your server-side generated HTML. It lets you carve up a page into independent frames, which can be lazy-loaded and operate as independent components. And finally, helps you make partial page updates using just HTML and a set of CRUD-like container tags. These three techniques reduce the amount of custom JavaScript that many web applications need to write by an order of magnitude. And for the few dynamic bits that are left, you're invited to finish the job with [Stimulus](https://github.com/hotwired/stimulus).
3
+ [Turbo](https://turbo.hotwired.dev) gives you the speed of a single-page web application without having to write any JavaScript. Turbo accelerates links and form submissions without requiring you to change your server-side generated HTML. It lets you carve up a page into independent frames, which can be lazy-loaded and operate as independent components. And finally, helps you make partial page updates using just HTML and a set of CRUD-like container tags. These three techniques reduce the amount of custom JavaScript that many web applications need to write by an order of magnitude. And for the few dynamic bits that are left, you're invited to finish the job with [Stimulus](https://github.com/hotwired/stimulus).
4
4
 
5
5
  On top of accelerating web applications, Turbo was built from the ground-up to form the foundation of hybrid native applications. Write the navigational shell of your Android or iOS app using the standard platform tooling, then seamlessly fill in features from the web, following native navigation patterns. Not every mobile screen needs to be written in Swift or Kotlin to feel native. With Turbo, you spend less time wrangling JSON, waiting on app stores to approve updates, or reimplementing features you've already created in HTML.
6
6
 
@@ -50,17 +50,19 @@ import "@hotwired/turbo-rails"
50
50
 
51
51
  ## Usage
52
52
 
53
- You can watch [the video introduction to Hotwire](https://hotwire.dev/#screencast), which focuses extensively on demonstration Turbo in a Rails demo. Then you should familiarize yourself with [Turbo handbook](https://turbo.hotwire.dev/handbook/introduction) to understand Drive, Frames, and Streams in-depth. Finally, dive into the code documentation by starting with [`Turbo::FramesHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/frames_helper.rb), [`Turbo::StreamsHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/streams_helper.rb), [`Turbo::Streams::TagBuilder`](https://github.com/hotwired/turbo-rails/blob/main/app/models/turbo/streams/tag_builder.rb), and [`Turbo::Broadcastable`](https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb).
53
+ You can watch [the video introduction to Hotwire](https://hotwired.dev/#screencast), which focuses extensively on demonstration Turbo in a Rails demo. Then you should familiarize yourself with [Turbo handbook](https://turbo.hotwired.dev/handbook/introduction) to understand Drive, Frames, and Streams in-depth. Finally, dive into the code documentation by starting with [`Turbo::FramesHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/frames_helper.rb), [`Turbo::StreamsHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/streams_helper.rb), [`Turbo::Streams::TagBuilder`](https://github.com/hotwired/turbo-rails/blob/main/app/models/turbo/streams/tag_builder.rb), and [`Turbo::Broadcastable`](https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb).
54
54
 
55
55
 
56
56
  ## Compatibility with Rails UJS
57
57
 
58
- Turbo can coexist with Rails UJS, but you need to take a series of ugprade steps to make it happen. See [the upgrading guide](https://github.com/hotwired/turbo-rails/blob/main/UPGRADING.md).
58
+ Turbo can coexist with Rails UJS, but you need to take a series of upgrade steps to make it happen. See [the upgrading guide](https://github.com/hotwired/turbo-rails/blob/main/UPGRADING.md).
59
59
 
60
60
 
61
61
  ## Development
62
62
 
63
63
  * To run the Rails tests: `bundle exec rake`.
64
+ * To install dependencies: `bundle install`
65
+ * To prepare the test database: `cd tests/dummy; RAILS_ENV=test ./bin/rails db:migrate`
64
66
  * To compile the JavaScript for the asset pipeline: `yarn build`
65
67
 
66
68
 
@@ -63,6 +63,11 @@ class FrameElement extends HTMLElement {
63
63
  disconnectedCallback() {
64
64
  this.delegate.disconnect();
65
65
  }
66
+ reload() {
67
+ const {src: src} = this;
68
+ this.src = null;
69
+ this.src = src;
70
+ }
66
71
  attributeChangedCallback(name) {
67
72
  if (name == "loading") {
68
73
  this.delegate.loadingStyleChanged();
@@ -144,8 +149,6 @@ function getAnchor(url) {
144
149
  return url.hash.slice(1);
145
150
  } else if (anchorMatch = url.href.match(/#(.*)$/)) {
146
151
  return anchorMatch[1];
147
- } else {
148
- return "";
149
152
  }
150
153
  }
151
154
 
@@ -162,13 +165,13 @@ function isPrefixedBy(baseURL, url) {
162
165
  return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
163
166
  }
164
167
 
168
+ function getRequestURL(url) {
169
+ const anchor = getAnchor(url);
170
+ return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href;
171
+ }
172
+
165
173
  function toCacheKey(url) {
166
- const anchorLength = url.hash.length;
167
- if (anchorLength < 2) {
168
- return url.href;
169
- } else {
170
- return url.href.slice(0, -anchorLength);
171
- }
174
+ return getRequestURL(url);
172
175
  }
173
176
 
174
177
  function urlsAreEqual(left, right) {
@@ -325,6 +328,7 @@ function fetchMethodFromString(method) {
325
328
  class FetchRequest {
326
329
  constructor(delegate, method, location, body = new URLSearchParams) {
327
330
  this.abortController = new AbortController;
331
+ this.resolveRequestPromise = value => {};
328
332
  this.delegate = delegate;
329
333
  this.method = method;
330
334
  this.headers = this.defaultHeaders;
@@ -351,11 +355,7 @@ class FetchRequest {
351
355
  var _a, _b;
352
356
  const {fetchOptions: fetchOptions} = this;
353
357
  (_b = (_a = this.delegate).prepareHeadersForRequest) === null || _b === void 0 ? void 0 : _b.call(_a, this.headers, this);
354
- dispatch("turbo:before-fetch-request", {
355
- detail: {
356
- fetchOptions: fetchOptions
357
- }
358
- });
358
+ await this.allowRequestToBeIntercepted(fetchOptions);
359
359
  try {
360
360
  this.delegate.requestStarted(this);
361
361
  const response = await fetch(this.url.href, fetchOptions);
@@ -405,6 +405,18 @@ class FetchRequest {
405
405
  get abortSignal() {
406
406
  return this.abortController.signal;
407
407
  }
408
+ async allowRequestToBeIntercepted(fetchOptions) {
409
+ const requestInterception = new Promise((resolve => this.resolveRequestPromise = resolve));
410
+ const event = dispatch("turbo:before-fetch-request", {
411
+ cancelable: true,
412
+ detail: {
413
+ fetchOptions: fetchOptions,
414
+ url: this.url.href,
415
+ resume: this.resolveRequestPromise
416
+ }
417
+ });
418
+ if (event.defaultPrevented) await requestInterception;
419
+ }
408
420
  }
409
421
 
410
422
  function mergeFormDataEntries(url, entries) {
@@ -736,6 +748,8 @@ class FormInterceptor {
736
748
 
737
749
  class View {
738
750
  constructor(delegate, element) {
751
+ this.resolveRenderPromise = value => {};
752
+ this.resolveInterceptionPromise = value => {};
739
753
  this.delegate = delegate;
740
754
  this.element = element;
741
755
  }
@@ -743,6 +757,7 @@ class View {
743
757
  const element = this.snapshot.getElementForAnchor(anchor);
744
758
  if (element) {
745
759
  this.scrollToElement(element);
760
+ this.focusElement(element);
746
761
  } else {
747
762
  this.scrollToPosition({
748
763
  x: 0,
@@ -750,9 +765,23 @@ class View {
750
765
  });
751
766
  }
752
767
  }
768
+ scrollToAnchorFromLocation(location) {
769
+ this.scrollToAnchor(getAnchor(location));
770
+ }
753
771
  scrollToElement(element) {
754
772
  element.scrollIntoView();
755
773
  }
774
+ focusElement(element) {
775
+ if (element instanceof HTMLElement) {
776
+ if (element.hasAttribute("tabindex")) {
777
+ element.focus();
778
+ } else {
779
+ element.setAttribute("tabindex", "-1");
780
+ element.focus();
781
+ element.removeAttribute("tabindex");
782
+ }
783
+ }
784
+ }
756
785
  scrollToPosition({x: x, y: y}) {
757
786
  this.scrollRoot.scrollTo(x, y);
758
787
  }
@@ -760,20 +789,22 @@ class View {
760
789
  return window;
761
790
  }
762
791
  async render(renderer) {
763
- if (this.renderer) {
764
- throw new Error("rendering is already in progress");
765
- }
766
792
  const {isPreview: isPreview, shouldRender: shouldRender, newSnapshot: snapshot} = renderer;
767
793
  if (shouldRender) {
768
794
  try {
795
+ this.renderPromise = new Promise((resolve => this.resolveRenderPromise = resolve));
769
796
  this.renderer = renderer;
770
797
  this.prepareToRenderSnapshot(renderer);
771
- this.delegate.viewWillRenderSnapshot(snapshot, isPreview);
798
+ const renderInterception = new Promise((resolve => this.resolveInterceptionPromise = resolve));
799
+ const immediateRender = this.delegate.allowsImmediateRender(snapshot, this.resolveInterceptionPromise);
800
+ if (!immediateRender) await renderInterception;
772
801
  await this.renderSnapshot(renderer);
773
802
  this.delegate.viewRenderedSnapshot(snapshot, isPreview);
774
803
  this.finishRenderingSnapshot(renderer);
775
804
  } finally {
776
805
  delete this.renderer;
806
+ this.resolveRenderPromise(undefined);
807
+ delete this.renderPromise;
777
808
  }
778
809
  } else {
779
810
  this.invalidate();
@@ -824,7 +855,7 @@ class LinkInterceptor {
824
855
  if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url)) {
825
856
  this.clickEvent.preventDefault();
826
857
  event.preventDefault();
827
- this.convertLinkWithMethodClickToFormSubmission(event.target) || this.delegate.linkClickIntercepted(event.target, event.detail.url);
858
+ this.delegate.linkClickIntercepted(event.target, event.detail.url);
828
859
  }
829
860
  }
830
861
  delete this.clickEvent;
@@ -845,21 +876,6 @@ class LinkInterceptor {
845
876
  document.removeEventListener("turbo:click", this.linkClicked);
846
877
  document.removeEventListener("turbo:before-visit", this.willVisit);
847
878
  }
848
- convertLinkWithMethodClickToFormSubmission(link) {
849
- var _a;
850
- const linkMethod = link.getAttribute("data-turbo-method") || link.getAttribute("data-method");
851
- if (linkMethod) {
852
- const form = document.createElement("form");
853
- form.method = linkMethod;
854
- form.action = link.getAttribute("href") || "undefined";
855
- (_a = link.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(form, link);
856
- return dispatch("submit", {
857
- target: form
858
- });
859
- } else {
860
- return false;
861
- }
862
- }
863
879
  respondsToEventTarget(target) {
864
880
  const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
865
881
  return element && element.closest("turbo-frame, html") == this.element;
@@ -1150,7 +1166,7 @@ ProgressBar.animationDuration = 300;
1150
1166
  class HeadSnapshot extends Snapshot {
1151
1167
  constructor() {
1152
1168
  super(...arguments);
1153
- this.detailsByOuterHTML = this.children.reduce(((result, element) => {
1169
+ this.detailsByOuterHTML = this.children.filter((element => !elementIsNoscript(element))).reduce(((result, element) => {
1154
1170
  const {outerHTML: outerHTML} = element;
1155
1171
  const details = outerHTML in result ? result[outerHTML] : {
1156
1172
  type: elementType(element),
@@ -1217,6 +1233,11 @@ function elementIsScript(element) {
1217
1233
  return tagName == "script";
1218
1234
  }
1219
1235
 
1236
+ function elementIsNoscript(element) {
1237
+ const tagName = element.tagName.toLowerCase();
1238
+ return tagName == "noscript";
1239
+ }
1240
+
1220
1241
  function elementIsStylesheet(element) {
1221
1242
  const tagName = element.tagName.toLowerCase();
1222
1243
  return tagName == "style" || tagName == "link" && element.getAttribute("rel") == "stylesheet";
@@ -1319,6 +1340,7 @@ class Visit {
1319
1340
  this.referrer = referrer;
1320
1341
  this.snapshotHTML = snapshotHTML;
1321
1342
  this.response = response;
1343
+ this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
1322
1344
  }
1323
1345
  get adapter() {
1324
1346
  return this.delegate.adapter;
@@ -1411,6 +1433,7 @@ class Visit {
1411
1433
  const {statusCode: statusCode, responseHTML: responseHTML} = this.response;
1412
1434
  this.render((async () => {
1413
1435
  this.cacheSnapshot();
1436
+ if (this.view.renderPromise) await this.view.renderPromise;
1414
1437
  if (isSuccessful(statusCode) && responseHTML != null) {
1415
1438
  await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML));
1416
1439
  this.adapter.visitRendered(this);
@@ -1445,10 +1468,15 @@ class Visit {
1445
1468
  const isPreview = this.shouldIssueRequest();
1446
1469
  this.render((async () => {
1447
1470
  this.cacheSnapshot();
1448
- await this.view.renderPage(snapshot, isPreview);
1449
- this.adapter.visitRendered(this);
1450
- if (!isPreview) {
1451
- this.complete();
1471
+ if (this.isSamePage) {
1472
+ this.adapter.visitRendered(this);
1473
+ } else {
1474
+ if (this.view.renderPromise) await this.view.renderPromise;
1475
+ await this.view.renderPage(snapshot, isPreview);
1476
+ this.adapter.visitRendered(this);
1477
+ if (!isPreview) {
1478
+ this.complete();
1479
+ }
1452
1480
  }
1453
1481
  }));
1454
1482
  }
@@ -1460,6 +1488,14 @@ class Visit {
1460
1488
  this.followedRedirect = true;
1461
1489
  }
1462
1490
  }
1491
+ goToSamePageAnchor() {
1492
+ if (this.isSamePage) {
1493
+ this.render((async () => {
1494
+ this.cacheSnapshot();
1495
+ this.adapter.visitRendered(this);
1496
+ }));
1497
+ }
1498
+ }
1463
1499
  requestStarted() {
1464
1500
  this.startRequest();
1465
1501
  }
@@ -1502,10 +1538,13 @@ class Visit {
1502
1538
  performScroll() {
1503
1539
  if (!this.scrolled) {
1504
1540
  if (this.action == "restore") {
1505
- this.scrollToRestoredPosition() || this.scrollToTop();
1541
+ this.scrollToRestoredPosition() || this.scrollToAnchor() || this.scrollToTop();
1506
1542
  } else {
1507
1543
  this.scrollToAnchor() || this.scrollToTop();
1508
1544
  }
1545
+ if (this.isSamePage) {
1546
+ this.delegate.visitScrolledToSamePageLocation(this.view.lastRenderedLocation, this.location);
1547
+ }
1509
1548
  this.scrolled = true;
1510
1549
  }
1511
1550
  }
@@ -1517,8 +1556,9 @@ class Visit {
1517
1556
  }
1518
1557
  }
1519
1558
  scrollToAnchor() {
1520
- if (getAnchor(this.location)) {
1521
- this.view.scrollToAnchor(getAnchor(this.location));
1559
+ const anchor = getAnchor(this.location);
1560
+ if (anchor != null) {
1561
+ this.view.scrollToAnchor(anchor);
1522
1562
  return true;
1523
1563
  }
1524
1564
  }
@@ -1548,7 +1588,13 @@ class Visit {
1548
1588
  return typeof this.response == "object";
1549
1589
  }
1550
1590
  shouldIssueRequest() {
1551
- return this.action == "restore" ? !this.hasCachedSnapshot() : true;
1591
+ if (this.isSamePage) {
1592
+ return false;
1593
+ } else if (this.action == "restore") {
1594
+ return !this.hasCachedSnapshot();
1595
+ } else {
1596
+ return true;
1597
+ }
1552
1598
  }
1553
1599
  cacheSnapshot() {
1554
1600
  if (!this.snapshotCached) {
@@ -1561,7 +1607,7 @@ class Visit {
1561
1607
  await new Promise((resolve => {
1562
1608
  this.frame = requestAnimationFrame((() => resolve()));
1563
1609
  }));
1564
- callback();
1610
+ await callback();
1565
1611
  delete this.frame;
1566
1612
  this.performScroll();
1567
1613
  }
@@ -1591,6 +1637,7 @@ class BrowserAdapter {
1591
1637
  visitStarted(visit) {
1592
1638
  visit.issueRequest();
1593
1639
  visit.changeHistory();
1640
+ visit.goToSamePageAnchor();
1594
1641
  visit.loadCachedSnapshot();
1595
1642
  }
1596
1643
  visitRequestStarted(visit) {
@@ -1968,6 +2015,12 @@ class Navigator {
1968
2015
  visitCompleted(visit) {
1969
2016
  this.delegate.visitCompleted(visit);
1970
2017
  }
2018
+ locationWithActionIsSamePage(location, action) {
2019
+ return getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) && (getAnchor(location) != null || action == "restore");
2020
+ }
2021
+ visitScrolledToSamePageLocation(oldURL, newURL) {
2022
+ this.delegate.visitScrolledToSamePageLocation(oldURL, newURL);
2023
+ }
1971
2024
  get location() {
1972
2025
  return this.history.location;
1973
2026
  }
@@ -2318,7 +2371,7 @@ class PageView extends View {
2318
2371
  }
2319
2372
  renderError(snapshot) {
2320
2373
  const renderer = new ErrorRenderer(this.snapshot, snapshot, false);
2321
- this.render(renderer);
2374
+ return this.render(renderer);
2322
2375
  }
2323
2376
  clearSnapshotCache() {
2324
2377
  this.snapshotCache.clear();
@@ -2436,10 +2489,26 @@ class Session {
2436
2489
  }
2437
2490
  followedLinkToLocation(link, location) {
2438
2491
  const action = this.getActionForLink(link);
2439
- this.visit(location.href, {
2492
+ this.convertLinkWithMethodClickToFormSubmission(link) || this.visit(location.href, {
2440
2493
  action: action
2441
2494
  });
2442
2495
  }
2496
+ convertLinkWithMethodClickToFormSubmission(link) {
2497
+ var _a;
2498
+ const linkMethod = link.getAttribute("data-turbo-method");
2499
+ if (linkMethod) {
2500
+ const form = document.createElement("form");
2501
+ form.method = linkMethod;
2502
+ form.action = link.getAttribute("href") || "undefined";
2503
+ (_a = link.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(form, link);
2504
+ return dispatch("submit", {
2505
+ cancelable: true,
2506
+ target: form
2507
+ });
2508
+ } else {
2509
+ return false;
2510
+ }
2511
+ }
2443
2512
  allowsVisitingLocation(location) {
2444
2513
  return this.applicationAllowsVisitingLocation(location);
2445
2514
  }
@@ -2449,11 +2518,17 @@ class Session {
2449
2518
  }
2450
2519
  visitStarted(visit) {
2451
2520
  extendURLWithDeprecatedProperties(visit.location);
2452
- this.notifyApplicationAfterVisitingLocation(visit.location);
2521
+ this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
2453
2522
  }
2454
2523
  visitCompleted(visit) {
2455
2524
  this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
2456
2525
  }
2526
+ locationWithActionIsSamePage(location, action) {
2527
+ return this.navigator.locationWithActionIsSamePage(location, action);
2528
+ }
2529
+ visitScrolledToSamePageLocation(oldURL, newURL) {
2530
+ this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
2531
+ }
2457
2532
  willSubmitForm(form, submitter) {
2458
2533
  return elementIsNavigable(form) && elementIsNavigable(submitter);
2459
2534
  }
@@ -2476,8 +2551,9 @@ class Session {
2476
2551
  viewWillCacheSnapshot() {
2477
2552
  this.notifyApplicationBeforeCachingSnapshot();
2478
2553
  }
2479
- viewWillRenderSnapshot({element: element}, isPreview) {
2480
- this.notifyApplicationBeforeRender(element);
2554
+ allowsImmediateRender({element: element}, resume) {
2555
+ const event = this.notifyApplicationBeforeRender(element, resume);
2556
+ return !event.defaultPrevented;
2481
2557
  }
2482
2558
  viewRenderedSnapshot(snapshot, isPreview) {
2483
2559
  this.view.lastRenderedLocation = this.history.location;
@@ -2511,21 +2587,24 @@ class Session {
2511
2587
  cancelable: true
2512
2588
  });
2513
2589
  }
2514
- notifyApplicationAfterVisitingLocation(location) {
2590
+ notifyApplicationAfterVisitingLocation(location, action) {
2515
2591
  return dispatch("turbo:visit", {
2516
2592
  detail: {
2517
- url: location.href
2593
+ url: location.href,
2594
+ action: action
2518
2595
  }
2519
2596
  });
2520
2597
  }
2521
2598
  notifyApplicationBeforeCachingSnapshot() {
2522
2599
  return dispatch("turbo:before-cache");
2523
2600
  }
2524
- notifyApplicationBeforeRender(newBody) {
2601
+ notifyApplicationBeforeRender(newBody, resume) {
2525
2602
  return dispatch("turbo:before-render", {
2526
2603
  detail: {
2527
- newBody: newBody
2528
- }
2604
+ newBody: newBody,
2605
+ resume: resume
2606
+ },
2607
+ cancelable: true
2529
2608
  });
2530
2609
  }
2531
2610
  notifyApplicationAfterRender() {
@@ -2539,6 +2618,12 @@ class Session {
2539
2618
  }
2540
2619
  });
2541
2620
  }
2621
+ notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) {
2622
+ dispatchEvent(new HashChangeEvent("hashchange", {
2623
+ oldURL: oldURL.toString(),
2624
+ newURL: newURL.toString()
2625
+ }));
2626
+ }
2542
2627
  getActionForLink(link) {
2543
2628
  const action = link.getAttribute("data-turbo-action");
2544
2629
  return isAction(action) ? action : "advance";
@@ -2648,6 +2733,7 @@ class FrameController {
2648
2733
  const {body: body} = parseHTMLDocument(html);
2649
2734
  const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
2650
2735
  const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
2736
+ if (this.view.renderPromise) await this.view.renderPromise;
2651
2737
  await this.view.render(renderer);
2652
2738
  }
2653
2739
  } catch (error) {
@@ -2659,7 +2745,11 @@ class FrameController {
2659
2745
  this.loadSourceURL();
2660
2746
  }
2661
2747
  shouldInterceptLinkClick(element, url) {
2662
- return this.shouldInterceptNavigation(element);
2748
+ if (element.hasAttribute("data-turbo-method")) {
2749
+ return false;
2750
+ } else {
2751
+ return this.shouldInterceptNavigation(element);
2752
+ }
2663
2753
  }
2664
2754
  linkClickIntercepted(element, url) {
2665
2755
  this.navigateFrame(element, url);
@@ -2722,7 +2812,9 @@ class FrameController {
2722
2812
  const frame = this.findFrameElement(formSubmission.formElement);
2723
2813
  frame.removeAttribute("busy");
2724
2814
  }
2725
- viewWillRenderSnapshot(snapshot, isPreview) {}
2815
+ allowsImmediateRender(snapshot, resume) {
2816
+ return true;
2817
+ }
2726
2818
  viewRenderedSnapshot(snapshot, isPreview) {}
2727
2819
  viewInvalidated() {}
2728
2820
  async visit(url) {
@@ -2988,7 +3080,7 @@ customElements.define("turbo-stream", StreamElement);
2988
3080
 
2989
3081
  Load your application’s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.
2990
3082
 
2991
- For more information, see: https://turbo.hotwire.dev/handbook/building#working-with-script-elements
3083
+ For more information, see: https://turbo.hotwired.dev/handbook/building#working-with-script-elements
2992
3084
 
2993
3085
  ——
2994
3086
  Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
@@ -3036,6 +3128,8 @@ function setProgressBarDelay(delay) {
3036
3128
  var Turbo = Object.freeze({
3037
3129
  __proto__: null,
3038
3130
  navigator: navigator,
3131
+ PageRenderer: PageRenderer,
3132
+ PageSnapshot: PageSnapshot,
3039
3133
  start: start,
3040
3134
  registerAdapter: registerAdapter,
3041
3135
  visit: visit,
@@ -3052,6 +3146,8 @@ start();
3052
3146
 
3053
3147
  var turbo_es2017Esm = Object.freeze({
3054
3148
  __proto__: null,
3149
+ PageRenderer: PageRenderer,
3150
+ PageSnapshot: PageSnapshot,
3055
3151
  clearCache: clearCache,
3056
3152
  connectStreamSource: connectStreamSource,
3057
3153
  disconnectStreamSource: disconnectStreamSource,
@@ -13,6 +13,10 @@ module Turbo::Streams::Broadcasts
13
13
  broadcast_action_to *streamables, action: :replace, target: target, **rendering
14
14
  end
15
15
 
16
+ def broadcast_update_to(*streamables, target:, **rendering)
17
+ broadcast_action_to *streamables, action: :update, target: target, **rendering
18
+ end
19
+
16
20
  def broadcast_before_to(*streamables, target:, **rendering)
17
21
  broadcast_action_to *streamables, action: :before, target: target, **rendering
18
22
  end
@@ -40,6 +44,10 @@ module Turbo::Streams::Broadcasts
40
44
  broadcast_action_later_to *streamables, action: :replace, target: target, **rendering
41
45
  end
42
46
 
47
+ def broadcast_update_later_to(*streamables, target:, **rendering)
48
+ broadcast_action_later_to *streamables, action: :update, target: target, **rendering
49
+ end
50
+
43
51
  def broadcast_before_later_to(*streamables, target:, **rendering)
44
52
  broadcast_action_later_to *streamables, action: :before, target: target, **rendering
45
53
  end
@@ -6,11 +6,20 @@ module Turbo::Streams::ActionHelper
6
6
  #
7
7
  # turbo_stream_action_tag "replace", target: "message_1", template: %(<div id="message_1">Hello!</div>)
8
8
  # # => <turbo-stream action="replace" target="message_1"><template><div id="message_1">Hello!</div></template></turbo-stream>
9
- def turbo_stream_action_tag(action, target:, template: nil)
10
- target = convert_to_turbo_stream_dom_id(target)
9
+ #
10
+ # turbo_stream_action_tag "replace", targets: "message_1", template: %(<div id="message_1">Hello!</div>)
11
+ # # => <turbo-stream action="replace" targets="message_1"><template><div id="message_1">Hello!</div></template></turbo-stream>
12
+ def turbo_stream_action_tag(action, target: nil, targets: nil, template: nil)
11
13
  template = action.to_sym == :remove ? "" : "<template>#{template}</template>"
12
14
 
13
- %(<turbo-stream action="#{action}" target="#{target}">#{template}</turbo-stream>).html_safe
15
+ if target
16
+ target = convert_to_turbo_stream_dom_id(target)
17
+ %(<turbo-stream action="#{action}" target="#{target}">#{template}</turbo-stream>).html_safe
18
+ elsif targets
19
+ %(<turbo-stream action="#{action}" targets="#{targets}">#{template}</turbo-stream>).html_safe
20
+ else
21
+ raise ArgumentError, "target or targets must be supplied"
22
+ end
14
23
  end
15
24
 
16
25
  private
@@ -75,8 +75,8 @@ module Turbo::Broadcastable
75
75
  #
76
76
  # # Sends <turbo-stream action="remove" target="clearance_5"></turbo-stream> to the stream named "identity:2:clearances"
77
77
  # clearance.broadcast_remove_to examiner.identity, :clearances
78
- def broadcast_remove_to(*streamables)
79
- Turbo::StreamsChannel.broadcast_remove_to *streamables, target: self
78
+ def broadcast_remove_to(*streamables, target: self)
79
+ Turbo::StreamsChannel.broadcast_remove_to *streamables, target: target
80
80
  end
81
81
 
82
82
  # Same as <tt>#broadcast_remove_to</tt>, but the designated stream is automatically set to the current model.
@@ -103,6 +103,25 @@ module Turbo::Broadcastable
103
103
  broadcast_replace_to self, **rendering
104
104
  end
105
105
 
106
+ # Update this broadcastable model in the dom for subscribers of the stream name identified by the passed
107
+ # <tt>streamables</tt>. The rendering parameters can be set by appending named arguments to the call. Examples:
108
+ #
109
+ # # Sends <turbo-stream action="update" target="clearance_5"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
110
+ # # to the stream named "identity:2:clearances"
111
+ # clearance.broadcast_update_to examiner.identity, :clearances
112
+ #
113
+ # # Sends <turbo-stream action="update" target="clearance_5"><template><div id="clearance_5">Other partial</div></template></turbo-stream>
114
+ # # to the stream named "identity:2:clearances"
115
+ # clearance.broadcast_update_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 }
116
+ def broadcast_update_to(*streamables, **rendering)
117
+ Turbo::StreamsChannel.broadcast_update_to *streamables, target: self, **broadcast_rendering_with_defaults(rendering)
118
+ end
119
+
120
+ # Same as <tt>#broadcast_update_to</tt>, but the designated stream is automatically set to the current model.
121
+ def broadcast_update(**rendering)
122
+ broadcast_update_to self, **rendering
123
+ end
124
+
106
125
  # Insert a rendering of this broadcastable model before the target identified by it's dom id passed as <tt>target</tt>
107
126
  # for subscribers of the stream name identified by the passed <tt>streamables</tt>. The rendering parameters can be set by
108
127
  # appending named arguments to the call. Examples:
@@ -202,6 +221,16 @@ module Turbo::Broadcastable
202
221
  broadcast_replace_later_to self, **rendering
203
222
  end
204
223
 
224
+ # Same as <tt>broadcast_update_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
225
+ def broadcast_update_later_to(*streamables, **rendering)
226
+ Turbo::StreamsChannel.broadcast_update_later_to *streamables, target: self, **broadcast_rendering_with_defaults(rendering)
227
+ end
228
+
229
+ # Same as <tt>#broadcast_update_later_to</tt>, but the designated stream is automatically set to the current model.
230
+ def broadcast_update_later(**rendering)
231
+ broadcast_update_later_to self, **rendering
232
+ end
233
+
205
234
  # Same as <tt>broadcast_append_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
206
235
  def broadcast_append_later_to(*streamables, target: broadcast_target_default, **rendering)
207
236
  Turbo::StreamsChannel.broadcast_append_later_to *streamables, target: target, **broadcast_rendering_with_defaults(rendering)
@@ -40,6 +40,16 @@ class Turbo::Streams::TagBuilder
40
40
  action :remove, target, allow_inferred_rendering: false
41
41
  end
42
42
 
43
+ # Removes the <tt>targets</tt> from the dom. The targets can either be a CSS selector string or an object that responds to
44
+ # <tt>to_key</tt>, which is then called and passed through <tt>ActionView::RecordIdentifier.dom_id</tt> (all Active Records
45
+ # do). Examples:
46
+ #
47
+ # <%= turbo_stream.remove_all ".clearance_item" %>
48
+ # <%= turbo_stream.remove_all clearance %>
49
+ def remove_all(targets)
50
+ action_all :remove, targets, allow_inferred_rendering: false
51
+ end
52
+
43
53
  # Replace the <tt>target</tt> in the dom with the either the <tt>content</tt> passed in, a rendering result determined
44
54
  # by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the target as a record. Examples:
45
55
  #
@@ -53,6 +63,19 @@ class Turbo::Streams::TagBuilder
53
63
  action :replace, target, content, **rendering, &block
54
64
  end
55
65
 
66
+ # Replace the <tt>targets</tt> in the dom with the either the <tt>content</tt> passed in, a rendering result determined
67
+ # by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the target as a record. Examples:
68
+ #
69
+ # <%= turbo_stream.replace_all ".clearance_item", "<div class='clearance_item'>Replace the dom target identified by the class clearance_item</div>" %>
70
+ # <%= turbo_stream.replace_all clearance %>
71
+ # <%= turbo_stream.replace_all clearance, partial: "clearances/clearance", locals: { title: "Hello" } %>
72
+ # <%= turbo_stream.replace_all ".clearance_item" do %>
73
+ # <div class='.clearance_item'>Replace the dom target identified by the class clearance_item</div>
74
+ # <% end %>
75
+ def replace_all(targets, content = nil, **rendering, &block)
76
+ action_all :replace, targets, content, **rendering, &block
77
+ end
78
+
56
79
  # Insert the <tt>content</tt> passed in, a rendering result determined by the <tt>rendering</tt> keyword arguments,
57
80
  # the content in the block, or the rendering of the target as a record before the <tt>target</tt> in the dom. Examples:
58
81
  #
@@ -66,6 +89,19 @@ class Turbo::Streams::TagBuilder
66
89
  action :before, target, content, **rendering, &block
67
90
  end
68
91
 
92
+ # Insert the <tt>content</tt> passed in, a rendering result determined by the <tt>rendering</tt> keyword arguments,
93
+ # the content in the block, or the rendering of the target as a record before the <tt>targets</tt> in the dom. Examples:
94
+ #
95
+ # <%= turbo_stream.before_all ".clearance_item", "<div class='clearance_item'>Insert before the dom target identified by the class clearance_item</div>" %>
96
+ # <%= turbo_stream.before_all clearance %>
97
+ # <%= turbo_stream.before_all clearance, partial: "clearances/clearance", locals: { title: "Hello" } %>
98
+ # <%= turbo_stream.before_all ".clearance_item" do %>
99
+ # <div class='clearance_item'>Insert before the dom target identified by clearance_item</div>
100
+ # <% end %>
101
+ def before_all(targets, content = nil, **rendering, &block)
102
+ action_all :before, targets, content, **rendering, &block
103
+ end
104
+
69
105
  # Insert the <tt>content</tt> passed in, a rendering result determined by the <tt>rendering</tt> keyword arguments,
70
106
  # the content in the block, or the rendering of the target as a record after the <tt>target</tt> in the dom. Examples:
71
107
  #
@@ -79,6 +115,19 @@ class Turbo::Streams::TagBuilder
79
115
  action :after, target, content, **rendering, &block
80
116
  end
81
117
 
118
+ # Insert the <tt>content</tt> passed in, a rendering result determined by the <tt>rendering</tt> keyword arguments,
119
+ # the content in the block, or the rendering of the target as a record after the <tt>targets</tt> in the dom. Examples:
120
+ #
121
+ # <%= turbo_stream.after_all ".clearance_item", "<div class='clearance_item'>Insert after the dom target identified by the class clearance_item</div>" %>
122
+ # <%= turbo_stream.after_all clearance %>
123
+ # <%= turbo_stream.after_all clearance, partial: "clearances/clearance", locals: { title: "Hello" } %>
124
+ # <%= turbo_stream.after_all "clearance_item" do %>
125
+ # <div class='clearance_item'>Insert after the dom target identified by the class clearance_item</div>
126
+ # <% end %>
127
+ def after_all(targets, content = nil, **rendering, &block)
128
+ action_all :after, targets, content, **rendering, &block
129
+ end
130
+
82
131
  # Update the <tt>target</tt> in the dom with the either the <tt>content</tt> passed in or a rendering result determined
83
132
  # by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the target as a record. Examples:
84
133
  #
@@ -92,6 +141,19 @@ class Turbo::Streams::TagBuilder
92
141
  action :update, target, content, **rendering, &block
93
142
  end
94
143
 
144
+ # Update the <tt>targets</tt> in the dom with the either the <tt>content</tt> passed in or a rendering result determined
145
+ # by the <tt>rendering</tt> keyword arguments, the content in the block, or the rendering of the targets as a record. Examples:
146
+ #
147
+ # <%= turbo_stream.update_all "clearance_item", "Update the content of the dom target identified by the class clearance_item" %>
148
+ # <%= turbo_stream.update_all clearance %>
149
+ # <%= turbo_stream.update_all clearance, partial: "clearances/new_clearance", locals: { title: "Hello" } %>
150
+ # <%= turbo_stream.update_all "clearance_item" do %>
151
+ # Update the content of the dom target identified by the class clearance_item
152
+ # <% end %>
153
+ def update_all(targets, content = nil, **rendering, &block)
154
+ action_all :update, targets, content, **rendering, &block
155
+ end
156
+
95
157
  # Append to the target in the dom identified with <tt>target</tt> either the <tt>content</tt> passed in or a
96
158
  # rendering result determined by the <tt>rendering</tt> keyword arguments, the content in the block,
97
159
  # or the rendering of the content as a record. Examples:
@@ -106,6 +168,20 @@ class Turbo::Streams::TagBuilder
106
168
  action :append, target, content, **rendering, &block
107
169
  end
108
170
 
171
+ # Append to the targets in the dom identified with <tt>targets</tt> either the <tt>content</tt> passed in or a
172
+ # rendering result determined by the <tt>rendering</tt> keyword arguments, the content in the block,
173
+ # or the rendering of the content as a record. Examples:
174
+ #
175
+ # <%= turbo_stream.append_all ".clearances", "<div class='clearance_item'>Append this to .clearance_group</div>" %>
176
+ # <%= turbo_stream.append_all ".clearances", clearance %>
177
+ # <%= turbo_stream.append_all ".clearances", partial: "clearances/new_clearance", locals: { clearance: clearance } %>
178
+ # <%= turbo_stream.append_all ".clearances" do %>
179
+ # <div id='clearance_item'>Append this to .clearances</div>
180
+ # <% end %>
181
+ def append_all(targets, content = nil, **rendering, &block)
182
+ action_all :append, targets, content, **rendering, &block
183
+ end
184
+
109
185
  # Prepend to the target in the dom identified with <tt>target</tt> either the <tt>content</tt> passed in or a
110
186
  # rendering result determined by the <tt>rendering</tt> keyword arguments or the content in the block,
111
187
  # or the rendering of the content as a record. Examples:
@@ -120,20 +196,34 @@ class Turbo::Streams::TagBuilder
120
196
  action :prepend, target, content, **rendering, &block
121
197
  end
122
198
 
123
- # Send an action of the type <tt>name</tt>. Options described in the concrete methods.
199
+ # Prepend to the targets in the dom identified with <tt>targets</tt> either the <tt>content</tt> passed in or a
200
+ # rendering result determined by the <tt>rendering</tt> keyword arguments or the content in the block,
201
+ # or the rendering of the content as a record. Examples:
202
+ #
203
+ # <%= turbo_stream.prepend_all ".clearances", "<div class='clearance_item'>Prepend this to .clearances</div>" %>
204
+ # <%= turbo_stream.prepend_all ".clearances", clearance %>
205
+ # <%= turbo_stream.prepend_all ".clearances", partial: "clearances/new_clearance", locals: { clearance: clearance } %>
206
+ # <%= turbo_stream.prepend_all ".clearances" do %>
207
+ # <div class='clearance_item'>Prepend this to .clearances</div>
208
+ # <% end %>
209
+ def prepend_all(targets, content = nil, **rendering, &block)
210
+ action_all :prepend, targets, content, **rendering, &block
211
+ end
212
+
213
+ # Send an action of the type <tt>name</tt> to <tt>target</tt>. Options described in the concrete methods.
124
214
  def action(name, target, content = nil, allow_inferred_rendering: true, **rendering, &block)
125
215
  target_name = extract_target_name_from(target)
216
+ template = render_template(target, content, allow_inferred_rendering: allow_inferred_rendering, **rendering, &block)
126
217
 
127
- case
128
- when content
129
- turbo_stream_action_tag name, target: target_name, template: (render_record(content) if allow_inferred_rendering) || content
130
- when block_given?
131
- turbo_stream_action_tag name, target: target_name, template: @view_context.capture(&block)
132
- when rendering.any?
133
- turbo_stream_action_tag name, target: target_name, template: @view_context.render(formats: [ :html ], **rendering)
134
- else
135
- turbo_stream_action_tag name, target: target_name, template: (render_record(target) if allow_inferred_rendering)
136
- end
218
+ turbo_stream_action_tag name, target: target_name, template: template
219
+ end
220
+
221
+ # Send an action of the type <tt>name</tt> to <tt>targets</tt>. Options described in the concrete methods.
222
+ def action_all(name, targets, content = nil, allow_inferred_rendering: true, **rendering, &block)
223
+ targets_name = extract_target_name_from(targets)
224
+ template = render_template(targets, content, allow_inferred_rendering: allow_inferred_rendering, **rendering, &block)
225
+
226
+ turbo_stream_action_tag name, targets: targets_name, template: template
137
227
  end
138
228
 
139
229
  private
@@ -145,6 +235,19 @@ class Turbo::Streams::TagBuilder
145
235
  end
146
236
  end
147
237
 
238
+ def render_template(target, content = nil, allow_inferred_rendering: true, **rendering, &block)
239
+ case
240
+ when content
241
+ allow_inferred_rendering ? (render_record(content) || content) : content
242
+ when block_given?
243
+ @view_context.capture(&block)
244
+ when rendering.any?
245
+ @view_context.render(formats: [ :html ], **rendering)
246
+ else
247
+ render_record(target) if allow_inferred_rendering
248
+ end
249
+ end
250
+
148
251
  def render_record(possible_record)
149
252
  if possible_record.respond_to?(:to_partial_path)
150
253
  record = possible_record
@@ -1,5 +1,6 @@
1
1
  APPLICATION_LAYOUT_PATH = Rails.root.join("app/views/layouts/application.html.erb")
2
2
  IMPORTMAP_PATH = Rails.root.join("app/assets/javascripts/importmap.json.erb")
3
+ CABLE_CONFIG_PATH = Rails.root.join("config/cable.yml")
3
4
 
4
5
  if APPLICATION_LAYOUT_PATH.exist?
5
6
  say "Yield head in application layout for cache helper"
@@ -22,10 +23,14 @@ else
22
23
  say %( Add <%= javascript_include_tag("turbo", type: "module-shim") %> and <%= yield :head %> within the <head> tag after Stimulus includes in your custom layout.)
23
24
  end
24
25
 
25
- say "Enable redis in bundle"
26
- uncomment_lines "Gemfile", %(gem 'redis')
26
+ if CABLE_CONFIG_PATH.exist?
27
+ say "Enable redis in bundle"
28
+ uncomment_lines "Gemfile", %(gem 'redis')
27
29
 
28
- say "Switch development cable to use redis"
29
- gsub_file "config/cable.yml", /development:\n\s+adapter: async/, "development:\n adapter: redis\n url: redis://localhost:6379/1"
30
+ say "Switch development cable to use redis"
31
+ gsub_file CABLE_CONFIG_PATH.to_s, /development:\n\s+adapter: async/, "development:\n adapter: redis\n url: redis://localhost:6379/1"
32
+ else
33
+ say 'ActionCable config file (config/cable.yml) is missing. Uncomment "gem \'redis\'" in your Gemfile and create config/cable.yml to use the Turbo Streams broadcast feature.'
34
+ end
30
35
 
31
36
  say "Turbo successfully installed ⚡️", :green
@@ -1,6 +1,7 @@
1
1
  # Some Rails versions use commonJS(require) others use ESM(import).
2
2
  TURBOLINKS_REGEX = /(import .* from "turbolinks".*\n|require\("turbolinks"\).*\n)/.freeze
3
3
  ACTIVE_STORAGE_REGEX = /(import.*ActiveStorage|require.*@rails\/activestorage.*)/.freeze
4
+ CABLE_CONFIG_PATH = Rails.root.join("config/cable.yml")
4
5
 
5
6
  abort "❌ Webpacker not found. Exiting." unless defined?(Webpacker::Engine)
6
7
 
@@ -15,10 +16,14 @@ run "#{RbConfig.ruby} bin/yarn remove turbolinks"
15
16
  gsub_file "#{Webpacker.config.source_entry_path}/application.js", TURBOLINKS_REGEX, ''
16
17
  gsub_file "#{Webpacker.config.source_entry_path}/application.js", /Turbolinks.start.*\n/, ''
17
18
 
18
- say "Enable redis in bundle"
19
- uncomment_lines "Gemfile", %(gem 'redis')
19
+ if CABLE_CONFIG_PATH.exist?
20
+ say "Enable redis in bundle"
21
+ uncomment_lines "Gemfile", %(gem 'redis')
20
22
 
21
- say "Switch development cable to use redis"
22
- gsub_file "config/cable.yml", /development:\n\s+adapter: async/, "development:\n adapter: redis\n url: redis://localhost:6379/1"
23
+ say "Switch development cable to use redis"
24
+ gsub_file CABLE_CONFIG_PATH.to_s, /development:\n\s+adapter: async/, "development:\n adapter: redis\n url: redis://localhost:6379/1"
25
+ else
26
+ say 'ActionCable config file (config/cable.yml) is missing. Uncomment "gem \'redis\'" in your Gemfile and create config/cable.yml to use the Turbo Streams broadcast feature.'
27
+ end
23
28
 
24
29
  say "Turbo successfully installed ⚡️", :green
data/lib/turbo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Turbo
2
- VERSION = "0.5.12"
2
+ VERSION = "0.6.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turbo-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.12
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Stephenson
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2021-06-30 00:00:00.000000000 Z
13
+ date: 2021-07-21 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rails