Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tags.script() doesn't execute when it's a child of a JSXTag #26

Open
cpsievert opened this issue Apr 6, 2022 · 5 comments · May be fixed by #27
Open

tags.script() doesn't execute when it's a child of a JSXTag #26

cpsievert opened this issue Apr 6, 2022 · 5 comments · May be fixed by #27

Comments

@cpsievert
Copy link
Collaborator

For example, when you open example.html, you should get a 'hello' alert box (but you currently get nothing).

This effectively means you can use things like shiny.ui.panel_absolute(draggable=True) inside a ui.page_navbar (because it's all one big JSX component

from htmltools import jsx_tag_create, tags, HTML, HTMLDocument

MyComponent = jsx_tag_create("MyComponent")

HTMLDocument(
  tags.script(HTML(
      "function MyComponent(props) { return React.createElement('h4', props, 'Hello World') }"
  )),
  MyComponent(
      tags.script("alert('hello')"),
      style={"color": "red"}
  )
).save_html("example.html")
@wch
Copy link
Collaborator

wch commented Apr 8, 2022

This is a good explanation of why the script tag doesn't execute: https://stackoverflow.com/a/64815699/412655

React DOM (the renderer for react on web) uses createElement calls to render JSX into DOM elements.

createElement uses the innerHTML DOM API to finally add these to the DOM ([see code in React source][1]). innerHTML does not execute script tag added as a security consideration. And this is the reason why in turn rendering script tags in React doesn't work as expected.

@wch
Copy link
Collaborator

wch commented Apr 8, 2022

I've been thinking about another way of dealing with the JSX stuff.

First issue: the JS code generation is a bit risky instead of writing JS code, it'll be more robust to create a JSON data structure

from htmltools import *

Foo = jsx_tag_create("Foo")
x = Foo(
  "hello",
  span(
    "world",
    Foo("more text", z=100)
  ),
  tags.script("alert('This is the script!');"),
  x=[1,2,3],
  y="abc"
)
print(str(x))
#> <script type="text/javascript">
#> (function() {
#>   var container = new DocumentFragment();
#>   ReactDOM.render(
#>     React.createElement(
#>       Foo, {"x": [1, 2, 3], "y": "abc"},
#>       "hello",
#>       React.createElement(
#>         'span', {},
#>         "world",
#>         React.createElement(
#>           Foo, {"z": 100},
#>           "more text"
#>         ),
#>       ),
#>       React.createElement(
#>         'script', {},
#>         "alert('This is the script!');"
#>       )
#>     )
#>   , container);
#>   document.currentScript.after(container);
#> })();
#> </script>

Instead of generating the JS code, we can encode the data structure like this. (For clarity, this is written as TypeScript.)

type HtmlTag = {
  type: "html";
  name: string;
  attrs?: Record<string, string>;
  children?: Array<string | Tag>;
};

type JsxTag = {
  type: "jsx";
  name: string;
  attrs?: Record<string, any>;
  children?: Array<string | Tag>;
};

type Tag = HtmlTag | JsxTag;

const x: Tag = {
  type: "jsx",
  name: "Foo",
  attrs: { x: [1, 2, 3], y: "abc" },
  children: [
    "hello",
    {
      type: "html",
      name: "span",
      children: [
        {
          type: "jsx",
          name: "Foo",
          attrs: { z: 100 },
          children: ["more text"],
        },
      ],
    },
    {
      type: "html",
      name: "script",
      children: ["alert('This is the script!');"],
    },
  ],
};

insertJSX(x, container);

One advantage of this is that we can use Python's json library for conversion, which will make the conversion of arbitrary attributes more robust than our current JS code generation code.

Another advantage is that it leaves us with a data structure which we can manipulate from the JavaScript side. For example, we can recurse over it looking for specific types tags and have customized behavior with them.

The next issue is, how do we handle the specific case of script tags? (The current problem is that those tags do not execute when they're inside of a JSXTag object.)

Some possibilities:

See this SO question for a bit more.

If we go this way, we'll also need to think about whether there are any weird corner cases that can trip us up. For example:

  • How do we handle text that's wrapped in HTML() on the Python side?

@cpsievert
Copy link
Collaborator Author

Have you considered how we would support jsx() with this new approach?

@wch
Copy link
Collaborator

wch commented Apr 11, 2022

Have you considered how we would support jsx() with this new approach?

Two comments about jsx():

First, I think we should use another name, like raw_js() or inline_js(), since those are more accurate descriptions of what's going on -- the code isn't JSX, but rather JS which we don't want to be quoted or escaped.

Second, in terms of the data structures, we could package it up with something like this:

{
  type: "raw_js",
  children: ["() => console.log('here')"]
}

@wch
Copy link
Collaborator

wch commented Apr 13, 2022

Some notes on the JSX stuff: I did some experiments using WebComponents. https://stackblitz.com/edit/web-platform-lkzkdq
In order to be able to inspect and manipulate child nodes,, the connectedCallback() needs to use setTimeout( ,0). This sometimes results in a FOUC, although it can be mitigated by using manipulating the visibility of the components.

Another approach that I tested is is to send custom tags (but not WebComponents) and manipulate/transform them into HTML tags before inserting them into the DOM. With this method, there’s no FOUC, but the code needed to manipulate the content is a bit verbose and clunky.

For the methods above, <script> tags run without any special effort. With WebComponents, the <script> tag will run after the connectedCallback(), but before any code that was scheduled with a setTimeout().

A third is to use the htm library, which can transform code that's very similar to JSX, but does not require a heavy-weight transpilation tool like Babel (htm is only 600 bytes). We could generate the JSX-like code on the server and send it to the client to be processed by htm.

Note: If htm is used with React, then it will not automatically run <script> tags. If htm is used with Preact, it will automatically run <script> tags. React example | Preact example. It may make sense to use Preact for our components, since Preact is lighter weight, and we are not using a lot of React features.

All that said, it might not be necessary to use htm. What it does is parse JSX-like code into a tree data structure (example here), then pass that data structure to React or Preact. So instead of going from Python data structure to JSX-like string to this tree data structure, we could go directly from the Python data structure to this tree data structure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants