0

I have a theme that I am converting into an Angular 4 admin panel. The theme has a file called app.js containing a class which I am trying to convert into my layout.component.ts In app.js there are several blocks of code that try to access functions outside the class but I have converted all functions into methods of my layout.component.ts;

$(window).resize(function(){
  this.resizePageContent();
});

Running this gives a Javascript error. However there is a method on my layout.component.ts that should be used in this case.;

this.resizePageContent is not a function

So I am wondering which is the best way to convert this such that the layout.component.ts method is called instead. This is what I have tried but I am not so sure whether this is the best way to do it and why it works.

$(window).resize(()=>{
      this.resizePageContent();
    });

After replacing it with the above code the errors have disappeared.

This is a preview of app.js It has over 700 lines of code so I will not be able to paste it all here;

Update:

var App = function() {
    /* Helper variables - set in uiInit() */
    var page, pageContent, header, footer, sidebar, sScroll, sidebarAlt, sScrollAlt;

    /* Initialization UI Code */
    var uiInit = function() {...};

    /* Page Loading functionality */
    var pageLoading = function(){..};

    /* Gets window width cross browser */
    var getWindowWidth = function(){...};

    /* Sidebar Navigation functionality */
    var handleNav = function() {..};

    /* Scrolls the page (static layout) or the sidebar scroll element (fixed header/sidebars layout) to a specific position - Used when a submenu opens */
    var handlePageScroll = function(sElem, sHeightDiff, sSpeed) {...};

    /* Sidebar Functionality */
    var handleSidebar = function(mode, extra) {...};

    /* Resize #page-content to fill empty space if exists */
    var resizePageContent = function() {...};

    /* Interactive blocks functionality */
    var interactiveBlocks = function() {...};

    /* Scroll to top functionality */
    var scrollToTop = function() {...};

    /* Demo chat functionality (in sidebar) */
    var chatUi = function() {...};

    /* Template Options, change features functionality */
    var templateOptions = function() {...};

    /* Datatables basic Bootstrap integration (pagination integration included under the Datatables plugin in plugins.js) */
    var dtIntegration = function() {...};

    /* Print functionality - Hides all sidebars, prints the page and then restores them (To fix an issue with CSS print styles in webkit browsers)  */
    var handlePrint = function() {...};

    return {
        init: function() {
            uiInit(); // Initialize UI Code
            pageLoading(); // Initialize Page Loading
        },
        sidebar: function(mode, extra) {
            handleSidebar(mode, extra); // Handle sidebars - access functionality from everywhere
        },
        datatables: function() {
            dtIntegration(); // Datatables Bootstrap integration
        },
        pagePrint: function() {
            handlePrint(); // Print functionality
        }
    };
}();

/* Initialize app when page loads */
$(function(){ App.init(); });

This is my layout.component.ts;

import { Component, OnInit } from '@angular/core';
declare var jQuery: any;
declare var $: any;
declare var window: any;
declare var document: any;
declare  var Cookies: any;

@Component({
  selector: 'app-layout',
  templateUrl: './layout.component.html',
  styleUrls: ['./layout.component.css']
})
export class LayoutComponent implements OnInit {
  public page;
  public pageContent;
  public header;
  public footer;
  public sidebar;
  public sScroll;
  public sidebarAlt;
  public sScrollAlt;

  constructor() { }

  ngOnInit() {
    this.init();
  }

  uiInit (): void {...}

  pageLoading (): void {...}

  getWindowWidth (): any {...}

  handleNav (): any {...}

  handlePageScroll (sElem, sHeightDiff, sSpeed): void {...}

