Can't locate element by id in scala-js-dom - scala

I'm trying to write text to an element in the DOM:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>The Scala.js Tutorial</title>
<!-- Include Scala.js compiled code -->
<script type="text/javascript" src="./target/scala-2.13/hello-world-fastopt/main.js"></script>
</head>
<body>
<div id="output"></div>
</body>
</html>
However, the element is null:
package hello
import org.scalajs.dom
object TutorialApp {
def main(args: Array[String]): Unit = {
println(dom.document.getElementById("output")) // null
dom.document.onload = (e) => {
println(dom.document.getElementById("output")) // null
}
}
}
What's needed to do to get to the element with id "output"?
Edit: Answers to questions:
def main(args: Array[String]): Unit = {
// Uncaught TypeError: Cannot read property 'innerHTML' of null
println("innerHTML: " + dom.document.body.innerHTML)
dom.document.onload = (e) => {
println("onload") // Doesn't get called
}
}
Locally, compiling with sbt fastOptJS. The end result line is:
[success] Total time: 2 s, completed Dec 13, 2020 4:30:36 PM
The HTML is as is rendered in the HTML page (hello-world-template/index-dev.html) (by viewing source), verbatim. It's printing "Initial text" inside the <DIV>.
Printing the body's innerHTML in main() results in TypeError (see code comment). The onload() in the code seems to not get called at all, as a println() call inside of it doesn't print.
Yes, it printed null in the browser console.

In your code:
def main(args: Array[String]): Unit = {
println(dom.document.getElementById("output")) // null
dom.document.onload = (e) => {
println(dom.document.getElementById("output")) // null
}
}
The first println prints null because at this point the browser hasn't finished parsing HTML / instantiating the DOM (simplifying a bit...). The browser reads the HTML top to bottom and builds up the DOM tree as it goes, and when it sees your script tag, it "blocks", i.e. downloads and executes the script first before reading the rest of the DOM. That's when your main runs, and why the output div could not be found at this stage – the browser didn't get to it yet.
To solve this, you could move your script tag to be below the <body> tag in your HTML, so that the script would be downloaded and executed after the document is all parsed and the DOM all initialized.
But a nicer solution is to delay DOM access in the script until the browser fires an event indicating it's safe to do that. That's what you're trying to achieve by assigning dom.document.onload, but this is kinda "old style" in JS world, and is not universally supported by all browsers (specifically dom.document.onload I mean, I think assigning dom.window.onload might work just fine).
Ultimately it's best to use modern syntax for this:
dom.window.addEventListener("load", ev => {
println(dom.document.getElementById("output"))
})
You can also use the "DOMContentLoaded" event instead of "load", they serve the same purpose, but have slightly different timing related to waiting for resources like images. Does not matter in your case though. Check MDN for details if curious.

Related

Using a Vue3 component as a Leaflet popup

This previous SO question shows how we can use a Vue2 component as the content of a LeafletJS popup. I've been unable to get this working with Vue3.
Extracting the relevant section of my code, I have:
<script setup lang="ts">
import { ref } from 'vue'
import L, { type Content } from 'leaflet'
import type { FeatureCollection, Feature } from 'geojson'
import LeafletPopup from '#/components/LeafletPopup.vue'
// This ref will be matched by Vue to the element with the same ref name
const popupDialogElement = ref(null)
function addFeaturePopup(feature:Feature, layer:L.GeoJSON) {
if (popupDialogElement?.value !== null) {
const content:Content = popupDialogElement.value as HTMLElement
layer.bindPopup(() => content.$el)
}
}
</script>
<template>
<div class="map-container">
<section id="map">
</section>
<leaflet-popup ref="popupDialogElement" v-show="false">
</leaflet-popup>
</div>
</template>
This does produce a popup when I click on the map, but it has no content.
If, instead, I change line 14 to:
layer.bindPopup(() => content.$el.innerHTML)
then I do get a popup with the HTML markup I expect, but unsurprisingly I lose all of the Vue behaviours I need (event handling, etc).
Inspecting the addFeaturePopup function in the JS debugger, the content does seem to be an instance of HTMLElement, so I'm not sure why it's not working to pass it to Leaflet's bindPopup method. I assume this has something to do with how Vue3 handles references, but as yet I can't see a way around it.
Update 2022-06-09
As requested, here's the console.log output: I've put it in a gist as it's quite long
So just to document the solution I ended up using, I needed to add an additional style rule in addition to the general skeleton outlined in the question:
<style>
.leaflet-popup-content >* {
display: block !important;
}
</style>
This overrides the display:none that is attached to the DOM node by v-show=false. It would be nice not to need the !important, but I wasn't able to make the rule selective enough in my experiments.

MouseEvent.target returns an EventTarget instead of a HTMLElement when clicked inside an iframe, in ScalaJs

MouseEvent.target returns an EventTarget instead of a HTMLElement when clicked inside an iframe, in ScalaJs.
src/main/scala/tutorial/webapp/TutorialApp.scala:
package tutorial.webapp
import org.scalajs.dom._
import org.scalajs.dom.raw._
import scala.scalajs.js
object TutorialApp {
def main(args: Array[String]): Unit = {
window.document.body.innerHTML = "<p><b>main window</b></p>"
val iframe = document.createElement("iframe")
document.body.appendChild(iframe)
val iframeWindow = iframe.asInstanceOf[HTMLIFrameElement].contentWindow
iframeWindow.document.body.innerHTML = "<p><b>iframe</b></p>"
window.document.addEventListener("click", clicked)
// this works as expected:
// clicking on the 'main window' text, produces this console log:
// - clicked an HTMLElement B
// - parent is an HTMLParagraphElement P
iframeWindow.document.addEventListener("click", clicked) // this doesn't
// this does not work as expected:
// clicking on the 'iframe' text, produces this console log:
// - clicked an EventTarget B
// - parent is an HTMLElement P
}
def clicked(mouseEvent: MouseEvent) {
mouseEvent.target match {
case e: HTMLElement => console.log("clicked an HTMLElement", e.asInstanceOf[HTMLElement].tagName)
case e: EventTarget => console.log("clicked an EventTarget", e.asInstanceOf[HTMLElement].tagName)
}
val parent = mouseEvent.target.asInstanceOf[HTMLParagraphElement].parentElement
parent match {
case e: HTMLParagraphElement => console.log("parent is an HTMLParagraphElement", e.asInstanceOf[HTMLElement].tagName)
case e: HTMLElement => console.log("parent is an HTMLElement", e.asInstanceOf[HTMLElement].tagName)
}
}
}
index.html
<html>
<body>
<!-- Include Scala.js compiled code -->
<script type="text/javascript" src="./target/scala-2.12/scala-js-tutorial-fastopt.js"></script>
</body>
</html>
When I click inside the iframe on the <h1>iframe</h1>, I get an EventTarget instead of an HTMLElement. Casting it to HTMLElement works, but e.parentElement is an HTMLElement instead of HTMLParagraphElement.
Why and how to solve it?
Hoping someone will provide a more precise answer, but in the absence of that:
Iframes are typically loaded from other URLs, often from another origin, and their security model reflects that primary use case – the document inside the iframe is quite isolated from the parent document. I'm not quite sure which exact restriction you're hitting though as manually creating iframes like in your example is not very common.
Usually, the Javascript code running inside the iframe needs to be willing to communicate with Javascript code running in the parent window. Currently in your example you only have code running in the parent window, as the iframe itself does not load any scripts.
Depending on your exact use case, there are several ways to achieve this. For example, you could post custom events to the parent window as described in How to communicate between iframe and the parent site?
Iframes can be really annoying if you don't want the isolation that they come with.

Simple play/scala benchmark, rendering a view versus a raw text output comparison

