For Wt 4.15.0
1. Prerequisites
This introduction assumes that you have a reasonable understanding of Wt itself, in particular its stateful session model and the widget concept. If you haven’t done so yet, you may want to go through the Wt tutorial first.
2. Introduction
Since Wt abstracts away the JavaScript layer, it can be difficult to figure out how to use existing JavaScript libraries with Wt. This tutorial will show you some of the tools Wt provides to make this easier, as well as giving you a concrete example.
The example we will use shows a way to use the selection module from D3.js directly in Wt in order to create SVG images.
This example is included in the Wt distribution, in
examples/d3-selection.
3. Loading the library
Before being able to use the library, we need to load it. In Wt, this can be done using the WApplication::require() method, which will make the client load the script at the given URL.
In our case, we will load the D3.js library that is stored in the
docroot of the application, in the d3.v7.min.js file.
WApplication* app = WApplication::instance();
// Load D3.js and WD3Widget.js scripts if not already loaded
std::string d3Filename = "d3.v7.min.js";
app->require(d3Filename);
WApplication::require() will only load the script once per client session, so you can call this method multiple times without worrying about loading the script again. This allows to call this method during the first rendering of a widget, allowing you to load the library only when a widget that uses it is rendered.
4. Using JavaScript on widgets
4.1. Executing JavaScript code
Now that we know how to load the library, we need a way to execute JavaScript code from Wt. To do so, you can either use the WApplication::doJavaScript() method, which will execute the given JavaScript code on the client, or the WWidget::doJavaScript() method, which will execute the given JavaScript code the next time the widget is rendered or updated. This prevents the JavaScript code from being executed before the widget is actually rendered, or after it has been removed from the client side, which would cause errors in case the JavaScript code expects the widget to still be present.
In your JavaScript code, you will likely want to access the DOM element of the widget. This is possible by using the WWidget::jsRef() method, which returns a JavaScript expression that evaluates to the DOM element of the widget.
4.2. JavaScript members
Most of the time, you will want to have a JavaScript object that handles the interaction between the library and the widget, as it makes it easier to manage multiple instances of the widget class. In Wt, you can set such a JavaScript object using the WWidget::setJavaScriptMember() method. This method takes 2 arguments: the name of the member, and the JavaScript expression that evaluates to the object you want to set.
|
You should not use names that start with |
You can then call functions of the JavaScript member, using the WWidget::callJavaScriptMember() method, or access it directly in JavaScript since it is now a member of the DOM element of the widget (which is itself accessible using WWidget::jsRef()).
In our case, we set a d3WidgetObj member of the widget during its
first rendering, in the defineJavaScript() method (which is also
where we load the JavaScript library).
void WD3Widget::defineJavaScript()
{
// Only define JS once
if (jsDefined_) {
return;
}
WApplication* app = WApplication::instance();
// Load D3.js and WD3Widget.js scripts if not already loaded
std::string d3Filename = "d3.v7.min.js";
app->require(d3Filename);
app->require("WD3Widget.js", "WD3Widget");
// Instantiate the WD3Widget JS object for this widget
WStringStream ss;
ss << "new WD3Widget("
<< app->javaScriptClass() << ","
<< jsRef() << ","
<< width().toPixels() << ","
<< height().toPixels() << ");";
setJavaScriptMember("d3WidgetObj", ss.str());
jsDefined_ = true;
}
As you can see, we do also load the WD3Widget.js file. This file
contains the definition of the WD3Widget class that we assign to the
JavaScript member. This allows to avoid having to write all of the
JavaScript code in a string, which is cumbersome and error-prone.
4.3. Sending signals in JavaScript
Note that we pass the
WApplication::javaScriptClass()
to the WD3Widget constructor. This is the name of the main
JavaScript class that Wt uses, and its emit() methods can be used
to send signals from the client side to the server side.
To do so, you first need to have JSignal. This class allows you to define a signal that can be emitted from the client side, and that can be connected to a slot on the server side.
The signal is tied to a widget and has a name that is used to identify it. It also supports up to 6 arguments that can be passed to the slot when the signal is emitted.
Once we have a JSignal, we can generate the JavaScript code that will emit the signal with the method JSignal::createCall(), or the method JSignal::createEventCall() if you also want to receive the event that triggered it in the parameters.
|
A widget cannot have multiple signals with the same name. |
5. Interfacing with the library
You should now have all of the tools necessary to use JavaScript in Wt. However, you will still need to write the JavaScript code that interfaces with the library you want to use. How you do so will highly depend on the library itself, and what you want to do with it. We will however, give you a few tips on how to do so while explaining the example with the D3 library.
5.1. Choosing the widget type
First, we should extend WWidget or one of its subclasses. While this is not strictly necessary, it is a good idea to do so since it is an object-oriented design.
In our case, we will extend the WInteractWidget class as nothing we do not want to use any functionality from another widget, but this will depend on what you want to do with the library. For instance, the WTextEdit, that uses the TinyMCE library to provide a rich text editor, extends the WTextArea class, and the WLeafletMap class, that uses the Leaflet library to create an interactive map, extends the WCompositeWidget class.
5.2. Interface logic
The widget class is the interface between Wt and the library. It should provide the necessary methods to interact with the library.
In our example, we want to be able to draw an SVG image using D3 selections. For this reason, we are going to design the widget like a WPaintedWidget. The idea is to be able to create and manipulate D3 selections inside of an update function, which is called when the widget is rendered or updated.
// WD3Widget: A widget for embedding D3.js visualizations in Wt
class WD3Widget: public WInteractWidget
{
// Constructor
WD3Widget();
// Returns the root SVG selection
Selection body() { return Selection(svgRef(), this); }
protected:
// D3 initialization and update hooks
virtual void initializeD3();
virtual void updateD3();
// Returns the DOM element type (DIV)
DomElementType domElementType() const override { return DomElementType::DIV; }
// Renders the widget and applies JS updates
void render(WFlags<RenderFlag> flags) override;
private:
long long lastObjectId_; // Last used D3 object id
bool jsDefined_; // Whether JS is defined
JSignal<long long> selectionSignal_; // Selection signal
WStringStream jsUpdates_; // JS update stream
// Defines the required JavaScript for the widget
void defineJavaScript();
// Returns the SVG JS reference
std::string svgRef() const { return jsRef() + ".d3WidgetObj.body()"; }
// Returns a JS string literal or empty string
static std::string jsStringLiteralOrEmpty(const std::string& str);
};
WD3Widget::WD3Widget()
: lastObjectId_(0),
jsDefined_(false),
selectionSignal_(this, "selectionSignal")
{ }
void WD3Widget::initializeD3()
{ }
void WD3Widget::updateD3()
{ }
void WD3Widget::render(WFlags<RenderFlag> flags)
{
// Ensure the required JavaScript is defined
defineJavaScript();
// Call initializeD3() on full render
if (flags.test(RenderFlag::Full)) {
initializeD3();
}
updateD3();
// Execute the JS updates if any
if (!jsUpdates_.empty()) {
doJavaScript(jsUpdates_.str());
jsUpdates_.clear();
}
WInteractWidget::render(flags);
}
void WD3Widget::defineJavaScript()
{
// Only define JS once
if (jsDefined_) {
return;
}
WApplication* app = WApplication::instance();
// Load D3.js and WD3Widget.js scripts if not already loaded
std::string d3Filename = "d3.v7.min.js";
app->require(d3Filename);
app->require("WD3Widget.js", "WD3Widget");
// Instantiate the WD3Widget JS object for this widget
WStringStream ss;
ss << "new WD3Widget("
<< app->javaScriptClass() << ","
<< jsRef() << ","
<< width().toPixels() << ","
<< height().toPixels() << ");";
setJavaScriptMember("d3WidgetObj", ss.str());
jsDefined_ = true;
}
std::string WD3Widget::jsStringLiteralOrEmpty(const std::string& str)
{
return str.empty() ? "" : jsStringLiteral(str);
}
We want to be able to create and manipulate D3 selections inside of
the updateD3() and initializeD3() methods, similarly to how
you would do it JavaScript. For this, we create a class that represents
a D3 selection, and add the necessary methods to it and to the widget
class to manipulate those selections.
First, we create a D3Object class that represents a D3 object and
its state in JavaScript. This class is a generic class that would be
used to represent any D3 object, and handle how those are added or
removed from the JavaScript state. Since we will only use selections in
this example, this could have been directly implemented in the
Selection class, but this allows for a clearer explanation in this
tutorial.
// D3Object: Represents a D3 object and its JS state
struct D3Object
{
public:
// Constructor
D3Object(const std::string& js = "", WD3Widget* widget = nullptr);
// Copy/move constructors and assignment
D3Object(const D3Object& other);
D3Object& operator=(const D3Object& other);
D3Object(D3Object&& other);
D3Object& operator=(D3Object&& other);
// Destructor
~D3Object();
// Returns true if the object is valid
bool isValid() const { return widget_; }
operator bool() const { return isValid(); }
// Persists the D3 object in JS
void persist() const;
// Pushes JS updates to the widget
void pushUpdates() const;
WD3Widget* widget_; // Parent widget
mutable std::shared_ptr<D3ObjectRef> savedRef_; // Saved JS ref
mutable std::string js_; // JS code
mutable WStringStream jsUpdates_; // JS updates
};
WD3Widget::D3Object::D3Object(const std::string& js, WD3Widget* widget)
: widget_(widget),
savedRef_(nullptr),
js_(js)
{ }
WD3Widget::D3Object::D3Object(const D3Object& other)
: widget_(other.widget_),
savedRef_(other.savedRef_)
{
// Ensure the other object is persisted before copying its JS reference
other.persist();
js_ = other.js_;
}
WD3Widget::D3Object& WD3Widget::D3Object::operator=(const D3Object& other)
{
widget_ = other.widget_;
savedRef_ = other.savedRef_;
// Ensure the other object is persisted before copying its JS reference
other.persist();
js_ = other.js_;
return *this;
}
WD3Widget::D3Object::D3Object(D3Object&& other)
: widget_(std::move(other.widget_)),
savedRef_(std::move(other.savedRef_)),
js_(std::move(other.js_))
{
jsUpdates_ = std::move(other.jsUpdates_);
// Reset other's jsUpdates_ to avoid pushing updates at deletion
other.jsUpdates_ = WStringStream();
}
WD3Widget::D3Object& WD3Widget::D3Object::operator=(D3Object&& other)
{
widget_ = std::move(other.widget_);
savedRef_ = std::move(other.savedRef_);
js_ = std::move(other.js_);
jsUpdates_ = std::move(other.jsUpdates_);
// Reset other's jsUpdates_ to avoid pushing updates at deletion
other.jsUpdates_ = WStringStream();
return *this;
}
WD3Widget::D3Object::~D3Object()
{
pushUpdates();
}
void WD3Widget::D3Object::persist() const
{
// Push any pending JS updates
pushUpdates();
// If the object is valid and not already persisted, create a new
// D3ObjectRef and update js_ to the new JS reference
if (isValid() && (!savedRef_ || savedRef_->jsRef() != js_)) {
savedRef_ = std::make_shared<D3ObjectRef>(js_, widget_);
js_ = savedRef_->jsRef();
}
}
void WD3Widget::D3Object::pushUpdates() const
{
// If there are pending JS updates, append them to the widget's
// jsUpdates_ stream
if (isValid() && !jsUpdates_.empty()) {
widget_->jsUpdates_ << js_ << jsUpdates_.str() << ";";
jsUpdates_.clear();
}
}
How this class works is quite simple. It has a js_ member that
contains the JavaScript code that allows to access the D3 object (or
create it if it does not exist yet), and it has a jsUpdates_ member
that collects the code for the updates that needs to be executed on the
client side to this object.
The updates are pushed to the WD3Widget’s `jsUpdates_ member
when the changes need to be executed, with the pushUpdates()
method. This method is called when the object is destroyed, copied, or
when the persist() method is called, in order to ensure that the
updates are pushed in the right order.
Since we want to be able to use the same selection across multiple
updates of the widget, we need a way to store them on the JavaScript
side (as a Selection is different depending on the state the SVG
element was in when it was created). For this reason, we added the
possibility to store the selection using the persist() method.
To manage the selections that are saved on the JavaScript side, we
decided to manage it via its own class, D3ObjectRef, which saves
the D3 object on the JavaScript side and automatically deletes it when
it is not needed anymore. The D3 object does therefore create a new
D3ObjectRef when persist is called for the first time, and stores
it in a shared pointer so that it can be shared with its copies, only
deleting the reference when the last copy is destroyed.
|
This way of managing the objects is used here to make the use of
Selections look like it does in D3.js. This makes sense here because a
Selection is not meant to be added to the image, and |
// D3ObjectRef: Reference to a D3 object managed in JS
class D3ObjectRef: public WObject
{
public:
// Constructor: saves a D3 object reference
D3ObjectRef(const std::string& obj, WD3Widget* widget);
// Destructor: removes the D3 object reference
~D3ObjectRef();
D3ObjectRef(const D3ObjectRef&) = delete;
D3ObjectRef& operator=(const D3ObjectRef&) = delete;
D3ObjectRef(D3ObjectRef&&) = delete;
D3ObjectRef& operator=(D3ObjectRef&&) = delete;
// Returns the JavaScript reference string
std::string jsRef() const { return jsRef_; }
// Returns the D3 object id
long long d3Id() const { return id_; }
// Signal emitted when this object is clicked
Signal<>& clicked();
private:
WD3Widget* widget_; // Parent widget
bool connected_; // Whether signal is connected
std::string jsRef_; // JavaScript reference
long long id_; // D3 object id
Signal<> clicked_; // Clicked signal
// Emits clicked_ if the id matches
void emitIfTargeted(long long id);
};
WD3Widget::D3ObjectRef::D3ObjectRef(const std::string& obj, WD3Widget* widget)
: widget_(widget),
connected_(false)
{
// Assign a unique id to the D3 object
id_ = widget->lastObjectId_++;
// Instruct the JS side to save the D3 object reference
widget_->jsUpdates_ << widget_->jsRef() << ".d3WidgetObj.saveD3Obj(" << id_ << "," << obj <<");";
// Build a JS reference string for later use
jsRef_ = widget_->jsRef() + ".d3WidgetObj.getD3Obj(" + std::to_string(id_) + ")";
}
WD3Widget::D3ObjectRef::~D3ObjectRef()
{
// Instruct the JS side to remove the D3 object reference
widget_->jsUpdates_ << widget_->jsRef() << ".d3WidgetObj.removeD3Obj(" << id_ << ");";
}
Signal<>& WD3Widget::D3ObjectRef::clicked()
{
// Connect the selectionSignal_ to this object's emitIfTargeted if not
// already connected
if (!connected_) {
widget_->selectionSignal_.connect(this, &D3ObjectRef::emitIfTargeted);
connected_ = true;
}
// Return the clicked_ signal, which can be used to listen for click
// events on this D3 object
return clicked_;
}
void WD3Widget::D3ObjectRef::emitIfTargeted(long long id)
{
// Emit the clicked_ signal if the provided id matches this object's id
if (id == id_) {
clicked_.emit();
}
}
You can see that the D3ObjectRef uses an id to be able to uniquely
identify each D3 object on the JavaScript side. Those are stored in an
object saved in "d3WidgetObj" JavaScript member of the WD3Widget.
In the JavaScript code, the object is called D3Objs.
class WD3Widget {
constructor(APP, widget, width, height) {
this.svgWidth = width;
this.svgHeight = height;
this.D3Objs = {};
this.svg = d3.create("svg")
.attr("width", this.svgWidth)
.attr("height", this.svgHeight)
.attr("overflow", "hidden");
widget.appendChild(this.svg.node());
this.saveD3Obj = function(id, obj) {
this.D3Objs[id] = obj;
};
this.getD3Obj = function(id) {
return this.D3Objs[id];
};
this.removeD3Obj = function(id) {
delete this.D3Objs[id];
};
this.body = function() {
return this.svg;
};
}
}
We can now finally talk about the Selection class in itself.
class Selection
{
public:
// Constructors
Selection();
Selection(const std::string& js, WD3Widget* widget);
// Returns true if the selection is valid
bool isValid() const { return obj_.isValid(); }
operator bool() const { return obj_; }
// D3 selection API methods
Selection& attr(const std::string& name, const std::string& value); // Set attribute
Selection& attr(const std::string& name, const JsObject& value);
Selection& classed(const std::string& name, bool value); // Set class
Selection& classed(const std::string& name, const JsObject& value = JsObject());
Selection& style(const std::string& name, const std::string& value, const std::string& priority = ""); // Set style
Selection& style(const std::string& name, const JsObject& value, const std::string& priority = "");
Selection& property(const std::string& name, const std::string& value); // Set property
Selection& property(const std::string& name, const JsObject& value);
Selection& text(const std::string& value); // Set text
Selection& text(const JsObject& value);
Selection append(const std::string& type); // Append element
Selection append(const JsObject& type);
Selection insert(const std::string& type, const std::string& before); // Insert element
Selection insert(const JsObject& type, const std::string& before);
Selection insert(const std::string& type, const JsObject& before = JsObject());
Selection insert(const JsObject& type, const JsObject& before = JsObject());
Selection remove(); // Remove element
Selection clone(bool deep); // Clone selection
Selection sort(const JsObject& compare = JsObject()); // Sort selection
Selection& order(); // Order selection
Selection& raise(); // Raise selection
Selection& lower(); // Lower selection
Selection data(const JsObject& data, const JsObject& key = JsObject()) const; // Bind data
Selection join(const JsObject& enter,
const JsObject& update = JsObject(),
const JsObject& exit = JsObject()); // Join selections
Selection join(const std::string& enter,
const JsObject& update = JsObject(),
const JsObject& exit = JsObject());
Selection enter() const; // Enter selection
Selection exit() const; // Exit selection
Selection& datum(const std::string& value); // Set datum
Selection& datum(const JsObject& value);
Selection merge(const Selection& other); // Merge selections
// D3 selection traversal
Selection select(const std::string& selector = "") const;
Selection selectAll(const std::string& selector = "") const;
Selection selectChild(const std::string& selector = "") const;
Selection selectChildren(const std::string& selector = "") const;
Selection filter(const std::string& filter) const;
Selection& selection() { return *this; } // Return self
Selection& each(const JsObject& callback); // For each
Selection& on(const std::string& event, const JsObject& callback, const JsObject& options = JsObject()); // Event handler
// Call a JS function with arguments on the selection
template<typename... Args>
Selection& call(const JsObject& callback, const Args&... args);
Selection& persist(); // Persist selection
void pushUpdates() const; // Push JS updates
Signal<>& clicked(); // Clicked signal
private:
D3Object obj_; // Underlying D3 object
void throwIfInvalid() const; // Throws if invalid
Selection getModified(const std::string& modification); // Returns modified selection
// Helper to push JS arguments
template <typename T>
void pushArgs(const T& t) { obj_.jsUpdates_ << "," << t; }
void pushArgs(const std::string& s);
void pushArgs(const JsObject& obj);
void pushArgs(const bool& b);
void pushArgs(const Selection& selection);
template<typename T, typename... Args>
void pushArgs(const T& t, Args... args) {
pushArgs(t);
pushArgs(args...);
}
};
Most of what this class does is mirroring the way D3 selections work.
We won’t go into too much detail about most of the methods, which are,
for the most part, only adding the necessary JavaScript code to the
jsUpdates_ stream and saving the D3 object reference if needed.
Those functions are quite straightforward, and you can find them in the example code. We will therefore not show the full code of those functions here, but only show the one linked to the event handling.
WD3Widget::Selection& WD3Widget::Selection::on(const std::string& event, const JsObject& callback, const JsObject& options)
{
throwIfInvalid();
obj_.jsUpdates_ << ".on(" << jsStringLiteral(event) << "," << callback.js_;
if (!options.js_.empty()) {
obj_.jsUpdates_ << "," << options.js_;
}
obj_.jsUpdates_ << ")";
return *this;
}
Signal<>& WD3Widget::Selection::clicked()
{
throwIfInvalid();
// Persist the selection and retrieve the clicked signal from the
// saved D3ObjectRef
persist();
Signal<>& res = obj_.savedRef_->clicked();
// Make the click handler emit the selectionSignal_
WStringStream ss;
ss << "function(e) {"
<< obj_.widget_->selectionSignal_.createCall({"" + std::to_string(obj_.savedRef_->d3Id())})
<< "}";
on("click", JsObject(ss.str()));
return res;
}
We can see here that the clicked signal of a Selection is actually
triggered by the selectionSignal_ of the WD3Widget widget. Here,
only 1 JSignal is used to handle all the
clicks on all the selections, and we only emit the clicked signal if
the id of the selection matches the id passed as argument by the
JSignal. This is done to avoid having to
create a JSignal for each selection,
which would have to be managed by the WD3Widget. This also avoids
potential warnings due to JSignal being
emitted after having been deleted from the server side.
I would also like to point out the use of WWebWidget::jsStringLiteral(). This method ensures that the string is properly escaped for use in JavaScript, furthermore, it will escape any single quotes in the string, which is both useful and helps preventing JavaScript injection attacks. When using strings in JavaScript, you should therefore always use this method or one of its counterparts: DomElement::jsStringLiteral() or WString::jsStringLiteral() to avoid security issues.
6. Summary
This tutorial focused on integrating external JavaScript libraries into Wt applications, abstracting away the JavaScript layer.
We began by showing how to load external JavaScript. We then explained how to execute JavaScript code from Wt, and how to give it access to DOM elements. We also covered utilizing JavaScript members to manage library interactions directly on widgets. Additionally, we detailed how to send signals from JavaScript to C++.
The tutorial used a D3.js example to illustrate interfacing with a library. We demonstrated how to create a new widget that acts as an interface to D3’s selection API, efficiently managing JavaScript object lifetimes and updates.
We hope this tutorial gave you ideas on how to integrate any JavaScript functionalities you wish to use into your Wt applications.