Problem/Motivation

The once feature is used in almost all core behaviors. The once feature is implemented as a jQuery plugin, making it impossible to remove jQuery from core scripts.

Proposed resolution

Implement a version of the once feature that does not depend on jQuery

Remaining tasks

  1. Write the code
  2. Benchmarks to validate the improvement: benchmark
  3. Fill in the change notice
  4. Create a change notice for the element.matches polyfill
  5. Have jsdoc for all functions
  6. Make the new once feature available as a standalone library that follows the requirements outlined in #3176918: [policy, no patch] Publishing / Maintaining JS libraries produced by Drupal that do not have a dependency on Drupal

Dependency Evaluation

https://www.drupal.org/project/once is considered Drupal Core, there are no runtime dependencies. For dev dependencies see #3199444: Dependency evaluation

User interface changes

None

API changes

  1. Create a new core/once library
  2. Introduce the once global variable to eslint config
  3. A new library with 4 functions: once, once.filter (equivalent to jQuery.fn.findOnce), once.remove, once.find (new function not previously possible with the jQuery implementation)
  4. When once is called on elements it add a new data-once attribute to each element that holds all the once ids called

Release notes snippet

The once library has been added to replace the functionality provided by for jQuery .once function. The change record for the once library details how to use the new functionality.

Before

# mymodule.libraries.yml
myfeature:
  js: 
    js/myfeature.js: {}
  dependencies:
    - core/drupal
    - core/jquery
    - core/jquery.once
# js/myfeature.js
(function ($, Drupal) {
  Drupal.behaviors.myfeature = {
    attach(context) {
      const $elements = $(context).find('.myfeature').once('myfeature');
    }
  };
}(jQuery, Drupal));

After (removing jQuery dependency)

# mymodule.libraries.yml
myfeature:
  js: 
    js/myfeature.js: {}
  dependencies:
    - core/drupal
    - core/once
# js/myfeature.js
(function (Drupal, once) {
  Drupal.behaviors.myfeature = {
    attach(context) {
      const elements = once('myfeature', '.myfeature', context);
    }
  };
}(Drupal, once));

Release notes snippet

This adds the core/once library, a standalone library that offers the same benefits as core/jquery.once but without the jQuery dependency.

Original report by droplet

>Inspired from http://eleks.github.io/js2js/. Now I totally rewritten jQuery.once into Plain JavaScript Code. Aiming to provide a modern pattern widely used everywhere, not just the Drupal way.

Features / Changes:

  • Remove dependency on jQuery.
  • Use HTML5 data-* attribute (not the jQuery.data()). It's better for debugging and work with other scripts. eg. You now able to query it directly: document.querySelectorAll(["data-drupal-once"]))
  • Performance improvements
  • Fully pass jQuery.once testcases
  • Light weight & More easier to adopt other libs. eg. Converting to jQuery Chaining way: https://github.com/KayLeung/dropletOnce/blob/master/jquery.droplet.once.js

Performance testing result:
https://docs.google.com/spreadsheets/d/1KXVKZS-HJhbQFgUYUmzPzMpJpEMOPXyG...
** Noticeable performance diff on iPhones.

Testing Repo:
https://github.com/KayLeung/dropletOnce

The reason I do not like jQuery.Once 2.0:

  • jQuery adding overhead. Including libs size and execution time ( I'm doubt the performance improves between jQuery Once 1.0 .class way and Version 2.0 jQuery.data on modern browsers )
  • No standard identify for themer (.once-class)
  • No visual debugging info for themer ( You can't see the Onced elements in devtools. You must use JS to loop over to see if that elements if Onced or not. This is not friendly for HTML/CSS themers)
  • One way synced only. jQuery.Once 2.0 use jQuery.data to track Onced elements. It will not synced back to HTML DOM.
  • Not a modern way. Popular frameworks use standard HTML5 data-* attribute. Not the jQuery.data()
CommentFileSizeAuthor
#171 core-once-2402103-171.patch26.19 KBnod_
#169 interdiff-154-169.txt42.77 KBnod_
#169 core-once-2402103-169.patch26.27 KBnod_
#162 core-once-2402103-162.patch39.07 KBnod_
#162 interdiff-154-162.txt30.63 KBnod_
#154 interdiff-149-154.txt653 bytesnod_
#154 core-once-2402103-154.patch39.07 KBnod_
#149 interdiff_147_149.txt1.11 KBanmolgoyal74
#149 core-once-2402103-149.patch39.06 KBanmolgoyal74
#147 interdiff-130-147.txt34.9 KBnod_
#147 core-once-2402103-147.patch38.97 KBnod_
#139 interdiff__130-139.txt34.94 KBbnjmnm
#139 2402103-139--tests.patch38.96 KBbnjmnm
#130 core-once-2402103-130.patch38.85 KBnod_
#130 interdiff-126-130.txt33.81 KBnod_
#126 interdiff-124-126.txt37.04 KBnod_
#126 core-once-2402103-126.patch38.31 KBnod_
#125 interdiff-121-125.txt47.67 KBnod_
#125 core-once-2402103-125.patch38.53 KBnod_
#121 interdiff-117-121.txt24.06 KBnod_
#121 core-once-2402103-121.patch35.33 KBnod_
#117 core-once-2402103-117.patch31.57 KBnod_
#114 core-once-2402103-114.patch23.37 KBnod_
#111 core-once-2402103-111.patch23.72 KBnod_
#106 2402103-106.patch45.48 KBbnjmnm
#106 interdiff_105-106.txt10.66 KBbnjmnm
#105 interdiff-103-105.txt31.55 KBnod_
#105 core-once-2402103-105.patch55.01 KBnod_
#104 core-once-2402103-104.patch54.98 KBnod_
#103 core-once-2402103-103.patch54.71 KBnod_
#96 interdiff_95-96.txt3.56 KBbnjmnm
#96 2402103-96.patch52.77 KBbnjmnm
#89 interdiff-81-89.patch13.57 KBnod_
#89 core-once-2402103-89.patch51.67 KBnod_
#81 interdiff_77-81.txt13.71 KBbnjmnm
#81 2402103-81.patch47.89 KBbnjmnm
#77 core-once-2402103-77.patch47.54 KBnod_
#77 interdiff-73-77.txt8.65 KBnod_
#74 interdiff-71-73.txt1.32 KBnod_
#74 core-once-2402103-73.patch43.16 KBnod_
#71 core-once-2402103-71.patch43.16 KBnod_
#71 interdiff-62-71.txt34.54 KBnod_
#63 interdiff-57-62.txt6.68 KBnod_
#63 core-once-2402103-62.patch44 KBnod_
#57 core-once-2402103-57.patch43.45 KBnod_
#57 interdiff-54-57.txt3.83 KBnod_
#56 interdiff-54-56.txt2.74 KBnod_
#56 core-once-2402103-56.patch42.13 KBnod_
#54 core-once-2402103-54.patch42.89 KBnod_
#53 interdiff-47-53.txt34.34 KBnod_
#53 core-once-2402103-53.patch42.97 KBnod_
#47 core-once-2402103-47.patch30.65 KBnod_
#47 interdiff-45-47.txt4.42 KBnod_
#45 interdiff-38-45.txt9.93 KBnod_
#45 core-once-2402103-45.patch29.35 KBnod_
#38 2402103-jquery-once-38.patch28.28 KBlarowlan
#38 2402103-jquery-once-38-interdiff.txt17.8 KBlarowlan
#34 2402103-jquery-once-34.patch22.84 KBlarowlan
#34 2402103-jquery-once-34-interdiff.txt8.26 KBlarowlan
#15 with-droplet-once.patch10.66 KBbnjmnm
#30 patch-2314324-30.patch20.88 KBflxa
#30 interdiff-30.txt1.9 KBflxa
#18 core-once-2402103-18.patch17.9 KBnod_
#25 2402103-jquery-once-25.patch20.62 KBlarowlan
#17 core-once-2402103-17.patch9.29 KBnod_
#15 2402103-15-JQUERY-ONCE-TEST.patch5.94 KBbnjmnm
#25 2402103-jquery-once-interdiff-25.txt7.69 KBlarowlan

Issue fork drupal-2402103

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

droplet’s picture

Issue summary: View changes
droplet’s picture

Issue summary: View changes

Adding testcase and making it 100% pass.

droplet’s picture

Issue summary: View changes
droplet’s picture

Issue summary: View changes
droplet’s picture

Issue summary: View changes

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

nod_’s picture

A patch maybe? It looks good. Wanted to use data attributes during the rewrite of jquery.once.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

avpaderno’s picture

Version: 8.6.x-dev » 8.9.x-dev

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

bnjmnm’s picture

Issue summary: View changes
StatusFileSize
new5.94 KB
new10.66 KB

This is a simple test that verifies once() is working properly. For the coverage to fully confirm that the once() replacement is working It would also need to cover once's findOnce() and removeOnce() functions.

Another patch is attached that is an attempt at replacing jquery once with droplet once. This exposed several things things that are needed before this can work:

  1. The jquery shim returns vanilla JS, but needs to return Jquery objects
  2. The jquery shim doesn't connect the removeOnce() function
  3. Dropletonce does not have an equivalent function for findOnce(), so consequently the shim does not have an equivalent.
nod_’s picture

nod_’s picture

StatusFileSize
new9.29 KB

with the findOnce code.

Simplified the code a bit, sorry @droplet, removed the comments to make it more readable. Everything seems to be working.

once file:

(() => {
  "use strict";

  function checkId(id) {
    if (typeof id !== "string") {
      throw new TypeError("The Once id parameter must be a string");
    }
    return "data-once-" + id;
  }

  function once(elements, id) {
    const dataId = checkId(id);
    const filtered = Array.prototype.filter.call(
      elements, (element) => !element.hasAttribute(dataId));

    filtered.forEach((element) => element.setAttribute(dataId, ''));
    return filtered;
  }

  once.remove = (elements, id) => {
    const dataId = checkId(id);
    const found = once.find(elements, id);
    Array.prototype.forEach.call(found,
        element => element.removeAttribute(dataId));
    return found;
  };

  once.find = (elements, id) => {
    const dataId = checkId(id);
    return Array.prototype.filter.call(elements,
        element => element.hasAttribute(dataId));
  };

  window.once = once;
})();

once jquery file

(function ($, once) {
  "use strict";

  $.fn.once = function (id) {
    return $(once(this, id));
  }
  $.fn.findOnce = function (id) {
    return $(once.find(this, id));
  }
  $.fn.removeOnce = function (id) {
    return $(once.remove(this, id));
  }

})(jQuery, once);

( edit ) messed up the once.remove function in the patch, use the one from the comment instead.

nod_’s picture

StatusFileSize
new17.9 KB

went ahead and fixed date.es6.js which had some old school once processing. put the .es6 files to make it work on IE11 (so I can test the date polyfill)

droplet’s picture

If that's not an issue, I'm okay to drop my copyright and move it into Misc folder.

A better name than dropletOnce :)

jibran’s picture

Patch looks great. I'm so excited to see this change happening in core.

  1. +++ b/core/modules/system/tests/modules/once_test/js/once-test.es6.js
    @@ -0,0 +1,17 @@
    +(($, Drupal) => {
    ...
    +})(jQuery, Drupal);
    

    I think we should add a test for once() just with Vanilla JS.

  2. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Once/OnceTest.php
    @@ -0,0 +1,40 @@
    +class OnceTest extends WebDriverTestBase {
    

    This can be a nightwatch test now.

shaal’s picture

catch’s picture

Priority: Normal » Major

Bumping to major since this blocks a lot of the work in #3052002: [meta] Replace JQuery with vanilla Javascript in core.

catch’s picture

Status: Active » Needs work
Issue tags: +Drupal 10

There's a patch here :)

larowlan’s picture

Assigned: Unassigned » larowlan
+++ b/core/assets/vendor/droplet-once/droplet.once.es6.js
@@ -0,0 +1,47 @@
+    filtered.forEach((element) => element.setAttribute(dataId, ''));
...
+        element => element.removeAttribute(dataId));
...
+        element => element.hasAttribute(dataId));

