dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.21k stars 1.57k forks source link

js-interop: document limitations with ES6 #39995

Open MarvinHannott opened 4 years ago

MarvinHannott commented 4 years ago

Describe the issue you're seeing External getters don't work with JS let and const declarations. It does work, however, with var declaration.

Does it happen in Dartium or when compiled to JavaScript? Compile to JavaScript with webdev build.

Failing code:

// example.js
const greetings = "Hello World";
// example.dart
@JS()
external String get greetings;
// main.dart
import 'example.dart' as example;
void main(){
  print(example.greetings) // prints 'null'
}

Working code:

// example.js
var greetings = "Hello World";
// main.dart
import 'example.dart' as example;
void main(){
  print(example.greetings) // prints 'Hello World'
}
natebosch commented 4 years ago

The problem is that with var the variable is available through window., but it isn't available with either let or const. https://stackoverflow.com/a/28776236/374798

We either need to document that these aren't accessible through Dart, or find a new way to access them.

cc @sigmundch

MarvinHannott commented 4 years ago

The problem is that with var the variable is available through window., but it isn't available with either let or const. https://stackoverflow.com/a/28776236/374798

We either need to document that these aren't accessible through Dart, or find a new way to access them.

cc @sigmundch

You are right, that didn't occure to me. And the same goes for ES2015 classes as I have noticed. It might be worth mentioning that only ES5 is fully supported and that Babel should be used.

sigmundch commented 4 years ago

minor update: I just updated the title and merged a few issues here to track them all in one place.

ES6 let, class, and const are not hoisted to the window scope so they are not visible to JS-interop. We need to properly document what are the recommended ways to access these symbols.

See also #40102, #35737

You are right, that didn't occure to me. And the same goes for ES2015 classes as I have noticed. It might be worth mentioning that only ES5 is fully supported and that Babel should be used.

Note that you shouldn't have to convert to ES5 with babel, it would be sufficient to expose the symbols that can't be hoisted. For example:

class MyClass {...}
window.MyClass = MyClass;

would work too.

RaghuMudem commented 3 years ago

I am looking for the solution for the issue #35737. It directed me here for more discussion on the issue. Is there any suggestion for that issue solution.

Sorry actual issue has been marked as closed. So keeping the comment here for solution.

Thanks in advance.

sigmundch commented 3 years ago

@RaghuMudem - the example in #35737 was exporting an ES6 class:

export class HelloWorld {
  sayHello() {
      return "HelloWorld"
  }
}

So the suggestion in https://github.com/dart-lang/sdk/issues/39995#issuecomment-643000112 applies here. That is, add in JavaScript an extra line that actually exposes the class definition as a top-level member on the window:

export class HelloWorld {
  sayHello() {
      return "HelloWorld"
  }
}
window.HelloWorld = HelloWorld;

This will make it possible for JS-interop to find that definition.

Was this the pattern you were looking for?

RaghuMudem commented 3 years ago

Hi @sigmundch, Thanks for your reply. I am loading a html content with video element in a IFrameElement using HtmlElementView. In my Javascript method, i am trying to get the video element by document.getElementById('video');. But it always giving me null.

And the same video is able to find initApp method. initApp method is called with the following code.

document.addEventListener('DOMContentLoaded', initApp);

And my complete javascript code is here. I also added comments to make it clear on the problematic areas.

var PlayerHandler = /** @class */ (function () {
    function PlayerHandler() {}

    PlayerHandler.prototype.bark = function () {
        console.log("bark ..");
    };
        PlayerHandler.prototype.videoTest = function (url) {
              const video = document.getElementById('video'); **// This is always return null.**
              console.log("videoTest video "+video +" window.video "+window.video); **//window.video also null. Wanted video object here**
        };
    return PlayerHandler;
}());
var myModule = { PlayerHandler: PlayerHandler };
window.PlayerHandler = PlayerHandler;

function initApp() {
     initPlayer();
}

async function initPlayer() {
  // Create a Player instance.
  const video = document.getElementById('video'); **//This always returns video element**
  window.video = video;
  console.log('initPlayer video '+video);

}
document.addEventListener('DOMContentLoaded', initApp);

Here is the console logs:

initPlayer video [object HTMLVideoElement]
bark ..
videoTest video null window.video null

Would be very helpful if anyone can suggest me to get the video element in the places mentioned above.

