Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: use a Map instead of an Object in dom/data #32180

Merged
merged 10 commits into from
Mar 2, 2021

Conversation

alpadev
Copy link
Contributor

@alpadev alpadev commented Nov 17, 2020

@alpadev alpadev requested a review from a team as a code owner November 17, 2020 20:01
@alpadev
Copy link
Contributor Author

alpadev commented Nov 18, 2020

@XhmikosR I realised something in the dom/data spec https://github.com/twbs/bootstrap/blob/main/js/tests/unit/dom/data.spec.js#L30-L44

The test says 'should change data if something is already stored' but it expects the bsKey to be defined. Also if data.test is updated, there would be no need to call setData again as the object from the first call remains the same.

@@ -24,7 +24,7 @@ const mapData = (() => {
id++
}

storeData[element.bsKey.id] = data
storeData.set(element.bsKey.id, data)
Copy link
Collaborator

@rohit2sharma95 rohit2sharma95 Nov 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The element can not be used directly here? 🤔
Ref: #32123 (comment)

My suggestions: alpadev#1

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep the element should be used here to the data

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uhm that's a bit contradictory to this response #32123 (comment) but well

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha yes sorry @alpadev but things can change 🤷

@XhmikosR
Copy link
Member

XhmikosR commented Nov 20, 2020

FYI this results in a nice size decrease (approx. 1.3-1.8% from the compressed gzip files) 🙂 https://ja2r7.app.goo.gl/5pUhLMyZD1iuZgYe9

I moved this to beta1 @Johann-S. Tests do seem to pass, although I notice a few of them are removed.

@rohit2sharma95
Copy link
Collaborator

Yes, we discussed the tests here: alpadev#1, though
Few of them are removed, I am not sure which tests should be added again 🙂

js/src/alert.js Outdated
@@ -49,7 +49,7 @@ class Alert {
this._element = element

if (this._element) {
Data.setData(element, DATA_KEY, this)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW the DATA_KEY constants are only used in one place in each of the plugins except for popover.js and tooltip.js. The minifier already inlines those, so I guess we better keep them for consistency.

js/src/dom/data.js Outdated Show resolved Hide resolved
@Johann-S
Copy link
Member

LGTM 👍 since we don't allow different instance on an element 👌

Johann-S
Johann-S previously approved these changes Nov 20, 2020
@XhmikosR XhmikosR changed the title refactor: use Map instead of Object in dom/data refactor: use a Map instead of an Object in dom/data Nov 21, 2020
@XhmikosR
Copy link
Member

@Johann-S My only concerns are:

  1. if the removed tests should all be removed and if we still cover all cases we did before, like passing non-bs prefixed data attributes etc
  2. I suppose if we want to support multiple keys in the future it shouldn't be a lot of work, right?

All in all, I love this change; it makes the code simpler and also saves us a good amount of code. I just want to make sure we aren't missing anything and that we won't be circling around in the future 🙂

@alpadev
Copy link
Contributor Author

alpadev commented Nov 21, 2020

If it's expected to have multiple keys/instances on one element in a closer future, this might be not the ideal implementation to go with, I guess.

This basically removes the circuit of mapping an UID (and a DATA_KEY), that both been stored in an object at the elements' bsKey, to an instance that is saved in the mapData, and instead uses the 1:1 relationship of element and instance directly with the element being the key to the mapDatas' instance.

So before it was like:
A PluginInstance <> mapData.id <> element.bsKey.id mapping, with the DATA_KEY only being some kind of validation to check if Data.getData or Data.removeData is called with the same DATA_KEY, that has been stored in element.bsKey.key when Data.setData was called (for the first time).

Now it's like:
A PluginInstance <> mapData.element <> element mapping, with the DATA_KEY not being used here at all.

Another thing to mention, is that Plugin.getInstance(element) isn't "type-safe" anymore. Before if I had a Tooltip bound to an element, Tooltip.getInstance(element) would return some Tooltip-Instance, while Button.getInstance(element) returned null. Now every Plugin.getInstance returns whatever is saved in mapData for that element or null if nothing saved.

@XhmikosR
Copy link
Member

XhmikosR commented Nov 21, 2020 via email

@alpadev
Copy link
Contributor Author

alpadev commented Nov 21, 2020

If I was to propose some implementation for the data, it should look something like this:

const elementMap = new Map(); // or WeakMap() even.. tbh lacking experience with WeakMaps

export default {
  set(element, key, instance) {
    if (!elementMap.has(element)) {
      elementMap.set(element, new Map());
    }

    const instanceMap = elementMap.get(element);

    // make it clear we only want one instance per element
    // can be removed at a later time when multiple key/instances are allowed
    if (!instanceMap.has(key) && instanceMap.size !== 0) {
      throw new Error(
        "Sorry, we currently allow only one instance per element."
      );
    }

    instanceMap.set(key, instance);
  },

  get(element, key) {
    if (elementMap.has(element)) {
      return elementMap.get(element).get(key) || null;
    }

    return null;
  },

  remove(element, key) {
    if (elementMap.has(element)) {
      const instanceMap = elementMap.get(element);

      instanceMap.delete(key);

      // free up element references if there are no instances left for an element
      // I guess this wouldn't be necessary with a WeakMap
      if (instanceMap.size === 0) {
        elementMap.delete(element);
      }
    }
  }
};

This uses both an element and a key to store some data/instance. It'd also allow for multiple key<>instance mappings, at a later point if it's decided to be necessary.

@twbs twbs deleted a comment from Jbo666 Nov 22, 2020
@XhmikosR
Copy link
Member

@alpadev yup, that sounds like a better approach since that seems closer to what we have right now :)

@rohit2sharma95 rohit2sharma95 mentioned this pull request Nov 24, 2020
6 tasks
@Johann-S
Copy link
Member

@alpadev said :

Another thing to mention, is that Plugin.getInstance(element) isn't "type-safe" anymore. Before if I had a Tooltip bound to an element, Tooltip.getInstance(element) would return some Tooltip-Instance, while Button.getInstance(element) returned null. Now every Plugin.getInstance returns whatever is saved in mapData for that element or null if nothing saved.

The old behavior should be kept here, it's important to be type safe.

And I like your other implementation (#32180 (comment))

BTW when I created Data object, I tried to be as close as possible to how jQuery store data when we call .data on a jQuery object

@XhmikosR
Copy link
Member

XhmikosR commented Nov 25, 2020

So, assuming we follow @alpadev's suggestion, would this still be considered a breaking change? If so, we need to get this done in beta1, otherwise we can move it to beta2.

@rohit2sharma95
Copy link
Collaborator

It is not a breaking change then IMO 🤔
Because as per the comment of @alpadev the old behaviour would be kept here.

Copy link
Member

@XhmikosR XhmikosR left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some last comments to address

@alpadev
Copy link
Contributor Author

alpadev commented Feb 24, 2021

I'm not a big fan on console.logs in build files, but in this case I think it make sense to let people know why it doesn't work.

Personally I'm still against allowing people to have multiple plugin instances I think we'll have weird behavior and I don't see any valid use cases currently, that's all for my two cents 😄

I didn't mean to enforce my opinion here 🙂. My intention was just to explain my standpoint and to receive some input.

As I see it we have those options:

Allow the use of multiple components per element

  • Just let it happen and have the implementers figure out problems that may occur
  • Allow it but show a message (console.error/warn/log) to give them a hint <-- this is how it is for now

Disallow the use of multiple components

  • Throw an error describing the problem
  • Overwrite the first component but disposing it before (with or without a message)
  • Overwrite the first component without cleaning up (with or without a message)
  • Ignore/reject the second component (with or without a message) <-- this is how it is for now

There are some last comments to address

Yeah sorry.. I got my chance to try that thing called Covid and been totally mashed potato in my head for the last week or so. I'm on it.

js/src/dom/data.js Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Refactor data.js to use a Map
4 participants