For my reference: is there a reason we use hasAttribute/setAttribute/removeAttribute instead of interacting directly with element.dataset ? According to https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/da... that's supported in all the browsers we need.

I'm fixing the tests now.

larowlan’s picture

Status: Needs work » Needs review
StatusFileSize
new7.69 KB
new20.62 KB

Fixes the tests and adds a test-case for Vanilla use of the once library

I'm not a fan of nightwatch, so didn't touch that.

More of a jest fan myself, shameless plug: https://www.npmjs.com/package/twig-testing-library

Status: Needs review » Needs work

The last submitted patch, 25: 2402103-jquery-once-25.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

larowlan’s picture

Assigned: larowlan » Unassigned

larowlan credited flxa.

larowlan’s picture

@flxa and I spent some time debugging the fails

The issue is that droplet.once expects a NodeList but sometimes its a jquery object, and sometimes we receive the whole dom document,

flxa is working on making sure we do some casting there, we'll probably have to add a deprecation notice too.

flxa’s picture

Status: Needs work » Needs review
StatusFileSize
new1.9 KB
new20.88 KB

Seems like the console error is popping up because one of the "elements" is actually the full #document. This filters that out and normalises the jQuery objects into arrays for `once`, `once.remove` and `once.find`.

Status: Needs review » Needs work

The last submitted patch, 30: patch-2314324-30.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

nod_’s picture

I'd rather not have anything related to jQuery in the new once script, if we need to call something specific it needs to be done in the jquery.droplet.once script. the once lib should take an array-like object or an single element (todo) as input and always returns either an array or a single element as output.

Also a general clean-up is necessary, renaming files, adding comments, finding the right place to put the script because here it's in the assets/vendor directory but the code doesn't come from a github repo like the original jQuery.once script does.

Didn't use dataset because at the time it wasn't supported by all browsers we needed but you're right that we can update that too.

larowlan’s picture

Assigned: Unassigned » larowlan

Yeah we came to the same conclusion
Taking those bits on

larowlan’s picture

Assigned: larowlan » Unassigned
Status: Needs work » Needs review
StatusFileSize
new8.26 KB
new22.84 KB

or an single element (todo) as input and always returns either an array or a single element as output.

How would this work in the single element scenario if the element was already executed

e.g. once(document.getElementById('some-id'), 'some-once').appendChild(document.createElement('div'))

In the scenario where this runs a second time, the return can't be chained.

This still to be done (if its worthwhile).

Here's the switch to a file in misc and removal of the droplet naming 😿 and license stuff.

Seeing some tests passing locally.

Interdiff is from #25

droplet’s picture

Didn't use dataset because at the time it wasn't supported by all browsers we needed but you're right that we can update that too.

When I code it, no reason and no choice :)

But actually it makes diff

