I’m working with the inheritance chain of 3 extendable classes.
Renderer (Base) -> Mailbox (Child) -> MailboxInbox (final child)
Each of them has it’s own render
method. Child’s just override the Renderer (Base) render
method At the end they anyway should use the render
method of the Renderer (Base) class. My problem that when I’m executing some method in the Renderer class (createElementWithTextInside) it doesn’t execute exactly the Renderer’s render method (Renderer.render()) but it rather starts for the end of the extendable class chaing. Eg it starts to walk through:
1 – MailboxInbox.render
2 – Mailbox.render
3 – Renderer.render
I understand how js classes/inheritance works under the hood. Javascript just extends objects prototypes and the script walks through them etc. but my question: How to avoid this behavior and directly call the Renderer.render method inside it’s method when I need to?
I can’t save __self in the Renderer because it will be anyway pointing to this
the current instance (MailboxInbox). Also I can’t use .bind/lambda (arrow function) because I need to save the context
class Renderer {
get noResultContent() {
const p = this.createElement('p', 'no-result-msg');
const textNode = document.createTextNode('No result');
p.appendChild(textNode);
return p;
};
createElement(tag, className) {
if (!tag) throw new Error('Tag must be passed!');
const res = document.createElement(tag);
res.classList.add(className);
return res;
}
createTextNode(text) {
const res = document.createTextNode(text);
return res;
}
/**
* automatically creates an el based on tag, text node and insert the text node into the el
*/
createElementWithTextInside(tag, className, content) {
const el = this.createElement(tag, className);
const text = this.createTextNode(content);
this.render(el, text);
return el;
}
checkIfDomEl = el => el instanceof Element;
render(el, content) {
console.log('3: RENDERER RENDER')
if (!this.checkIfDomEl(el)) throw new Error(`Please, pass the valid DOM el. Received: ${el}, ${typeof el} `);
const finalContent = content || this.noResultContent;
el.appendChild(finalContent);
}
}
--------
class Mailbox extends Renderer {
rootEl;
constructor(selector) {
super();
this.rootEl = document.querySelector(selector);
}
renderTitle(content) {
if (!content) return null;
const titleEl = super.createElementWithTextInside('h3', 'mailbox-title', content)
super.render(this.rootEl, titleEl);
}
render() {
console.log('2: MAILBOX RENDER')
super.render(this.rootEl);
}
}
--------
class MailboxInbox extends Mailbox {
title = "Inbox"
constructor(params) {
const { selector } = params;
super(selector);
}
renderTitle() {
super.renderTitle(this.title);
}
render() {
console.log('1: INBOX RENDER')
super.render();
}
}
--------
const inbox = new MailboxInbox({selector: '#container'});
inbox.renderTitle()
Here it just renders in console:
1: INBOX RENDER
2: MAILBOX RENDER
3: RENDERER RENDER
Thanks for any help!
Kind regards!
Update
Basically I just wanted to have a basic render
class that could accept arguments: (el,content)
(content is optional) and in the Children I wanted to overrided it in their own .render()
methods with some predefined el
and content
etc. but if I try to execute renderer.render()
method from inside the Renderer, it walks through the whole chain and my arguments are lost because in MailboxInbox the render method currently doesn’t accept any arguments so I either should make it to accept the arguments and pass them through the whole chain or just define in Renderer some dedicated class like baseRender
and call it directly
2
Answers
You can technically replace
this.render(el, text);
bywhich circumvents the inherited property lookup. However, this is not a good practice in general, forfeiting the
class
inheritance advantages.Fortunately, you already identified the actual problem in your update:
This does incorrectly override the method with an incompatible signature, breaking the Liskov substitution principle.
And you already identified the potenial solutions as well:
Both are fine. In the latter, instead of naming the methods
render
andbaseRender
, I would recommend something likerenderContent(el, content)
andrenderDefault(el)
, which actually differ in their signature – and which could both be overridden. However, looking at the implementation ofrender
when called with two arguments, it imo doesn’t really do anything useful other than callingel.appendChild(content)
, so I’d rather drop it entirely and just callappendChild
(unless you need the ability to override the concreted behaviour, e.g. by doingel.prepend(content)
instead).Javascript allows to build any prototype chain you want.
In our case we extend our
MailboxInbox
withRenderer
but then we inject a limitedMailbox
class version withoutrender
method betweenMailboxInbox
andRenderer
. So therender
starts working as you wanted. The question is whether it’s right to do is just another one. I’ve just shown a way how your prototype chain could be made: