Adding JavaScript to my Plotly Dash app (Python) - javascript

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
});
})
}

Related

Custom Element (Web Component) won't accept keyboard input when inserted by a CKEditor 5 plugin

I'm in the initial stages of developing a plugin that will allow the user to insert placeholder elements into HTML content that will be processed server-side and used to incorporate some simple logic into a generated PDF document. To this end, I'm attempting to insert a custom element that I've defined using the web components API.
class NSLoop extends HTMLElement {
constructor() {
super();
}
get source() {
return this.getAttribute('source');
}
get as() {
return this.getAttribute('as');
}
}
window.customElements.define('ns-loop', NSLoop);
The contents of loopediting.js:
import Plugin from "#ckeditor/ckeditor5-core/src/plugin";
import Widget from "#ckeditor/ckeditor5-widget/src/widget";
import {viewToModelPositionOutsideModelElement} from "#ckeditor/ckeditor5-widget/src/utils";
import LoopCommand from "./loopcommand";
export default class LoopEditing extends Plugin {
static get requires() {
return [Widget];
}
constructor(editor) {
super(editor);
}
init() {
this._defineSchema();
this._defineConverters();
this.editor.commands.add('loop', new LoopCommand(this.editor));
this.editor.editing.mapper.on('viewToModelPosition', viewToModelPositionOutsideModelElement(this.editor.model, viewElement => viewElement.is('element', 'ns-loop')));
}
_defineSchema() {
const schema = this.editor.model.schema;
schema.register('loop', {
isBlock: false,
isLimit: false,
isObject: false,
isInline: false,
isSelectable: false,
isContent: false,
allowWhere: '$block',
allowAttributes: ['for', 'as'],
});
schema.extend( '$text', {
allowIn: 'loop'
} );
schema.extend( '$block', {
allowIn: 'loop'
} );
}
_defineConverters() {
const conversion = this.editor.conversion;
conversion.for('upcast').elementToElement({
view: {
name: 'ns-loop',
},
model: (viewElement, {write: modelWriter}) => {
const source = viewElement.getAttribute('for');
const as = viewElement.getAttribute('as');
return modelWriter.createElement('loop', {source: source, as: as});
}
});
conversion.for('editingDowncast').elementToElement({
model: 'loop',
view: (modelItem, {writer: viewWriter}) => {
const widgetElement = createLoopView(modelItem, viewWriter);
return widgetElement;
}
});
function createLoopView(modelItem, viewWriter) {
const source = modelItem.getAttribute('source');
const as = modelItem.getAttribute('as');
const loopElement = viewWriter.createContainerElement('ns-loop', {'for': source, 'as': as});
return loopElement;
}
}
}
This code works, in the sense that an <ns-loop> element is successfully inserted into the editor content; however, I am not able to edit this element's content. Any keyboard input is inserted into a <p> before the <ns-loop> element, and any text selection disappears once the mouse stops moving. Additionally, it is only possible to place the cursor at the beginning of the element.
If I simply swap out 'ns-loop' as the tag name for 'div' or 'p', I am able to type within the element without issue, so I suspect that I am missing something in the schema definition to make CKEditor aware that this element is "allowed" to be typed in, however I have no idea what I may have missed -- as far as I'm aware, that's what I should be achieving with the schema.extend() calls.
I have tried innumerable variations of allowedIn, allowedWhere, inheritAllFrom, isBlock, isLimit, etc within the schema definition, with no apparent change in behaviour.
Can anyone provide any insight?
Edit: Some additional information I just noticed - when the cursor is within the <ns-loop> element, the Heading/Paragraph dropdown menu is empty. That may be relevant.
Edit 2: Aaand I found the culprit staring me in the face.
this.editor.editing.mapper.on('viewToModelPosition', viewToModelPositionOutsideModelElement(this.editor.model, viewElement => viewElement.is('element', 'ns-loop')));
I'm new to the CKE5 plugin space, and was using other plugins as a reference point, and I guess I copied that code from another plugin. Removing that code solves the problem.
As noted in the second edit, the culprit was the code,
this.editor.editing.mapper.on('viewToModelPosition', viewToModelPositionOutsideModelElement(this.editor.model, viewElement => viewElement.is('element', 'ns-loop')));
which I apparently copied from another plugin I was using for reference. Removing this code has solved the immediate problem.
I'll accept this answer and close the question once the 2-day timer is up.

