skip to Main Content

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

Fiddle

    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


  1. How to directly execute a base class method, ignoring the inheritance chain, in Javascript?

    You can technically replace this.render(el, text); by

    Renderer.prototype.render.call(this, el, text);
    

    which 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:

    my arguments are lost because in MailboxInbox, the render method currently doesn’t accept any arguments

    This does incorrectly override the method with an incompatible signature, breaking the Liskov substitution principle.

    And you already identified the potenial solutions as well:

    I either should make it to accept the arguments and pass them through the whole chain, or just define in Renderer some dedicated method like baseRender and call that directly

    Both are fine. In the latter, instead of naming the methods render and baseRender, I would recommend something like renderContent(el, content) and renderDefault(el), which actually differ in their signature – and which could both be overridden. However, looking at the implementation of render when called with two arguments, it imo doesn’t really do anything useful other than calling el.appendChild(content), so I’d rather drop it entirely and just call appendChild (unless you need the ability to override the concreted behaviour, e.g. by doing el.prepend(content) instead).

    Login or Signup to reply.
  2. Javascript allows to build any prototype chain you want.
    In our case we extend our MailboxInbox with Renderer but then we inject a limited Mailbox class version without render method between MailboxInbox and Renderer. So the render 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:

    function injectParentClass(destinationClass, parentClass, removeProps = []){
    
      // remove props from the prototype
    
      const proto = Object.getOwnPropertyDescriptors(parentClass.prototype);
      
      removeProps.forEach(prop => delete proto[prop]);
      delete proto.constructor;
    
      // make `super` working in the destinationClass
      const injectedPrototype = function(){
        const factory = parentClass.bind(this);
        new factory(...arguments);
      }
      
      // copy the remaining props to the injected prototype
      for(const k in proto){
        Object.defineProperty(injectedPrototype, k, proto[k]);
      }
      
      // make the injection
      const parentPrototype = Object.getPrototypeOf(destinationClass.prototype);
    
      Object.setPrototypeOf(destinationClass.prototype, injectedPrototype);
      Object.setPrototypeOf(injectedPrototype, parentPrototype);
    
    }
    
    injectParentClass(MailboxInbox, Mailbox, ['render']);
    
    
    const mailbox = new Mailbox('#container'); 
    mailbox.renderTitle('Mailbox title')
    
    const inbox = new MailboxInbox({selector: '#container'}); 
    inbox.renderTitle()
    <div id="container"></div>
    <script>
    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 Renderer {
    
      title = "Inbox"
    
      constructor(params) {
       const { selector } = params;
       super(selector); 
      }
    
      renderTitle() {
        super.renderTitle(this.title);
      }
    
      render() {
        console.log('1: INBOX RENDER')
        super.render();
      }
    }
    
    </script>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search