- setAttribute is still more performant than dataset as I known. (not noticeable and able to skip I bet, that's around 2000 calls max in Drupal only)

-

document.querySelectorAll(`body`)[0].dataset['aBBa'] = 'test'
document.querySelectorAll(`body`)[0].setAttribute('cDDc', 'test')

setAttribute is friendly for PHP developer (and JS beginners) at this case. (more predictable, without thinking)

document.querySelectorAll(`body`)[0].dataset['a-b-ba'] // FAILED
document.querySelectorAll(`body`)[0].dataset['a-b-ba'] ='test2' // FAILED
nod_’s picture

  1. +++ b/core/assets/vendor/droplet-once/jquery.droplet.once.js
    @@ -0,0 +1,40 @@
    +    if (this instanceof Document) {
    +      return $(once(this.body, id));
    +    }
    +
    +    return $(once(this, id));
    

    can we avoid multiple returns?

    let el = this;
    if (this instanceof Document) {
      el = this.body;
    }
    
  2. +++ b/core/misc/date.es6.js
    @@ -3,7 +3,7 @@
    +(function($, Modernizr, Drupal, once) {
    

    need to update eslint config to add the "once" variable to the global scope

  3. +++ b/core/misc/drupal.once.es6.js
    @@ -0,0 +1,43 @@
    +    return id.toLowerCase().replace(/-(.)/g, (match, group) => group.toUpperCase());
    

    was missing before too but we should probably check for spaces and other unconventional characters in the id. Also like droplet said, this part is less clear than with setAttribute

  4. +++ b/core/misc/drupal.once.es6.js
    @@ -0,0 +1,43 @@
    +    Array.prototype.forEach.call(found,
    

    find returns an array, so we don't need to use the array prototype

  5. +++ b/core/modules/system/tests/modules/once_test/js/vanilla-once-test.es6.js
    @@ -0,0 +1,20 @@
    +      Array.prototype.forEach.call(once(document.querySelectorAll('#added-once'), 'use-once'), el => {
    

    once returns an array so we can call forEach directly on the return.

Status: Needs review » Needs work

The last submitted patch, 34: 2402103-jquery-once-34.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

larowlan’s picture

Status: Needs work » Needs review
StatusFileSize
new17.8 KB
new28.28 KB

Here's a patch that

  • Returns to using setAttribute/hasAttribute/removeAttribute
  • Removes the old jquery.once files
  • Removes the leftover droplet.once transpiled files in the last patch
  • Fixes 2,3 and 4 from #36
  • Introduces a set to track seen objects that are not elements. E.g. Node preview JS uses $(document).once(), off-canvas dialog uses $(window).once(). This was the source of some of the test-fails, as these objects don't support dataset or hasAttribute/setAttribute/removeAttribute. I realise there's no easy way to debug that set (e.g. via inspector) but there isn't now in those scenarios either. Manually tested off-canvas, node preview and media library and all seem to be working now.
larowlan’s picture

FYI support for Set as used here is good in all the browser we're interested in - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global...

Status: Needs review » Needs work

The last submitted patch, 38: 2402103-jquery-once-38.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

droplet’s picture

I'd say off node is an inappropriate usage. shift them to HTML or body, can't?

One reason I shift it to data-attr instead of a plain object (which is a better performance) is to provide themeable features via CSS. (And able to once via PHP)

Even no code standard that forcing to do it but off canvas should do Once here and missing detach. (2 bugs)

    $mainCanvasWrapper: $('[data-off-canvas-main-canvas]'),

Same here, should registered on the container. and `.node-preview-container` should be named`.node-preview` also (2 bugs)

      const $preview = $(context).once('node-preview');
      if ($(context).find('.node-preview-container').length) {
        $preview.on(
          'click.preview',
          'a:not([href^="#"], .node-preview-container a)',
          clickPreviewModal,
        );
      }
droplet’s picture

In other word,

the Once elements that removed from the node, anything related to that Once should be disappeared.

larowlan’s picture

I'd say off node is an inappropriate usage. shift them to HTML or body, can't?

Yes, but we have to maintain BC. We can fix core calls - but we can't know what things are done in contrib and custom code that will break if we force it to only use node elements. So I think we have to keep supporting that, but we need to trigger a deprecation error to force people to use it on elements instead. I think here is the place to do that too.

droplet’s picture

I preferred to remove it and no BC. (this is a bug more than a feature)

nonElements = new Set()
nonElements.add('document.querySelectorAll("#toolbar-administration")') // Since it isn't cached, so no pointers
jQuery('#toolbar-administration').remove()
jQuery('body')[0].id = '#toolbar-administration'
nonElements.has('document.querySelectorAll("#toolbar-administration")') === true ? 'INCORRECT' : 'CORRECT'

thoughts?

nod_’s picture

Status: Needs work » Needs review
StatusFileSize
new29.35 KB
new9.93 KB

the non element set has a little problem, we only track the element, not the id it is called with:

once(document, "test1") => OK 
once(document, "test2") => KO empty

JS in Drupal has always been very dom-based. Because for what we do with it, the DOM is fast enough, especially these days.

One of the concern when we move to jquery once instead of using the processed class names was that information about what was processed and what was not lived outside the dom in a jquery-specific part. Having a data attribute with this information brings us back to having all the data in the DOM, I would still not tell people to use it for styling because that is not the goal (add that class in the js that call the once function), the goal is to expose the information about what has been processed in an easily accessible way*.

About the window/document issue: both should be replaced with document.documentElement because we're not doing SPA in drupal, the window == the document == the page. For SPA, just don't use window/document in the once function, easy. The deprecation can be added to the jquery extention only.

Thinking about it the initial call to attachbehaviors is wrong. Everything uses jquery so we don't see it but instead of Drupal.attachBehaviors(document, drupalSettings); it should be Drupal.attachBehaviors(document.documentElement, drupalSettings); because that's what it is that's for another issue though.

Haven't looked at the test failures. Ran eslint, updated the patch with the aliasing method, remove the Set part, started to add the jsdoc, deprecation notice need some work. interdiff is a bit rubbish because of the move but essentially we're back on the previous version of once.js, and the jquery.drupal.once file has a new method.

* I'll try to get all that in a single data-once attribute because I'm not a fan of having too many data-once attributes.

Status: Needs review » Needs work

The last submitted patch, 45: core-once-2402103-45.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

nod_’s picture

Status: Needs work » Needs review
StatusFileSize
new4.42 KB
new30.65 KB

Patch that puts everything in data-once attribute. Can still use css with the ~= attribute selector for exemple: [data-once~="contextualToolbar-init"].

Also, how do we go about deprecation warning in the testbot?

Status: Needs review » Needs work

The last submitted patch, 47: core-once-2402103-47.patch, failed testing. View results

larowlan’s picture

Also, how do we go about deprecation warning in the testbot?

We fix the usages in core :)

All of those 38 fails are deprecation errors, so if we fix the usages in core to not use window or document this should be green 🎉

This is looking nice @nod_

droplet’s picture

regex missing `+`

' test                     xxx '.split(/[\11\12\14\15\40]/)

- what if we take `checkId` away and let the error happening. This is the only place in Core JS doing the args checking I think.
And should we check it or generate a valid ID from it?
And should we do further limitation? for example `/^[a-z0-9-]+$/` only?

once(document.querySelectorAll('body'), '') // BUG

- Should we make Once `elements` accept `.selectors` also?

Once(context.querySelectorAll('.selector'), 'id')
Once('.selectors', 'id')

// or 
Once(context.querySelectorAll('.selector'), 'id')
$Once('.selectors', 'id')

- On the `once.remove`. Using regex replace is simpler

'input input-data'.replace(/\binput\b/, '')

- `once`, I preferred to code like below, hehe

    filtered.forEach(element => {
      let value = dataId;
      if (element.hasAttribute(attrName)) {
        value = `${element.getAttribute(attrName).trim()} ${dataId}`;
      }

      element.setAttribute(attrName, value);
    });
nod_’s picture

We do have arguments checking in core, see Drupal.ajax :)
We should keep checkId. the function shouldn't change the id to a "safe" subset or generate one, if it's too forgiving it'll be a pain when we want to deprecate or fix a bug somewhere. I took the regex from a DOMTokenList polyfill, it follows the same rules as css class names like this.

we should fail on empty ids, you're right.

I don't want to allow selectors because then we'd need to allow passing a context element, and then it's just a shortcut for context.qSA and doesn't add anything. I'd rather keep this thing specialized and if we need some syntactic sugar on top we can put that in Drupa.whatever.

Removing with the regex leaves some extra spaces so if we add/remove a lot, we'll have a lot of extra spaces. and running a replace(wsRE, ' ') would work, but it doesn't feel right.

I prefer your code too there, though I would go with splitting an array again. Was on the fence mixing regular dom methods and browser specific implementation of reading/writting attributes.

To get around the deprecations warning, because I don't really want to touch all those modules and files we can do an argument checking in the once methods and let the aliasing on the jquery side. meanging we support using window/document when you go through jQuery but not when you use the vanilla lib. Would that be ok?

droplet’s picture

Hmm...

Acutally I wonder if `wsRE` is correct or not also.

Then, can we remove `values.includes(dataId)`. `once.find` does the job already.

Technically, a loop to remove is required since it puts the value into a single once-data.

once-data="same same same same same" // is a valid usage
once-data-same once-data-same // technically won't exist

may perform a simple benchmark between the last few approaches and take the balance.

nod_’s picture

Status: Needs work » Needs review
StatusFileSize
new42.97 KB
new34.34 KB

was removing the .includes() in a new patch :)

Added a little something to prevent duplicate values that would 'break' the remove function. babel add some polyfills to the script but still lighter than jquery :p

Created a stub for the change notice to have an URL to point to.

Changed a bit the patch to deprecate the existing lib instead of removing it right away. Got rid of the deprecation notice in the jquery interface because, well either we use jQuery or we don't and when we update the code to not use jQuery we'll see that using once on window/document will not work. Safer than trying to update the code of other modules in this patch. we can deprecate the jQuery interface in 9.2.x once the rest of core doesn't rely on it.
Trying to get that in rather quickly so that claro and especially olivero can use it before getting in core.

nod_’s picture

StatusFileSize
new42.89 KB

just moved the declaration of drupal.once in the right alphabetical place in core.libraries.yml

droplet’s picture

Good idea to use Set... BUT the bad news is IE11 doesn't really support Set.

in IE 11:

// WORKS
var mySet = new Set();
mySet.add(1); 
mySet.add(2); 
mySet.forEach(function(e) {
 console.log(e)
})
// FAILED
var mySet = new Set(1);
mySet.forEach(function(e) {
 console.log(e)
})
nod_’s picture

StatusFileSize
new42.13 KB
new2.74 KB

IE11... there was another issue with the .matches method, added a polyfill for it. Now it works on IE9+.

nod_’s picture

StatusFileSize
new3.83 KB
new43.45 KB

forgot the polyfill in the patch

droplet’s picture

no strong demand but if we want to standardize it a bit and no extra function call outside the loop:

const wsRE = /[\11\12\14\15\40]+/;
const onceItemList =
	' input-leading input  input-data input input        input-traling ';

function t2({ list, addItem, removeItem }) {
	const result = [];

	list
		.trim()
		.split(wsRE)
		.forEach(function (item) {
			if (result.indexOf(item) < 0) {
				if (item !== removeItem) {
					result.push(item);
				}
			}
		});

	if (addItem) {
		result.push(addItem);
	}

	return result;
}

console.log(t2({ list: onceItemList }));
console.log(t2({ list: onceItemList, removeItem: 'input' }));
console.log(t2({ list: onceItemList, addItem: 'new' }));

I will drop the Javascript destructuring if extra polyfill matters/required. It's a private method, passing extra args each time won't hurt.

EDIT: maybe exporting the token function also, somebody may want to re-use it.

nod_’s picture

I don't think we'll handle more than 20 items in this token list, iterating over it 2,3, or 4 time doesn't matter much compared to using the dom for filtering and storing the data.

What's left I think is:

  • benchmark the new version against jquery.once
  • write up the change notice
  • add a change notice for the matches polyfill
  • finish the jsdoc

Then we're good to go no?

droplet’s picture

benchmark the new version against jquery.once

I benchmarked it on another day, much faster (on large dataset).

module page has many divs `admin/modules`

console.time('d.once');
  for (var i = 0; i <= 1; i++) {
    once(document.querySelectorAll('div'), 'dC' + i);
  }
console.timeEnd('d.once');
console.log(`total: ${jQuery('div').length} divs`)

I'm leaving now. My code is better than my English. I will go back soon if no one reviews it.

nod_’s picture

Ok I see it now, I'll update the patch. Removed the foreach for updating the attributes so we go over the elements only once, not twice like before.

I'm making a benchmark on jsperf to try out a few things and to find the threshold when the perf impact of using the dom becomes an issue vs jquery.

droplet’s picture

@nod_
sorry if there's misunderstood. the remaining work is doc, so I can't help as usual :)

the engine behind jsperf
https://benchmarkjs.com/

BTW, on the benchmark, looping once vs 5 in #60 is not linear growth.

nod_’s picture

StatusFileSize
new44 KB
new6.68 KB

once seems better than the jQuery once surprisingly:
https://www.measurethat.net/Benchmarks/Show/8844/1/drupal-once-variables...

I'll see if I have time to find out how many elements we use it on average.
Saw that it's not linear, we can tweak the parameters of the test in the url above to find how it behave in various situations:

droplet’s picture

generally, each Once has no cache in global, even a re-call is no cache at all. So if we're going to find out the real env, it should not cached also.

droplet’s picture

it may help you to make a decision:
https://github.com/GoogleChrome/lighthouse/blob/master/lighthouse-core/a...

on my PC takes 14ms for 3000 divs
it's 286 Once in total in DrupalCore. The bad case, we called them all at once and 10 times, we still keep it close to 60fps I think..

(damn, my old iPhone 8 Plus is twice faster than my PC (AMD 3900x) on #63 test)

nod_’s picture

Updated the test to query the elements in each loop :)

Thanks for the lighthouse link, never had the curiosity to look into it so it's educational. And yeah the iphone JS performance is ridiculous, so much better than any phone by far as well as most desktop.

nod_’s picture

Issue summary: View changes
nod_’s picture

Issue summary: View changes
nod_’s picture

Issue summary: View changes
nod_’s picture

Issue summary: View changes

Could use some help with review of the change notice https://www.drupal.org/node/3158256

also the library is called core/drupal.once but it's not in the Drupal namespace and I don't think it should be, might change it to core/once to reflect the actual implementation.

nod_’s picture

Issue summary: View changes
StatusFileSize
new34.54 KB
new43.16 KB

renamed the libs core/once and core/once.jquery. The name works this way because it's the jquery interface of the once library, not the jquery extension once. Updated the IS and change notice.

Also I changed the weight of the once.jquery file so that is it added after the jquery.once file, That way if both libraries are on the page everything uses the new library.

nod_’s picture

Issue summary: View changes

Now we only need the jsdoc comments for the functions.

Status: Needs review » Needs work

The last submitted patch, 71: core-once-2402103-71.patch, failed testing. View results

nod_’s picture

Status: Needs work » Needs review
StatusFileSize
new43.16 KB
new1.32 KB

sorry my bad had some test code leftover.

droplet’s picture

Why styling with Once data-attr is bad? Do you suggest the below usage or other thoughts? (I've read comment #45 already)

const elements = once('.x', 'x');
$(elements).addClass('processed')

Enforce the code quality with the code is the best thing.

Acutally we should code like this: (GOOD)

<div class="js-prefix for-single-component">
<div class="x">
<div class="y">
<div class="z">
</div>

But Drupal CORE code standard/current usages are: (BAD, Extremely BAD)

<div class="for-single-component">
<div class="x js-prefix-x">
<div class="y js-prefix-y">
<div class="z js-prefix-z">
</div>

This is wrong:
So some concepts like decouple can't apply to here.

<div class="x js-prefix-x">
<div class="y js-prefix-y">


<div class="js-prefix for-single-component">
<div class="z js-prefix-z">
</div>

data-attr is act as a state class on BEM
** This is about the Styling only. js-prefix for JS selectors is another story. (Also many bad js-prefix-selector usages)

once()
addClass('processed')
once.remove()

If we do above and the `.processed` still left. Is it correct or expected?

nod_’s picture

so for the styling, flashback from 6 years ago #2348321-26: Upgrade to jQuery Once 2.x, we could argue that it's a code quality issue in a specific project… But we don't want to make it easy for people to do the bad thing.

In core not all the once calls are followed by an addClass, so I would think there is often a different way of doing the styling. What would it simplify to use data attributes for styling? the JS side or the CSS side?

For the code I wouldn't use jquery for class changes but yes essentially if a class is needed I would do:

const elements = once(document.querySelectorAll('.x'), 'x')
  .forEach(el => el.classList.add('x-processed'));

For the styling I don't have a clear idea of how it should look like, just that is shouldn't be tied directly to the data-once attribute.

for the remove we can add that to the detach behavior to do:

once()
addClass('processed')
once.remove()
removeClass('processed')
nod_’s picture

StatusFileSize
new8.65 KB
new47.54 KB

first pass at the jsdoc. Need help from good english writers :)

nod_’s picture

droplet’s picture

#76

Technically, scripts needed extra HTML class (especially, on the component top wrapper) should not use Once. Fully decoupled.

$('target:not(.processed)').init()

But if you tried to make it depends on Once (, that helping to reduce the repeated code), that's no reason not to reuse it. Once & Script is a couple now.

nod_’s picture

That make sense, I removed the sentence about not using data-once for styling from the change record.

bnjmnm’s picture

StatusFileSize
new47.89 KB
new13.71 KB

This provides some jsdoc revisions as requested in ##7 I did a little bit of function and variable renaming that I thought would help readability.

There was one bit I was tempted to change but didn't:

+++ b/core/misc/once/once.es6.js
@@ -0,0 +1,219 @@
+  const attrName = 'data-once';

I wasn't sure what the intent was having this as a variable since this isn't an overridable class. It seems like it would be easier for a contributor to understand if the attribute name is used directly instead of attrName, but this seemed intentional enough that I didn't want to change it.

nod_’s picture

Thank you! names are better like this agreed.

Yes in a variable is intentional, I'm not loosing hope that we'll minify core JS at some point, then it'll help… trivially :p.

Makes it easier to change it, like you did :)

nod_’s picture

Commenting the interdiff:

  1. +++ b/core/misc/once/once.es6.js
    @@ -171,16 +179,17 @@
    -   *   An array-like object containing all the elements to be checked.
    +   *   A NodeList or array of elements to remove the once id from.
    

    It's a detail but array-like is on purpose, the following works and will be treated like an array (this is how a jQuery object is built btw):

    {
     0: dom0,
     1: dom1,
     2: dom2,
     length: 3
    }
  2. +++ b/core/misc/once/once.jquery.es6.js
    @@ -1,11 +1,11 @@
    - * Wrapper between jQuery and core/once.
    + * Wrapper between core/once.jQuery and core/once.
    

    this is not accurate. The file is the core/once.jquery lib, it's a wrapper of core/once for jQuery

Looks much better now, updated the change notice with the new attribute name. Good to go for me :)

