I'm new to Web components and I am trying to create a very simple component to understand how it works. But I have a problem creating one. I followed the steps mentioned in both chrome and Mozilla docs but I still cant create one successfully and also couldn't find the problem.
class toolTip extends HTMLElement {
var msg = this.getAttribute('msg');
var value = this.getAttribute('value');
console.log(msg);
console.log(value);
this.innerHTML = msg + ' - ' + value;
}
customElements.define('mdm-tooltip', toolTip);
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Web Components</title>
</head>
<body>
<mdm-tooltip value='1st tooltip' msg='this the 1st tooltip created using WC'></mdm-tooltip>
<mdm-tooltip value='2nd tooltip' msg='I replaced the existing text'>Im the existing text</mdm-tooltip>
</body>
<script src="main.js" defer></script>
</html>
This is the error browser throws,
I'm running this code in Chrome V67.0.3396.99
Within a class, you need to define methods that actually contain executable code. In your case, your code looks a lot like initialization code, so a constructor seems appropriate.
class ToolTip extends HTMLElement {
constructor() {
let msg = this.getAttribute('msg');
let value = this.getAttribute('value');
console.log(msg);
console.log(value);
this.innerHTML = msg + ' - ' + value;
}
}
customElements.define('mdm-tooltip', ToolTip);
Also, one of the naming conventions in JavaScript is that classes should be pascal-cased (start with a capital letter).
J.P. ten Berge is mostly correct. But... According to the rules of a Web Component Constructor you can not and should not do several things:
https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-conformance
4.13.2 Requirements for custom element constructors
When authoring custom element constructors, authors are bound by the following conformance requirements:
A parameter-less call to super() must be the first statement in the constructor body, to establish the correct prototype chain and this value before any further code is run.
A return statement must not appear anywhere inside the constructor body, unless it is a simple early-return (return or return this).
The constructor must not use the document.write() or document.open(type, replace) methods.
The element's attributes and children must not be inspected, as in the non-upgrade case none will be present, and relying on upgrades makes the element less usable.
The element must not gain any attributes or children, as this violates the expectations of consumers who use the createElement or createElementNS methods.
In general, work should be deferred to connectedCallback as much as possible—especially work involving fetching resources or rendering. However, note that connectedCallback can be called more than once, so any initialization work that is truly one-time will need a guard to prevent it from running twice.
In general, the constructor should be used to set up initial state and default values, and to set up event listeners and possibly a shadow root.
Moving the code into the connectedCallback is a better plan:
class ToolTip extends HTMLElement {
connectedCallback() {
var msg = this.getAttribute('msg');
var value = this.getAttribute('value');
console.log(msg);
console.log(value);
this.innerHTML = msg + ' - ' + value;
}
}
customElements.define('mdm-tooltip', ToolTip);
<mdm-tooltip msg="help me" value="10"></mdm-tooltip>
But you can also change it to something like this:
class ToolTip extends HTMLElement {
constructor() {
super();
this._msg = '';
this._value = '';
}
static get observedAttributes() {
return [ 'value', 'msg' ];
}
connectedCallback() {
this._render();
}
attributeChangedCallback(attr, oldVal, newVal) {
if (oldVal !== newVal) {
this['_'+attr] = newVal; // This will set either `this._msg` or `this._value`
this._render();
}
}
_render() {
this.innerHTML = `${this._msg} - ${this._value}`;
}
}
customElements.define('mdm-tooltip', ToolTip);
setTimeout(() => {
var el = document.querySelector('mdm-tooltip');
el.setAttribute('value', 'Free');
el.setAttribute('msg', 'I like getting stuff for');
}, 1000);
<mdm-tooltip msg="Help Me" value="10"></mdm-tooltip>
In this example we use observedAttributes and attributeChangedCallback to see when either the value or msg attributes change. When they do we re-render the component.
You can also use properties when setting values:
class ToolTip extends HTMLElement {
constructor() {
super();
this._msg = '';
this._value = '';
}
static get observedAttributes() {
return [ 'value', 'msg' ];
}
connectedCallback() {
this._render();
}
attributeChangedCallback(attr, oldVal, newVal) {
if (oldVal !== newVal) {
this['_'+attr] = newVal; // This will set either `this._msg` or `this._value`
this._render();
}
}
get msg() {
return this._msg;
}
set msg(val) {
if (this._msg !== val) {
this._msg = val;
this._render();
}
}
get value() {
return this._value;
}
set value(val) {
if (this._value !== val) {
this._value = val;
this._render();
}
}
_render() {
this.innerHTML = `${this._msg} - ${this._value}`;
}
}
customElements.define('mdm-tooltip', ToolTip);
var el = document.createElement('mdm-tooltip');
el.value = 10;
el.msg = 'Five times two equals';
document.querySelector('.here').append(el);
setTimeout(() => {
var el = document.querySelector('mdm-tooltip');
el.value = [1,2,3];
el.msg = 'I like getting stuff for';
}, 2000);
<div class="here"></div>
In this example I added properties for value and msg. Now, instead of having to use setAttribute you can now set the properties directly and the properties do not need to be strings like the attributes do.
Related
I need to edit the "Speed gauge" widget to show zero value when certain condition is met. This action should be executed in the onDataUpdated() function.
This widget inherits methods from the "TbAnalogueRadialGauge" class, which contains an update() method. If I'm not wrong, this would be its implementation:
update() {
if (this.ctx.data.length > 0) {
const cellData = this.ctx.data[0];
if (cellData.data.length > 0) {
const tvPair = cellData.data[cellData.data.length -
1];
const value = tvPair[1];
if (value !== this.gauge.value) {
this.gauge.value = value;
}
}
}
}
TbAnalogueRadialGauge extends TbAnalogueGauge. TbAnalogueGauge exends TbBaseGauge, where update() is implemented.
It seems to access the this.gauge.value property to update the gauge value. However, when I try to access this property from widget development IDE, it turns out to be undefined.
self.onDataUpdated = function() {
self.ctx.gauge.update();
console.log(self.ctx.gauge.value) // output: undefined
}
Does anyone have any ideas about how to access this property?
You need the following
self.ctx.gauge.gauge.value
https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-autonomous-example:htmlelement
In the specification they've provided an example for Creating an autonomous custom element. However, they've left _updateRendering() method implementation for the readers.
class FlagIcon extends HTMLElement {
constructor() {
super();
this._countryCode = null;
}
static observedAttributes = ["country"];
attributeChangedCallback(name, oldValue, newValue) {
// name will always be "country" due to observedAttributes
this._countryCode = newValue;
this._updateRendering();
}
connectedCallback() {
this._updateRendering();
}
get country() {
return this._countryCode;
}
set country(v) {
this.setAttribute("country", v);
}
_updateRendering() {
// Left as an exercise for the reader. But, you'll probably want to
// check this.ownerDocument.defaultView to see if we've been
// inserted into a document with a browsing context, and avoid
// doing any work if not.
}
}
An issue has been raised to provide the remaining implementation for better understanding of the topic and quickly move on.
Issue: https://github.com/whatwg/html/issues/3029
What code can we put there to get the required functionality?
Your solution fails, once the flag is set you can never change the value.
That "exercise" is old.. very old.. and contrived to show everything Custom Elements can do.
And it is plain wrong.. key is WHEN attributeChanged runs, and what the old/new values are
And attributeChangedCallback runs BEFORE connectedCallback; that is why https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected was added.
Your code gets 3 parameters in attributeChangedCallback, but you can't do anything with them, because execution always goes to the _updateRendering method.
If the point of the exercise is to learn when Observed attributes change I would use:
Code also available in JSFiddle: https://jsfiddle.net/dannye/43ud1wvn/
<script>
class FlagIcon extends HTMLElement {
static observedAttributes = ["country"];
log(...args) {
document.body.appendChild(document.createElement("div"))
.innerHTML = `${this.id} - ${args.join` `}`;
}
attributeChangedCallback(name, oldValue, newValue) {
this.log("<b>attributeChangedCallback:</b>", `("${name}" , "${oldValue}", "${newValue}" )`);
if (this.isConnected) {
if (newValue == oldValue) this.log(`Don't call SETTER ${name} again!`);
else this[name] = newValue; // call SETTER
} else this.log("is not a DOM element yet!!!");
}
connectedCallback() {
this.log("<b>connectedCallback</b>, this.img:", this.img || "not defined");
this.img = document.createElement("img");
this.append(this.img); // append isn't available in IE11
this.country = this.getAttribute("country") || "EmptyCountry";
}
get country() { // the Attribute is the truth, no need for private variables
return this.getAttribute("country");
}
set country(v) {
this.log("SETTER country:", v);
// Properties and Attributes are in sync,
// but setAttribute will trigger attributeChanged one more time!
this.setAttribute("country", v);
if (this.img) this.img.src = `//flagcdn.com/20x15/${v}.png`;
else this.log("can't set country", v);
}
}
customElements.define("flag-icon", FlagIcon);
document.body.onclick = () => {
flag1.country = "nl";
flag2.setAttribute("country", "nl");
}
</script>
<flag-icon id="flag1" country="in"></flag-icon><br>
<flag-icon id="flag2" country="us"></flag-icon><br>
This is just one way, it all depends on what/when/how your Custom Elements needs to do updates.
It also matters WHEN the CustomElement is defined; before or after the DOM is parsed. Most developers just whack deferred or method on their scripts, without understanding what it implies.
Always test your Web Component with code that defines the Custom Element BEFORE it is used in the DOM.
A Real World <flag-icon> Web Component
Would be optimized:
<script>
customElements.define("flag-icon", class extends HTMLElement {
static observedAttributes = ["country"];
attributeChangedCallback() {
this.isConnected && this.connectedCallback();
}
connectedCallback() {
this.img = this.img || this.appendChild(document.createElement("img"));
this.img.src = `//flagcdn.com/120x90/${this.country}.png`;
}
get country() {
return this.getAttribute("country") || console.error("Missing country attribute",this);
}
set country(v) {
this.setAttribute("country", v);
}
});
</script>
<flag-icon id="flag1" country="gb"></flag-icon>
<flag-icon id="flag2" country="eu"></flag-icon>
Or NO External Images at all
Using the FlagMeister Web Component which creates all SVG client-side
<script src="//flagmeister.github.io/elements.flagmeister.min.js"></script>
<div style="display:grid;grid-template-columns: repeat(3,1fr);gap:1em">
<flag-jollyroger></flag-jollyroger>
<flag-un></flag-un>
<flag-lgbt></flag-lgbt>
</div>
Here is the complete code to achieve the same requirements:
<!DOCTYPE html>
<html lang="en">
<body>
<flag-icon country="in"></flag-icon><br>
<flag-icon country="nl"></flag-icon><br>
<flag-icon country="us"></flag-icon><br>
</body>
<script>
class FlagIcon extends HTMLElement {
constructor() {
super();
this._countryCode = null;
}
static observedAttributes = ["country"];
attributeChangedCallback(name, oldValue, newValue) {
// name will always be "country" due to observedAttributes
this._countryCode = newValue;
this._updateRendering();
}
connectedCallback() {
this._updateRendering();
}
get country() {
return this._countryCode;
}
set country(v) {
this.setAttribute("country", v);
}
_updateRendering() {
//**remaining code**
if (this.ownerDocument.defaultView && !this.hasChildNodes()) {
var flag = document.createElement("img");
flag.src = "https://flagcdn.com/24x18/" + this._countryCode + ".png";
this.appendChild(flag);
}
}
}
customElements.define("flag-icon", FlagIcon);
</script>
</html>
Note: images may take time to load depending on the internet speed.
Let me know if I've missed anything.
I'm pretty new to Angular and programming in general.
I wanted to change the background image of my Page by using the setInterval method. It should change every second but for some reason, it changes much faster.
Component:
export class AppComponent implements OnInit {
images: Image[] = [];
changeBackgroundCounter = 0;
constructor(private imagesService: ImagesService) {}
getImage() {
setInterval(() => {
this.changeBackgroundCounter = this.changeBackgroundCounter + 1;
if (this.changeBackgroundCounter > this.images.length - 1) {
this.changeBackgroundCounter = 0;
}
}, 1000);
return this.images[this.changeBackgroundCounter].image;
}
ngOnInit() {
this.images = this.imagesService.getImages();
console.log(this.images[0]);
}
}
Template:
<div [ngStyle]="{'background-image': 'url('+ getImage() + ')'}" [ngClass]="{imageBackground: getImage()}">
Stackblitz link
In your template, you have
<div [ngStyle]="{'background-image': 'url('+ getImage() + ')'}" [ngClass]="{imageBackground: getImage()}">
This means angular keeps calling the getImage() method to find out what the background should be. This will happen very frequently. Each time the method is called, a new interval is created, so there end up being loads of them. You can see this by putting a line of logging within your interval and you will see how often it's being triggered.
setInterval(() => {
console.log('interval triggered'); // <------- add this line to see how often this code is running
this.changeBackgroundCounter = this.changeBackgroundCounter + 1;
if (this.changeBackgroundCounter > this.images.length - 1) {
this.changeBackgroundCounter = 0;
}
}, 1000);
To fix your problem, you need to call getImage() only once, which can be done within ngOnInit(). The template can get the image from images[this.changeBackgroundCounter].image.
You're complicating your code for nothing. Create a variable, equal to a string, and assign it a new value every X seconds in your ngOnInit() !
Then set the background image equals to that variable, and voilĂ !
Here is what it look like in code :
export class AppComponent implements OnInit {
images: Image[] = [];
actualImage: string;
changeBackgroundCounter = 0;
constructor(private imagesService: ImagesService) {}
ngOnInit() {
this.images = this.imagesService.getImages();
this.actualImage = this.images[0].image;
setInterval(() => {
this.changeBackgroundCounter++;
if (this.changeBackgroundCounter > this.images.length - 1) {
this.changeBackgroundCounter = 0;
}
this.actualImage = this.images[this.changeBackgroundCounter].image;
}, 5000);
}
}
I kept as much as possible of your inital code. My new variable is called actualImage, I set a default value in my ngOnInit, right after you get all your images from your service.
Then I call setInterval and set a new value to actualImage every 5 seconds !
https://stackblitz.com/edit/angular-setinterval-f5bghq
CARE: When using setInterval, be used to clear it on ngOnDestroy(), it can lead to some weird bugs you don't want to get involved in.
Simply create an other variable, type any, and do the following :
this.interval = setInterval(() => {...})
ngOnDestroy() {
clearInterval(this.interval);
}
According to a prior SO answer, you can implement getPriority for a forge viewer Tool. And according to another SO answer extending the ToolInterface does not work. Hence, me not extending the ToolInterface implementing my Tool like so:
class MyCustomExtension extends Autodesk.Viewing.Extension {
constructor(viewer, options) {
super(viewer, options);
this.theiaUtil = new TheiaUtil(this);
}
getPriority() {
console.log("Theia#getPriority called! ", (this.getPriority && this.getPriority() || 0));
return 100000;
}
...
}
My tool's priority is returned as 0 in the ToolController, although it shouldn't:
function getPriority(tool)
{
return tool.getPriority instanceof Function && tool.getPriority() || 0;
}
I don't know why this function returns 0 as tool.getPriority instanceof Function returns true if I call MyCustomExtension.getPriority myself.
Note that ToolInterface is implemented like so:
function ToolInterface()
{
this.names = [ "unnamed" ];
this.getNames = function() { return this.names; };
this.getName = function() { return this.names[0]; };
this.getPriority = function() { return 0; };
this.register = function() {};
this.deregister = function() {};
this.activate = function(name, viewerApi) {};
this.deactivate = function(name) {};
this.update = function(highResTimestamp) { return false; };
this.handleSingleClick = function( event, button ) { return false; };
this.handleDoubleClick = function( event, button ) { return false; };
this.handleSingleTap = function( event ) { return false; };
this.handleDoubleTap = function( event ) { return false; };
// ...
}
Because of that, simply extending the ToolInterface class won't work because all these properties and functions added to the instance in the constructor will take precedence over your actual class methods. This is also likely the reason why you're seeing the priority value returned as zero - when you call myTool.getPriority(), you are not actually calling your getPriority method, but rather the default function which was assigned to this.getPriority in ToolInterface's constructor.
To work around this issue I would recommend explicitly deleting the corresponding fields in your class' constructor (something I explain in my blog post on implementing custom Forge Viewer tools):
class DrawTool extends Autodesk.Viewing.ToolInterface {
constructor() {
super();
this.names = ['box-drawing-tool', 'sphere-drawing-tool'];
// Hack: delete functions defined *on the instance* of the tool.
// We want the tool controller to call our class methods instead.
delete this.register;
delete this.deregister;
delete this.activate;
delete this.deactivate;
delete this.getPriority;
delete this.handleMouseMove;
delete this.handleButtonDown;
delete this.handleButtonUp;
delete this.handleSingleClick;
}
register() {
console.log('DrawTool registered.');
}
deregister() {
console.log('DrawTool unregistered.');
}
activate(name, viewer) {
console.log('DrawTool activated.');
}
deactivate(name) {
console.log('DrawTool deactivated.');
}
getPriority() {
return 42; // Or feel free to use any number higher than 0 (which is the priority of all the default viewer tools)
}
// ...
}
TL;DR: Activate the tool in button click event from a toolbar button instead of the extension's load method.
class MyExtension extends Autodesk.Viewing.Extension {
...
onToolbarCreated(toolbar) {
const MyToolName = 'My.Tool.Name'
let button = new Autodesk.Viewing.UI.Button('my-tool-button');
button.onClick = (e) => {
const controller = this.viewer.toolController;
if (controller.isToolActivated(MyToolName)) {
controller.deactivateTool(MyToolName);
button.setState(Autodesk.Viewing.UI.Button.State.INACTIVE);
} else {
controller.activateTool(MyToolName);
button.setState(Autodesk.Viewing.UI.Button.State.ACTIVE);
}
};
}
...
}
I activated the tool instantly after registering it in the Extension's load method. Petr Broz's github repo from his blog post loads the tool from a button in the toolbar. So I moved the activation of the tool to a button click in the toolbar which worked for me.
Maybe somebody knows about the best way to implement data binding in CreateJS? E.g. when changes of properties in ClassA call some listener-functions in ClassB ?
In Flash(Flex) it's possible to use some meta-tags to tell compiler which properties should be used as bindable. After that, during compilation compiler makes some changes in the code (e.g. wraps the required properties into get/set methods, and in the set methods there are dispatching events functionality).
Do we have something similar in CreateJS?
How it works in AS3 (a very simple example):
public class ClassA
{
[Bindable]
public var bindableProperty:String;
}
public class ClassB
{
protected var classA:ClassA;
public function GameModel()
{
this.classA = new ClassA();
BindingUtils.bindSetter(this.bindingCallback, this.classA, "bindableProperty");
}
public function bindingCallback()
{
// Do something after binding callback
}
}
There's no near equivalent to the [Bindable] Flex meta-tag in JavaScript or CreateJS.
In the future, Object.observe() could be a close equivalent to Flex's bindSetter.
For now you can use getter/setter properties to invoke callbacks when properties are changed. Here's a simple example:
function bindSetter(host, property, callback) {
if(!host[property + "_bindings"]){
host[property + "_bindings"] = [];
host["_" + property] = host[property];
Object.defineProperty(host, property, {
get: function() {
return host["_" + property];
},
set: function(newValue) {
host["_" + property] = newValue;
host[property + "_bindings"].forEach(function(callback){
callback(newValue);
});
}
});
}
host[property + "_bindings"].push(callback);
}
Now you can use bindSetter in a similar way to Flex:
var user = { name: "Aaron" }
bindSetter(user, "name", function(newValue){
log("Callback: " + newValue);
});
bindSetter(user, "name", function(newValue){
log("Another callback: " + newValue);
});
log("Initial value: " + user.name);
user.name = "Joe";
Should output:
Initial value: Aaron
Callback: Joe
Another callback: Joe
Binding to DOM element values is another problem, though, as they do not behave the same as regular JavaScript objects. Of course, there are many JS frameworks out there to accomplish data-binding with DOM elements, like Angular, Backbone, Knockout, etc. Mileage will vary when trying to mix other frameworks with CreateJS, though.
Update:
An equivalent unbindSetter could be done as follows:
function unbindSetter(host, property, callback){
var bindings = host[property + "_bindings"];
if(bindings){
var index = bindings.indexOf(callback);
if(index > -1){
bindings.splice(index, 1);
}
}
}
Now you can remove a callback that was previously added:
var user = { name: "Aaron" }
bindSetter(user, "name", myCallback);
function myCallback(newValue){
alert("Callback: " + newValue);
}
user.name = "Joe"; // alerts "Callback: Joe"
unbindSetter(user, "name", myCallback);
user.name = "Bob"; // no alert