Struggling with Javascript and esbuild (Rails 7)

I am trying to use Datatables with Rails 7 and esbuild. For that I need to

  1. Load datatables.net-bs5.js to make the Javascript constructor Datatable available
  2. Run some Javascript in the view to call the Datatable constructor.

But practically it does not work: the js is loaded but the constructor is only available in application.js, not within index.html.erb. Any idea what to do?

In application.js

import * as bootstrap from "bootstrap"
import DataTable from "datatables.net-bs5"

In index.html.erb (from DataTables example - Non-jQuery initialisation)

<script>
  document.addEventListener('DOMContentLoaded', function () {
    let table = new DataTable('#example');
  });
</script>

But every time I get the following error message

Uncaught ReferenceError: DataTable is not defined

at HTMLDocument. ((index):50:17)

Knowing that the javascript seems properly loaded and that I donā€™t get any error message in the rails console.

Untitled

Just to complete this, in Rails 6 with webpack, the way of working was to modify the environment.js file from the webpack config to make it available in the view.

environment.config.merge({
  output: {
    library: ['Packs', '[name]'],
    libraryTarget: 'window',
  }
})

Classes/functions in bundled packages are usually not made available in the global namespace. The ā€œRails Wayā€ to solve this is to use a Stimulus controller:

import { Controller } from '@hotwired/stimulus'

export default class extends Controller {
  connect () {
    new DataTable('#example');
  }
}

Thank you but Iā€™m not sure to understand.

You mean that my index.html.erb should contain the following?

<script>
 import { Controller } from '@hotwired/stimulus'
 
 export default class extends Controller {
   connect () {
        document.addEventListener('DOMContentLoaded', function () {
        let table = new DataTable('#example');
     });
   }
 }
</script>

No. When you are using a bundler like esbuild/webpack you cannot use classes/functions from node packages in your HTML. They simply are not available there. They are only available for code running inside your application.js file.

To solve this problem, Rails recommends the first party library Stimulus. In a nutshell, it is designed to replace eventListeners that run on DOMContentLoaded, onclick and other such events. It also will take you less than 1 hour to go through the guidebook I linked and learn it. After you go through the install process you are going to create a file called datable_controller.js and place the code I gave you there.

Then on the page you need data table, you will tell Stimulus to execute the code in that file:

<div id="dataTable" data-controller="data_table">
  <!-- HTML of the data table goes here -->
</div>

That said, while this is the ā€œRails Wayā€ and will make your life easier as you add more JS code (which Iā€™m thinking you will, since you are using a bundler like esbuild), you can ā€˜cheatā€™ and just tell esbuild to make DataTable available for you in your html. Chris from GoRails has a video for jQuery, that you can simple adjust for DataTable:

1 Like

I worked a bit with Simulus but Iā€™m not yet familiar with itā€¦ will go deeper. Iā€™m not sure it answers my question as my target is to put the javascript in the .html.erb.

I went through this video, but the solution recommended is quite bad in my opinion: it consists in putting all the Javascript in the assets/javascript folder (application.js and ./src), which is very poor practice in the case of a big project.

Indeed, since every table needs to be initialized and customized individually (define which column has to be sortable, what is the data type, the headerā€¦) it would mean that the javascript folder would contain the initializer for maybe 50 different tables.

Actually, the part of the video I was referring to, is where he sets jquery in the window. Basically, somewhere in your application.js do this:

import DataTable from "datatables.net-bs5"
window.DataTable = DataTable

This should make DataTable available in your HTML.

Sadly it does not work :frowning:

application.js

import "@hotwired/turbo-rails"
import "./controllers"
import * as bootstrap from "bootstrap"
import DataTable from "datatables.net-bs5"

window.Datatable = Datatable

Just to complete a bit, window.DataTable makes DataTable function available in the view. :slight_smile:

