materialize icon indicating copy to clipboard operation
materialize copied to clipboard

Use Materialize as modular components

Open geosigno opened this issue 6 years ago • 33 comments

Hi,

I have a lot of issue to have materialize working as modular component. I don't want to have to load the entire JS library (almost 200KB) but only some components.

I was used to Foundation, which was working very simply. But with Materialize, everytime I want to add some component, it is long process to find which component need which dependency.

For example the Modal component

import 'materialize-css/js/modal';

Then I got the error Cash is not defined, so:

import 'materialize-css/js/cash'; import 'materialize-css/js/modal';

Then I got the Component is not defined, so again:

import 'materialize-css/js/component'; import 'materialize-css/js/cash'; import 'materialize-css/js/modal';

But here, I am still getting the issue.

I know several issues have already been reported, but none of them has been abe to help me so far...

Is there (probably) a better to achieve what I am trying to do?

I can't find a dependency documentation, which will list all module interdependency. Is this plan?

Thanks.

geosigno avatar May 31 '18 12:05 geosigno

You can take a look at our Gruntfile to get a better sense of how we are compiling things. But in general, you should import in this order:

'js/cash.js',
'js/component.js',
'js/global.js',
'js/anime.min.js',

acburst avatar May 31 '18 20:05 acburst

Still having the issue

import 'materialize-css/js/cash.js'; import 'materialize-css/js/component.js'; import 'materialize-css/js/global.js'; import 'materialize-css/js/anime.min.js'; import 'materialize-css/js/modal.js';

$(document).ready(function() { $('.modal').modal(); });

This error:

app.js:14686 Uncaught ReferenceError: Component is not defined at app.js:14686 at Object._defaults.alignment (app.js:14940) at __webpack_require__ (app.js:20) at Object.<anonymous> (app.js:13164) at __webpack_require__ (app.js:20) at Object.defineProperty.value (app.js:10475) at __webpack_require__ (app.js:20)

geosigno avatar Jun 01 '18 05:06 geosigno

I use Webpack to bundle the JS, maybe the issue is linked to this?

I tried to import all modules such this:

import 'materialize-css/js/cash.js';
import 'materialize-css/js/component.js';
import 'materialize-css/js/global.js';
import 'materialize-css/js/anime.min.js';
import 'materialize-css/js/collapsible.js';
import 'materialize-css/js/dropdown.js';
import 'materialize-css/js/modal.js';
import 'materialize-css/js/materialbox.js';
import 'materialize-css/js/parallax.js';
import 'materialize-css/js/tabs.js';
import 'materialize-css/js/tooltip.js';
import 'materialize-css/js/waves.js';
import 'materialize-css/js/toasts.js';
import 'materialize-css/js/sidenav.js';
import 'materialize-css/js/scrollspy.js';
import 'materialize-css/js/autocomplete.js';
import 'materialize-css/js/forms.js';
import 'materialize-css/js/slider.js';
import 'materialize-css/js/cards.js';
import 'materialize-css/js/chips.js';
import 'materialize-css/js/pushpin.js';
import 'materialize-css/js/buttons.js';
import 'materialize-css/js/datepicker.js';
import 'materialize-css/js/timepicker.js';
import 'materialize-css/js/characterCounter.js';
import 'materialize-css/js/carousel.js';
import 'materialize-css/js/tapTarget.js';
import 'materialize-css/js/select.js';
import 'materialize-css/js/range.js';

I get the same issue.. only the following code work correctly:

import 'materialize-css/';

but this is very heavy to use only the modal.

geosigno avatar Jun 05 '18 10:06 geosigno

Same here, except I can't do

import 'materialize-css/';

then it says jquery is not defined O_o

eboye avatar Jun 07 '18 03:06 eboye

Did you import jQuery, something as follow:

window.$ = window.jQuery = require('jquery');

geosigno avatar Jun 07 '18 05:06 geosigno

@geosenna no, I've tried

import $ from 'jquery';

and also

import jQuery from 'jquery';

but didn't work. Moved to gulp for now as this project I'm working on needs to be done fast 😠

eboye avatar Jun 07 '18 23:06 eboye

@acburst any idea?

geosigno avatar Jun 08 '18 10:06 geosigno

