DataTables API undefined with Symfony Webpack Encore - javascript

I'm using Webpack Encore with Symfony 3.4 (part of migration to Symfony 4).
I have Datatables (installed via NPM to node_modules) working with jQuery but the api functions such as .columns are returning: .column is not a function at
Package Versions:
jQuery 2.14.4
Datatables 1.10.19
Webpack Encore 0.27.0
Webpack app.js:
global.$ = global.jQuery = require('jquery');
require('bootstrap');
global.moment = require('moment');
require('datatables.net-dt');
$.fn.dataTable = $.fn.DataTable = global.DataTable = require('datatables.net');
webpack.config.js:
var Encore = require('#symfony/webpack-encore');
Encore
// directory where compiled assets will be stored
.setOutputPath('code/web/build/')
// public path used by the web server to access the output path
.setPublicPath('/build')
// only needed for CDN's or sub-directory deploy
//.setManifestKeyPrefix('build/')
.addEntry('site', './assets/js/site/app.js')
// will require an extra script tag for runtime.js
// but, you probably want this, unless you're building a single-page app
.enableSingleRuntimeChunk()
.cleanupOutputBeforeBuild()
.enableSourceMaps(!Encore.isProduction())
// enables hashed filenames (e.g. app.abc123.css)
.enableVersioning(Encore.isProduction())
// uncomment if you use TypeScript
//.enableTypeScriptLoader()
// uncomment if you use Sass/SCSS files
//.enableSassLoader()
// uncomment if you're having problems with a jQuery plugin
//.autoProvidejQuery()
;
module.exports = Encore.getWebpackConfig();
Javascript example in template.html.twig (extends base html file):
{{ encore_entry_script_tags('site') }}
<script type="text/javascript">
$(document).ready(function() {
var $dtable;
$dtable = $('#simpleTable')
.DataTable({
data: data,
deferRender: true,
scrollX: false,
searching: true,
paging: true,
pageLength: 25});
console.log($dtable);
// Error occurs here
var column = $dtable.column(index);
});
</script>
A console log of $dtable immediately after instantiation outputs the following which seems to confirm an Api instance isn't created?
Is it possible this is related to the DataTable loader that uses the AMD method due to Webpack?
jquery.dataTables.js:
(function( factory ) {
"use strict";
if ( typeof define === 'function' && define.amd ) {
define( ['jquery'], function ( $ ) {
return factory( $, window, document );
} );
}
else if ( typeof exports === 'object' ) {
module.exports = function (root, $) {
if ( ! root ) {
// CommonJS environments without a window global must pass a
// root. This will give an error otherwise
root = window;
}
if ( ! $ ) {
$ = typeof window !== 'undefined' ? // jQuery's factory checks for a global window
require('jquery') :
require('jquery')( root );
}
return factory( $, root, root.document );
};
}
else {
factory( jQuery, window, document );
}
}

The AMD loader may be a problem here,
Can you try to disable AMD Loader it to see if it work :
var config = Encore.getWebpackConfig();
config.module.rules.unshift({
parser: {
amd: false,
}
});
module.exports = config;
Otherwise you could try to make it work with AMD Loader :
First install DataTables :
npm install datatables.net
Then DataTables style (bootstrap) :
npm install datatables.net-bs
Then the imports-loader plugin
Modify your webpack.config.js to make an exception for datatables:
module: {
loaders: [
{
test: /datatables\.net.*/,
loader: 'imports?define=>false'
}
]
}
Now you should be able to use DataTables :
import 'datatables.net';
import dt from 'datatables.net-bs';
dt(window, $);
//And now use it
$('#exampleDatatable').DataTable();

The issue I see is with line:
$.fn.dataTable = $.fn.DataTable = global.DataTable = require('datatables.net');
dataTable() and DataTable() are not equivalent.
The correct functions will be added to Jquery by the factory, no need to set these. Actually, if I can guess, the above code will overwrite DataTable() with dataTable().
Change to:
require('datatables.net');
It should work.

Maybe it's a stupid idea ... but what is the data value in your template-defined call $('#simpleTable').DataTable({data:data, ...}) call?
According to docs (3rd paragraph) setting the data option (even with an empty value) will override the data from the table. If your value data is undefined, this might turn your data-rich table into a vegetable in the eyes of DataTable.
I assume that your table is fine, the module is loaded fine, ... so the data might be the problem ...
If, however, it does contain useful data, would you mind posting some example of it? ;o)

