foundation / foundation-sites

The most advanced responsive front-end framework in the world. Quickly create prototypes and production code for sites that work on any kind of device.
https://get.foundation
MIT License
29.66k stars 5.49k forks source link

Importing JS via Webpack resulting in odd Reveal behavior (I think due to jQuery conflict) #10488

Closed simshaun closed 7 years ago

simshaun commented 7 years ago

I was excited to upgrade to 6.4 and shift to importing specific JS modules using Webpack. However, I'm finding that js/foundation.util.triggers.js isn't playing nicely because it's importing its own version of jQuery (node_modules/foundation-sites/node_modules/jquery/).

Clicking my button that has a data-open attribute is not opening the Reveal modal. Here's the weird part:

I can add a few console.log lines in node_modules/foundation-sites/js/foundation.util.triggers.js and see that the Triggers.Listeners.Basic.openListener is being called when I click my button. I can also see that .triggerHandler('open.zf.trigger' ...) is being called on the modal element just as it should be. But the modal doesn't open.

I've been trying to figure this out for hours now, and from what I can tell, it's because Foundation is using its own instance/version of jQuery. Inspecting the modal element in the browser shows that jQuery has attached its metadata in two separate namespaces (one for my app's jQuery, one for Foundation's jQuery):

image

If I bind my app's version of jQuery to window.$ and modify the triggers method in node_modules/foundation-sites/js/foundation.util.triggers.js to call window.$ instead of just $, then the modal does open.

How to reproduce this bug:

  1. Set up a project with Webpack, jQuery, and Foundation. Below is my foundation.js that I'm importing specific modules with:
import $ from 'jquery';
import { Foundation } from 'foundation-sites/js/foundation.core';

import { rtl, GetYoDigits, transitionend } from 'foundation-sites/js/foundation.util.core';
import { Box } from 'foundation-sites/js/foundation.util.box';
import { Keyboard } from 'foundation-sites/js/foundation.util.keyboard';
import { MediaQuery } from 'foundation-sites/js/foundation.util.mediaQuery';
import { Motion, Move } from 'foundation-sites/js/foundation.util.motion';
import { Nest } from 'foundation-sites/js/foundation.util.nest';
import { onImagesLoaded } from 'foundation-sites/js/foundation.util.imageLoader';
import { Timer } from 'foundation-sites/js/foundation.util.timer';
import { Touch } from 'foundation-sites/js/foundation.util.touch';
import { Triggers } from 'foundation-sites/js/foundation.util.triggers';

import { Dropdown } from 'foundation-sites/js/foundation.dropdown';
import { OffCanvas } from 'foundation-sites/js/foundation.offcanvas';
import { Reveal } from 'foundation-sites/js/foundation.reveal';
import { Tabs } from 'foundation-sites/js/foundation.tabs';

window.$ = $;

Foundation.addToJquery($);

// Add Foundation Utils to Foundation global namespace for backwards compatibility.
Foundation.rtl = rtl;
Foundation.GetYoDigits = GetYoDigits;
Foundation.transitionend = transitionend;
Foundation.Box = Box;
Foundation.onImagesLoaded = onImagesLoaded;
Foundation.Keyboard = Keyboard;
Foundation.MediaQuery = MediaQuery;
Foundation.Motion = Motion;
Foundation.Move = Move;
Foundation.Nest = Nest;
Foundation.Timer = Timer;

Touch.init($);
Triggers.init($, Foundation);

Foundation.plugin(Dropdown, 'Dropdown');
Foundation.plugin(OffCanvas, 'OffCanvas');
Foundation.plugin(Reveal, 'Reveal');
Foundation.plugin(Tabs, 'Tabs');

$(document).foundation();
  1. Set up a modal with new Reveal($modalEl) and a button to open it that has the data-open="idOfModal" attribute.

  2. Click the button.

What should happen:

The Reveal modal should open, but doesn't.

Browser(s) and Device(s) tested on:

Win/Chrome

Foundation Version(s) you are using:

6.4.1

Other things I've tried.

  1. import Foundation from 'foundation-sites'; then new Foundation.Reveal(...). That didn't work.
  2. import 'foundation-sites/dist/js/foundation'; then new Foundation.Reveal(...). That didn't work either.

Other things affected

This isn't just limited to data-open. When the modal is open, the button that has a data-close attribute won't close the modal either. I can fix that as well by changing Triggers.Listeners.Basic.closeListener to call window.$(this).trigger('close.zf.trigger') instead of just $(this).trigger('close.zf.trigger'). Obviously relying on that global is not a good idea, but I don't know how to properly fix this.

My temporary solution

If I manually trigger the needed events using my app's jQuery instance, the Reveal opens and closes as it should:

$(document).on('click', '[data-close]', (ev) => {
  $(ev.currentTarget).trigger('close.zf.trigger');
});

function triggerOpen($el) {
  $el.data('open').split(' ').forEach((id) => {
    $(`#${id}`).triggerHandler('open.zf.trigger', [$el]);
  });
}

$(document).on('click', '[data-open]', (ev) => {
  triggerOpen($(ev.currentTarget));
});
stonehz commented 7 years ago

having a very similar problem 👍

drewfranklin commented 7 years ago

@simshaun @stonehz I found that adding this to the webpack.config.js worked for me, or at least I think this is related to your problem but I might be wrong.

// webpack.config.js
module.exports = {
    ...
    resolve: {
        alias: {
            jquery: "jquery/src/jquery"
        }
    }
};

Here is the stackoverflow for context https://stackoverflow.com/questions/28969861/managing-jquery-plugin-dependency-in-webpack#28989476

simshaun commented 7 years ago

That got me down the right path to get it fixed. Thanks.

module.exports = {
  ...
  resolve: {
    alias: {
      jquery: path.resolve(__dirname, 'node_modules/jquery/dist/jquery.js'),
    },
  },
};

The difference is I needed to point to the dist version of jQuery because the src version of jQuery expects "define" to be available. (I have disabled AMD-style loading in Webpack to make some other JS libs behave.)

For anybody that's curious, this config tells Webpack to force your version of jQuery when anything requires it.

Oh, and be careful if supporting Internet Explorer and importing directly from Foundation's src. It uses some ES2015 syntax that you need to make sure is transpiled to ES5. e.g.

module: {
  rules: [
    {
      test: /.js$/,
      exclude: {
        test: path.resolve(__dirname, 'node_modules'),
        exclude: path.resolve(__dirname, 'node_modules/foundation-sites'),
      },
      use: [
        {
          loader: 'babel-loader',
          options: {
            cacheDirectory: true,
            presets: [
              [
                'env',
                {
                  targets: {
                    browsers: 'ie 10',
                  },
                },
              ],
            ],
          },
        },
      ],
    },
  ],
}