RaghuMudem commented 3 years ago

Hi @sigmundch, Thanks for your reply. I am loading a html content with video element in a IFrameElement using HtmlElementView. In my Javascript method, i am trying to get the video element by document.getElementById('video');. But it always giving me null.

And the same video is able to find initApp method. initApp method is called with the following code.

document.addEventListener('DOMContentLoaded', initApp);

And my complete javascript code is here. I also added comments to make it clear on the problematic areas.

var PlayerHandler = /** @class */ (function () {
    function PlayerHandler() {}

    PlayerHandler.prototype.bark = function () {
        console.log("bark ..");
    };
        PlayerHandler.prototype.videoTest = function (url) {
              const video = document.getElementById('video'); **// This is always return null.**
              console.log("videoTest video "+video +" window.video "+window.video); **//window.video also null. Wanted video object here**
        };
    return PlayerHandler;
}());
var myModule = { PlayerHandler: PlayerHandler };
window.PlayerHandler = PlayerHandler;

function initApp() {
     initPlayer();
}

async function initPlayer() {
  // Create a Player instance.
  const video = document.getElementById('video'); **//This always returns video element**
  window.video = video;
  console.log('initPlayer video '+video);

}
document.addEventListener('DOMContentLoaded', initApp);

Here is the console logs:

initPlayer video [object HTMLVideoElement]
bark ..
videoTest video null window.video null

Would be very helpful if anyone can suggest me to get the video element in the places mentioned above.

I found a workaround for this. Posting the clue here for others who has the similar problem.

Description about the solution:

  1. I have wrapped my iframe with in html.DivElement(). I have given a id for the html.DivElement(). Example: final div = html.DivElement()..id = 'player-iframe-root';
  2. Given a id for iframe also. Lets say iframe id is html.IFrameElement()..id = 'player-iframe'
  3. Inside iframe my video element look like. <video id="video" controls autoplay></video>
  4. Here is my complete div with iframe code written in dart program:
_element = html.IFrameElement()
      ..style.border = 'none'
      ..id = 'player-iframe'
      ..srcdoc =
      """ <!DOCTYPE html>
          <html>
            <head>
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <style>
            video {
              width: 100%;
              height: auto;
            }
            </style>
            </head>
            <body style="background-color:white;">
              <video id="video" controls autoplay></video>
                            <!-- Your application source: -->
              <script src="../../../web/player-handler.js"></script>
            </body>
          </html>
      """;
    final div = html.DivElement()..id = 'player-iframe-root';

    final container = div..append(_element);
    // ignore:undefined_prefixed_name
    ui.platformViewRegistry.registerViewFactory(
      'player-$textureId',
          (int viewId) => container,
    );
  1. Added this content into HtmlElementView widget in my dart widget.
    Widget build(BuildContext context) {
    return Scaffold(
      appBar: null,
      body: Container(
        child: HtmlElementView(
              viewType: 'player-$textureId',
            ),
      ),
    );
    }
  2. And fetching the video element inside my js intero call on videoTest() method call.

    class PlayerHandler {
    
    videoTest() {
    /*The tree order of the html should be <div id=player-iframe-root><iframe id=player-iframe><video/></iframe></div> .
    And this html is created dynamically in dart code as mentioned in point 4 */
         var div = document.getElementById('player-iframe-root');
         var iframe = div.querySelector("#player-iframe");
         this._video = iframe.contentWindow.document.getElementsByTagName("video")[0]; **//Video element available here and ready to use :)**
        console.log('this._video '+this._video);
    }
    }
    window.PlayerHandler = PlayerHandler;
  3. My js-interop is here for reference.
    
    @JS()
    library playerHandler;

import 'package:js/js.dart';

@JS("PlayerHandler") class PlayerHandler { external factory PlayerHandler(); external void videoTest(); }


Hope this clue can help for someone who is working for **Flutter-web-iframe-video-js-interop** combination :)
PoojaAggarwal01 commented 3 years ago

I am getting dart.global.MyClass is not a constructor if I add script elements dynamically but works fine if script is added directly to index.html.

I have requirement to add scripts dynamically only. Can you please help me with the same?

sigmundch commented 1 year ago

@MaryaBelanger @srujzs - FYI - another user hit this issue again this week. It may be worth adding a section about this in some of our upcoming site changes (maybe under some kind of FAQ?)