The DataTables documentation for using with NPM show that the export is a function that returns the DataTable API.
var dt = require( 'datatables.net' )(); // N.b. the extra parenthesis.
So it's likely you'd need to alter your global assignment; something like this might work:
$.fn.dataTable = $.fn.DataTable = global.DataTable = require('datatables.net')(); // N.b. the extra parentheses.

Related

Load bootstrap JS with esbuild

I'm trying to import the bootstrap JS in my esbuild build:
// index.js
import 'jquery-import'
import 'bootstrap'
// jquery-import.js
import jquery from 'jquery'
window.jQuery = window.$ = jquery;
The build target is esm. The problem is that the following bootstrap code is run:
factory(exports, require('jquery'), require('popper.js'));
This does not use my global window.jQuery but rather requires its own, which means that my global window.$.fn doesn't include the Bootstrap extensions. Also, I think that this makes jQuery load twice.
Is there a way of fixing this without adapting the bootstrap source code?
A little more background on this:
The bootstrap source code begins with the following statements (lightly adapted for better readability):
(function (global, factory) {
if (typeof exports === 'object' && typeof module !== 'undefined') {
// Option 1: The route taken by esbuild
factory(exports, require('jquery'), require('popper.js'));
} else if (typeof define === 'function' && define.amd) {
// Option 2
define(['exports', 'jquery', 'popper.js'], factory);
} else {
// Option 2
(global = global || self, factory(global.bootstrap = {}, global.jQuery, global.Popper));
}
}(this, function (exports, $, Popper) { 'use strict';
I have tried commenting out individual options, and neither 1, 2 or 3 work by just taking the already globally defined window.jQuery.
Many thanks for your help!
The reason it's not working, is for some reason your esbuild is trying to import a UMD file (typically used for old Node and browser hybrid code).
Having looked at bootstrap's package.json, it seems that "module": "dist/js/bootstrap.js", which exports bootstrap as modules, which is probably what esbuild should be referencing.
So to fix this without fixing the esbuild config, I'd conjecture you could try:
import 'bootstrap/dist/js/bootstrap'
...or to fix it with esbuild config, add this:
const buildOpts = {
mainFields: ['module', 'main'],
}
This setting tells the bundler to prioritises module, then main entrypoints.
Normally this is module before main, but it may be the other way round because you set platform to "node".

module.exports not available on dependency (import)

Recently upgraded a project to Gatsby 3 whose dependency is Webpack 5. In one of the .tsx classes, an import to the library countdown is done. The import returns an empty object every time, {}.
Looking at the code from "countdown" library I see they export the module like this:
/*global window */
var module;
var countdown = (
function(module) {
'use strict';
...
if (module && module.exports) {
module.exports = countdown;
} else if (typeof window.define === 'function' && typeof window.define.amd !== 'undefined') {
window.define('countdown', [], function() {
return countdown;
});
}
return countdown;
})(module);
Using console.log inside the library I see that module.exports is actually undefined, it seems that var module; is overriding whatever value Node makes available when the import is called. To test the hypothesis out I removed the var module; and removed it as an argument to the function, and everything worked. Of course, that's not the answer to my problem since this is a dependency library, I have no right to touch its code.
I can't figure out what the upgrade to Webpack 5 could have broken to not make module.exports available to countdown.js even though they declare the variable.
I looked at webpack updates and tried to tell it to use commonjs or import-loader on countdown.js and pass it "module" (different test cases have been commented out):
module: {
rules: [
{
test: /\countdown.js?$/,
use: {
loader: 'babel-loader',
},
// loader: "imports-loader",
// options: {
// syntax: "default",
// type: "commonjs"
// },
// use: [
// {
// loader: "imports-loader",
// options: {
// thisArg: "module"
// },
// },
// ],
},
],
}
None of that work. I can't quite figure out what changed in Webpack 5 to cause countdown's way of exporting the library to break.
Any ideas?
To make it clear: the library countdown is not mine, and when using previous webpack version it worked great when importing and using it.
Turns out the package has been updated but version update hasn't been propagated. There's a request soliciting that.
In the meantime a colleague had a solution, thought I'd share it here in case anyone can use it until version is updated. In summary it modifies the script from the library after installation is done, it removes the declared global variable that is overriding the one that Node is supposed to pass.
In a file called yarn-postinstall.js (or whatever you please):
fixCountdownLib();
function fixCountdownLib() {
const fs = require("fs");
const countdownLibPath = __dirname + "/node_modules/countdown/countdown.js";
const countdownLibCode = fs.readFileSync(countdownLibPath).toString();
const fixedCountdownLibCode = countdownLibCode.replace("var module;", "");
fs.writeFileSync(countdownLibPath, fixedCountdownLibCode);
}
Then update package.json scripts to point to that file.
"scripts": {
"postinstall": "node yarn-postinstall.js"
},
The module variable isn't an object, and doesn't have a property exports.
Changing var module; to var module = {exports: {}} should work. But I'm not sure you should be checking for module.exports, since it's supposed to be undefined anyways, I guess?

How to properly load local AMD modules with jspm/system.js

I am having a really hard time using local AMD modules in an Aurelia project that uses es6, JSPM, and system.js. I am hoping someone out there can help me configure my project to enable me to import/load my AMD modules and use them in my project. The local AMD style modules are in a format similar to the following:
define
(['require',
'lib/alerts/STARTSTOP',
'lib/alerts/STOPPED',
...
],
function( require, STARTSTOP, STOPPED, ... ) {
return {
alert: function( data ) {
var type = data.type;
var ev = data.event;
var cls = require( 'lib/alerts/' + ev );
return new cls( data );
}
};
});
When I try to import/load this module into an es6 module I am running into the following error: Uncaught TypeError: Unexpected anonymous AMD define.. I get this error when trying to load the module in either of the following ways:
System.import('lib/alerts/AlertFactory').then( (m) => {
console.log( m );
});
or
import AlertFactory from 'lib/alerts/AlertFactory.js';
I have also made sure to add the following script to my index.html:
<script>
window.define = System.amdDefine;
window.require = window.requirejs = System.amdRequire;
</script>
In addition to the above I have also added a meta format property to my config.js file, but that hasn't seemed to help either.
meta: {
...
"lib/alerts/*.js": {
"format": "amd"
}
}
Does anyone have any ideas on why I am running into the error I am seeing and how to properly load my modules? I appreciate any help/insight you can offer.
UPDATE
I finally realized that the main issue here is that I'm trying to use existing AMD modules in and Aurelia project, and the default Aurelia gulp build assumes that all code is written in ES6 and not mixed with AMD. That's why I'm having issues. Vanilla jspm/system.js handle a mix of module formats, but Aurelia does not out of the box.
Just put your AMD modules out of src so babel will not be able to transpile it. Here is working solution I use to import jquery modules:
First, I have local_packages folder in project root and I have jquery module local_packages/somelib/js/mymodule.js
Then in config.js
paths: {
...
"local/*": "local_packages/*",
}
map: {
...
"somelib": "local/somelib",
"somelib1": "/local_packages/somelib1",
}
And finally my import looks like: import 'somelib/js/mymodule';

requirejs dynamically calling custom modules

I'm trying to dynamically load custom modules based on a data attribute. It's almost working perfectly but for some reasons my module is called twice with different paths and I can't figure why.
My project structure looks like that:
+ Assets
+ js
• main.js
+ libs
+ modules
• mod.env.js
• mod.utils.js
• mod.session.js
+ plugins
+ views
• signup.js
• login.js
• home.js
In my main.js file I have some basic configuration:
require.config({
baseUrl: '/Assets/js',
paths: {
// Libs
'jquery' : 'libs/jquery/jquery-2.0.3.min',
// Module for the project
'env': 'modules/atmco.env',
'utils': 'modules/atmco.utils',
'session': 'modules/atmco.session'
}
});
Still in the main.js file is where I put the logic for the conditial loading of the modules:
require(['require', 'jquery','env'],
function ( require, $, env ) {
'use strict';
function init() {
// Grab the modules/pages on the data attribute of the body
var modules = $('body').data('modules') || '';
var pages = $('body').data('page') || '';
// Initialize the environment stuff for your project
env.initEnv();
if ( pages ) {
require(['./views/' + pages.toLowerCase().split(/\s*,\s*/)[0]]);
}
}
// Initialize the application when the dom is ready
$(function () {
init();
});
}
);
My page has the right attributes (<body data-page="Signup" data-module="">) but for some reasons requirejs tries to call 2 different files:
The custom module is called as expected
"/Assets/js/views/signup.js"
Then it tries to call "/Assets/js/signup.js" which doesn't exists
Finally, here's a look at how I define my custom module, including the custom name. It seems pretty basic:
define('view/signup',
['utils'],
function ( utils ) {
console.log('my module' + utils.lang );
return {
};
}
);
If anyone could point me to my mistake it would really help me with my app and understanding better how requirejs works. Thanks a lot!
Found the solution:
Naming (or naming in fact) the module was actually messing the module definition. Therefor I just needed to adjust the code to:
define(
['utils'],
function ( utils ) {
console.log('my module' + utils.lang );
return {
};
}
);
This question really helped me out figuring it:
Requirejs function in define not executed

Using private jquery with RequireJS - issue after optimisation

I'm putting together a framework using requireJS with a CDN version of jQuery (as is now the recommended approach) and having some issue when optimizing the code. The output is namespaced and I'm specifying that each module use a private version of jquery as outlined in the documentation:
require.config({
// Add this map config in addition to any baseUrl or
// paths config you may already have in the project.
map: {
// '*' means all modules will get 'jquery-private'
// for their 'jquery' dependency.
'*': { 'jquery': 'jquery-private' },
// 'jquery-private' wants the real jQuery module
// though. If this line was not here, there would
// be an unresolvable cyclic dependency.
'jquery-private': { 'jquery': 'jquery' }
}
});
// and the 'jquery-private' module, in the
// jquery-private.js file:
define(['jquery'], function (jq) {
return jq.noConflict( true );
});
The problem I'm seeing after optimization is that "jq" is undefined in the "jquery-private.js" file.
Any ideas? I've tried setting jq = $ but that seems to destroy the global.
Thanks.
Here is what I did to get the jQuery CDN & optimization sample linked from the RequireJS jQuery Instructions page to work with the Mapping Modules to use noConflict section that you pasted in your original question.
1 - Forked the sample
2 - Created file www/js/lib/jquery-private.js with this content
define(['jquery'], function (jq) {
return jq.noConflict( true );
});
3 - Modified www/js/app.js to paste the map section so the require.config now looks like this:
requirejs.config({
"baseUrl": "js/lib",
"paths": {
"app": "../app",
"jquery": "//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min"
},
map: {
'*': { 'jquery': 'jquery-private' },
'jquery-private': { 'jquery': 'jquery' }
}
});
4 - Modified www/js/app/main.js to use jqlocal instead of $ (just to prove to myself that it's not the global jQuery:
define(["jquery", "jquery.alpha", "jquery.beta"], function(jqlocal) {
jqlocal(function() {
jqlocal('body').alpha().beta();
});
});
5 - Changed to the tools folder and ran:
node r.js -o build.js
6 - Changed to the www-build folder that was created and ran servedir (doesn't really matter what web server but that's what I use for dev)
7 - Browsed to the local address & port number of the app (in my case http://localhost:8000/app.html) and saw:
Alpha is Go!
Beta is Go!
You can see the end result here
To get this working I changed the way I was using Require (possibly how I should have been doing it all along). This information might prove useful to others, so I thought I'd put it out there.
Previously I was specifying any dependencies in the defined module:
define( [ "dep1", "dep2", "jquery" ], function( var1, var2, jq ) {
This worked fine initially, but failed when optimized. I moved the dependencies to the require function call including this module and it then started to work OK both pre and post optimisation, with jquery being used privately:
require( [ 'jquery', 'dep1', 'dep2' ], function( jq, var1, var2 ) {
formValidator.formValidationInit( jq( el ) );
});
I wouldn't have thought this would have made a difference, but it seemed too.
It is also worth noting that I had to change the jquery-private file as it was still throwing up an issue concerning "jq" not being defined. I am now setting jq equal to the global $ and returning it so it can be used pivately:
define(['jquery'], function () {
var jq = $;
return jq.noConflict( true );
});

Categories

Resources