@geosenna I have done the following and works:

  • In component.js replace the first line with export default class Component {

in your modal file add at the top add: import Component from '../path-to/component';

In the source bundle (application.js in my app) I don't import component.js again, only in the modal or whatever other 'component' you need to import it from there.

Hope this helps.

MaikelGG avatar Jun 15 '18 07:06 MaikelGG

I have done what you said, but I get the same error.

app.js:17024 Uncaught ReferenceError: Component is not defined
    at app.js:17024
    at Object.<anonymous> (app.js:17384)
    at __webpack_require__ (app.js:20)
    at Object.<anonymous> (app.js:10845)
    at __webpack_require__ (app.js:20)
    at Object.defineProperty.value (app.js:10477)
    at __webpack_require__ (app.js:20)
    at Object._slice (app.js:12803)
    at __webpack_require__ (app.js:20)
    at Object.ua (app.js:10920)
import 'materialize-css/js/cash.js';
import Component from 'materialize-css/js/component.js';
import 'materialize-css/js/global.js';
import 'materialize-css/js/anime.min.js';
import 'materialize-css/js/modal.js';

And I modify the Component.js file itself from materialize.

geosigno avatar Jun 15 '18 10:06 geosigno

You need to move 'import Component from 'materialize-css/js/component.js';' to modal.js

MaikelGG avatar Jun 15 '18 10:06 MaikelGG

Actually this this the content of the modal.js file, not my source bundle.

geosigno avatar Jun 15 '18 11:06 geosigno

any though @acburst ?

I still have the same issue:

Uncaught ReferenceError: Component is not defined

at class Modal extends Component {

geosigno avatar Jun 28 '18 10:06 geosigno

I am really struggling with this... Is anyone succeed to using both Webpack and Materialize Modal?

geosigno avatar Jul 24 '18 13:07 geosigno

I haven't been able to use it as modules, but I've managed to make it work with webpack (the RC version). Here's the app.js:

// noinspection JSLint
import jQuery from "jquery"; // jshint ignore:line
// noinspection JSLint
window.$ = window.jQuery = jQuery;
// noinspection JSLint
import * as M from "materialize-css"; // jshint ignore:line

document.addEventListener('DOMContentLoaded', function () {
  // noinspection JSLint
  'use strict';
  // noinspection JSUnresolvedVariable
  M.Sidenav.init(document.querySelectorAll('.sidenav'), {
    edge: 'right'
  });
  M.Collapsible.init(document.querySelectorAll('.collapsible'), {
    accordion: false
  });
  if (document.querySelectorAll('.parallax').length) {
    M.Parallax.init(document.querySelectorAll('.parallax'), {
      responsiveThreshold: 0
    });
  }
  if (document.querySelectorAll('.tooltipped').length) {
    M.Tooltip.init(document.querySelectorAll('.tooltipped'));
  }
});

Here's the package.json:

{
  "private": true,
  "scripts": {
    "dev": "npm run development",
    "development": "cross-env NODE_ENV=development webpack --progress --hide-modules --config build/webpack.config.js",
    "prod": "npm run production",
    "production": "cross-env NODE_ENV=production webpack --progress --hide-modules --config build/webpack.config.js",
    "watch": "npm run development -- --watch && webpack-dev-server --progress --colors"
  },
  "dependencies": {
    "jquery": "^3.3.1",
    "materialize-css": "^1.0.0-rc.2"
  },
  "devDependencies": {
    "autoprefixer": "^8.2.0",
    "babel-core": "^6.24.1",
    "babel-loader": "^7.1.0",
    "babel-preset-env": "^1.3.3",
    "browser-sync": "^2.18.8",
    "browser-sync-webpack-plugin": "^2.2.2",
    "clean-webpack-plugin": "^0.1.16",
    "copy-webpack-plugin": "^4.0.1",
    "cross-env": "^5.0.1",
    "css-loader": "^0.28.0",
    "dotenv": "^5.0.1",
    "eslint": "^4.10.0",
    "eslint-loader": "^2.0.0",
    "extract-text-webpack-plugin": "^3.0.0",
    "file-loader": "^1.1.11",
    "imagemin-jpeg-recompress": "^5.1.0",
    "imagemin-webpack-plugin": "^2.1.0",
    "img-loader": "^2.0.0",
    "isdev": "^1.0.1",
    "node-sass": "^4.5.2",
    "postcss-loader": "^2.1.3",
    "sass-loader": "^6.0.3",
    "style-loader": "^0.20.3",
    "stylelint": "^9.1.3",
    "stylelint-webpack-plugin": "^0.10.3",
    "webpack": "~3.11.0",
    "webpack-cli": "^2.0.13",
    "webpack-dev-server": "^3.1.1",
    "webpack-merge": "^4.1.0"
  }
}

If anyone is interested, I can make a boilerplate od sorts that I'm using as there are some more config files.

eboye avatar Jul 24 '18 16:07 eboye

Yes me too, my issue is about using the modal as component because of this error:

Uncaught ReferenceError: Component is not defined

geosigno avatar Jul 26 '18 18:07 geosigno

I am using webpack with the modular components. So if you need to transpile to es5, please add babel in your set up. This example is the dropdown, not the modal, but that should work similar. I do not use jQuery, only cash-dom. The animate file is from the materialize repository, as there is one change that makes it not possible to load it via npm, but that is not relevant for this issue.

I hope this helps as a reference to get a working modal in your application.

app.js:

import "cash-dom";
import 'components/global/global';
import "components/animate/anime";
import "components/dropdown/dropdown";
import "init/init";

package.json:

"dependencies": {
    "cash-dom": "2.1.6",
  },
  "devDependencies": {
    "webpack-dev-server": "2.11.2"
  }

components/global/global.js: (modified)

(function(window) {
  window.M = {};  
})(window);

M.keys = {
  TAB: 9,
  ENTER: 13,
  ESC: 27,
  ARROW_UP: 38,
  ARROW_DOWN: 40
};

/**
 * TabPress Keydown handler
 */
M.tabPressed = false;
M.keyDown = false;
let docHandleKeydown = function(e) {
  M.keyDown = true;
  if (e.which === M.keys.TAB || e.which === M.keys.ARROW_DOWN || e.which === M.keys.ARROW_UP) {
    M.tabPressed = true;
  }
};
let docHandleKeyup = function(e) {
  M.keyDown = false;
  if (e.which === M.keys.TAB || e.which === M.keys.ARROW_DOWN || e.which === M.keys.ARROW_UP) {
    M.tabPressed = false;
  }
};
let docHandleFocus = function(e) {
  if (M.keyDown) {
    document.body.classList.add('keyboard-focused');
  }
};
let docHandleBlur = function(e) {
  document.body.classList.remove('keyboard-focused');
};
document.addEventListener('keydown', docHandleKeydown, true);
document.addEventListener('keyup', docHandleKeyup, true);
document.addEventListener('focus', docHandleFocus, true);
document.addEventListener('blur', docHandleBlur, true);


/**
 * Gets id of component from a trigger
 * @param {Element} trigger  trigger
 * @returns {string}
 */
M.getIdFromTrigger = function(trigger) {
  let id = trigger.getAttribute('data-target');
  if (!id) {
    id = trigger.getAttribute('href');
    if (id) {
      id = id.slice(1);
    } else {
      id = '';
    }
  }
  return id;
};

M.getPositionFromTrigger = function(trigger) {
  let position = trigger.dataset.alignment;
  return position;
};

M.getCoverTrigger = function(trigger) {
  let coverTrigger = trigger.dataset.cover;
  return coverTrigger;
};

/**
 * Automatically initialize components
 * @param {Element} context  DOM Element to search within for components
 */
M.AutoInit = function(context) {
  // Use document.body if no context is given
  let root = !!context ? context : document.body;

  let registry = {
    Dropdown: root.querySelectorAll('.dropdown-trigger:not(.no-autoinit)')
  };

  for (let pluginName in registry) {
    let plugin = M[pluginName];
    plugin.init(registry[pluginName]);
  }
};

M.checkPossibleAlignments = function(el, container, bounding, offset) {
  let canAlign = {
    top: true,
    right: true,
    bottom: true,
    left: true,
    spaceOnTop: null,
    spaceOnRight: null,
    spaceOnBottom: null,
    spaceOnLeft: null
  };

  let containerAllowsOverflow = getComputedStyle(container).overflow === 'visible';
  let containerRect = container.getBoundingClientRect();
  let containerHeight = Math.min(containerRect.height, window.innerHeight);
  let containerWidth = Math.min(containerRect.width, window.innerWidth);
  let elOffsetRect = el.getBoundingClientRect();

  let scrollLeft = container.scrollLeft;
  let scrollTop = container.scrollTop;

  let scrolledX = bounding.left - scrollLeft;
  let scrolledYTopEdge = bounding.top - scrollTop;
  let scrolledYBottomEdge = bounding.top + elOffsetRect.height - scrollTop;

  // Check for container and viewport for left
  canAlign.spaceOnRight = !containerAllowsOverflow
    ? containerWidth - (scrolledX + bounding.width)
    : window.innerWidth - (elOffsetRect.left + bounding.width);
  if (canAlign.spaceOnRight < 0) {
    canAlign.left = false;
  }

  // Check for container and viewport for Right
  canAlign.spaceOnLeft = !containerAllowsOverflow
    ? scrolledX - bounding.width + elOffsetRect.width
    : elOffsetRect.right - bounding.width;
  if (canAlign.spaceOnLeft < 0) {
    canAlign.right = false;
  }

  // Check for container and viewport for Top
  canAlign.spaceOnBottom = !containerAllowsOverflow
    ? containerHeight - (scrolledYTopEdge + bounding.height + offset)
    : window.innerHeight - (elOffsetRect.top + bounding.height + offset);
  if (canAlign.spaceOnBottom < 0) {
    canAlign.top = false;
  }

  // Check for container and viewport for Bottom
  canAlign.spaceOnTop = !containerAllowsOverflow
    ? scrolledYBottomEdge - (bounding.height - offset)
    : elOffsetRect.bottom - (bounding.height + offset);
  if (canAlign.spaceOnTop < 0) {
    canAlign.bottom = false;
  }

  return canAlign;
};

components/global/component.js (imported in dropdown.js)

export default class Component {
    /**
     * Generic constructor for all components
     * @constructor
     * @param {Element} el
     * @param {Object} options
     */
    constructor(classDef, el, options) {
      // Display error if el is valid HTML Element
      if (!(el instanceof Element)) {
        console.error(Error(el + ' is not an HTML Element'));
      }
  
      // If exists, destroy and reinitialize in child
      let ins = classDef.getInstance(el);
      if (!!ins) {
        ins.destroy();
      }
  
      this.el = el;
      this.$el = cash(el);
    }
  
    /**
     * Initializes components
     * @param {class} classDef
     * @param {Element | NodeList | jQuery} els
     * @param {Object} options
     */
    static init(classDef, els, options) {
      let instances = null;
      if (els instanceof Element) {
        instances = new classDef(els, options);
      } else if (!!els && (els.jquery || els.cash || els instanceof NodeList)) {
        let instancesArr = [];
        for (let i = 0; i < els.length; i++) {
          instancesArr.push(new classDef(els[i], options));
        }
        instances = instancesArr;
      }
  
      return instances;
    }
  }

components/dropdown/dropdown.js: (modified)

import Component from '../global/component';

(function($, anim) {
  'use strict';

  let _defaults = {
    alignment: 'left',
    autoFocus: true,
    constrainWidth: false,
    container: null,
    coverTrigger: false,
    closeOnClick: true,
    hover: false,
    inDuration: 150,
    outDuration: 250,
    onOpenStart: null,
    onOpenEnd: null,
    onCloseStart: null,
    onCloseEnd: null,
    onItemClick: null
  };

  /**
   * @class
   */
  class Dropdown extends Component {
    constructor(el, options) {
      super(Dropdown, el, options);

      this.el.M_Dropdown = this;
      Dropdown._dropdowns.push(this);

      this.id = M.getIdFromTrigger(el);
      this.position = M.getPositionFromTrigger(el);
      this.coverTrigger = M.getCoverTrigger(el);
      this.dropdownEl = document.getElementById(this.id);
      this.$dropdownEl = $(this.dropdownEl);

      /**
       * Options for the dropdown
       * @member Dropdown#options
       * @prop {String} [alignment='left'] - Edge which the dropdown is aligned to
       * @prop {Boolean} [autoFocus=true] - Automatically focus dropdown el for keyboard
       * @prop {Boolean} [constrainWidth=true] - Constrain width to width of the button
       * @prop {Element} container - Container element to attach dropdown to (optional)
       * @prop {Boolean} [coverTrigger=true] - Place dropdown over trigger
       * @prop {Boolean} [closeOnClick=true] - Close on click of dropdown item
       * @prop {Boolean} [hover=false] - Open dropdown on hover
       * @prop {Number} [inDuration=150] - Duration of open animation in ms
       * @prop {Number} [outDuration=250] - Duration of close animation in ms
       * @prop {Function} onOpenStart - Function called when dropdown starts opening
       * @prop {Function} onOpenEnd - Function called when dropdown finishes opening
       * @prop {Function} onCloseStart - Function called when dropdown starts closing
       * @prop {Function} onCloseEnd - Function called when dropdown finishes closing
       */
      this.options = $.extend({}, Dropdown.defaults, options);

      /**
       * Describes open/close state of dropdown
       * @type {Boolean}
       */
      this.isOpen = false;

      /**
       * Describes if dropdown content is scrollable
       * @type {Boolean}
       */
      this.isScrollable = false;

      /**
       * Describes if touch moving on dropdown content
       * @type {Boolean}
       */
      this.isTouchMoving = false;

      this.focusedIndex = -1;
      this.filterQuery = [];

      /**
       * Read the position from the data-alignment attribute.
       */
      if(this.position) {
        this.options.alignment = this.position;
      }

      if(this.coverTrigger === "true") {
        this.options.coverTrigger = true;
      }

      // Move dropdown-content after dropdown-trigger
      if (!!this.options.container) {
        $(this.options.container).append(this.dropdownEl);
      } else {
        this.$el.after(this.dropdownEl);
      }

      this._makeDropdownFocusable();
      this._resetFilterQueryBound = this._resetFilterQuery.bind(this);
      this._handleDocumentClickBound = this._handleDocumentClick.bind(this);
      this._handleDocumentTouchmoveBound = this._handleDocumentTouchmove.bind(this);
      this._handleDropdownClickBound = this._handleDropdownClick.bind(this);
      this._handleDropdownKeydownBound = this._handleDropdownKeydown.bind(this);
      this._handleTriggerKeydownBound = this._handleTriggerKeydown.bind(this);
      this._setupEventHandlers();
    }

    static get defaults() {
      return _defaults;
    }

    static init(els, options) {
      return super.init(this, els, options);
    }

    /**
     * Get Instance
     */
    static getInstance(el) {
      let domElem = !!el.jquery ? el[0] : el;
      return domElem.M_Dropdown;
    }

    /**
     * Teardown component
     */
    destroy() {
      this._resetDropdownStyles();
      this._removeEventHandlers();
      Dropdown._dropdowns.splice(Dropdown._dropdowns.indexOf(this), 1);
      this.el.M_Dropdown = undefined;
    }

    /**
     * Setup Event Handlers
     */
    _setupEventHandlers() {
      // Trigger keydown handler
      this.el.addEventListener('keydown', this._handleTriggerKeydownBound);

      // Item click handler
      this.dropdownEl.addEventListener('click', this._handleDropdownClickBound);

      // Hover event handlers
      if (this.options.hover) {
        this._handleMouseEnterBound = this._handleMouseEnter.bind(this);
        this.el.addEventListener('mouseenter', this._handleMouseEnterBound);
        this._handleMouseLeaveBound = this._handleMouseLeave.bind(this);
        this.el.addEventListener('mouseleave', this._handleMouseLeaveBound);
        this.dropdownEl.addEventListener('mouseleave', this._handleMouseLeaveBound);

        // Click event handlers
      } else {
        this._handleClickBound = this._handleClick.bind(this);
        this.el.addEventListener('click', this._handleClickBound);
      }
    }

    /**
     * Remove Event Handlers
     */
    _removeEventHandlers() {
      this.el.removeEventListener('keydown', this._handleTriggerKeydownBound);
      this.dropdownEl.removeEventListener('click', this._handleDropdownClickBound);

      if (this.options.hover) {
        this.el.removeEventListener('mouseenter', this._handleMouseEnterBound);
        this.el.removeEventListener('mouseleave', this._handleMouseLeaveBound);
        this.dropdownEl.removeEventListener('mouseleave', this._handleMouseLeaveBound);
      } else {
        this.el.removeEventListener('click', this._handleClickBound);
      }
    }

    _setupTemporaryEventHandlers() {
      // Use capture phase event handler to prevent click
      document.body.addEventListener('click', this._handleDocumentClickBound, true);
      document.body.addEventListener('touchend', this._handleDocumentClickBound);
      document.body.addEventListener('touchmove', this._handleDocumentTouchmoveBound);
      this.dropdownEl.addEventListener('keydown', this._handleDropdownKeydownBound);
    }

    _removeTemporaryEventHandlers() {
      // Use capture phase event handler to prevent click
      document.body.removeEventListener('click', this._handleDocumentClickBound, true);
      document.body.removeEventListener('touchend', this._handleDocumentClickBound);
      document.body.removeEventListener('touchmove', this._handleDocumentTouchmoveBound);
      this.dropdownEl.removeEventListener('keydown', this._handleDropdownKeydownBound);
    }

    _handleClick(e) {
      e.preventDefault();
      this.open();
    }

    _handleMouseEnter() {
      this.open();
    }

    _handleMouseLeave(e) {
      let toEl = e.toElement || e.relatedTarget;
      let leaveToDropdownContent = !!$(toEl).closest('.dropdown-content').length;
      let leaveToActiveDropdownTrigger = false;

      let $closestTrigger = $(toEl).closest('.dropdown-trigger');
      if (
        $closestTrigger.length &&
        !!$closestTrigger[0].M_Dropdown &&
        $closestTrigger[0].M_Dropdown.isOpen
      ) {
        leaveToActiveDropdownTrigger = true;
      }

      // Close hover dropdown if mouse did not leave to either active dropdown-trigger or dropdown-content
      if (!leaveToActiveDropdownTrigger && !leaveToDropdownContent) {
        this.close();
      }
    }

    _handleDocumentClick(e) {
      let $target = $(e.target);
      if (
        this.options.closeOnClick &&
        $target.closest('.dropdown-content').length &&
        !this.isTouchMoving
      ) {
        // isTouchMoving to check if scrolling on mobile.
        setTimeout(() => {
          this.close();
        }, 0);
      } else if (
        $target.closest('.dropdown-trigger').length ||
        !$target.closest('.dropdown-content').length
      ) {
        setTimeout(() => {
          this.close();
        }, 0);
      }
      this.isTouchMoving = false;
    }

    _handleTriggerKeydown(e) {
      // ARROW DOWN OR ENTER WHEN SELECT IS CLOSED - open Dropdown
      if ((e.which === M.keys.ARROW_DOWN || e.which === M.keys.ENTER) && !this.isOpen) {
        e.preventDefault();
        this.open();
      }
    }

    /**
     * Handle Document Touchmove
     * @param {Event} e
     */
    _handleDocumentTouchmove(e) {
      let $target = $(e.target);
      if ($target.closest('.dropdown-content').length) {
        this.isTouchMoving = true;
      }
    }

    /**
     * Handle Dropdown Click
     * @param {Event} e
     */
    _handleDropdownClick(e) {
      // onItemClick callback
      if (typeof this.options.onItemClick === 'function') {
        let itemEl = $(e.target).closest('li')[0];
        this.options.onItemClick.call(this, itemEl);
      }
    }

    /**
     * Handle Dropdown Keydown
     * @param {Event} e
     */
    _handleDropdownKeydown(e) {
      if (e.which === M.keys.TAB) {
        e.preventDefault();
        this.close();

        // Navigate down dropdown list
      } else if ((e.which === M.keys.ARROW_DOWN || e.which === M.keys.ARROW_UP) && this.isOpen) {
        e.preventDefault();
        let direction = e.which === M.keys.ARROW_DOWN ? 1 : -1;
        let newFocusedIndex = this.focusedIndex;
        let foundNewIndex = false;
        do {
          newFocusedIndex = newFocusedIndex + direction;

          if (
            !!this.dropdownEl.children[newFocusedIndex] &&
            this.dropdownEl.children[newFocusedIndex].tabIndex !== -1
          ) {
            foundNewIndex = true;
            break;
          }
        } while (newFocusedIndex < this.dropdownEl.children.length && newFocusedIndex >= 0);

        if (foundNewIndex) {
          this.focusedIndex = newFocusedIndex;
          this._focusFocusedItem();
        }

        // ENTER selects choice on focused item
      } else if (e.which === M.keys.ENTER && this.isOpen) {
        // Search for <a> and <button>
        let focusedElement = this.dropdownEl.children[this.focusedIndex];
        let $activatableElement = $(focusedElement)
          .find('a, button')
          .first();

        // Click a or button tag if exists, otherwise click li tag
        !!$activatableElement.length ? $activatableElement[0].click() : focusedElement.click();

        // Close dropdown on ESC
      } else if (e.which === M.keys.ESC && this.isOpen) {
        e.preventDefault();
        this.close();
      }

      // CASE WHEN USER TYPE LETTERS
      let letter = String.fromCharCode(e.which).toLowerCase(),
        nonLetters = [9, 13, 27, 38, 40];
      if (letter && nonLetters.indexOf(e.which) === -1) {
        this.filterQuery.push(letter);

        let string = this.filterQuery.join(''),
          newOptionEl = $(this.dropdownEl)
            .find('li')
            .filter((el) => {
              return (
                $(el)
                  .text()
                  .toLowerCase()
                  .indexOf(string) === 0
              );
            })[0];

        if (newOptionEl) {
          this.focusedIndex = $(newOptionEl).index();
          this._focusFocusedItem();
        }
      }

      this.filterTimeout = setTimeout(this._resetFilterQueryBound, 1000);
    }

    /**
     * Setup dropdown
     */
    _resetFilterQuery() {
      this.filterQuery = [];
    }

    _resetDropdownStyles() {
      this.$dropdownEl.css({
        display: '',
        width: '',
        height: '',
        left: '',
        top: '',
        'transform-origin': '',
        transform: '',
        opacity: ''
      });
    }

    _makeDropdownFocusable() {
      // Needed for arrow key navigation
      this.dropdownEl.tabIndex = 0;
    }

    _focusFocusedItem() {
      if (
        this.focusedIndex >= 0 &&
        this.focusedIndex < this.dropdownEl.children.length &&
        this.options.autoFocus
      ) {
        this.dropdownEl.children[this.focusedIndex].focus();
      }
    }

    _getDropdownPosition() {
      let offsetParentBRect = this.el.offsetParent.getBoundingClientRect();
      let triggerBRect = this.el.getBoundingClientRect();
      let dropdownBRect = this.dropdownEl.getBoundingClientRect();

      let idealHeight = dropdownBRect.height;
      let idealWidth = dropdownBRect.width;
      let idealXPos = triggerBRect.left - dropdownBRect.left;
      let idealYPos = triggerBRect.top - dropdownBRect.top;

      let dropdownBounds = {
        left: idealXPos,
        top: idealYPos,
        height: idealHeight,
        width: idealWidth
      };

      // Countainer here will be closest ancestor with overflow: hidden
      let closestOverflowParent = this.dropdownEl.offsetParent;
      let alignments = M.checkPossibleAlignments(
        this.el,
        closestOverflowParent,
        dropdownBounds,
        this.options.coverTrigger ? 0 : triggerBRect.height
      );

      let verticalAlignment = 'top';
      let horizontalAlignment = this.options.alignment;
      idealYPos += this.options.coverTrigger ? 0 : triggerBRect.height;

      // Reset isScrollable
      this.isScrollable = false;

      if (!alignments.top) {
        if (alignments.bottom) {
          verticalAlignment = 'bottom';
        } else {
          this.isScrollable = true;

          // Determine which side has most space and cutoff at correct height
          if (alignments.spaceOnTop > alignments.spaceOnBottom) {
            verticalAlignment = 'bottom';
            idealHeight += alignments.spaceOnTop;
            idealYPos -= alignments.spaceOnTop;
          } else {
            idealHeight += alignments.spaceOnBottom;
          }
        }
      }

      // If preferred horizontal alignment is possible
      if (!alignments[horizontalAlignment]) {
        let oppositeAlignment = horizontalAlignment === 'left' ? 'right' : 'left';
        if (alignments[oppositeAlignment]) {
          horizontalAlignment = oppositeAlignment;
        } else {
          // Determine which side has most space and cutoff at correct height
          if (alignments.spaceOnLeft > alignments.spaceOnRight) {
            horizontalAlignment = 'right';
            idealWidth += alignments.spaceOnLeft;
            idealXPos -= alignments.spaceOnLeft;
          } else {
            horizontalAlignment = 'left';
            idealWidth += alignments.spaceOnRight;
          }
        }
      }

      if (verticalAlignment === 'bottom') {
        idealYPos =
          idealYPos - dropdownBRect.height + (this.options.coverTrigger ? triggerBRect.height : 0);
      }
      if (horizontalAlignment === 'right') {
        idealXPos = idealXPos - dropdownBRect.width + triggerBRect.width;
      }
      return {
        x: idealXPos,
        y: idealYPos,
        verticalAlignment: verticalAlignment,
        horizontalAlignment: horizontalAlignment,
        height: idealHeight,
        width: idealWidth
      };
    }

    /**
     * Animate in dropdown
     */
    _animateIn() {
      anim.remove(this.dropdownEl);
      anim({
        targets: this.dropdownEl,
        opacity: {
          value: [0, 1],
          easing: 'easeOutQuad'
        },
        scaleX: [0.3, 1],
        scaleY: [0.3, 1],
        duration: this.options.inDuration,
        easing: 'easeOutQuint',
        complete: (anim) => {
          if (this.options.autoFocus) {
            this.dropdownEl.focus();
          }

          // onOpenEnd callback
          if (typeof this.options.onOpenEnd === 'function') {
            let elem = anim.animatables[0].target;
            this.options.onOpenEnd.call(elem, this.el);
          }
        }
      });
    }

    /**
     * Animate out dropdown
     */
    _animateOut() {
      anim.remove(this.dropdownEl);
      anim({
        targets: this.dropdownEl,
        opacity: {
          value: 0,
          easing: 'easeOutQuint'
        },
        scaleX: 0.3,
        scaleY: 0.3,
        duration: this.options.outDuration,
        easing: 'easeOutQuint',
        complete: (anim) => {
          this._resetDropdownStyles();

          // onCloseEnd callback
          if (typeof this.options.onCloseEnd === 'function') {
            let elem = anim.animatables[0].target;
            this.options.onCloseEnd.call(this, this.el);
          }
        }
      });
    }

    /**
     * Place dropdown
     */
    _placeDropdown() {
      // Set width before calculating positionInfo
      let idealWidth = this.options.constrainWidth
        ? this.el.getBoundingClientRect().width
        : this.dropdownEl.getBoundingClientRect().width;
      this.dropdownEl.style.width = idealWidth + 'px';

      let positionInfo = this._getDropdownPosition();
      this.dropdownEl.style.left = positionInfo.x + 'px';
      this.dropdownEl.style.top = positionInfo.y + 'px';
      this.dropdownEl.style.height = positionInfo.height + 'px';
      this.dropdownEl.style.width = positionInfo.width + 'px';
      this.dropdownEl.style.transformOrigin = `${
        positionInfo.horizontalAlignment === 'left' ? '0' : '100%'
      } ${positionInfo.verticalAlignment === 'top' ? '0' : '100%'}`;
    }

    /**
     * Open Dropdown
     */
    open() {
      if (this.isOpen) {
        return;
      }
      this.isOpen = true;

      // onOpenStart callback
      if (typeof this.options.onOpenStart === 'function') {
        this.options.onOpenStart.call(this, this.el);
      }

      // Reset styles
      this._resetDropdownStyles();
      this.dropdownEl.style.display = 'block';

      this._placeDropdown();
      this._animateIn();
      this._setupTemporaryEventHandlers();
    }

    /**
     * Close Dropdown
     */
    close() {
      if (!this.isOpen) {
        return;
      }
      this.isOpen = false;
      this.focusedIndex = -1;

      // onCloseStart callback
      if (typeof this.options.onCloseStart === 'function') {
        this.options.onCloseStart.call(this, this.el);
      }

      this._animateOut();
      this._removeTemporaryEventHandlers();

      if (this.options.autoFocus) {
        this.el.focus();
      }
    }

    /**
     * Recalculate dimensions
     */
    recalculateDimensions() {
      if (this.isOpen) {
        this.$dropdownEl.css({
          width: '',
          height: '',
          left: '',
          top: '',
          'transform-origin': ''
        });
        this._placeDropdown();
      }
    }
  }

  /**
   * @static
   * @memberof Dropdown
   */
  Dropdown._dropdowns = [];

  window.M.Dropdown = Dropdown;

})(cash, M.anime);

init/init.js:

document.addEventListener('DOMContentLoaded', function() {
    M.Dropdown.init($('.dropdown-trigger'));
});

MaikelGG avatar Jul 30 '18 13:07 MaikelGG

@geosenna were you able to get it figured out? I’m just getting into materialize-css and I wanted to make sure I could do modular imports. For instance, if I just wanted the animations and a couple components, how to go about doing that. Did the suggestion from @maikelGG help?

klcantrell avatar Aug 30 '18 14:08 klcantrell

I didn't try it yet.. I am really relaying on materialize team to do something official about it.

geosigno avatar Aug 30 '18 18:08 geosigno

@geosenna I have done the following and works:

  • In component.js replace the first line with export default class Component {

in your modal file add at the top add: import Component from '../path-to/component';

In the source bundle (application.js in my app) I don't import component.js again, only in the modal or whatever other 'component' you need to import it from there.

Hope this helps.

I've fixed my moduling problem with @MaikelGG 's answer (quoted here). I was having the Component is not defined error.

I'm using Webpack for bundling with babel-loader. I simply copied the component,js and sidenav.js from the node-modules folder to my assets/js folder, and made the changes there.Then it's just importing them in my index.js file. I've used Materialize's Gruntfile for the import order and loaded the equivalent babel plugins in Webpack as seen in their package.json. Sample config from my project:

> index.js:

import "materialize-css/js/cash";
import "./materialize-js/component";
import "materialize-css/js/global";
import "materialize-css/js/anime.min";
import "materialize-css/js/waves";
import "./materialize-js/sidenav";
import "materialize-css/js/forms";

> ./materialize-js/component:

1. export default class Component {

> ./materialize-js/sidenav:

1. import Component from "./component";

> webpack.config.js:

use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"],
            plugins: [
              "@babel/plugin-transform-arrow-functions",
              "@babel/plugin-transform-block-scoping",
              "@babel/plugin-transform-classes",
              "@babel/plugin-transform-template-literals",
              "@babel/plugin-transform-object-super"
            ]
          }
        }

> package.json (fyi)

"devDependencies": {
    "@babel/core": "^7.1.5",
    "@babel/preset-env": "^7.1.5",
    "babel-loader": "^8.0.4",
    "css-loader": "^1.0.1",
    "materialize-css": "^1.0.0",
    "mini-css-extract-plugin": "^0.4.4",
    "node-sass": "^4.10.0",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.0",
    "webpack": "^4.25.1",
    "webpack-cli": "^3.1.2"
  }

Haven't encountered any errors so far. I hope my input is useful! Cheers ~

cristovaov avatar Nov 17 '18 17:11 cristovaov

Thanks. Finally i use a fork where I remove the component I don't use from the gruntfile source when compiling the JS.

geosigno avatar Dec 03 '18 08:12 geosigno

@geosigno Can you share the fork you're using? I'm able to follow @cristovaov's suggested modifications, but have to update all the components that I'm using similar to sidenav.js.

hishammalik avatar May 27 '19 01:05 hishammalik

https://github.com/geosigno/materialize

geosigno avatar May 27 '19 05:05 geosigno

To make tooltip work, with ember octane:

  app.import('node_modules/materialize-css/js/cash.js');
  app.import('node_modules/materialize-css/js/component.js');
  app.import('node_modules/materialize-css/js/global.js');
  app.import('node_modules/materialize-css/js/tooltip.js');

brunoocasali avatar Jun 20 '19 19:06 brunoocasali

This is what I ended up doing:

import {
    Modal,
    Dropdown
} from 'materialize-css';

document.addEventListener('DOMContentLoaded', function () {
    var modals = M.Modal.init(document.querySelectorAll('.modal'), {});
    var dropdowns = M.Dropdown.init(document.querySelectorAll('.dropdown-trigger'), {});
});

Import the components you want to use and initialize them individually.

Hope it helps!

bruno-fernandes avatar Sep 18 '19 16:09 bruno-fernandes

@bruno-fernandes this, from my testing, imports the whole lib. I only imported Cash, Component and Global and all functions of the lib are working.

ChildishGiant avatar Nov 14 '19 17:11 ChildishGiant

do you have remove all the not used components from sass? how we can import only components needed in sass files?

joneldiablo avatar Mar 09 '20 17:03 joneldiablo

do you have remove all the not used components from sass? how we can import only components needed in sass files?

I just copy the main sass file from materialize, update the paths and then tag out the components I don't need.

eboye avatar Mar 09 '20 21:03 eboye

you have config the theming variables and filter out the imports on materialize.scss file? ok thank you!! =D

joneldiablo avatar Mar 09 '20 22:03 joneldiablo

you have config the theming variables and filter out the imports on materialize.scss file? ok thank you!! =D

Yup, something like this:

@charset "UTF-8";

// Color
//@import "~materialize-css/sass/components/color-variables";
@import "colors";
@import "~materialize-css/sass/components/color-classes";
// Variables;
//@import "~materialize-css/sass/components/variables";
@import "vars";
// Reset
@import "~materialize-css/sass/components/normalize";
// components
@import "~materialize-css/sass/components/global";
//@import "~materialize-css/sass/components/badges";
//@import "~materialize-css/sass/components/icons-material-design";
//@import "~materialize-css/sass/components/grid";
@import "grid";
@import "~materialize-css/sass/components/navbar";
@import "~materialize-css/sass/components/typography";
//@import "~materialize-css/sass/components/transitions";
@import "~materialize-css/sass/components/cards";
//@import "~materialize-css/sass/components/toast";
//@import "~materialize-css/sass/components/tabs";
//@import "~materialize-css/sass/components/tooltip";
@import "~materialize-css/sass/components/buttons";
@import "~materialize-css/sass/components/dropdown";
@import "~materialize-css/sass/components/waves";
//@import "~materialize-css/sass/components/modal";
//@import "~materialize-css/sass/components/collapsible";
//@import "~materialize-css/sass/components/chips";
//@import "~materialize-css/sass/components/materialbox";
//@import "~materialize-css/sass/components/forms/forms";
//@import "~materialize-css/sass/components/table_of_contents";
//@import "~materialize-css/sass/components/sidenav";
//@import "~materialize-css/sass/components/preloader";
//@import "~materialize-css/sass/components/slider";
//@import "~materialize-css/sass/components/carousel";
//@import "~materialize-css/sass/components/tapTarget";
//@import "~materialize-css/sass/components/pulse";
//@import "~materialize-css/sass/components/datepicker";
//@import "~materialize-css/sass/components/timepicker";

And I override some files by tagging them, copying it's contents and placing in new file and then make changes.

eboye avatar Mar 10 '20 04:03 eboye

For those who want to import "modules", without forking/copypasting original classes. As mentioned here: https://github.com/Dogfalo/materialize/issues/5958#issuecomment-397577854 all JS imports are working except class Component. Webpack is just excluding it from compilation

As a workaround, you can use ProvidePlugin and exports-loader plugin to fix it. In webpack configuration add the following plugin usage:

plugins: [new webpack.ProvidePlugin({Component: 'exports-loader?Component!materialize-css/js/component.js'})]

(if you want to read about the plugin, please check https://webpack.js.org/guides/shimming/#shimming-globals)

Works perfectly for me, and Component class is exported automatically everywhere.

bugy avatar Apr 04 '20 12:04 bugy

With exports-loader being updated, meaning this needs updating again, is there any chance this'll ever get fixed?

ChildishGiant avatar Jun 17 '20 13:06 ChildishGiant

@geosenna I have done the following and works:

  • In component.js replace the first line with export default class Component {

in your modal file add at the top add: import Component from '../path-to/component'; In the source bundle (application.js in my app) I don't import component.js again, only in the modal or whatever other 'component' you need to import it from there. Hope this helps.

I've fixed my moduling problem with @MaikelGG 's answer (quoted here). I was having the Component is not defined error.

I'm using Webpack for bundling with babel-loader. I simply copied the component,js and sidenav.js from the node-modules folder to my assets/js folder, and made the changes there.Then it's just importing them in my index.js file. I've used Materialize's Gruntfile for the import order and loaded the equivalent babel plugins in Webpack as seen in their package.json. Sample config from my project:

> index.js:

import "materialize-css/js/cash";
import "./materialize-js/component";
import "materialize-css/js/global";
import "materialize-css/js/anime.min";
import "materialize-css/js/waves";
import "./materialize-js/sidenav";
import "materialize-css/js/forms";

> ./materialize-js/component:

1. export default class Component {

> ./materialize-js/sidenav:

1. import Component from "./component";

> webpack.config.js:

use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"],
            plugins: [
              "@babel/plugin-transform-arrow-functions",
              "@babel/plugin-transform-block-scoping",
              "@babel/plugin-transform-classes",
              "@babel/plugin-transform-template-literals",
              "@babel/plugin-transform-object-super"
            ]
          }
        }

> package.json (fyi)

"devDependencies": {
    "@babel/core": "^7.1.5",
    "@babel/preset-env": "^7.1.5",
    "babel-loader": "^8.0.4",
    "css-loader": "^1.0.1",
    "materialize-css": "^1.0.0",
    "mini-css-extract-plugin": "^0.4.4",
    "node-sass": "^4.10.0",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.0",
    "webpack": "^4.25.1",
    "webpack-cli": "^3.1.2"
  }

Haven't encountered any errors so far. I hope my input is useful! Cheers ~

This worked for me, but I had to make the following adjustments for working with Nextjs serverside react framework. There were issues with when things were loading in, server side vs client side.

// require instead of import (have to do this often for things that need window) if (typeof window !== 'undefined') { var cash = require ("materialize-css/js/cash"); var Component = require("../materialize/js/component.js"); var M = require("materialize-css/js/global"); var S = require('../materialize/js/sidenav'); var C = require('../materialize/js/collapsible.js'); var sidenav = document.querySelectorAll('.sidenav'); var sinstances = M.Sidenav.init(sidenav, {}); var collapisable = document.querySelectorAll('.collapsible'); var cinstances = M.Collapsible.init(collapisable, {}); } //anime.min.js had to be imported in components like sidenav.js and collapisble.js import Component from "./component"; import "./anime.min.js"; (function($, anim) {

tlchatt avatar Dec 31 '20 20:12 tlchatt

@ChildishGiant for the exports-loader 1.1.1 I believe it should look like this:

plugins: [new webpack.ProvidePlugin({Component: 'exports-loader?type=commonjs&exports=single Component!materialize-css/js/component.js'})]

Unfortunately, I cannot test it, because I switched to vue-cli and not managing webpack plugins anymore

And actually there is another solution, without using the ProvidePlugin: before importing any materialize.js code, you could write

import Component from 'exports-loader?exports=default Component!materialize-css/js/component.js'
global.Component = Component;

bugy avatar Jan 11 '21 20:01 bugy