ʒ (root, $) {
            if (!root) {
              root = window;
            }
            if (!$ || !$.fn.dataTable) {
              $ = require_jquery_dataTables()(root, $).$;
            }
      ā€¦

BUT, it just doesnā€™t do anything. I guess this means I need to export other variables through window to make it work.

I have set up a controller that initializes DataTables. This was in Stimulus 1/Rails 5.2, using DataTables as a jquery plugin. With Stimulus 3 I think there are better ways to pass data from the view to the controller.

Also, since this is an old project that used DataTables long before Stimulus was released, Iā€™m still using DataTables as a jQuery plugin. So youā€™ll need to adapt the controller to what Stimulus 3 expects, and change the DataTables init from jquery plugin style to something like what you have listed above.

table_controller.js

import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "table", "row", "modal" ]

  connect() {
    // console.log("table controller connected");
    
    this.dt = null; // datatables instance can be plugged in

    const controllerName = `stimulus_${this.identifier}`;
    this.modalID = `${controllerName}_modal`;
    
    if (this.data.get('lazyload') !== null) {
      this.lazyload = new LazyLoad({ elements_selector: `.${this.data.get('lazyload')}` });
    }
    
    const event = new CustomEvent("table-controller-connected", {detail: {element: this.element, table: this.tableTarget}});
    // fire connected event later so controller can finish connecting events
    window.requestAnimationFrame(function() {
      window.dispatchEvent(event);
    });
  }
  
  // actions
  setupDataTable(event) {
    if (this.dt == null) {
      // DataTables is a jquery function
      this.dt = $(this.tableTarget).DataTable(event.detail.dt_options);
      // console.log("DataTable set up")
      if (this.lazyload) {
        this.lazyload.update();
      }
    }
  }
  
  replaceAllRows(event) {
    if (this.dt == null) { return; }
    this.dt.clear();
    this.dt.rows.add(event.detail.rows).draw();
    if (this.lazyload) {
      this.lazyload.update();
    }
  }
}

The controller also does other things like triggering handling click events to open an edit form in a bootstrap modal, and updating the row after updating the data, but I didnā€™t include that here. The setupDataTable() and replaceAllRows() functions show how you can interact with your datatable variable in the controller.

index.html.erb

<div class="table-responsive" data-controller="table" data-table-lazyload="lazy" data-action="setup-dt->table#setupDataTable">
	<table class='table table-condensed table-striped table-hover clickable-rows' id='pics_table' 
		data-target="table.table" data-action="click->table#click replace-rows->table#replaceAllRows">
		<thead>
			<tr>
				<th>Name</th>
				<th>Status</th>
				<th width="150px">Image</th>
			</tr>
		</thead>
	</table>
</div>

<script>
	
	window.addEventListener("table-controller-connected", function(event){
		console.log("table controller connected event");
		const element = event.detail.element;
		
		const dt_options = {
			lengthMenu: [ [-1, 50, 200], ['All', '50', '200'] ],
			infoCallback: function( settings, start, end, max, total, pre ) { return start +" to "+ end +" of "+ total +" thing"+ (total==1?'':'s') +" found"; },
		  fixedHeader: { headerOffset: 50 }, 
			data: rows, 
			columns: [
				{ data: 'name', render: {_: 'display', sort: 'data', filter: 'data'} }, 
				{ data: 'status', render: {_: 'display', sort: 'data', filter: 'data'} }, 
				{ data: 'image' }, 
			], 
		}
		
    const dtEvt = new CustomEvent("setup-dt", {detail: {dt_options: dt_options}});
		element.dispatchEvent(dtEvt);
	}, false);
	
	const rows = <%= render(partial: 'rows', formats: [:json]).html_safe %>;
	
</script>

Another place I use this datatable with an empty dataset; a search form populates the table with an ajax request:

  fetch(url).then(response => response.json())
    .then(data => {
  	  // console.log(data);
      const dtEvt = new CustomEvent("replace-rows", {detail: {rows: data.houses}});
      const table = document.querySelector('#pics_table');
      table.dispatchEvent(dtEvt);
    }
  )

Obviously you can use this concept with html table rows as well, but you may have to work with html for row updates too.

So the initialization process Iā€™m using is:

  1. Controller connect() runs, fires custom event
  2. View responds to custom event, builds DataTable init options object, fires custom event to trigger controller action
  3. Controller action setupDataTable() initializes DataTable instance with view-specific options.

With Stimulus 3 you could use values to avoid this event dance if you wanted. You could then use the various values youā€™d provide in the markup to build a DataTables options object in the controllerā€™s connect() function and instantiate the DataTables instance.

Thanks for sharing thisā€¦ I had a look at it and tried similar with Stimulus, but basically, it does not solve anything. The challenge is not the integration of Datatables with the .erb, I do it. The challenges comes from jsbundling, which is designed to hide javascript from the view.

For info, the creator of datatables helped me on this. The solution is here

GitHub - Vorkosigan76/rails7-datatables2

Basically

  • there were typos in my html (shame on me)
  • I (strangely) needed to add parenthesis when making the DataTable available through window
1 Like

The javascript libraries are not hidden from the Stimulus controllers, though. So what that controller does is access DataTables only in the controller, while still allowing you to specify the table-specific configuration options in the view.

