I'm building a dashboard using Dash in Python. I have configured all the graphs nicely (it's running on the server here) and the next step is to create a responsive navbar and a footer. Currently looks like this:
And when I shrink the width, it looks like this:
I want to add functionality to this button so it would hide the three links on click. I'm trying to toggle the CSS 'active' attribute using JavaScript with this piece of code:
var toggleButton = document.getElementsByClassName('toggle-button')[0]
var navBarLinks = document.getElementsByClassName('navbar-links')[0]
function toggleFunction() {
navBarLinks.classList.toggle('active')
}
toggleButton.addEventListener('click', toggleFunction)
Basically, when the navbar-links class is active, I want it to be set as display: flex, and when it's not active I want it to be display: none
The HTML elements defined in Python screen are here:
html.Nav([
html.Div('Covid-19 global data Dashboard', className='dashboard-title'),
html.A([html.Span(className='bar'),
html.Span(className='bar'),
html.Span(className='bar')],
href='#', className='toggle-button'),
html.Div(
html.Ul([
html.Li(html.A('Linked-In', href='#')),
html.Li(html.A('Source Code', href='#')),
html.Li(html.A('CSV Data', href='#'))
]),
className='navbar-links'),
], className='navbar')
I didn't expect that there would be issues with accessing elements through JavaScript. After doing some research I found out that JavaScript when executes getElementsByClassName function the returned value is null. That is because the function is run before the page is rendered (as far as I understand). It gives me this error:
This project is getting quite big, so I don't know which parts should I include in this post, but I will share the git repository and the preview of the page. Is there an easy solution to it?
You can defer the execution of JavaScript code until after React has loaded via the DeferScript component from dash-extensions. Here is a small example,
import dash
import dash_html_components as html
from html import unescape
from dash_extensions import DeferScript
mxgraph = r'{"highlight":"#0000ff","nav":true,"resize":true,"toolbar":"zoom layers lightbox","edit":"_blank","xml":"<mxfile host=\"app.diagrams.net\" modified=\"2021-06-07T06:06:13.695Z\" agent=\"5.0 (Windows)\" etag=\"4lPJKNab0_B4ArwMh0-7\" version=\"14.7.6\"><diagram id=\"YgMnHLNxFGq_Sfquzsd6\" name=\"Page-1\">jZJNT4QwEIZ/DUcToOriVVw1JruJcjDxYho60iaFIaUs4K+3yJSPbDbZSzN95qPTdyZgadm/GF7LAwrQQRyKPmBPQRzvktidIxgmwB4IFEaJCUULyNQvEAyJtkpAswm0iNqqegtzrCrI7YZxY7Dbhv2g3r5a8wLOQJZzfU4/lbByoslduPBXUIX0L0cheUrugwk0kgvsVojtA5YaRDtZZZ+CHrXzukx5zxe8c2MGKntNgknk8bs8fsj3+KtuDhxP+HZDVU5ct/RhatYOXgGDbSVgLBIG7LGTykJW83z0dm7kjklbaneLnEnlwFjoL/YZzb93WwNYgjWDC6EEdkuC0cZEO7p3i/6RF1WutL8nxmnkxVx6UcUZJIy/LgP49622mO3/AA==</diagram></mxfile>"}'
app = dash.Dash(__name__)
app.layout = html.Div([
html.Div(className='mxgraph', style={"maxWidth": "100%"}, **{'data-mxgraph': unescape(mxgraph)}),
DeferScript(src='https://viewer.diagrams.net/js/viewer-static.min.js')
])
if __name__ == '__main__':
app.run_server()
Dash callback solution (no Javascript):
import dash
import dash_html_components as html
from dash.dependencies import Output, Input, State
navbar_base_class = "navbar-links"
app = dash.Dash(__name__)
app.layout = html.Nav(
[
html.Div("Covid-19 global data Dashboard", className="dashboard-title"),
html.A(
id="toggle-button",
children=[
html.Span(className="bar"),
html.Span(className="bar"),
html.Span(className="bar"),
],
href="#",
className="toggle-button",
),
html.Div(
id="navbar-links",
children=html.Ul(
children=[
html.Li(html.A("Linked-In", href="#")),
html.Li(html.A("Source Code", href="#")),
html.Li(html.A("CSV Data", href="#")),
],
),
className=navbar_base_class,
),
],
className="navbar",
)
#app.callback(
Output("navbar-links", "className"),
Input("toggle-button", "n_clicks"),
State("navbar-links", "className"),
prevent_initial_call=True,
)
def callback(n_clicks, current_classes):
if "active" in current_classes:
return navbar_base_class
return navbar_base_class + " active"
if __name__ == "__main__":
app.run_server(debug=True)
The idea of the code above is to take the toggle-button click as Input and the current value of navbar-links as State. We can use this state to determine if we should add the active class or remove it. The new className value is returned in the callback.
Javascript solution:
window.addEventListener("load", function () {
var toggleButton = document.getElementsByClassName("toggle-button")[0];
var navBarLinks = document.getElementsByClassName("navbar-links")[0];
function toggleFunction() {
navBarLinks.classList.toggle("active");
}
toggleButton.addEventListener("click", toggleFunction);
});
The load event is fired when the whole page has loaded, including all dependent resources such as stylesheets and images. This is in contrast to DOMContentLoaded, which is fired as soon as the page DOM has been loaded, without waiting for resources to finish loading.
https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event
DOMContentLoaded would be preferable to use, but it only works for me with load.
If you need a pure JS solution you need to use MutationObserver. I've wrote a little helper function we are currently using that did the trick. Another suggestion would be to change the mutation to an element on screen then fire an event to handle the rest
/**
*
* #param {string} id
* #param {*} event
* #param {(this: HTMLElement, ev: any) => any} callback
* #param {boolean | AddEventListenerOptions} options
*/
function attachEventToDash(id, event, callback, options) {
debugger;
var observer = new MutationObserver(function (_mutations, obs) {
var ele = document.getElementById(id);
if (ele) {
debugger;
ele.addEventListener(event, callback, options)
obs.disconnect();
}
});
window.addEventListener('DOMContentLoaded', function () {
observer.observe(document, {
childList: true,
subtree: true
});
})
}
This is my flow: Content script -> Fetch data using background.js -> Return data and inject the new HTML into the webpage.
Now I have to show a tooltip using Popper.js but I get the error "document" is undefined.
It's strange because I am in content-scripts.js. How can I get the newly updated dom after my injection?
content-scripts.js
chrome.runtime.sendMessage({command: "sales_data", data: payload}, function(items) {
let view = new SearchResultsView(items)
view.injectToDom()
const popcorn = document.querySelector('#popcorn'); // document is undefined
const tooltip = document.querySelector('#tooltip'); // document is undefined
createPopper(popcorn, tooltip, {
placement: 'top',
});
});
SearchResultsView/InjectDom()
let html = `<div id="popcorn" aria-describedby="tooltip"></div>
<div id="tooltip" role="tooltip">
My tooltip
<div id="arrow" data-popper-arrow></div>
</div>`
//...
element.insertAdjacentHTML('afterbegin', html);
SOLVED.
I had to import tippy.js.
import { createPopper } from '#popperjs/core';
import tippy from 'tippy.js';
tippy('#button', {
content: 'My tooltip!',
});
I'm trying to use autoComplete.js.
I have npm installed it:
npm i #tarekraafat/autocomplete.js
Then imported it in a js file:
import autoComplete from "#tarekraafat/autocomplete.js/dist/js/autoComplete";
Also added a div with an id of autoComplete:
<input id="autoComplete" tabindex="1"> <!-- Default "id" value = "autoComplete">`
In the file where I imported the library, I copied the code that is on the website in step 4 of the how to use part.
But, I get the error:
autocompletejs.js:43 Uncaught ReferenceError: resultsListID is not defined
What am I doing wrong? I followed the steps as in the documentation but I get this error...
Any ideas on what might be the issue?
This error is caused, by the fact that here
resultsList: { // Rendered results list object | (Optional)
render: true,
container: source => {
resultsListID = "food_List";
return resultsListID;
},
destination: document.querySelector("#autoComplete"),
position: "afterend",
element: "ul"
},
resultsListID variable have never been initiated. It is possible to fix this byt adding var at the start of where resultsListID is assigned making something like:
var resultsListID = "food_List";
resultsList: { // Rendered results list object | (Optional)
render: true,
container: source => {
var resultsListID = "food_List";
return resultsListID;
},
destination: document.querySelector("#autoComplete"),
position: "afterend",
element: "ul"
},
Noticed, that this part is optional, and if one chooses to keep it, as far as my understanding is going all results will be wrapped in the container, that has id provided in the resultsListID variable
I've got the following codeblock in the layout.js
import $ from "jquery";
import LayoutModel from 'js/models/LayoutModel';
let elements = {
$window: $(window),
$html: $('html'),
$content: $('#content')
};
let viewmodel = null;
export function init(options) {
viewmodel = new LayoutModel(options, elements);
ko.applyBindings(viewmodel, elements.$content[0]);
}
and I load it in the view with system.js and call the init() with options. The options contains values and it has to be in the view because some of it comes from the back-end model.
System.import('resources/javascript/layout').then(function(controller) {
var options = {
something: 'value'
};
controller.init(options);
});
This solution works while it's not bundled with jspm:
jspm bundle resources\javascript\layout resources\javascript\dist\layout.min.js --minify
In production I have to do it, but in this case I'm not able to use the system.import() and the .then(), because I have to load the bundled script with a normal <script> tag:
<script src="resources\javascript\dist\layout.min.js"></script>
So the question: How can I bundle and minify it and call the init() method with the options?
Thank you!
I am trying to test this piece of HTML generated by React JS:
<div class="media-img">
<img src='images-name.jpg' />
</div>
As you can see, the image has no class name, and I can't put one on. I am using Jasmine to try and test that the src of the image is correct. But I can't seem to get hold of the image element in React. In jQuery this would be really easy, I could use $('.media-img img'), is there a way to traverse the DOM in React?
This is what I have so far:
var React = require('react'),
TestUtils = React.addons.TestUtils,
Component = require('component'),
renderedComponent;
function renderComponent(data) {
data = data || {};
renderedComponent = TestUtils.renderIntoDocument(
React.createElement(Component, data)
);
}
var imageURL = 'http://lorempixel.com/g/400/200/',
image,
imageSection;
renderComponent({
src: imageURL
});
imageSection = TestUtils.scryRenderedComponentsWithType(renderedComponent, 'media-img');
image = TestUtils.findRenderedDOMComponentWithTag(imageSection, 'img');
expect(image.src).toBe(imageURL);
But it isn't working. It seems that you can't pass a React component into another as I've tried to do at the bottom. So how can you traverse the virtual DOM?
I found an answer. Instead of attempting to pass a React component into another to test a subsection of the rendered DOM, I looked through the entire rendered DOM for 'img'. Normally this would be a bit inefficient as the rendered DOM could be huge and full of 'img' tags, but as this is a unit test with limited data, the rendered DOM is small and predictable. I used this:
image = TestUtils.findRenderedDOMComponentWithTag(renderedContributor, 'img');
Then I didn't need the 'imageSection' variable at all. Then I could call the DOM Node, then look at its 'src', by adding:
image.getDOMNode().src;
So my complete amended code looks like this:
var React = require('react'),
TestUtils = React.addons.TestUtils,
Component = require('component'),
renderedComponent;
function renderComponent(data) {
data = data || {};
renderedComponent = TestUtils.renderIntoDocument(
React.createElement(Component, data)
);
}
var imageURL = 'http://lorempixel.com/g/400/200/',
image;
renderComponent({
src: imageURL
});
image = TestUtils.findRenderedDOMComponentWithTag(renderedComponent, 'img');
expect(image.getDOMNode().src).toBe(imageURL);