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
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 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
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 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'm trying to throw a custom error with my "CustomError" class name printed in the console instead of "Error", with no success:
class CustomError extends Error {
constructor(message: string) {
super(`Lorem "${message}" ipsum dolor.`);
this.name = 'CustomError';
}
}
throw new CustomError('foo');
The output is Uncaught Error: Lorem "foo" ipsum dolor.
What I expect: Uncaught CustomError: Lorem "foo" ipsum dolor.
I wonder if that can be done using TS only (without messing with JS prototypes)?
Are you using typescript version 2.1, and transpiling to ES5? Check this section of the breaking changes page for possible issues and workaround: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
The relevant bit:
As a recommendation, you can manually adjust the prototype immediately after any super(...) calls.
class FooError extends Error {
constructor(m: string) {
super(m);
// Set the prototype explicitly.
Object.setPrototypeOf(this, FooError.prototype);
}
sayHello() {
return "hello " + this.message;
}
}
However, any subclass of FooError will have to manually set the prototype as well. For runtimes that don't support Object.setPrototypeOf, you may instead be able to use __proto__.
Unfortunately, these workarounds will not work on Internet Explorer 10 and prior. One can manually copy methods from the prototype onto the instance itself (i.e. FooError.prototype onto this), but the prototype chain itself cannot be fixed.
The problem is that Javascript's built-in class Error breaks the prototype chain by switching the object to be constructed (i.e. this) to a new, different object, when you call super and that new object doesn't have the expected prototype chain, i.e. it's an instance of Error not of CustomError.
This problem can be elegantly solved using 'new.target', which is supported since Typescript 2.2, see here: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html
class CustomError extends Error {
constructor(message?: string) {
// 'Error' breaks prototype chain here
super(message);
// restore prototype chain
const actualProto = new.target.prototype;
if (Object.setPrototypeOf) { Object.setPrototypeOf(this, actualProto); }
else { this.__proto__ = actualProto; }
}
}
Using new.target has the advantage that you don't have to hardcode the prototype, like some other answers here proposed. That again has the advantage that classes inheriting from CustomError will automatically also get the correct prototype chain.
If you were to hardcode the prototype (e.g. Object.setPrototype(this, CustomError.prototype)), CustomError itself would have a working prototype chain, but any classes inheriting from CustomError would be broken, e.g. instances of a class VeryCustomError < CustomError would not be instanceof VeryCustomError as expected, but only instanceof CustomError.
See also: https://github.com/Microsoft/TypeScript/issues/13965#issuecomment-278570200
As of TypeScript 2.2 it can be done via new.target.prototype.
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#example
class CustomError extends Error {
constructor(message?: string) {
super(message); // 'Error' breaks prototype chain here
this.name = 'CustomError';
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
}
}
It works correctly in ES2015 (https://jsfiddle.net/x40n2gyr/). Most likely, the problem is that the TypeScript compiler is transpiling to ES5, and Error cannot be correctly subclassed using only ES5 features; it can only be correctly subclassed using ES2015 and above features (class or, more obscurely, Reflect.construct). This is because when you call Error as a function (rather than via new or, in ES2015, super or Reflect.construct), it ignores this and creates a new Error.
You'll probably have to live with the imperfect output until you can target ES2015 or higher...
I literally never post on SO, but my team is working on a TypeScript project, and we needed to create many custom error classes, while also targeting es5. It would have been incredibly tedious to do the suggested fix in every single error class. But we found that we were able to have a downstream effect on all subsequent error classes by creating a main custom error class, and having the rest of our errors extend that class. Inside of that main error class we did the following to have that downstream effect of updating the prototype:
class MainErrorClass extends Error {
constructor() {
super()
Object.setPrototypeOf(this, new.target.prototype)
}
}
class SomeNewError extends MainErrorClass {}
...
Using new.target.prototype was the key to getting all of the inheriting error classes to be updated without needing to update the constructor of each one.
Just hoping this saves someone else a headache in the future!
I ran into the same problem in my typescript project a few days ago. To make it work, I use the implementation from MDN using only vanilla js. So your error would look something like the following:
function CustomError(message) {
this.name = 'CustomError';
this.message = message || 'Default Message';
this.stack = (new Error()).stack;
}
CustomError.prototype = Object.create(Error.prototype);
CustomError.prototype.constructor = CustomError;
throw new CustomError('foo');
It doesn't seem to work in SO code snippet, but it does in the chrome console and in my typescript project:
I was having this problem in a nodejs server. what worked for me was to transpile down to es2017 in which these issues seems to be fixed.
Edit tsconfig to
"target": "es2017"
Try this...
class CustomError extends Error {
constructor(message: string) {
super(`Lorem "${message}" ipsum dolor.`)
}
get name() { return this.constructor.name }
}
throw new CustomError('foo')