Vue and Prismic rich text: add event listener to a span node

The content of my Vue app is fetched from Prismic (an API CMS). I have a rich text block, some parts of which are wrapped inside span tags with a specific class. I want to get those span nodes with Vue and add to them an event listener.
With JS, this code would work:
var selectedSpanElements = document.querySelectorAll('.className');
selectedSpanElements[0].style.color = "red"
But when I use this code in Vue, I can see that it works just a fraction of a second before Vue updates the DOM. I've tried using this code on mounted, beforeupdate, updated, ready hooks... Nothing has worked.
Update: Some hours later, I found that with the HTMLSerializer I can add HTML code to the span tag. But this is regular HTML, I cannot access to Vue methods.
#Bruja
I was able to find a solution using a closure. The folks at Prismic reminded/showed me.
Of note, per Phil Snow's comment above: If you are using Nuxt you won't have access to Vue's functionality and will have to go old-school JS.
Here is an example where you can pass in component-level props, data, methods, etc... to the prismic htmlSerializer:
<template>
<div>
<prismic-rich-text
:field="data"
:htmlSerializer="anotherHtmlSerializer((startNumber = list.start_number))"
/>
</div>
</template>
import prismicDOM from 'prismic-dom';
export default {
methods: {
anotherHtmlSerializer(startNumber = 1) {
const Elements = prismicDOM.RichText.Elements;
const that = this;
return function(type, element, content, children) {
// To add more elements and customizations use this as a reference:
// https://prismic.io/docs/vuejs/beyond-the-api/html-serializer
that.testMethod(startNumber);
switch (type) {
case Elements.oList:
return `<ol start=${startNumber}>${children.join('')}</ol>`;
}
// Return null to stick with the default behavior for everything else
return null;
};
},
testMethod(startNumber) {
console.log('test method here');
console.log(startNumber);
}
}
};
I believe you are on the right track looking into the HTML Serializer. If you want all your .specialClass <span> elements to trigger a click event that calls specialmethod() this should work for you:
import prismicDOM from 'prismic-dom';
const Elements = prismicDOM.RichText.Elements;
export default function (type, element, content, children) {
// I'm not 100% sure if element.className is correct, investigate with your devTools if it doesn't work
if (type === Elements.span && element.className === "specialClass") {
return `<span #click="specialMethod">${content}</span>`;
}
// Return null to stick with the default behavior for everything else
return null;
};

Rails 5 javascript for Ajax controlled partial is acting strangely & it can't initialize bootstrap-star-rating inputs as it should

I am using this js library (bootstrap-stars-rating) to provide star ratings using jquery in my Rails 5 application
It works well except that if I am calling it inside a partial that is renerdered by Ajax helper, it will not initialize the javascript code necessary to render the stars
I tried to solve this by making a globally accessible function in my webpacker init file for that library like this:
import "bootstrap-star-rating";
import "bootstrap-star-rating/css/star-rating.css";
import "bootstrap-star-rating/js/star-rating.js";
import "bootstrap-star-rating/themes/krajee-fas/theme.min.css";
const rating_options = {
size: 'xs',
showCaption: false,
step: 0.5,
filledStar: '<i class="fas fa-star"></i>',
emptyStar: '<i class="far fa-star"></i>',
theme: 'krajee-fas',
hoverOnClear: false
}
// initialize the star rating across the application
const initStarRating = () => {
document.addEventListener("turbolinks:load", () => {
console.log('hey I was run');
$(".bootstrap-rating").rating(rating_options);
}, {passive: true});
}
// initialize specific star rating in a specific form for ajax
const initStarRatingForObj = (obj) => {
{
console.log(obj)
console.log(obj.find(".bootstrap-rating"))
obj.find(".bootstrap-rating").rating(rating_options);
}
}
// binding the function so I can access it globally
window.initStarRatingForObj = initStarRatingForObj
export { initStarRating }
The problem is that if I call this function above in the js.erb file that renders the partial, passing the correct object to the function initStarRatingForObj, it will break & give error that it can't find the rating() function ((that one called in the initStarRating))
This is very weird as the rating() function is already available & working in that file, so how does it work in initStarRating() but not in the initStarRatingForObj() ?
I have been looking into this for hours but it doesn't work, I hope someone can point out what the issue is.
Note:
The same partial is working well in the same page if it is not rendered by Ajax response