nod_’s picture

Issue summary: View changes
droplet’s picture

+++ b/core/misc/once/once.es6.js
@@ -42,44 +41,44 @@
+  function checkElement(itemToCheck) {

element is better IMO.

checkElement can be removed also. We need not check everything unless the top layer passing mixed types unpredictably (exception) or a security issue.

droplet’s picture

BTW,

haha, I don't want over-engineering but.... I think we can swap the args. It's more close to the jQuery.once

once(id, elements)

and we able to set a default value for `elements` in `once.find`...

  once.find = (id, elements = []) => {
   if(!elements.length) {
      elements = document.querySelectorAll('html')
   }
    const dataId = checkId(id);
    return filterAndModify(elements, `[${attrName}~="${dataId}"]`);
  };

The Once ID is equal/very smiliar to HTML ID, I think.

If we promoted named params.. that's also a good chosen (It's being popular now)

nod_’s picture

I'd like to keep the check element, it doesn't cost much to do the check and we make sure that people sending unexpected things have a very precise information about what's wrong. I'd keep it more for DX than correctness.

In what situation would you use once('id') without specifying the elements? If it's because of a behavior you only want run once per page, I would extend the behaviors to support this, not extend this simple library to help with this. If it's for behaviors I would

Named params, not a fan here, it does remove the order issue but it kinda makes things longer:

