As I progress with the notes I took driving The Thing Is ... and I approach completion, slowly I reach the point were we need to look at more specific code examples, and it is time therefore for the great unveiling ... The code rests on github:
https://github.com/bernd-wechner/Copy-with-Style
and we will digress quickly from the exploration of detecting changes in the DOM, to introduce the basic implementation and cover some of the JavaScript curios uncovered in implementing it. These are notes I took while implementing it, meaning they constituted learning for me, and struck me as not very intuitive warranting a specific mention and memory jog).
A JavaScript Class
It's convenient to capture feature-provision such as this (copying an HTML buffer to the system clipboard with styles intact) in a JavaScript Class. Convenient because classes encapsulate code and isolate it from the broader JavaScript environment providing a clean interface.
JavaScript classes are fairly customary and familiar in modern languages, and Javascript offers them in a fairly familiar manner. There's a great introduction on mozilla.org.
The main surprise we touched on in this little library is that members are not declared in the customary manner. That is:
-
Properties need no
var
,const
orlet
are just assigned a value. In a truly Pythonesque manner. -
Methods also need no
function
in fact won't accept them. They are declared otherwise, just a functions are, but without thefunction
keyword. Go figure.
I have to admit, this is a most puzzling feature of JavaScript syntax if ever I have seen one.
Private Members
Private members are a relative (and welcome) newcomer to JavaScript, and are declared simply by using #
as the first character in the name of the property or method. Alas being relatively new this causes havoc with some of the linters, beautifiers and editors out there. Let's hope that goes away soon (confident it will). For the record I use Eclipse with Wild Web Developer, the Atom editor as beautifier and online JavaScript linters.
Still privacy is a thing well worth requesting and respecting and one of features Python classes lack. It is a welcome additions to JavaScript.
this
is a Thing
this
is a keyword with different meanings in different contexts. As far as we were concerned, inside a class it refers to the instance of the class. And so this.property
is how to refer to a property of this instance of the class.
Unfortunately in event handlers, this
is an event instance, and if the handler is a class method, well ... when it refers to this.property
no joy is had, as this is an event and not an instance of the class. Python handles "this" much more elegantly in my opinion making the instance references explicit arguments to methods, but this is not a piece on Python.
The solution in JavaScript is the bind()
method, which functions have, and which provides the value for this
.
So method
passed as an event handler will have this
holding an event, not the class instance, but method.bind(object)
will have object
as this
when it runs. Of course, if we want it have the class instance as this
it's simply method.bind(this)
which is a kind of JavaScript custom it turns out.
This (double entendre inescapable) is seen clearly in the #copy_to_clipboard()
method in Copy With Style but also in a couple of schedulers and an observer.
This then is the heart of Copy With Style in a sense:
#copy_to_clipboard() {
function handler(event) {
if (this.debug) console.log(`copy event handler triggered...`)
event.clipboardData.setData('text/html', this.HTML);
event.clipboardData.setData('text/plain', this.text);
event.preventDefault();
document.removeEventListener('copy', handler, true);
}
document.addEventListener('copy', handler.bind(this), true);
document.execCommand('copy');
}
As you can see, it wants this.HTML
and this.text
which are Copy With Style instance properties set by this.prepare_copy()
and so we make the class instance available in this
by passing not handler
but handler.bind(this)
to the copy event listener.
That is another JavaScript curio that takes some getting used to. The binding of this to this ...
async
is a Thing Too
The async
keyword is rather misleading alas. As JavaScript is essentially single threaded and it alludes to, well, multithreading or multiprocessing but it is not to be. As discussed above, all async does is flag a function as a Promise. That is, a function runs and returns, but an async function just returns having put the function on the end of an event loop queue. It will run then, as soon as possible after everything else that was queued runs. What this does is free the event loop up to run the functions already ahead of this newly queued async function.
But if we await
the function when we call it, then it does not return immediately. It queues the async function as before, but not before telling the promise to call back right here when it's done. That is await
provides a means to continue running at this line as if we'd never left (state preserved), while permitting the queued promise to run (by saving its state and returning).
To summarise, if we have two functions:
function normal() { console.log("I'm normal"); return "I'm done"; }
async function oddball() {console.log("I'm an oddball"); return "I'm done"; }
then calls to these functions operate as follows:
result = normal()
runs normal()
and returns when it's done. result contains "I'm done".
result = await oddball()
puts oddball()
at the end of the event loop queue, and waits for its turn to come round, and for it to run and then returns with result containing "I'm done" (the mechanism of that wait is though, to save state, and register with the promise a call-back to this line with state intact when it's done - creating the local illusion of a blocking call while actually returning - and it's because await returns, and only returns a promise that it can only be used in async
functions, which are functions that return promises.
result = oddball()
puts oddball()
at the end of the event loop queue, and returns immediately, with result
now a Promise object. Not much use if we want its returned value. We can't get hold of it's returned value this way.
We can though, get hold of the returned value of the function oddball()
... which is "I'm done" by attaching a function to it via .then(function_to_call_with_return_value_of_oddball_as_an argument)
sort of like this:
oddball().then(return_value => { result = return_value })
In case it's not clear this return_value => { result = return_value }
is just an anonymous function, we could as well write the above line:
function noname(return_value) { result = return_value; }
oddball().then(noname);
This will set the value of result
when oddball()
returns.
There's one problem. If we don't give oddball()
a chance to run first it won't complete. For example if we do this:
let result = "nothing"
function noname(return_value) { result = return_value; }
oddball().then(noname);
console.log(result)
Then on the console we will see:
nothing
I'm an oddball
That is, result, has the value "nothing" not the value that oddball()
returned ("I'm done") because we didn't give oddball()
a chance to run before we logged the result to the console. We need to pause our run, put ourselves on the event queue, let oddball()
run then if we check result
it will contain "I'm done". And that looks like this:
result = "nothing"
function noname(return_value) { result = return_value; }
result_is_ready = oddball().then(noname);
await result_is_ready;
console.log(result)
Where await
is the "pause" that puts our continuation onto the end of the event queue, and returns, and (calling code willing) lets oddball()
run. To wit, this will produce on the console:
I'm an oddball
I'm done
I wrote "Calling code willing" because await
returns, all it can do is cede control to whoever called the function it is in. it is not until this bubbled to the top of the JavaScript processing stack if you will that JavaScript is free to run tasks on the event queue.
That is consider this:
result = "nothing"
async function outer() {
async function inner() {
function noname(return_value) { result = return_value; }
result_is_ready = oddball().then(noname);
await result_is_ready;
console.log(result)
}
inner();
while (true) {}; // An infinite - neverending - loop
}
outer();
// JavaScript is done now.
oddball()
never runs, and it never calls back to the state-preserved await
because outer() never returns and the JavaScript engine remains occupied evermore with while (true) {};
. And while that's an extreme and artificial example, the point is that any code in place of that infinite loop or analogously placed after the promise was queued but running before JavaScript finished (like any code after outer()
and before // JavaScript is done now.
) will run before oddball()
ever does. Because JavaScript has to fall idle before it will take the next task off the queue. Running code always has the option and runs the risk, of hogging all of the JavaScript engine's attention!
An voila! This may appear convoluted, and it certainly is a little, but the central idea is that a Promise schedules the function to run by placing it at the end of an event queue, and when it runs, we can ask it to run a function of our choosing that receives its return value as an argument. But unless we take a break and pop ourselves onto the same event queue behind the promised function we will never see it fulfilled, this is the single-threaded nature of JavaScript on display.
In summary, async
means a function no longer returns its return value, but instead returns a Promise, and the return value can be accessed either by using await
or by offering the promise a function to call with the return value as an argument (using .then(function)
).
async
chains 🔗
Because an async function does not return the function's return value but a Promise object, to get the result we must await
it. Therein lies a small domino effect. await
can only be used in an async
function. That's nice little JavaScript rule there, a tautology even, as await returns a promise and async
is the keywords that flags a function as a promise returning function (and not a value returning function).
Which all means that if we want a result from an async
function we can only get it in an async
function (i.e. we can only await a result in an async function). And so one async begets another and they chain ... and next thing you know it all your functions are async
;-).
In Copy_With_Style, this chain of async
bubbles all the way up to the button click handlers copy()
and to_clipboard()
. At which point we breathe a sigh of relief because the click handler can accept an async
function, in no small part because it really doesn't care about a return value.
I see Jim provided a little insight into that on Stack Overflow. Namely that this area is not well documented and standardised to begin with (i.e. what the return value of event handlers does if anything). Either way we can take for granted and be thankful that the click handler is happy to have an async function.
Still, what does that mean? Consider this, click the button and your handler is called and runs. That's the normal scenario.
Now with an async function, click the button and the async function runs. But all that means is it puts the actual function on to the end of a event queue and it will run when its turn comes around. In short it doesn't run immediately and the return value isn't accessible in the same way.
In practice this means very little because, as it happens, JavaScript has a dedicated queue for promises, and resolves all the code generated promises before it looks at UI events, which have their own queue. These are generally called the micro and macro task queues, with the macro queue only consulted when the micro queue is empty and the micro queue only consulted when the JavaScript engine falls idle (has nothing else to do), and promises generally occupying the micro queue and UI events the macro queue.
Which is why, as we discussed earlier and see in the implementation of:
#defer_to_UI(how_long = 0) {
return new Promise(resolve => setTimeout(resolve, how_long));
}
A simple promise is not sufficient to see UI events processed, because promises are queued on the micro queue and executed before any UI events on the macro queue get to be handled. Which is where the browser supplied setTimeout()
function comes in which queues a function on the macro queue! In fact it should be names PutOnMacroQueue(function)
...
The Object as an Argument
Discussed under Bringing it All Together essentially the way JavaScript supports (or better said does not support) named function arguments with default values and optional provision by the caller (all Python does natively) it requires you to replace all arguments with a single object that has properties. And there's a syntax that is tolerable but fluffy.
The Width of <progress>
I'm pleased to implement a <progress>
element here, as the lack of one had been a long standing gripe with HTML with umpteen 3rd party solutions. Finally standardised and stylable. It's great to see HTML and JavaScript evolving.
To use it sensibly though we wanted to restrict updates to then and when it would cause a visible change, and so we needed a measure of elements to process (cycles in our function) per pixel of progress bar width. The former we know, the latter is tricky.
Turns out it has no property to tells us that. The nearest thing available is .clientWidth which strangely includes padding, and so we have to write:
#bar_width(progress_bar) {
const style = window.getComputedStyle(progress_bar);
return progress_bar.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight)
}
removing the padding. But to make sure I got it right I checked the width this returned and took screenshots of the progress bar in Chromium and Firefox and lo and behold both returned the self same width (133 pixels) and I measured the bar on Firefox as 133 pixels (tick ✔) but Chromium renders it 146 pixels wide (bzzzt ✘). Go figure. Little can done about that, but it seems browser support in this space might be a little variable if, in the ideal, close to the mark. And for my purposes the smaller value (the consistently returned 133) is fine as not only is the difference small, this is the conservative value yielding more cycles per pixel and fewer updates.
Top comments (0)