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
});
})
}
I have one function called loadmap(){} where im creating map.Im loading this function with
ionViewDidEnter() {
this.loadmap();
}
Inside loadmap i have
this.map = leaflet.map("map").fitWorld();
thats how i initialize map
This is how i remove map when user changes tab.
ionViewDidLeave(){
this.map.remove();
}
This is my .locate function:
var usermarker;
this.map.locate({
setView: true,
maxZoom: 120,
watch:true,
enableHighAccuracy:true
}).on("locationfound", e => {
if (!usermarker) {
usermarker = new L.marker(e.latlng).addTo(this.map);
} else {
usermarker.setLatLng(e.latlng);
}
}).on("locationerror", error => {
if (usermarker) {
this.map.removeLayer(usermarker);
usermarker = undefined;
}
});
The problem is in first time .locate function works.but if i change tab and go back to map tab .locate function doesnt work.if i remove watch option it works.
Thanks
You have to call map.stopLocate() besides map.remove():
Stops watching location previously initiated by map.locate({watch: true})
Live demo: https://plnkr.co/edit/PKMPjfX3zD3QdWmEI0iX?p=preview (use the "Toggle map" button to simulate your changing tabs)
That being said, it is true that Leaflet could automatically do this when using the remove map method. => Merged in PR Leaflet/Leaflet#5893
I have this:
<i [ngClass]="{'active': isGeoLocationButtonEnabled }" class="toggle fa fa-crosshairs" >
And the handler is:
private onStartZoomEvent(event) {
if (event.zoom != 12) {
console.log("Current zoom level is: ", event.zoom);
this._eventAggregator.trigger(new DisableGeolocationEvent());
this.disableGeolocationButton();
}
}
The event that is triggered looks like this:
private mapZoomChangedHandler() {
this._mapsWrapper.getMap().then((map: google.maps.Map) => {
let zoom = map.getZoom();
this._eventAggregator.trigger(new MapStartZoomEvent(zoom));
});
}
I am subscribing like this:
this._eventAggregator.subscribe(MapStartZoomEvent, this.onStartZoomEvent.bind(this));
The problem is that for the first time I change the map zoom, the button is not disabled. The second time I click it , it gets disabled. When I debug, everything seems to be ok, my boolean isGeoLocationButtonEnabled is set to false, but the active class stays (it is just the background color highlighting).
You don't show where isGeoLocationButtonEnabled is updated. I'm pretty sure that the Google Maps code runs outside Angulars zone.
Use zone.run(...) to run the code that updates the model inside Angulars zone so change detection is invoked afterwards
constructor(private zone:NgZone) {}
methodWhereEnabledIsUpdated() {
...
this.zone.run(() => {
isGeoLocationButtonEnabled = someValue;
});
...
}
i am using the ARCGIS Javascript API and trying to override the default right click behavior of the vertex points of a shape.
in ESRI's help it does list the onVertexClick event however from here it seems there is no way to determine if this is a right or left click event so i cannot override just the rightclick.
https://developers.arcgis.com/javascript/jsapi/edit.html
I am trying to set the right click behavour to just delete the current node/vertex instead of showing a menu with the option Delete.
EDIT
Here is the current event that exists within the ARCGIS api.
this.eventsList.push(dojo.connect(this._editToolbar, 'onVertexClick', $.proxy(this.addCustomVertexClickEvent, this)));
this event is already in the api however it does not return any way for me to determine left/right click.
your comment "listen for the click event then test the button attribute of the MouseEvent object" would work however i cant actually add a click event to the vertex points directly as these are inside the ARCGIS api code.
For anyone else who is looking for a way to do this without hacking around. You can listen to "contextmenu" (right click) events on the body, set a flag in the "contextmenu" handler to let the application know the current state. Simulate a click event to the "vertex handle" with a "mousedown", "mouseup" combination. In the "vertex-click" handler check for the right click flag set in the "contextmenu" handler
var editToolbar = new Edit(map, options);
var rightClick;
$('body').on('contextmenu', function(e) {
var target = e.target;
if(target.tagName === 'circle') {
// We only care about this event if it targeted a vertex
// which is visualized with an SVG circle element
// Set flag for right click
rightClick = true;
// Simulate click on vertex to allow esri vertex-click
// to fill in the data for us
var mouseDownEvt = new MouseEvent('mousedown', e.originalEvent);
target.dispatchEvent(mouseDownEvt);
var mouseUpEvt = new MouseEvent('mouseup', e.originalEvent);
target.dispatchEvent(mouseUpEvt);
// Since this event will be handled by us lets prevent default
// and stop propagation so the browser context menu doesnt appear
e.preventDefault();
e.stopPropagation();
}
});
editToolbar.on('vertex-click', function(e) {
if(rightClick) {
// Handle the right click on a vertex
rightClick = null;
}
});
after hearing back from ESRI it seems they do not provide this detail in their API so this is not possible yet.
I ended up doing this differently. I wanted to add a UI so the user could enter the XY of the point
// setup to allow editing
this.editToolbar = new EditToolbar(this.map, { allowDeleteVertices: false });
const rcMenuForGraphics = new RightClickVertexContextMenu();
const menu = rcMenuForGraphics.createMenu();
// bind to the map graphics as this is where the vertex editor is
this.map.graphics.on("mouse-over", (evt)=> {
// bind to the graphic underneath the mouse cursor
menu.bindDomNode(evt.graphic.getDojoShape().getNode());
});
this.map.graphics.on("mouse-out", (evt)=> {
menu.unBindDomNode(evt.graphic.getDojoShape().getNode());
});
this.editToolbar.on("vertex-click", (evt2) => {
rcMenuForGraphics.setCurrentTarget(evt2);
// evt2.vertexinfo.graphic.geometry.setX(evt2.vertexinfo.graphic.geometry.x - 1000);
})
// when the graphics layer is clicked start editing
gl.on("click", (evt: any) => {
this.map.setInfoWindowOnClick(false);
// tslint:disable-next-line: no-bitwise
const t: any = EditToolbar.MOVE | EditToolbar.EDIT_VERTICES;
this.editToolbar.deactivate();
this.editToolbar.activate(t, evt.graphic);
})
The code for the menu uses esri's vertex editor to grab the point, change its XY and then manually call the events to refresh the geometry. Only tested with polygon
import Menu = require("dijit/Menu");
import MenuItem = require("dijit/MenuItem");
import Graphic = require("esri/graphic");
import Edit = require("esri/toolbars/edit");
import Point = require("esri/geometry/Point");
class RightClickVertexContextMenu {
private curentTarget: { graphic: Graphic; vertexinfo: any; target: Edit; };
public createMenu() {
const menuForGraphics = new Menu({});
menuForGraphics.addChild(new MenuItem({
label: "Edit",
onClick: () => {
// this is a bit hooky. We grab the verx mover, change the x/y and then call the _moveStopHandler
console.log(this.curentTarget.vertexinfo);
const e: any = this.curentTarget.target;
const mover = e._vertexEditor._findMover(this.curentTarget.vertexinfo.graphic);
const g: Graphic = mover.graphic;
// add in a UI here to allow the user to set the new value. This just shifts the point to the left
g.setGeometry(new Point(mover.point.x - 1000, mover.point.y ))
e._vertexEditor._moveStopHandler(mover, {dx: 15});
this.curentTarget.target.refresh();
}
}));
menuForGraphics.addChild(new MenuItem({
label: "Delete",
onClick: () => {
// call the vertex delete handler
const ct: any = this.curentTarget.target;
ct._vertexEditor._deleteHandler(this.curentTarget.graphic)
}
}));
return menuForGraphics;
}
public setCurrentTarget(evt: { graphic: Graphic; vertexinfo: any; target: Edit; }) {
this.curentTarget = evt;
}
}
export = RightClickVertexContextMenu;
I've done the Tutorial for building Fiori-like UIs with SAPUI5 and tried adding a sort-function.
The Dropdown-Box is filled with the Names of the JSON-Model-Data:
{
"SalesOrderCollection": [
{
"SoId": "300000097",
"ApprovalStatus": "",
"Status": "Initial",
"ConfirmationStatus": "",
"BillingStatus": "Initial",
...
} ...
I implemented this function in my View Controller to fill the dropdown:
fillDropdown : function(evt) {
// Get current view and dropdownbox
var oView = this.getView();
var oDropdown = oView.byId("ddbox");
// Get names of nodes in model
var metaArray = Object.getOwnPropertyNames(oView.getModel()
.getProperty("/SalesOrderCollection/0"));
// Add every name to dropdownbox
var arrayLength = metaArray.length;
for ( var i = 0; i < arrayLength; i++) {
oDropdown.addItem(new sap.ui.core.ListItem("item" + i)
.setText(metaArray[i]));
}
}
Finally, here comes my problem:
How can I run this function automatically when the View gets rendered? I know the lifecycle hook functions onInit & onBeforeRendering but I can't use them in my XML-View:
I can register the eventhandler for UI-Elements like here:
<uicom:DropdownBox id="ddbox" editable="true" change="handleListSort">
But not for the View or the Page like I tried here:
<Page title="myPage" onBeforeRendering="fillDropdown">
<core:View controllerName="sap.ui.demo.myFiori.view.Master" (...) onBeforeRendering="fillDropdown">
Possible dirty workaround: Call the function when clicking on the Sort-Button in the IconTabBar
<IconTabBar select="fillDropdown">
Thanks for your help!
Update
If I used a JavaScript-View instead of an XML-View, I could simply implement my fillDropdown function in the onAfterRendering function of my View.
But why do XML Views not throw lifecycle hook events?
Update 2
I also can't use the onAfterRendering of my View Controller; oView.getModel().getProperty("/SalesOrderCollection/0"); returns no object because the content of my model is (whyever) only available after the hook functions.
onInit, onBeforeRendering, onAfterRendering, onExit are lifecycle events. You do not need to register from view. Your function should be called when you implement independent from the view type. Please have a look at here http://jsbin.com/hiyanuqu/1/edit
BTW, there is a similar built in control in sap.m library called Input with Suggestion.
https://openui5.hana.ondemand.com/test-resources/sap/m/demokit/explored/index.html#/sample/sap.m.sample.InputSuggestionsCustomFilter