How do I add a custom script to React component?

Long story short: I'm trying to add a front-end app to my portfolio site that uses React. I would like to integrate the app into the component as it renders. What I have setup right now is:
React component:
class Giphy extends Component {
constructor(props) {
super(props);
this.state = {src: 1}
this.handleClick = this.handleClick.bind(this);
}
handleClick(event) {
this.setState({src: event.target.value})
}
componentDidMount() {
const script = document.createElement("script");
script.type = "text/javascript";
script.src = "/scripts/giphyLogic.js";
document.body.appendChild(script);
}
...and a bunch of stuff in the render() method that doesn't matter
the script that I want to load involves a bunch of jQuery and simple JS stuff.
function displayButtons() {
$("#buttons").empty();
for (i=0; i<buttonArray.length; i++){
var a = $("<button type='button' class='btn btn-info'>");
var btnID = buttonArray[i].replace(/\s+/g, "+")
a.attr("id", btnID);
a.text(buttonArray[i]);
$("#buttons").append(a);
}
}
$("#addButton").on("click", function() {
var newButton = $(".form-control").val();
buttonArray.push(newButton);
displayButtons();
})
function displayGIFs() {
$(".btn-info").on("click", function() {
$("#resultsContainer").empty();
var subject = $(this).attr("id");
var giphyURL = "http://api.giphy.com/v1/gifs/search?q=" + subject + "&api_key=dc6zaTOxFJmzC";
$.ajax({ url: giphyURL, method: "GET"}).done(function(res) {
for (t=0; t<25; t++) {
var rating = res.data[t].rating;
var image = $("<img>");
var imgURLmoving = res.data[t].images.fixed_height.url;
var imgURLstill = res.data[t].images.fixed_height_still.url;
image.attr("src", imgURLstill);
image.attr("data-still", imgURLstill);
image.attr("data-moving", imgURLmoving);
image.attr("data-state", "still")
image.addClass("gif");
$("#resultsContainer").append("<p>" + rating + "</p");
$("#resultsContainer").append(image);
}
})
$(document.body).on("click", ".gif", function() {
var state = $(this).attr("data-state");
if (state === "still") {
$(this).attr("src", $(this).data("moving"));
$(this).attr("data-state", "moving");
} else {
$(this).attr("src", $(this).data("still"));
$(this).attr("data-state", "still");
}
})
})
}
displayButtons();
displayGIFs();
This all works on a standalone HTML document, but I can't seem to get the script to work properly. When the component loads and I inspect the page,
<script type="text/javascript" src="/scripts/giphyLogic.js"></script>
is there under the bundle.js script tag, but nothing from the script happens.
I get an "Uncaught SyntaxError: Unexpected token <" error that is attributed to giphyLogic.js:1 even though in the actual .js file, that line is blank. I've looked around, and this apparently happens when a file is included that doesn't exist, but the file is definitely there. I've double checked the path (by including an image in the same folder and loading the image successfully on the page) and it's correct.
Is there a way to resolve this, or am I going to have to create methods within the React component that I'm creating?
Do not mix jQuery and react. Learn how to use react properly by reading the well-written documentation. They can guide you through the many examples to get a simple app up and running.
Once again, do NOT use jQuery and react. jQuery wants to manually manipulate the DOM, and react manages a virtual DOM. The two will conflict more often than not, and you're going to have a bad time. If you have a very deep understanding of react, there are very few scenarios in which you could maybe use some jQuery, but nearly all of the time, it is to be avoided at all costs.
Obviously things like $.ajax() are fine, but for anything dealing with DOM manipulation, stay away. And if you only end up using jQuery for $.ajax() calls... you should switch to a leaner library like axios or use the native fetch API.

SAP UI5: Fill XML-Dropdown dynamically using Lifecycle Hook Function

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

Categories

Resources