I'm trying to setup a stimulus controller in a Rails 7 Bootstrap project. There are theme variables that are set to the window
object.
So in the browser console, i can enter window.theme.white
, and I get back #fff
. These variables are needed to properly initialize a map object.
However, inside the stimulus controller, window.theme
is undefined.
I have also tried the following:
var window = this.element.ownerDocument.defaultView;
inside the stimulus controller, and it seems to work, insofar as that I get the same window
object than in the browser, but the theme
object is not attached. So maybe it's another instance of window
? I'm stumped.
EDIT 1: my bad. window
object IS accesible. But the theme object attached to it is not. I have modified the title and question accordingly
This is the code that adds the theme to the windows object. It's imported as part of the application.js
main js file:
/*
* Add color theme colors to the window object
* so this can be used by the charts and vector maps
*/
const lightTheme = {
"id": "light",
"name": "Light",
"primary": "#3B7DDD",
"secondary": "#6c757d",
"success": "#1cbb8c",
"info": "#17a2b8",
"warning": "#fcb92c",
"danger": "#dc3545",
"white": "#fff",
"gray-100": "#f8f9fa",
"gray-200": "#e9ecef",
"gray-300": "#dee2e6",
"gray-400": "#ced4da",
"gray-500": "#adb5bd",
"gray-600": "#6c757d",
"gray-700": "#495057",
"gray-800": "#343a40",
"gray-900": "#212529",
"black": "#000"
};
const darkTheme = {
"id": "dark",
"name": "Dark",
"primary": "#3B7DDD",
"secondary": "#7a828a",
"success": "#1cbb8c",
"info": "#17a2b8",
"warning": "#fcb92c",
"danger": "#dc3545",
"white": "#222E3C",
"gray-100": "#384350",
"gray-200": "#4e5863",
"gray-300": "#646d77",
"gray-400": "#7a828a",
"gray-500": "#91979e",
"gray-600": "#a7abb1",
"gray-700": "#bdc0c5",
"gray-800": "#d3d5d8",
"gray-900": "#e9eaec",
"black": "#fff"
}
document.querySelectorAll("link[href]").forEach((link) => {
const href = link.href.split("/");
if(href.pop() === "dark.css"){
// Add theme to the window object
window.theme = darkTheme;
}
else {
// Add theme to the window object
window.theme = lightTheme;
}
});
So nothing fancy. Just global vars attached to the window
object.
This is the Stimulus controller that fails to find the window.theme
object:
import { Controller } from "@hotwired/stimulus"
import "../../modules/vector-maps/world"
export default class extends Controller {
static targets = ["targetMap"]
constructor() {
super();
this.map = new Object();
}
connect() {
this.InitMap();
}
mapResize(event) {
this.map.updateSize();
}
InitMap() {
var markers = [{
coords: [37.77, -122.41],
name: "San Francisco: 375"
},
{
coords: [40.71, -74.00],
name: "New York: 350"
},
{
coords: [39.09, -94.57],
name: "Kansas City: 250"
},
{
coords: [36.16, -115.13],
name: "Las Vegas: 275"
},
{
coords: [32.77, -96.79],
name: "Dallas: 225"
}
];
console.log('window object: ', window)
console.log('theme object attached to window: ', window.theme)
this.map = new jsVectorMap({
map: "world",
selector: "#world_map",
zoomButtons: true,
markers: markers,
markerStyle: {
initial: {
r: 9,
stroke: window.theme.white,
strokeWidth: 7,
stokeOpacity: .4,
fill: '#3B7DDD'
},
hover: {
fill: '#3B7DDD',
stroke: '#3B7DDD'
}
},
regionStyle: {
initial: {
fill: '#e9ecef'
}
},
zoomOnScroll: false
});
}
}
And this is the error, along with the console logs showing window.theme
as undefined
EDIT 2:
Copying the theme code into the stimulus controller constructor to set the theme global variables to the window
object does work. Maybe it's a race condition ?
Most likely, your application.js
code is running after your Stimulus application initialises.
You can either re-order the way these get added to your HTML so that Stimulus loads first or set up a way to know when the theme is ready.
Another approach could be to put your theme code into a Stimulus controller, but that comes with some edge cases to consider, especially as you probably want your theme code running as soon as possible.
Here is an example of using an event dispatching to know when the theme is ready.
// ... all other theme stuff
document.querySelectorAll("link[href]").forEach((link) => {
const href = link.href.split("/");
if(href.pop() === "dark.css"){
// Add theme to the window object
window.theme = darkTheme;
}
else {
// Add theme to the window object
window.theme = lightTheme;
}
});
// new code here
document.dispatchEvent(new CustomEvent('theme:ready', {
detail: { theme: window.theme },
cancelable: false
}));
Now in your Stimulus controller, you can set up a Promise to check if the theme is there and if not, wait for the event to fire before running your init method.
import { Controller } from "@hotwired/stimulus"
import "../../modules/vector-maps/world"
export default class extends Controller {
static targets = ["targetMap"]
constructor() {
super();
this.map = new Object();
}
async connect() {
theme = await new Promise(resolve => {
if (window.theme) resolve(window.theme)
document.addEventListener('theme:ready', ({ detail: { theme } }) => {
resolve(theme);
})
})
this.InitMap(theme); // passing in the theme value so that the method is easier to reason about, not accessing globals
// aside - methods starting with a capital could be confusing, best to use lowerCamelCase
}
// ... other methods
}
Note: You do not need to read out the theme and pass it to initMap
but it probably will be slightly better to understand what's happening this way.
Stimulus should wait for the promise to resolve before completing the connect method and initMap
will now only run when you have a theme.
Assuming application.js
is loading only just after your Stimulus code, this should happen in less than a 100ms. However, now you have code that does not need to worry about what order things load.