  handleSidebar (mode, extra?:any): any {
    if (mode === 'init') {
      // Init sidebars scrolling functionality
      this.handleSidebar('sidebar-scroll');
      this.handleSidebar('sidebar-alt-scroll');

      // Close the other sidebar if we hover over a partial one
      // In smaller screens (the same applies to resized browsers) two visible sidebars
      // could mess up our main content (not enough space), so we hide the other one :-)
      $('.sidebar-partial #sidebar')
          .mouseenter(function(){ this.handleSidebar('close-sidebar-alt'); });
      $('.sidebar-alt-partial #sidebar-alt')
          .mouseenter(function(){ this.handleSidebar('close-sidebar'); });
    } else {
      var windowW = this.getWindowWidth();

      if (mode === 'toggle-sidebar') {
        if ( windowW > 991) { // Toggle main sidebar in large screens (> 991px)
          this.page.toggleClass('sidebar-visible-lg');

          if (this.page.hasClass('sidebar-mini')) {
            this.page.toggleClass('sidebar-visible-lg-mini');
          }

          if (this.page.hasClass('sidebar-visible-lg')) {
            this.handleSidebar('close-sidebar-alt');
          }

          // If 'toggle-other' is set, open the alternative sidebar when we close this one
          if (extra === 'toggle-other') {
            if (!this.page.hasClass('sidebar-visible-lg')) {
              this.handleSidebar('open-sidebar-alt');
            }
          }
        } else { // Toggle main sidebar in small screens (< 992px)
          this.page.toggleClass('sidebar-visible-xs');

          if (this.page.hasClass('sidebar-visible-xs')) {
            this.handleSidebar('close-sidebar-alt');
          }
        }

        // Handle main sidebar scrolling functionality
        this.handleSidebar('sidebar-scroll');
      }
      else if (mode === 'toggle-sidebar-alt') {
        if ( windowW > 991) { // Toggle alternative sidebar in large screens (> 991px)
          this.page.toggleClass('sidebar-alt-visible-lg');

          if (this.page.hasClass('sidebar-alt-visible-lg')) {
            this.handleSidebar('close-sidebar');
          }

          // If 'toggle-other' is set open the main sidebar when we close the alternative
          if (extra === 'toggle-other') {
            if (!this.page.hasClass('sidebar-alt-visible-lg')) {
              this.handleSidebar('open-sidebar');
            }
          }
        } else { // Toggle alternative sidebar in small screens (< 992px)
          this.page.toggleClass('sidebar-alt-visible-xs');

          if (this.page.hasClass('sidebar-alt-visible-xs')) {
            this.handleSidebar('close-sidebar');
          }
        }
      }
      else if (mode === 'open-sidebar') {
        if ( windowW > 991) { // Open main sidebar in large screens (> 991px)
          if (this.page.hasClass('sidebar-mini')) { this.page.removeClass('sidebar-visible-lg-mini'); }
          this.page.addClass('sidebar-visible-lg');
        } else { // Open main sidebar in small screens (< 992px)
          this.page.addClass('sidebar-visible-xs');
        }

        // Close the other sidebar
        this.handleSidebar('close-sidebar-alt');
      }
      else if (mode === 'open-sidebar-alt') {
        if ( windowW > 991) { // Open alternative sidebar in large screens (> 991px)
          this.page.addClass('sidebar-alt-visible-lg');
        } else { // Open alternative sidebar in small screens (< 992px)
          this.page.addClass('sidebar-alt-visible-xs');
        }

        // Close the other sidebar
        this.handleSidebar('close-sidebar');
      }
      else if (mode === 'close-sidebar') {
        if ( windowW > 991) { // Close main sidebar in large screens (> 991px)
          this.page.removeClass('sidebar-visible-lg');
          if (this.page.hasClass('sidebar-mini')) { this.page.addClass('sidebar-visible-lg-mini'); }
        } else { // Close main sidebar in small screens (< 992px)
          this.page.removeClass('sidebar-visible-xs');
        }
      }
      else if (mode === 'close-sidebar-alt') {
        if ( windowW > 991) { // Close alternative sidebar in large screens (> 991px)
          this.page.removeClass('sidebar-alt-visible-lg');
        } else { // Close alternative sidebar in small screens (< 992px)
          this.page.removeClass('sidebar-alt-visible-xs');
        }
      }
      else if (mode === 'sidebar-scroll') { // Handle main sidebar scrolling
        if (this.page.hasClass('sidebar-mini') && this.page.hasClass('sidebar-visible-lg-mini') && (windowW > 991)) { // Destroy main sidebar scrolling when in mini sidebar mode
          if (this.sScroll.length && this.sScroll.parent('.slimScrollDiv').length) {
            this.sScroll
                .slimScroll({destroy: true});
            this.sScroll
                .attr('style', '');
          }
        }
        else if ((this.page.hasClass('header-fixed-top') || this.page.hasClass('header-fixed-bottom'))) {
          var sHeight = $(window).height();

          if (this.sScroll.length && (!this.sScroll.parent('.slimScrollDiv').length)) { // If scrolling does not exist init it..
            this.sScroll
                .slimScroll({
                  height: sHeight,
                  color: '#fff',
                  size: '3px',
                  touchScrollStep: 100
                });

            // Handle main sidebar's scrolling functionality on resize or orientation change
            var sScrollTimeout;

            $(window).on('resize orientationchange', function(){
              clearTimeout(sScrollTimeout);

              sScrollTimeout = setTimeout(function(){
                this.handleSidebar('sidebar-scroll');
              }, 150);
            });
          }
          else { // ..else resize scrolling height
            this.sScroll
                .add(this.sScroll.parent())
                .css('height', sHeight);
          }
        }
      }
      else if (mode === 'sidebar-alt-scroll') { // Init alternative sidebar scrolling
        if ((this.page.hasClass('header-fixed-top') || this.page.hasClass('header-fixed-bottom'))) {
          var sHeightAlt = $(window).height();

          if (this.sScrollAlt.length && (!this.sScrollAlt.parent('.slimScrollDiv').length)) { // If scrolling does not exist init it..
            this.sScrollAlt
                .slimScroll({
                  height: sHeightAlt,
                  color: '#fff',
                  size: '3px',
                  touchScrollStep: 100
                });

            // Resize alternative sidebar scrolling height on window resize or orientation change
            var sScrollAltTimeout;

            $(window).on('resize orientationchange', function(){
              clearTimeout(sScrollAltTimeout);

              sScrollAltTimeout = setTimeout(function(){
                this.handleSidebar('sidebar-alt-scroll');
              }, 150);
            });
          }
          else { // ..else resize scrolling height
            this.sScrollAlt
                .add(this.sScrollAlt.parent())
                .css('height', sHeightAlt);
          }
        }
      }
    }

    return false;
  }