I was just benchmarking a new play/scala application.
When performing a simple text output in my action, I get 60K requests per second.
If I render a view (see below), it drops to 13K per second.
Since views are just functions in scala, I would have expected that the extra overhead of calling a function wouldn't drop the requests per second down so dramatically.
I only ran the benchmark for 10-30 seconds, would it take longer for the jvm to optimize maybe or this is just expected behavour?
def index() = Action { implicit request: Request[AnyContent] =>
Ok("hello")
}
If I actually render a view, the requests per second drops to about 13K.
def index() = Action { implicit request: Request[AnyContent] =>
Ok(views.html.index())
}
/app/views/index.scala.html
#()
#main("Welcome to Play") {
<h1>Welcome to Play!</h1>
}
/app/views/main.scala.html
#*
* This template is called from the `index` template. This template
* handles the rendering of the page header and body tags. It takes
* two arguments, a `String` for the title of the page and an `Html`
* object to insert into the body of the page.
*#
#(title: String)(content: Html)
<!DOCTYPE html>
<html lang="en">
<head>
#* Here's where we render the page title `String`. *#
<title>#title</title>
<link rel="stylesheet" media="screen" href="#routes.Assets.versioned("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png" href="#routes.Assets.versioned("images/favicon.png")">
</head>
<body>
#* And here's where we render the `Html` object containing
* the page content. *#
#content
<script src="#routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
</body>
</html>
As I already said in my comment, the view is not a trivial function. It performs string concatenation and it also calls routes.Assets.versioned three times. Profiling session shows, that the view basically only waits on this function:
Drilling down, we learn, that the versioned function always re-reads the file from classpath:
Maybe you can open an issue and ask Play framework creators, whether serving of assets could be optimized better ?
Edit: I profiled two setups. First I modified HomeControllerSpec test:
"render the index page from a new instance of controller" in {
val controller = new HomeController(stubControllerComponents())
val indexAction = controller.index()
val fakeRequest = FakeRequest(GET, "/")
var i = 100000
while (i > 0) {
indexAction.apply(fakeRequest)
i -= 1
}
But this does not rule out, that some components can behave differently in production mode. So I ran sbt stage and started generated application, attached profiler to running JVM and executed 10000 requests to the profiled JVM. Result was identical though.

karma-runner: load scripts (and CSS) via DOM

I want to inject some CSS and JavaScript files via a preprocessor.
In my preprocessor I inject the html template to the body element.
I printed the result out via console.log(document.body) - you can see the result at the bottom. It looks good, but the script is not evaluated.
If I run console.log(window.foobar) in my test, it's undefined.
Actually I don't want to to inject simple scripts, I want to load some files via
<script src="build/app.js"></script>
I need it in every test, so I don't want to refactor every single test for the same code injection, that's the reason why I tried to put it into the html generated by karma.
<body><script> window.foobar = 'miau!';</script>
<!-- The scripts need to be at the end of body, so that some test running frameworks
(Angular Scenario, for example) need the body to be loaded so that it can insert its magic
into it. If it is before body, then it fails to find the body and crashes and burns in an epic
manner. -->
<script type="text/javascript">
// sets window.__karma__ and overrides console and error handling
// Use window.opener if this was opened by someone else - in a new window
if (window.opener) {
window.opener.karma.setupContext(window);
} else {
window.parent.karma.setupContext(window);
}
// All served files with the latest timestamps
window.__karma__.files = {
'/base/node_modules/mocha/mocha.js': '253e2fdce43a4b2eed46eb25139b784adbb5c47f',
'/base/node_modules/karma-mocha/lib/adapter.js': '3664759c75e6f4e496fef20ad115ce8233a0f7b5',
'/base/test/custom-test.js': 'abf5b0b3f4dbb62653c816b264a251c7fc264fb9',
'/base/test/build/build.css': 'df7e943e50164a1fc4b66e0a0c46fc86efdef656',
'/base/test/build/build.js': '9f0a39709e073846c73481453cdee8d37e528856',
'/base/test/build/test.js': '0ccd4711b9c887458f81cf1dedc04c6ed59abe43'
};
</script>
<!-- Dynamically replaced with <script> tags -->
<script type="text/javascript" src="/base/node_modules/mocha/mocha.js?253e2fdce43a4b2eed46eb25139b784adbb5c47f"></script>
<script type="text/javascript" src="/base/node_modules/karma-mocha/lib/adapter.js?3664759c75e6f4e496fef20ad115ce8233a0f7b5"></script>
<script type="text/javascript" src="/base/test/custom-test.js?abf5b0b3f4dbb62653c816b264a251c7fc264fb9"></script></body>
Karma introduces the page scripts/html just like ajax, so it wont execute once the append has finished.
You will need to append the files for each spec. I have a helper for this job:
function appendCSS(path){
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href='base/' + path;
document.body.appendChild(link)
}
function appendScript(path){
var link = document.createElement('script');
link.type = 'javascript';
link.src='base/' + path;
document.body.appendChild(link)
}
function loadAssets(page){
document.body.innerHTML = __html__['_site/' + (page || 'index') + '.html'];
appendCSS('_site/styles/demo.css');
appendCSS('_site/styles/' + page + '.css');
appendScript('_site/scripts/vendor.js');
appendScript('_site/scripts/' + page + '.js');
}
module.exports = {
loadAssets: loadAssets
};
In my spec i then simply call the helper, passing the name of the html page to be tested.
require('../helper').loadAssets('tested-page-name');
As you can see, i use the borwserify plugin, but i hope this helps.

Embed google-plus in GWT

I am trying to embed Google-Plus into my GWT Application. I would like it to be embedded into a HorizontalPanel. I did read +1button developers google. I didn't find any post about this particular problem in stackoverflow. My problem might be that I don't understand how to include the js into a GUI component. I would appreciate an Example of how to add the Google+ code into a Panel.
Here is how to do it:
Documentation:
<!-- Place this tag in your head or just before your close body tag -->
<script type="text/javascript" src="https://apis.google.com/js/plusone.js"></script>
<!-- Place this tag where you want the +1 button to render -->
<g:plusone></g:plusone>
in GWT:
private void drawPlusOne() {
String s = "<g:plusone href=\"http://urltoplusone.com\"></g:plusone>";
HTML h = new HTML(s);
somePanel.add(h);
// You can insert a script tag this way or via your .gwt.xml
Document doc = Document.get();
ScriptElement script = doc.createScriptElement();
script.setSrc("https://apis.google.com/js/plusone.js");
script.setType("text/javascript");
script.setLang("javascript");
doc.getBody().appendChild(script);
}
I've personally never embedded the +1 button in GWT, but the linked article seems pretty self explanatory.
In the section "A Simple Button", it indicates that the simplest way of implementing GooglePlus integration is to add this:
<script src="https://apis.google.com/js/plusone.js" />
<g:plusone></g:plusone>
First, the <script> tag should be included in your .gwt.xml file.
Then I'd implement the <g:plusone></g:plusone> like this:
public class GPlusOne extends SimplePanel {
public GPlusOne () {
super((Element)Document.get().createElement("g:plusone").cast());
}
}
(Note that this code is untested, but it's based on the simple concept that a SimplePanel can be extended to compile as any HTML element.)
Then you'd use the new GPlusOne element wherever you'd want the button to show.
I found a better way to do it:
Follow this example to have the button work on invocation on a normal html page (you can try one here http://jsfiddle.net/JQAdc/)
<!doctype html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<script src="https://apis.google.com/js/plusone.js">
{"parsetags": "explicit"}
</script>
<script type="text/javascript">
function gPlusBtn(id, params) {
/* window.alert("searching for "+ id +" with params: "+ params) */
paramsObj = eval( '('+params+')' );
gapi.plusone.render(id, paramsObj );
}
// params is here just for a reference to simulate what will come from gwt
params = '{href:"http://1vu.fr", size:"tall"}';
</script>
</head>
<body>
taken from http://jsfiddle.net/JQAdc/
<div id="gplus" />
<button onclick="gPlusBtn('gplus', params)">show!</button>
</body>
</html>
Then you can call a native method to trigger the button display on Activity start (if you're using MVP).
protected native void plusOneButton(String id, String params) /*-{
$wnd.gPlusBtn(id, params);
}-*/;
You can have multiple buttons with different urls, that's why id is left as a parameter.
NOTE: for me the raw HTML works on localhost, but the GWT version. I have to deploy to the server to be able to see the results