I have the following Node class which I am using to create a custom element node-element.
class Node extends SVGCircleElement{
static get observedAttributes() {
return ["coordinates"];
}
constructor()
{
super();
this.attributeMap = {coordinates:(coordinates)=>this.updateCoordinates(coordinates)}
}
connectedCallback(){
this.initNode();
}
updateCoordinates(coordinates)
{
this.setAttribute("cx",`${coordinates.x}`);
this.setAttribute("cy",`${coordinates.y}`);
this.setAttribute("r",50);
}
initNode()
{
this.className="node";
}
attributeChangedCallback(name,oldValue,newValue)
{
if(oldValue!==newValue)
{
this.attributeMap[name](JSON.parse(newValue))
}
}
}
I register this element using:-
customElements.define('node-element',Node);
I am creating this element as follows:-
let newNode = document.createElement("node-element");
This is where I get the following error:-
Uncaught TypeError: Illegal constructor
at new Node (index.js:9)
at SVGSVGElement.drawNode (index.js:43)
Line 43 corresponds to the createElement code.
Would love to be proven wrong, just spent 2 months on an SVG project
AFAIK, you can NOT extend SVG elements
You can only create Custom Elements in the HTML Namespace http://www.w3.org/1999/xhtml
SVG Elements are in the SVG Namespace http://www.w3.org/2000/svg
From the docs: https://html.spec.whatwg.org/multipage/custom-elements.html#element-definition
If the element interface for extends and the HTML namespace is HTMLUnknownElement,
then throw a "NotSupportedError" DOMException.
and
if namespace is not the HTML namespace, return null
The ongoing W3C discussion on allowing other namespaces is here: https://github.com/w3c/webcomponents/issues/634
The HTML Namespace has restrictions too
Apple/Safari implemented the Autonomous Custom Elements (extend from HTMLElement)
but refuses to implement Customized Built-In Elements (extend any Built-In element from the HTML Namespace)
If you want to generate SVG, you have to extend HTMLElement and generate the whole SVG tag:
<svg xmlns='http://www.w3.org/2000/svg' viewBox='V'><circle cx='X' cy='Y' r='R'/></svg>
Related Custom Element SVG StackOverflow Question and Answers
javascript - change the background of an active icon on a menu
Getting error while creating a custom element which extends SVGCircleElement
Related
First off, my apologies - I'm a complete novice when it comes to javascript so this is a bit above my head. I'm also fairly new to Odoo and have mostly stuck with python and XML customization thus far.
I'm trying to override a javascript method within a class to replace it completely with my own version. From the Odoo documentation (https://www.odoo.com/documentation/14.0/reference/javascript_reference.html#patching-an-existing-class) this should be a simple matter of using the .include() method to patch the original class with my new method. But when I do this I get an error Error while loading mymodule.CustomControlPanelModelExtension: TypeError: ControlPanelModelExtension.include is not a function
The original Odoo code that I'm trying to override:
odoo.define("web/static/src/js/control_panel/control_panel_model_extension.js", function (require) {
"use strict";
// a bunch of code here ...
class ControlPanelModelExtension extends ActionModel.Extension {
// more code here ...
// this is the method I'm trying to override
_getAutoCompletionFilterDomain(filter, filterQueryElements) {
// original method body here
}
// more code
}
// more code
});
Below is what I came up with based on the documentation but this gives me the error Error while loading mymodule.CustomControlPanelModelExtension: TypeError: ControlPanelModelExtension.include is not a function (this error is reported in browser dev tools console).
odoo.define('mymodule.CustomControlPanelModelExtension', function(require) {
"use strict";
var ControlPanelModelExtension = require('web/static/src/js/control_panel/control_panel_model_extension.js');
ControlPanelModelExtension.include({
// override _getAutoCompletionFilterDomain
_getAutoCompletionFilterDomain: function(filter, filterQueryElements) {
// my custom implementation here
},
});
});
Any ideas what I'm doing wrong here? I've tried various other things with extends and such but I don't think I want to extend - that won't replace the function in existing instances.
The problem here is that the include function is available only for the classes that inherit from OdooClass and in this case the class you are trying to inherit is a native JavaScript class.
Then, to add a property or method to a class, the prototype property of the object class must be modified.
odoo.define('mymodule.CustomControlPanelModelExtension', function(require) {
"use strict";
const ControlPanelModelExtension = require('web/static/src/js/control_panel/control_panel_model_extension.js');
function _getAutoCompletionFilterDomain(filter, filterQueryElements) {
// your custom implementation here
}
ControlPanelModelExtension.prototype._getAutoCompletionFilterDomain = _getAutoCompletionFilterDomain;
return ControlPanelModelExtension;
});
I can't get Angular (2+, not AngularJS) to play nice with my extended custom element, which is defined like so:
class FancyButton extends HTMLButtonElement {
connectedCallback() {
this.innerText = `I'm a fancy-button!`;
this.style.backgroundColor = 'tomato';
}
}
customElements.define("fancy-button", FancyButton, {
extends: "button"
});
And used like so:
<button is="fancy-button">Fancy button here</button>
The definition is fully compliant to web standards according to this Google Developer resource:
https://developers.google.com/web/fundamentals/web-components/customelements#extend
It's working fine in a vanilla web setup and in React, but Angular ignores it and shows a standard button, apparently ignoring the is="fancy-button" attribute.
Here is a stackblitz showing this in action.
One fancy-button is outside the Angular scope (index.html) and is working fine.
The other button is inside the Angular scope (app.component.html) and is NOT working.
Why oh why?
There are two types of Custom Elements:
Autonomous Custom Elements, which are classes that extend HTMLElement
Customized Built-in Elements, which are classes that extend a
specific type of element, such as extend HTMLButtonElement.
Customized Built-in Elements are not supported in Angular (more details
on this below). They are also still not supported in Safari (as of
Sep 2020: https://caniuse.com/#search=custom%20elements
).
Your example is a Customized Built-in Element. The workaround that has
worked for me is to rewrite any Customized Built-in Element as an Autonomous
Custom Element wrapped around a Built-in Element. This gives me the
encapsulation I want, it gives me a way to customize the built-in, and it works with Angular and Safari.
For your example above, the translation is:
class FancyButtonToo extends HTMLElement {
connectedCallback() {
const buttonElement = document.createElement('button');
this.appendChild(buttonElement);
buttonElement.innerText = "Fancy button #2 here!";
buttonElement.style.backgroundColor = 'tomato';
}
}
customElements.define("fancy-button-too", FancyButtonToo);
(The project also needed schemas: [ CUSTOM_ELEMENT_SCHEMAS] added
to app.module.ts). Full code here: stackblitz,
and it renders like this (original "fancy-button" left in for comparison):
Additional Info
Q: Are we certain Angular cannot support Customized Built-in Elements (as
opposed to, say, there being some obscure configuration we are not aware of)?
A: We are certain: The spec document at https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-autonomous-example
goes into deep detail on the differences between Autonomous Custom Elements
and Customized Built-in Elements. One important difference is in
programmatic construction of elements:
// Built-in Elements and Autonomous Custom Elements are created like this:
el = createElement(name);
// examples
el = createElement("button"); // normal built-in button
el = createElement("fancy-text"); // a custom element
// Customized Built-in Elements are created like this:
el = createElement(built-in-name, { is: custom-built-in-name });
// example
el = createElement("button", { is: "fancy-button" });
The relevant Angular template-rendering code is found at
https://github.com/angular/angular/blob/master/packages/platform-browser/src/dom/dom_renderer.ts
and as of the current version of Angular, you will find:
class DefaultDomRenderer2 implements Renderer2 {
/* ... */
createElement(name: string, namespace?: string): any {
if (namespace) {
return document.createElementNS(NAMESPACE_URIS[namespace] || namespace, name);
}
return document.createElement(name);
}
/* ... */
}
The Angular renderer currently does not have the extra code
needed to pass in the 2nd argument to createElement(); it cannot
create Custom Built-in Elements.
class DefaultDomRenderer2 implements Renderer2 {
// ...
create(name: string, namespaceOrOptions?: string | ElementCreationOptions) {
if (namespaceOrOptions && typeof namespaceOrOptions === 'string') {
// In cases where Ivy (not ViewEngine) is giving us the actual namespace, the look up by key
// will result in undefined, so we just return the namespace here.
return document.createElementNS(NAMESPACE_URIS[namespaceOrOptions] || namespaceOrOptions, name);
}
if (namespaceOrOptions && namespaceOrOptions.hasOwnProperty('is')) {
return document.createElement(name, namespaceOrOptions as ElementCreationOptions);
}
return document.createElement(name)
}
}
You can add the polyfill javascript file with your project and then it should work:
https://github.com/ungap/custom-elements
I'm currently learning TypeScript but I don't know how to solve this problem properly. I want to style a HTMLElement given as a argument to my styling function like this:
function styleElement(unstyled: HTMLElement) {
const styled = unstyled.cloneNode(); // styleElement should be a pure function
styled.style = { opacity: 0 }; // TypeScript says: Property 'style' does not exist on type 'Node'.
return styled;
}
I have checked the "Node" interface declaration and the style property is missing. As far as I know it is described in the W3C spec, that Node hasn't any style property. But how can I solve my problem with this limitation?
However overwriting the style property on a "HTMLElement" triggers a readonly error, which is strange too.
Thank you!
You can cast:
function styleElement(unstyled: HTMLElement) {
const styled = unstyled.cloneNode() as HTMLElement;
styled.style.opacity = 0 // Now works properly.
// You can't reassign HTMLElement#style, that won't work.
return styled;
}
I have a CustomElement with the following constructor:
export default class SomeCustomElement extends HTMLElement {
constructor(templateId) {
super();
this.insertTemplateInstance(templateId);
}
...
}
I can register that Element in Chrome without any Problems.
But using Firefox with the polyfill loaded by webcomponents-loader.js from https://github.com/webcomponents/webcomponentsjs I get the ErrorMessage TypeError: Illegal constructor when calling super().
Does anybody know what is causing this?
Some more Background:
Registering of the custom Elements happens like this:
window.addEventListener("WebComponentsReady", function () {
customElements.define(elementName, SomeCustomElement);
});
Use webcomponents-lite.js instead of webcomponent-loader.js if you don't want to have this kind of error, which is caused by the fact that the polyfills will be loaded asynchronously if you use webcomponents-loader.js.
The example below works fine with Firefox (and every modern browser):
class SomeCustomElement extends HTMLElement
{
constructor()
{
console.log( 'created' )
super()
}
connectedCallback() {
console.log( 'connected' )
this.innerHTML = "Hello"
}
}
customElements.define( 'c-e', SomeCustomElement )
<script src=https://rawgit.com/webcomponents/webcomponentsjs/master/webcomponents-lite.js></script>
<c-e></c-e>
However if you still want to use webcomponents-loader.js, you'll have to insert your custom element definition in an external file, and load it with HTML Imports:
<link rel="import" href="my-element.html">
Caveat upfront: I'm not a huge fan of html imports. I stumbled across this trying to get ES 6 class-based custom elements to work in Firefox. For a conditional-polyfill-loading-no-html-import solution based on the accepted answer, read on...
To conditionally load the polyfills gets a little tricky. Per #Supersharp's answer/comments, for some reason the polyfill must be loaded synchronously (despite there being no mention of this in the official documentation). So now you have two unappealing options: include it unconditionally to get the necessary synchronous loading or...use document.write:
<script>
;(function() {
var str = '';
// You could make this more fine-grained if desired by doing
// more feature detection and loading the minimal polyfill file
if (!window.customElements) str += '<script src="./node_modules/#webcomponents/webcomponentsjs/webcomponents-lite.js"></script>';
str += '<script src="./elements.js"></script>';
document.write(str);
})();
</script>
<foo-bar></foo-bar>
Then in elements.js:
class FooBar extends HTMLElement {
constructor () {
console.log("constructing");
super();
}
connectedCallback () {
console.log("connecting");
}
disconnectedCallback () {
console.log("disconnecting");
}
};
// Note that because of the synchronous loading we don't
// need to listen for the event
customElements.define('foo-bar', FooBar);
document.write is widely disliked for good reasons but this is IMHO a legitimate use case. Note that most of the objections here (no pre-fetch, etc.) can be ameliorated through use of service workers (for browsers that support them).
I've tried to define a new custom element extends HTMLFormElement like:
class PrintableFormElement extends HTMLFormElement {
constructor() {
super();
console.log("created: ", this);
}
print() { console.log(this.elements)); }
}
customElements.define("printable-form", PrintableFormElement, { extends: "form" });
This doesn't work right. <form is="printable-form" ...> doesn't have the print() method (define(...) seems to be failed), and new PrintableFormElement are fail with an error with Google Chrome 55.
new PrintableFormElement threw the following error:
Uncaught TypeError: Illegal constructor
at PrintableFormElement (<anonymous>:3:5)
at <anonymous>:1:1
I have no idea how to define a custom element extends with Custom Elements v1 in current Google Chrome.
Custom Elements v0 works well but I want to use it.
Customized Built-in Elements "v1" are not implemented yet in Chrome, you should use
a polyfill.
For more details, see also this question from SO:
How to create new instance of an extended class of custom elements