  resizePageContent (): void {...}

  interactiveBlocks (): void {...}

  scrollToTop (): any{...}

  chatUi (): any {...}

  templateOptions (): void {...}

  dtIntegration (): any {...}

  handlePrint (): void {...}


  //Methods from original object
  init (): void {
    this.uiInit(); // Initialize UI Code
    this.pageLoading(); // Initialize Page Loading
  }
  //Originally sidebar
  CallhandleSidebar(mode, extra): void {
    this.handleSidebar(mode, extra); // Handle sidebars - access functionality from everywhere
  }

  datatables() :void {
    this.dtIntegration(); // Datatables Bootstrap integration
  }

  pagePrint (): void {
    this.handlePrint(); // Print functionality
  }

}

Also could you give me an example of how TypeScript might output something like this;

$(window).resize(()=>{
  this.resizePageContent();
});

...just to get a good idea of how it works.

  • can you share the layout.component.ts original and edited file, then it will be good to answer your question – virsha Aug 10 '17 at 09:09
  • Possible duplicate of [How to access the correct \`this\` inside a callback?](https://stackoverflow.com/questions/20279484/how-to-access-the-correct-this-inside-a-callback) – Hitmands Aug 10 '17 at 09:18
  • I have added the component and app.js code. If this ()=>{} solves it. Then that is my answer –  Aug 10 '17 at 09:19
  • You can check my post, how can you write one liner code as well as what are the ways to listen for window resize event – virsha Aug 10 '17 at 09:32
  • Don't want to sound like a douche but why are you using jQuery in Angular? – Vic Aug 10 '17 at 09:37
  • This was just a quick conversation without going through all the previous developer's code, which has over 700 lines. But how would I write somethig like this without JQuery; `$('#sidebar-scroll')` or `$('[data-toggle="popover"], .enable-popover').popover({container: 'body', animation: true});` Supposing some external javascript files are attached to the element as well like bootstrap. –  Aug 10 '17 at 09:47

3 Answers3

2

In your case, using the code

$(window).resize(()=>{
  this.resizePageContent();
});

is the correct way for accessing functions and variables outside of the scope of the callback function.

When using

function() {
  this.something;
}

the 'this' is bound to the scope of the function, rather than of the class.

When using

() => {
    this.something
}

that is using ecmascript 6 arrow notation. Ecmascript 6 sees the introduction of the lexical this, where in the latter case the 'this' keyword refers to the class defined.

For more information see http://es6-features.org/#Lexicalthis and https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Arrow_functions#No_binding_of_this .

JeanPaul A.
  • 3,613
  • 1
  • 20
  • 29
0

If you have to do just delegate or one line coder in the callback, go for this:

$(window).resize(()=> this.resizePageContent());

Or you can use the following too resize the page content:

another question on stackoverflow to catch the window resizing event in Angular context

and make your resizePageContent() method to listen for that event.

virsha
  • 1,140
  • 4
  • 19
  • 40
0

Opinion based question, but personally the best way is to use a class field and assign an arrow function definition, and to -not- use jQuery while using angular:

window.addEventListener('resize', this.resizePageContent);

Within your class you should then define the method like this:

private resizePageContent: EventListener = (event: UIEvent): void => {

};

The big advantage of this, is that you can remove the event listener, which is impossible if you use an anonymous arrow function.

plunkr

Another option is to use bind(this) to retain the this context:

window.addEventListener('resize', this.resizePageContent.bind(this));

This way you do not need to assign your callback to a field of the class. But using bind creates a copy of the method, which makes it harder (not impossible) to remove the event listener

You can also just immediately listen to it from within your component using @HostListener. Removing of the event is done internally by angular on destroy of the component

@HostListener('window:resize', ['$event'])
public resizePageContent(event: Event) {}

The reason you were getting an error is because you were using the function keyword. This changes the this context to the anonymous function you are defining as callback

Poul Kruijt
  • 69,713
  • 12
  • 145
  • 149
  • `window.addEventListener('resize', this.resizePageContent);` will end up on loosing the context too. – Hitmands Aug 10 '17 at 09:18
  • @Hitmands no it will not, if you define the `resizePageContent` like I mention. Using a field property instead of a method. Test before you downvote :) – Poul Kruijt Aug 10 '17 at 09:20
  • tested... what's the value of `this` after passing the `this.resizePageContent` as a callback? – Hitmands Aug 10 '17 at 09:21
  • @Hitmands the class it's been called in. Let me clarify my code with types. As you can see you got a class field named `resizePageContent` which has as type `EventListener`, which is a method. Good thing about working this way, is that you can remove the eventListener if you no longer need it, so no memory leaks. Seems like a big advantage to me ;) – Poul Kruijt Aug 10 '17 at 09:23
  • this is ok `window.addEventListener('resize', this.resizePageContent.bind(this));` – Hitmands Aug 10 '17 at 09:31
  • @Hitmands so is the first one, just look at how i define the `resizePageContent`. I've created a [**plnkr**](https://plnkr.co/edit/c868ygygYAvS6jcj1qFg?p=preview) for you where you can see it in action. Resize the inner window, and find `App {name: "Angular! v4.3.3", resizePageContent: ƒ}` in your console :) – Poul Kruijt Aug 10 '17 at 09:34
  • your code will work because it gets transpiled down to es5 in this way and compiled into the constructor: `function App() { var _this = this; this.resizePageContent = function (event) { console.log(_this); //App }; this.name = "Angular! v" + VERSION.full; window.addEventListener('resize', this.resizePageContent); }` – Hitmands Aug 10 '17 at 09:40
  • as you can see, `_this` belongs to the outer scope... but if you target it into es6 it won't work... – Hitmands Aug 10 '17 at 09:41
  • Again, have you even tested it? There is no reason it won't work in es2016. You are either passing an anonymous arrow function, or you are passing a named one. Where you get this in ES2016 for a minimal example: `class TestThis { constructor() { this.resizePageContent = (event) => { console.log(this); }; window.addEventListener('resize', this.resizePageContent); } }` – Poul Kruijt Aug 10 '17 at 09:57
  • no! this is defining a function into the constructor. This is ES6 `class TestThis { constructor() { window.addEventListener('resize', this.resizePageContent); } resizePageContent() { console.log(this) } }` – Hitmands Aug 10 '17 at 09:58
  • Maybe we should chat... Anyways, that's what I'm saying, it's a `field`, not a method... and the field has as value an arrow function. You are defining a method, I am defining a field... I don't know how else to explain it to you :D – Poul Kruijt Aug 10 '17 at 10:00
  • @Hitmands Or, explain to me how else you would like to use `removeEventListener` inside ES6 if you are using `.bind(this)` or an anonymous arrow function. – Poul Kruijt Aug 10 '17 at 10:08
  • you can explicitly define your listener fn or use `HotListener` but absolutely don't use a trick of your compiler because you won't be able to shut down your transpiler once you won't longer need it... – Hitmands Aug 10 '17 at 10:12
  • But, I am explicitly defining it, as a field of the class instead of a local value, so I can use it throughout the class. And `HostListener` is angular, not es6. It's not a trick, it's best practice. For instance in angular, if you want to attach an event and use `useCapture`, you have to use `addEventListener` and can't use `HostListener`. Now how are you supposed to unbind this listener in `OnDestroy`? Afaik, the only way is a field property with a reference to the method. Otherwise you are having a serious memory leak – Poul Kruijt Aug 10 '17 at 10:21