Getting multiple instance of my Polymer.dart custom element turned out to be fairly easy. So easy, that it only took a four line hack. Tonight, I remove the hack so that I can do it for real. Even though the hack turned out to be small and self-contained, I originally suspected it to be a more significant problem which led me to explore the fix via a code spike. Now that I understand the problem, I have enough information to fix it the right way: by testing.
The hack had nothing to do with Polymer.dart, but with the manner in which I am loading JavaScript libraries for use via js-interop. To keep my code a black-box I do not load any JavaScript libraries with explicit
<script>
tags. Rather, I have my library dynamically append the necessary <script>
tags to the document. Once the load
event for the script fires, an instance of my ICE Code Editor object sees a Future completed so that the remainder of the initialization can occur.Last night's hack was to wait for a constant amount of time so that multiple instances would delay long enough for the JavaScript to be ready. That is a hack rather than a long-term solution because the delay needs to vary in time depending on latency in the network. Instead of a time-based delay, what I really need is a way to expose the when-javascript-ready Future to all instances of my
<ice-code-editor>
element.I suspect that there may be a more general solution that I need to extract for this, but I start with my specific problem by adding a second
<ice-code-editor>
custom element to my testing context:<head> <link rel="import" href="packages/ice_code_editor/polymer/ice_code_editor.html"> <script src="packages/polymer/boot.js"></script> <script src="packages/unittest/test_controller.js"></script> <script type="application/dart" src="test.dart"></script> </head> <body> <ice-code-editor src="embed_foo.html" line_number="42"></ice-code-editor> <ice-code-editor src="embed_bar.html" line_number="0"></ice-code-editor> <script src="packages/browser/interop.js"></script> </body>My existing tests that still set their expectations on the first element continue to pass:
PASS: [polymer] can embed code PASS: [polymer] can set line number PASS: [polymer] creates a shadow preview PASS: [polymer] creates an editor All 4 tests passed.The tests pass, but there is a mess of error output:
The errors are coming from no-longer hacked code. Rather than try to address the errors, I start with a test setting an expectation that the last
<ice-code-editor>
element on my test pages is using the embed_bar.html
source (instead of embed_foo.html
in the first element): group("multiple elements", (){
var it;
setUp((){
it = queryAll('ice-code-editor').last;
});
test("can embed code", (){
expect(
it.shadowRoot.query('h1').text,
contains('embed_bar.html')
);
});
});
That fails because there is only one functioning <ice-code-editor>
element on the page—the first one with embed_foo.html
as the source:PASS: [polymer] can embed code PASS: [polymer] can set line number PASS: [polymer] creates a shadow preview PASS: [polymer] creates an editor FAIL: [polymer] multiple elements can embed code Expected: contains 'embed_bar.html' Actual: 'The src is "embed_foo.html" (42)'To get this passing, I focus on the same area that got hacked up last night—the
_startAce()
method. This method is responsible for instantiating the JavaScript code that builds the editor part of ICE's editor/preview combo. Instead of adding a delay timer before instantiating the JavaScript objects, I wait for a soon-to-be-created Future, jsReady
to complete: _startAce() {
// ...
jsReady.then((_)=> _startJsAce());
// ...
}
Once the JavaScript is ready, then I start the JavaScript Ace objects.This
_startAce()
method is an instance method. I might have a hundred instances of the editor—each one of them needs its own Ace instance. But each and every instance of the editor needs to check for the same JavaScript ready. Enter a static getter:class Editor {
// ...
static Future get jsReady { /* ... */ }
// instance method
_startAce() {
jsReady.then((_)=> _startJsAce());
}
// ...
}
This is probably one of those cases in which a static method ought to be refactored into a separate singleton class. I'll suffer with the static method for now.In Dart, if an instance method cannot be found, a class method of the same name will be searched. This is how the call to the static
jsReady
getter method works from the _startAce()
instance method.For the
jsReady
function to return a Future, I need a Completer
object, which has both a future
property and a complete()
method which will fire any then()
callbacks attached to the Future: static Completer _waitForJS;
static Future get jsReady {
if (!_isAceJsAttached) {
_waitForJS = new Completer();
_attachScripts().
first.
onLoad.
listen((_)=> _waitForJS.complete());
}
return _waitForJS.future;
}
I already have a _isAceJsAttached
getter that will return true if a <script>
tag exists for the Ace source. If the Ace JavaScript files have not been attached to the document yet, I do so with _attachScripts()
and then listen for the load
event on the first of those <script>
tags. When loaded, I complete my new _waitForJs
completer, firing the then()
callbacks.And that does the trick:
unittest-suite-wait-for-done PASS: [polymer] can embed code PASS: [polymer] can set line number PASS: [polymer] creates a shadow preview PASS: [polymer] creates an editor PASS: [polymer] multiple elements can embed code All 5 tests passed. unittest-suite-successSo, in the end, a little future-based code goes a long way. Futures can be a little hard to explain (or maybe my brain is just fried), but they make for some easy-to-read, working code.
I think tomorrow that I will pick back up by taking this a step further. I would like to see if I can dynamically create and add my custom element to the document and test that it behaves as desired.
Day #894
No comments:
Post a Comment