once({ id: 'id', elements: document.querySelectorAll('div') });
once(document.querySelectorAll('div'), 'id');

About the arguments order, no special opinion the two parameters are required (and I don't think we should default to 'html') so either way is ok with me. If we want to go with consistency, the theme functions are called with theme('themefunc', options), not really the same but similar in some ways.

In any case, to me this is good enough either way (parameter order, documentation, etc.) and I'd rather wrap up this one and move on to removing jquery dependency from ajax.

droplet’s picture

In what situation would you use once('id') without specifying the elemen

that's once.find, not once

** params order only worth to change if we make the change to once.find

nod_’s picture

StatusFileSize
new51.67 KB
new13.57 KB

Sorry didn't read it right.

Thinking about it, I like the param order change. Makes it easier to see which "once" was called, only need to pay attention to the beginning of the line when reading the code, the messy "query" part can be ignored more easily.

in datejs for example (and document, should be context here but anyway):

Before After
once(
  document.querySelectorAll(
    '[data-…-elements="date-time"]',
  ),
  'datepicker',
)
once(
  'datepicker',
  document.querySelectorAll(
    '[data-…-elements="date-time"]',
  ),
 )

I'll probably regret this patch but here we go. The issue is that find is actually filter. So I went ahead and renamed find in filter, and created a new find function that does something more in line with what users would expect. There is no mapping in jQuery to the new function because it doesn't make much sense, nobody expect it and we don't want to encourage people using jQuery. The changes to date.js are mainly indentation, we can call forEach directly on the return of once().

I'll update the CR if we agree on those changes.

droplet’s picture

Makes it easier to see which "once" was called

True.

It's time for end game I think :)

Thanks ALL.

nod_’s picture

Issue summary: View changes

updated the CR. Who's up for RTBC?

nod_’s picture

Issue summary: View changes

updated the IS & some more tweaks to the CR.

And droplet should be credited first in the commit.

nod_’s picture

Status: Needs review » Reviewed & tested by the community

I feel this is ready.

We need to get things moving and this patch is needed for things to fix in the olivero issue.

bnjmnm’s picture

Status: Reviewed & tested by the community » Needs work

The new deprecation requires test coverage.
I'm still in the process of reviewing the full patch but mentioning this now as it may be actionable before I'm able to otherwise +1 #93

bnjmnm’s picture

Issue tags: +Needs release note

Tagging with needs release note.

  1. +++ b/core/misc/once/once.es6.js
    @@ -0,0 +1,259 @@
    +   */
    +  once.remove = (id, elements) => {
    +    const dataId = checkId(id);
    +    return filterAndModify(elements, `[${attrName}~="${dataId}"]`, element => {
    ...
    +   *   provided once id.
    +   */
    +  once.filter = (id, elements) => {
    +    const dataId = checkId(id);
    +    return filterAndModify(elements, `[${attrName}~="${dataId}"]`);
    ...
    +   *   provided once id.
    +   */
    +  once.find = (id, context = document.documentElement) => {
    +    const dataId = checkId(id);
    +    return (
    

    Since this is now part of core as opposed to a third party library with its own test coverage, the functions that will be called from other JS (find, filter, and remove) should get test coverage as well as the jQuery shimmed version.

    Would also be good for tests to that confirms the proper conversion of illegal spaces in once ids and how the jquery shim aliases window and document.

  2. +++ b/core/modules/system/tests/modules/once_test/js/vanilla-once-test.es6.js
    @@ -0,0 +1,20 @@
    +  Drupal.behaviors.vanillaOnceTest = {
    +    attach(context) {
    +      const addedOnce = document.createElement('div');
    +      addedOnce.classList.add('added-once');
    +      addedOnce.textContent = 'Added once!';
    +      const addedOften = document.createElement('div');
    

    I see examples of test-only JS with and without docs for the behaviors. Since there's additional work needed anyway, this and jqueryOnceTest should get the usual behavior docs.

  3. +++ b/core/modules/system/tests/modules/once_test/src/Form/OnceTestForm.php
    @@ -0,0 +1,65 @@
    +  public function buildForm(array $form, FormStateInterface $form_state, string $type = 'vanilla') {
    +    $form['content']['#attached']['library'][] = "once_test/{$type}_once_test";
    +    $form['trigger_message'] = [
    
    +++ b/core/tests/Drupal/FunctionalJavascriptTests/Once/OnceTest.php
    @@ -0,0 +1,57 @@
    +  public function testOnce($path) {
    +    $assert_session = $this->assertSession();
    +    $page = $this->getSession()->getPage();
    +    $this->drupalGet($path);
    +    $assert_session->assertWaitOnAjaxRequest();
    

    Especially since the $type arg for the form has a default value, the test should add an assertion that confirms the library chosen by dataprovider is actually loading. Something as simple as jquery-once-test and vanilla-once-test adding a file-specific DOM element that the test can check for.

bnjmnm’s picture

StatusFileSize
new52.77 KB
new3.56 KB

This addresses #95.2 and #95.3

#95.1 (testing the individual methods in the new once() library) is still needed. I'm wondering if this needs to be done in a Nightwatch test or if it can added as another test in FunctionalJavascript classes we've already added.

avpaderno’s picture

Status: Needs work » Needs review

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

nod_’s picture

forked and renamed droplet repo to keep the history and make some update to the code and dependencies. I think library tests should be sorted out on the library not in core #3176918: [policy, no patch] Publishing / Maintaining JS libraries produced by Drupal that do not have a dependency on Drupal.

bnjmnm’s picture

Status: Needs review » Needs work
+++ b/core/core.libraries.yml
+once:
+  version: VERSION
+  js:
+    misc/once/once.js: { weight: -19 }
+  dependencies:
+    - core/drupal.element.matches
+

If we're treating this like an external library (and it seems like that is necessary as it needs test coverage ill-suited for Drupal tests), the external repo should probably get tagged with a release and the library defintion changed, including once.js moved to assets/vendor.

Based on reviewing https://raw.githubusercontent.com/theodoreb/once/main/test/test.js it looks like some of the tests are commented out and test functionality that isn't otherwise tested. Before this would pass a dependency evaluation, those would need to be addressed + coverage for any other functions not yet covered. That is something I may be able to assist with if @nod_ can confirm the library will be continuing with that approach to testing.

nod_’s picture

Updated the gtihub repo. Release made. It's following everything we outlined in #3176918: [policy, no patch] Publishing / Maintaining JS libraries produced by Drupal that do not have a dependency on Drupal. There is a npm package: https://www.npmjs.com/package/once-dom (until we get the whole @drupal thing going).

Automated tests took something like 5 clicks to setup once "npm test" was configured and it runs test on both chrome and firefox. Code coverage is 100%.

And we have an officially published API documentation :) https://github.com/theodoreb/once/blob/main/API.md

nod_’s picture

Status: Needs work » Needs review
StatusFileSize
new54.71 KB

not sure we need the drupal tests, lib is tested.

Moved the once lib to assets/vendor. Pointed to my repo, we can change to the drupal/once repo when it exists. had to reroll for the date.es6.js file, the interdiff is in the libraries.yml, the rest is updating the once lib.

I realized that the compiled version of once will not work on IE11. I'll update that, we can fix later if there is nothing else.

nod_’s picture

StatusFileSize
new54.98 KB

lib compatible with ie-11 now, good to go for me.

nod_’s picture

StatusFileSize
new55.01 KB
new31.55 KB

small update so that the version number is in the once library file.

updated the change notice with the proper attribute name and added a link to the generated documentation.

bnjmnm’s picture

Issue summary: View changes
Issue tags: -Needs release note
StatusFileSize
new10.66 KB
new45.48 KB

Added a release notes snippet.

I reviewed the tests in the repo and confirmed they make the tests written in this issue redundant so I've removed those. The only additional change I made was updating the Drupal version in the deprecation.

As a result most of my changes have either been removed or now live in the library itself https://github.com/theodoreb/once, which means I'm 99% comfortable RTBC-ing this (assuming something in this patch didn't mess up tests).

If @nod_ can confirm he's OK with my test removal and deprecation message change I'd consider this a sufficient co-review and RTBC. I also ran this by committer @lauriii who confirmed this would be fine.

droplet’s picture

+++ b/core/core.libraries.yml
@@ -591,6 +597,33 @@ normalize:
+  remote: https://github.com/theodoreb/once
...
+once.jquery:

I bet we will remove once.jquery soon. A deprecated comment is needed.

nod_’s picture

I'm good with removing the tests :)

One side effect here is that if a project or contrib module uses core/jquery.once as a dependency, the resulting script loading will be:

<script src="/core/assets/vendor/once/once.min.js?v=2.1.1"></script>
<script src="/core/assets/vendor/jquery-once/jquery.once.min.js?v=2.2.3"></script>
<script src="/core/assets/vendor/once/once.jquery.min.js?v=2.1.1"></script>

Meaning that if one script depend on core/once.jquery, everyone uses it regardless of what they declared in their dependencies. I'm confident that won't break their code, but it does behaves unexpectedly, and it becomes very hard to use the old jQuery-once script.
So maybe to be safe we should only change olivero/date.js and not even use the new once jquery interface.

So unfortunately I think the safe thing to do is to not expose the once.jquery library (that way we don't have to deprecate it) and just update all the core code to use the vanillajs lib in a follow up, we can still get the lib in and update date.js in this patch.

droplet’s picture

#108

Good questions & points.

hmm. I think we have to rename the $.once to a new namespace and remove seamless updates. Let them opt-in with quick bulk SEARCH & REPLACE.

droplet’s picture

But my comment #109 creates another issue. The Drupal's jQuery binding have to find the old jquery.once data (https://github.com/RobLoach/jquery-once/blob/master/jquery.once.js#L174)

nod_’s picture

StatusFileSize
new23.72 KB

Safe subset of the patch that can be committed. no more once.jquery library, only add core/once and update the date script.

I think we should use jscodeshift and a custom codemod to update the js code automatically for core/contrib/etc.

nod_’s picture

Status: Needs review » Needs work

The last submitted patch, 111: core-once-2402103-111.patch, failed testing. View results

nod_’s picture

Status: Needs work » Needs review
StatusFileSize
new23.37 KB

Ok so deprecation needs to happen in a followup.

nod_’s picture

nod_’s picture

Issue summary: View changes
nod_’s picture

StatusFileSize
new31.57 KB

It's taking some back-and forth to find the right configuration for shipping the library files.

I added the unminifed source to make it possible for IDE to pickup the documentation and provide autocompletion for the once library. what's loading is still the minified file.

now i'm done

bnjmnm’s picture

Status: Needs review » Needs work

#117 looks very good. My only concern is that the only actual use of the new library is something that would be completely ignored by tests since it requires a condition that won't be the case in our tests. Although I'm quite confident this new library works fine within core, I think it would be good to refactor $().once to once() for an asset that is used in existing test coverage. Most of this would obviously happen in #3183149: Deprecate jquery.once and use the new once lib, but I like the practice of providing at least one common-use change with updates like this. This would be a test-confirmed assurance that such a change does not result in some unexpected conflict with Drupal.

This also needs a dependency evaluation and issue summary update since the approach slightly changed. I'll take care of those.

bnjmnm’s picture

Issue summary: View changes
nod_’s picture

how about every call updated #3183149-4: Deprecate jquery.once and use the new once lib :D

like droplet suggested might need to do the querySelectorAll inside the once call. It's a lot of noise to have that everywhere. Or better have a separate function that we can use as shortcut across core, or even

(function (qsa) {

  once('something', qsa('[data-drupal-selector]'))

}(document.querySelectorAll.bind(document)));
nod_’s picture

Status: Needs work » Needs review
StatusFileSize
new35.33 KB
new24.06 KB

there is a test for claro autocomplete, so I changed that one.

bnjmnm’s picture

Status: Needs review » Reviewed & tested by the community

The test in #121 serves as a nice confirmation that there isn't a mysterious breaking change going on.

The querySelectorAll change mentioned in #120 is probably more appropriate for #3183149: Deprecate jquery.once and use the new once lib, so I'll go ahead and RTBC. I am +1 on the noise reduction, though.

Much of this patch has moved to the library or #3183149: Deprecate jquery.once and use the new once lib. The result is this becomes much easier to review, and a more manageable scope overall, so this is good!

nod_’s picture

Some improvements to the library https://github.com/theodoreb/once/pull/3 (I should just do everything droplet tells me from the start :p)

Doesn't change anything for this issue, it'll be useful mainly the deprecation issue, but no reason to hold up this patch on that.

lauriii’s picture

Haven't reviewed the patch yet but since we're adding a new dependency, we need sign-off from release managers.

nod_’s picture

StatusFileSize
new38.53 KB
new47.67 KB

Update with latest version, makes calls to once shorter and more readable.

nod_’s picture

StatusFileSize
new38.31 KB
new37.04 KB

small upkeep fixing some inconsistencies in the lib and make a smaller minified file.

droplet’s picture

Does `context` matter? When I looking at the #3183149: Deprecate jquery.once and use the new once lib .......

Sometimes, I don't quite sure why a script needs `context` and Once at the same time.
In Drupal, `context` can be anything (from different contri-modules) and run again on your scoped code. This is not controllable. [right??]

** `context` only helps for faster query in Drupal

Once is mainly rewritten for Drupal. If I'm right, we should remove `context` fron Once.

Or still better API?? so you can call:

var $menu = $('.js-media-library-menu');
      $(once('media-library-menu-item', 'a', $menu))
nod_’s picture

I don't think it's a problem to keep the possibility of using a context, it's an optional parameter. In that example I'd do:

once('media-library-menu-item', '.js-media-library-menu a')

Allowing a jQuery object as a context makes us run into the issue I show at the bottom of #3183149-23: Deprecate jquery.once and use the new once lib so I'd rather not do that.

( edit ) having the context will help with the transition. We can always deprecate it later

droplet’s picture

Allowing a jQuery object as a context makes us run into the issue

Oh! I didn't noticed that before. Not a new request BTW.

( edit ) having the context will help with the transition. We can always deprecate it later

Yeah, that's what I think. But the codemods still possible to do the transition, right?

Just out of my curiosity. Anyone know that WHEN the `context` is matter?

If that matter, I wonder how many of our code doesn't pass `context` to Once.remove (intentionally)

Just thinking about remove this pattern from core code or new patch:
`$context.find('target')`

nod_’s picture

StatusFileSize
new33.81 KB
new38.85 KB

Update of the lib.

I'm expecting to go back to 1.0.0 when the lib moves away from my github fork, so I'm not too worried about having that many changes to the version at this point. It'll be a different npm package when we move that away from my fork.

nod_’s picture

And about the context i think we need to keep it. It's useful for ajax forms to know what to serialize/unserialize because context will be the part of the form that is targeted.

Also I think that the targeted context might not always be attached to the DOM, meaning that document.querySelectorAll() can't find it and we need to be able to do context.querySelectorAll to be able to target elements in a detached dom node.

xjm’s picture

Status: Reviewed & tested by the community » Needs work

Thanks everyone for your work on modernizing this dependency!

I've discussed the dependency addition proposed here with @catch, @gabesullice, @bnjmnm, @Dries, and @lauriii. This issue raises some interesting governance and maintenance questions.

#3176918: [policy, no patch] Publishing / Maintaining JS libraries produced by Drupal that do not have a dependency on Drupal is definitely relevant to this issue. The library does not need to depend on Drupal, and it would benefit from being in a separate repository for many of the same reasons the proposed Drupal JS client libraries would:

  1. Publishing to npm
  2. Easily setting up custom testing workflows
  3. No having to run the entire hour-long PHP core test suite for every WIP change.

Based on that, I think Once should be treated as a independent Drupal Core JavaScript package as described in #3176918: [policy, no patch] Publishing / Maintaining JS libraries produced by Drupal that do not have a dependency on Drupal (even given that its equivalent was an independent project maintained by individual Drupalists on GitHub).

However, the current policy draft is not finalized, and some important decisions about governance and the code origin are still not settled. So, I think it would be best to make this an independent package once we've settled that policy.

That said, neither @catch nor I want to see progress here blocked on the final decisions of the JavaScript package work. (As @catch pointed out, the point of #3176918: [policy, no patch] Publishing / Maintaining JS libraries produced by Drupal that do not have a dependency on Drupal is to accelerate Drupal JavaScript development, not to bog it down.) Given that, and given that this is going to be a production dependency of a production version of core, our proposed compromise is:

  1. For now, just add Once as a core library, rather than an external dependency. I reached out to @bnjmnm about how the test suite could work if the library were in core. Ben confirmed that they could be converted to Nightwatch tests, and is willing to implement this conversion if we go ahead with this as a core library.
     

  2. File a followup issue to make it one of the new packages covered by #3176918: [policy, no patch] Publishing / Maintaining JS libraries produced by Drupal that do not have a dependency on Drupal once we've established those policies, so that it can benefit from those same decisions and features (hopefully soon!).

Sorry that this means more work, as well as the overhead of core tests and queue, etc. It should be a temporary situation until #3176918: [policy, no patch] Publishing / Maintaining JS libraries produced by Drupal that do not have a dependency on Drupal is resolved and we can move the project to its own repo and publish the library on npm.

I'm leaving the RM review tag on because we'll want to review it again if we go ahead and implement the compromise.

nod_’s picture

Status: Needs work » Needs review

Thanks for the update. Given that, for now I would keep it as an external library and revisit the "core" status of the lib once the policy is agreed upon. The cost is way too high for what it is in this situation. We could name this library "core/once-dom" to map with the npm package name and keep the "core/once" namespace open for the "real thing".

Putting it in core will take time to figure out how to integrate with core tests, but there is also the build step to worry about. I'm using rollup and buble because they're very easy to use and there is no need for endless configuration, but that means we'd have to do a dependency audit for those and it's not going to be fun.

On my end I already spend a good amount of time on this to get the lib + updating all the core code, given that we have 2 green patches that put the lib and update all core code, it's going to be hard to justify spending more time on this to my employer, especially if it's for redoing the same work with tools that are not suited for the task. So I'm happy to reroll things, agree to keep the once lib up to date until it's under official core status, but I can't spend more company time on the compromise scenario and I won't spend personal time on this.

(also I'm much more worried about the second patch, so if there is anything that can help get it through happy to help. to that end I got the test coverage of all the JS functional tests if that can reassure people)

droplet’s picture

I've discussed the dependency addition proposed here with @catch, @gabesullice, @bnjmnm, @Dries, and @lauriii.

If one of these people give a quick feedback to #133, we can save time on unused review. Don't be silent. We have a strong heart.

but I can't spend more company time on the compromise scenario and I won't spend personal time on this.

+1.

--

In fact, jQuery.once also has no tests and no complaints over these years. If jQuery.once needs a test, Once also needs a test. I vote for no tests here but my exp told me Drupal needs a test (, no matter external dependency or not). However, #133 or #106 or other strict tests. That's no standards to follow. Ideally, we should find a way to test each Once usage on every test.

viappidu’s picture

Does a working example qualify as test? :)

((Drupal, once) => {
  function init(el) {
    var background = el.querySelector('div[data-type="background"]');
    window.addEventListener('scroll', function () {
      var c = -(window.scrollY / background.dataset.speed),
          n = "center " + c + "px";
      background.style.backgroundPosition = n;
    });
    var c = -(window.scrollY / background.dataset.speed),
        n = "center " + c + "px";
    background.style.backgroundPosition = n;
  }

  Drupal.behaviors.scrollingHero = {
    attach(context) {
      once('scrollingHero', context.querySelectorAll('.region--hero')).forEach(heroRegion => init(heroRegion));
    }

  };
})(Drupal, once);
nod_’s picture

@viappidu something like this yes, it would be to make sure drupal load the library properly, that it can be accessed from "drupal-style" javascript, and that it does what is expected.

As a sidenote you can simplify the code like this

once('scrollingHero', '.region--hero', context).forEach(init);
xjm’s picture

Just to confirm, @bnjmnm is definitely working on the Nightwatch version of the test suite. It's understandable if @nod_ and @droplet can't put additional work in on that front at this time. When @bnjmnm posts that patch, we can compare the two approaches and discuss with Dries as needed.

nod_’s picture

Issue summary: View changes
bnjmnm’s picture

StatusFileSize
new38.96 KB
new34.94 KB

This adds nightwatch equivalents of the tests in the library https://github.com/theodoreb/once/tree/v3.5.0/test. It is also moved to misc as it's not being treated as a vendor library at the moment (but hope to see soon 🙂)

Nightwatch isn't the ideal way to do these tests, nor is it ideal to rewrite existing+working tests for a different tool. I also think the work @droplet and @nod_ have done here is great and if getting it into core means adding some weird Nightwatch tests I'm happy do do them.

nod_’s picture

  1. +++ b/core/misc/once/once.es6.js
    @@ -0,0 +1,375 @@
    +// eslint-disable-next-line func-names
    +window.once = (function () {
    

    We can use an arrow function so we don't have to add an eslint-disable comment.

Checked the tests, they work and cover the same thing as the lib, so the code itself is RTBC for me.

My main question if we go this way, how the switch to a "properly" managed library will look like?
We had issues with JS file paths before that prevented some cleanup #2484623: Move all JS in modules to a js/ folder. So are we going to keep the core/once namespace for it and just swap the lib (and file path) or are we going to deprecate core/once and introduce a new library (core/@drupal/once? to follow the @drupal/once npm package name it'll be under?) and ask people to update their code again in the next release?

bnjmnm’s picture

Very good point in #140. I wonder if it would be possible to have this initial implementation be in core/assets/vendor as well. Here's something that came to mind that is odd, but as far as I can tell has fewer tradeoffs than switching js paths or creating two different libraries.

  • Everything in the approach that isn't 100% standard will be heavily documented with comments and readmes. This would be deviating from exact policy a bit, but far less than jQuery once!
  • The source can remain in core/misc, but we add build tools to compile it core/vendor/once, which also provides the benefit of minification. core/assets/vendor/once will have a README explaining that the source is coming from core, not an external.
  • In core.libraries.yml, it would be configured as a core library, with comments making it clear that the file being loaded from core/vendor is based on source in core/misc

There may be glaring caveats I overlooked in this proposed approach, but perhaps some version of this could work?

xjm’s picture

Issue tags: +Drupal Association, +Needs DA engineering feedback

We briefly discussed this issue on today's JS Menu Component initiative discussion with the DA and committers. Based on that discussion and feedback from nod_, I think we might want to attempt to adopt the "interim decision" for this. The MVP for standalone projects on d.o is:

  1. New project type
  2. Publishing to npm from that new project type

Per nod_, we can do everything else the hacky way (running the build steps locally, running tests with not-the-bestest-tools) so long as it's on npm as we intend, and then fix those things later. From a release management perspective, I'd love to have the initial commit be under the official Drupal namespace on npm as a stable initial release for us to commit it to core (so we don't have to change that later).

I'll reach out to @hestenet and ask about how long those initial steps might take. If it's months and months, we should maybe go ahead with @bnjmnm's patch, but if it's "We can have that for you in two weeks" then it might be worth using Once as the test case. Inaugurating a new tag for the feedback.

droplet’s picture

publish to npmjs.com isn't a must if that takes years. Any Git URL (even .gz) still works perfectly with npm/yarn. But a good portal is meaningful for marketing if we trying to get non-Drupal developers.

Patch #139 has a bug on test. will post soon when we have the first step.
and Once `context` has a potential bug:
https://developers.google.com/web/updates/2013/03/What-s-the-CSS-scope-p...

nod_’s picture

We might need to wait after the holidays for DA feedback (for a timeline on adding a JS project type to Drupal.org, as well as some gitlab CI access and setup for secure publication to the @drupal namespace on npm). It might not be in 2020 but it won't take years either. Maybe you haven't seen the news yet droplet #3187489: Add @justafish and @nod_ as core committers to MAINTAINERS.txt, so that should help keeping things moving :D


The potential issue with context doesn't feel like a big deal in the context of once, also people have been using qSA for years so they probably already know when it trips them up. In core there are a few places where we could see an issue, when there are descendant selectors such as :
details .details-title // Claro
#views-tabset a // views
div#views-live-preview a // views
div#views-live-preview input[type=submit] // views
#system-theme-settings .color-form // color module

And that's it, the rest of the selectors in once don't have a hierarchy "condition". It's selecting based on the class, data attribute, tag name, or id.

nod_’s picture

Small update: DA is taking actions around this topic as a priority, if all goes well we'll have everything this month. That will unlock a lot of things, this issue included.

nod_’s picture

DA just released the new project type #2474007: Add a “General” project type to Drupal.org on d.o. The Gitlab part is also on the way, so good news all around :)

I went ahead and created: https://www.drupal.org/project/once I'll push the repo tomorrow as it's getting late here.

nod_’s picture

Issue summary: View changes
Issue tags: -Needs DA engineering feedback
StatusFileSize
new38.97 KB
new34.9 KB

The once lib is now on NPM :D. We have a new project type and a pipeline that published code to npm under the @drupal namespace automatically, removing the tag for DA review.

updating the patch with latest code. For making use of the npm package itself in core see #3185289: Use package.json and rollup to manage third party JS libraries

updating the change notice with links and mention of the npm package.

catch’s picture

Title: Rewrite jQuery.once [Remove dependency on jQuery] » Add once.js to core
Issue tags: -Needs release manager review

Retitleing and removing the 'Needs release manager review' tag now that it's decided exactly where the new library lives.

anmolgoyal74’s picture

StatusFileSize
new39.06 KB
new1.11 KB

Fixed CI issues.

droplet’s picture

the patch is similar to the previous, only needs to change issue status mostly.

I'm okay to do a full REVIEW if no more hidden issues that need someone to approve

nod_’s picture

I'd say go for it, I don't see outstanding problems or concerns.

We'll need to have some integration tests to make sure the once lib works as intended for drupal core but we can do that in the follow-up issue where we replace everything in core, this one is long enough already.

nod_’s picture

@droplet confirmed it with the other committers, no hidden issue that needs approval.

droplet’s picture

+++ b/core/core.libraries.yml
@@ -269,6 +269,11 @@ drupal.dropbutton:
+    misc/polyfills/element.matches.js: {}

Only one thing. It's not really a bug but other polyfill have `{ weight: -20 }`. We should keep them the same, right?

+++ b/core/core.libraries.yml
@@ -721,6 +726,18 @@ normalize:
+    assets/vendor/once/once.min.js: { minified: true, weight: -19 }

not a bug either. and no written rules for it. But we keep all `minified` the last one in this file. :p

nod_’s picture

StatusFileSize
new39.07 KB
new653 bytes

Nice find, fixed.

droplet’s picture

Status: Needs review » Reviewed & tested by the community
justafish’s picture

Looks great! +1 🎉

alexpott’s picture

Issue summary: View changes
alexpott’s picture

Added a release note. This will probably be a highlight too.

xjm’s picture

Let's tag the 1.0.0 release of https://www.npmjs.com/package/@drupal/once before we commit this to HEAD. We need to add a stable version semver contract before core adds the code -- otherwise, it adds a release blocker for the release of 9.2, which isn't something we allow for minors.

If we run into any issues in #3183149: Deprecate jquery.once and use the new once lib that require public API changes to the library, we still have until the 9.2.0-beta1 commit deadline to release a 2.0.0 version of once.js, and 1.x can be EOLed in that situation. (The restrictions around version support etc. only kick in when the library ships in a stable version of core, which will be 9.2.0 in this case.)

alexpott’s picture

Related issues: +#3195360: Stable 1.0 version

#159 means we're blocked here on #3195360: Stable 1.0 version

nod_’s picture

Thanks for the details on version support. I'll start the dependency evaluation so that we can have a stable version by monday hopefully, the code that we bring in to core doesn't have dependencies, the evaluation is for dev dependencies.

I'm not anticipating any API change (or any code change) at this point, the patch in #3183149: Deprecate jquery.once and use the new once lib is already complete and green. I'd be surprised to find an edge case we didn't already run into.

nod_’s picture

StatusFileSize
new30.63 KB
new39.07 KB

built the 1.0.0 version locally, it'll be the same as the one we'll publish later. I'll update the patch again if necessary

droplet’s picture

Status: Reviewed & tested by the community » Needs review

Can you/we do it? haha

{"version":3,"file":"once.min.js","sources":["../src/once.js"]

well this patch revealed a bug. it pointed to parent folders. so the non-minified js file in this patch is useless.

nod_’s picture

non minified is for IDE integration, that way there is documentation when using the once lib.

But agreed that it's not 'clean' as far as the source mapping is concerned.

nod_’s picture

source map issue has been fixed, only need a release now :)

bnjmnm’s picture

I'm concerned this doesn't have any integration tests. The coverage of the library itself is excellent, but I think there should be some minimal coverage to confirm it is properly integrated with Drupal. I ran tests on a version of the #162 that removed the usage of once() from datepicker and Claro autocomplete, and the tests passed without error, but both would be scenarios that caused multiple instances of behavior that should only occur once. https://www.drupal.org/pift-ci-job/1991107. An abbreviated form of the core tests that were present as recently as #105 would work fine.

droplet’s picture

IMO, we need a global LOAD test for all libs in `core/core.libraries.yml`

And then, we need not test the 3rd party itself. `Once` is just another `jQuery`, that's no diff. We need not do extra for it.

and all these new-type libs are protected (, I believed). It's very difficult to remove a method from it.

#162 that removed the usage of once() from datepicker and Claro autocomplete, and the tests passed without error

This is my comment #134, we never have strict tests for many JS code & Once. In fact, we really have 1 or 2 wrong Once usages in the old day.

Technically, this is bad tests in modules, not missing tests of Once. (A simple dedicated test for Once in CORE won't prevent bugs happened)

nod_’s picture

Integration testing is going to happen, I wanted to do that in the next issue where we deprecate the jquery.once lib because this issue is long enough as it is.

nod_’s picture

StatusFileSize
new26.27 KB
new42.77 KB

We have a 1.0.0 release of @drupal/once :D

interdiff against #154 as it was the last RTBC. The diff in size is because the map file doesn't include the source inline.

To test you can edit any node with the claro admin theme, and do a once.find(); in the console to see the couple of fields that have been marked as processed (the author field is always there).

droplet’s picture

Status: Needs review » Reviewed & tested by the community
nod_’s picture

StatusFileSize
new26.19 KB

reroll after #3113649: Remove drupal.tabbingmanager's jQueryUI dependency (confilict in .estlintrc.json file)

alexpott credited Dries.

alexpott’s picture

Creditting @catch, @gabesullice, @bnjmnm, @Dries, @lauriii, @xjm for #132

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

I fixed #169 / #171 locally since the conflict was in core/.eslintrc.json and I run the custom commands locally prior to committing anyway.

I noticed that this patch had test coverage but that was then removed. I checked to ensure the once library has test coverage and it does - https://git.drupalcode.org/project/once/-/tree/1.0.x/test

Committed 5bce5ee and pushed to 9.2.x. Thanks!

  • alexpott committed 5bce5ee on 9.2.x
    Issue #2402103 by nod_, bnjmnm, larowlan, flxa, anmolgoyal74, droplet,...
nod_’s picture

That one makes me very happy, thank you everyone :)

larowlan’s picture

Congratulations folks

nod_’s picture

#3183149: Deprecate jquery.once and use the new once lib is complete, green, and in need of review, then we can say goodbye to jquery.once :)

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

xjm’s picture

This issue is a critical part of D10 development.