I have several listings Iā€™m handling similarly, with a click on the table row to open the edit form in a bootstrap modal, and updating the edited row in the table. So the controller also allows you to extract that functionality as well.

Thanks for the link to the sample database. I have a project Iā€™d like to move away from webpack and I have some listings using the controller and some listings using DataTables from the view so itā€™s useful to know how to get it assigned to the global window namespace.

1 Like

I#m using mostly columnDefs ā€¦, i.e.

columnDefs: { [ { "targets": "nosort", "orderable": false },
                         { "targets": "notvisible", "visible": false } ] }

ā€œnosortā€ and ā€œnotvisibleā€ are simple css classes. There is no need for 50 stimulus controllers. Iā€™m controlling some (not all) of the datatables behavior with plain css.

https://datatables.net/reference/option/columnDefs

Iā€™m not sure to understand. How would you define using CSS and a generic controller a table with

let table = new DataTable('#myTable', {
        "lengthChange":   false,
        "pageLength":     10,
        "lengthChange": false,
        stateSave: true,
        "searching":      false,
        "info":           false,
      "columnDefs": [
        { "orderable": false, targets: [1, 2, 3, 4] },
        { className: 'text-center', targets: [1, 2, 3, 4] }
      ],
        language:{
          url: '<%= url_for datatables_datatable_i18n_path %>'
        },
        "stripeClasses": [],
    })

From the datatables.net documentation columnDefs.targets can be one of the following:

  • A string - class name will be matched on the TH for the column (without a leading .)
<table>
  <thead>
  <th class="text-center nosort"></th>
  <th class="text-center nosort"></th>
  <th class="...."></th>
</thead>
<tbody>
...
</tbody>

Place the class in <th> and use the class in columDefs as target to control datatables properties. Alternative you can set datatables options with html5-data-attributes like

<th data-class-name='text-center'></th>

see Options

I can provide a rails example with a generic stimulus controller for datatables may be next weekend, if that helps. Generally I have 3 types of tables: a standard table with buttons, simple table without buttons and a table with buttons and fetching data remote via ajax. All should fit in one stimulus controller with some switches. If one table needs an additional option, i can use html5-data-attributes to add something specific for one table.

Indeed, I should give it a try. It would make my table headers look ugly but my Javascript code would disappear from the ERB

Hi @swobspace I would be curious to see your code, both the HTML side and the Stimulus controllerā€¦ I tried to make it work but without success.

  1. I dont get error message but basic initialization fails

  2. I get error messages when I pass some configuration parameters to the DataTable constructor

dataTables.bootstrap5.js:27 Uncaught TypeError: Cannot read properties of undefined (reading ā€˜dataTableā€™) at new module.exports (dataTables.bootstrap5.js:27:23) at HTMLDocument. (datatables_controller.js:12:19)

Hi @Cedric_Lefebvre, just finished my example this minute :wink: Look at GitHub - swobspace/rails-playground: Rails playground for testing stimulus, javascript and new rails features. It contains no static page, I always want to test ajax remote calls. It has a single stimulus controller for datatables used in 3 scenarios:

  • full example with Buttons, ColumnVisibilty, etc.
  • simpler table without buttons
  • remote table fetching data via ajax (i.e. server side processing).

Example with buttons, standard view:

<div data-controller="datatables">

Switch the buttons off:

<div data-controller="datatables" data-datatables-simple-value="true">

Server side processing:

<div data-controller="datatables" 
       data-datatables-url-value="<%= remote_stuff_path(format: :json) %>">

Let me know what you think.

I have had some time to review it and play with it. Some comments:

First of all, it works. However, it is not very elegant, for complex initializations I end up creating a lot of attributes to the table and writing the corresponding code in the controller to interpret it. In the end its much heavier and harder to read than putting the initialization code in the Javascript on the erb.

For me an elegant controller should look like:

import { Controller } from ā€œ@hotwired/stimulusā€

import ā€˜ā€¦/src/datatables-bs5ā€™

export default class extends Controller {

initialize() {

var myTable = $.fn.dataTable;

}

connect() {

const table = $(this.element.querySelector('table'))

// prepare options, optional add remote processing (not yet implemented)

let dtable = $(table).DataTable()

} // connect

} // Controller

I however noticed that many things can be done by using data-xxx tags in the erb.

table id=ā€œpeople_tableā€ class=ā€œtableā€ data-page-length=ā€œ5ā€ data-length-change=ā€œfalseā€ data-order=ā€œ[[ 2, "asc" ]]ā€

th data-orderable=ā€œfalseā€

However I have not been able to find a reference of these tags anywhere. Do you know if there is something?