Search code examples
phpoopoctobercmsoctobercms-plugins

Call to a member function setComponentContext() on null - OctoberCMS


For the past couple of months I've been learning PHP and using Octobercms and I'm still fairly new to it. The basic concept of what I'm trying to achieve is a page which contains 5 tiles. When I click the tile it replaces the current page with a partial depending on which tile you clicked and changes the url without having to reload the page. each tile has an id with the name of the partial it should return. e.g the settings tile has an data-id="settings". Here is a screenshot of the page

page

I've created my own plugin for this and placed the component on the page. On render it returns the dashboard partial which works and is shown in the screenshot above, the problem is when I click another tile. It makes the ajax call and calls my "test" method in my component and passes the tile id which contains the name of the partial to return. I use the method $this->renderPartial('partialName') however I get the following error

the error

Call to a member function setComponentContext() on null

Here are screenshots to the rest of my code:

My Javascript

$( document ).ready(function() {

    $(document).on('click','.tile',function() {

        let page = $(this).attr('id');

        history.pushState({page}, '', 'planner/' + page );

        getPage(page);

    });

    window.onpopstate = function(e){

        if(e.state){
        getPage(e.state.id);
    }

    };

    history.replaceState({page: null}, 'Default state', './planner');

    function getPage (page) {

        $.ajax({
            url: '/planner/' + page,
            type: 'GET',
            success: function(data){
                $('.page-container').html(data);
            },
            error: function(data) {
                console.log('Could not return page');
            }
        });

    }



});

My Router.php

<?php

Route::get('/planner/{page}', 'myName\budgetplanner\Components\app@test');

My Component

<?php

namespace myName\budgetplanner\Components;

use Db;

class app extends \Cms\Classes\ComponentBase
{
    public function componentDetails()
    {
      return [
            'name' => 'budgetplanner',
            'description' => 'Manage your finances.'
        ];
    }

    public function onRender()
    {
      echo ( $this->renderPartial('@dashboard.htm') );
    }

    public function test($page)
    {

      if ($page == 'undefined') {
        echo ( $this->renderPartial('@dashboard.htm') );
      }
      elseif ($page == 'overview') {
        echo ( $this->renderPartial('@overview.htm') );
      }
      elseif ($page == 'month') {
        echo ( $this->renderPartial('@month.htm') );
      }
      elseif ($page == 'reports') {
        echo ( $this->renderPartial('@reports.htm') );
      }
      elseif ($page == 'budget') {
        echo ( $this->renderPartial('@budget.htm') );
      }
      elseif ($page == 'settings') {
        echo ( $this->renderPartial('@settings.htm') );
      }

    }

}

I tried doing some testing and think I've found the issue but I don't really understand how to go about fixing it. here are some additional screenshots

I dump the component object testing component On render it looks fine onRender now its empty ? clicked settings page


Solution

  • This is Wrong way of handling Ajax request as you are breaking lifecycle of October CMS Page.

    you are getting error because you directly ask component to handle request Route::get('/planner/{page}', 'myName\budgetplanner\Components\app@test');

    as renderpartial need controller context and if you do route like this it will surely yield unexpected behaviour

    Ok, We get this, BUT then , How to do it correctly ?


    Your url lets say we use like this /planner/:type

    enter image description here

    Add framework and extra to sure layout for ajax-framework [ make sure you add them before your script ]

    <script src="{{ 'assets/javascript/jquery.js'|theme }}"></script>
    {% framework extras %}
    

    Your script should look like this

    <script>
    $(document).ready(function() {
        
        $(document).on('click','.tile',function() {
        
            let page = $(this).attr('data-tile');
            history.pushState({page}, '', '/planner/' + page );
            getPage(page);
        });
    
        window.onpopstate = function(e){       
            if(e.state){
                getPage(e.state.page);
            }
        };
        // not sure causing issues so commented
        // history.replaceState({page: null}, 'Default state', './planner');
        function getPage (page) {
            $.request('onRenderTile', { data: { type: page }})
        }
    });
    </script>
    

    Your Component

    public function onRender()
    {
        // if type is not passed default would be dashboard
        $type = $this->param('type', 'dashboard');
        return $this->onRenderTile($type)['#tile-area'];
    }
    
    public function onRenderTile($type = null)
    {
        $availableTiles = [
            'dashboard',
            'tile1',
            'tile2',
            'tile3',
        ];
    
        // if not passed any value then also check post request
        if(empty($type)) {
            $type = post('type');
        }
    
        // we check partial is valid other wise just return dashboard content
        if(in_array($type, $availableTiles)) {
            return ['#tile-area' => $this->renderPartial('@'.$type.'.htm')];
        }
        else {
            return ['#tile-area' => $this->renderPartial('@dashboard.htm')];
        }
    }
    

    Your patials

    file : _tiles.htm

    <div class="tile" data-tile="dashboard">Dashboard</div>
    <div class="tile" data-tile="tile1">Tile 1</div>
    <div class="tile" data-tile="tile2">Tile 2</div>
    <div class="tile" data-tile="tile3">Tile 3</div>
    

    file : dashboard.htm

    <div id="tile-area">
        {% partial __SELF__~"::_tiles" %}
        <h1>Dashboard</h1>
    </div>
    

    file : tile1.htm

    <div id="tile-area">
        {% partial __SELF__~"::_tiles" %}
        <h1>Tile 1 Content</h1>
    </div>
    

    file : tile2.htm

    <div id="tile-area">
        {% partial __SELF__~"::_tiles" %}
        <h1>Tile 2 Content</h1>
    </div>
    

    file : tile3.htm

    <div id="tile-area">
        {% partial __SELF__~"::_tiles" %}
        <h1>Tile 3 Content</h1>
    </div>
    

    Initially it will render default partial dashboard if no type is passed

    dashboard has all other tile links and dashboard content same as other tile partials.

    Now if you click any tile it will fire October Ajax framework request with handler onRenderTile and type (dashboard|tile1|tile2 ...)so it will properly call October lifecycle methods and finally render partial through onRenderTile and return json with key #tile-area and as value it will return content of posted partial name(type)

    OctoberCMS ajax framework is smart enough that it will just replace this content with the give id so it will search #tile-area and replace its content with new one.

    for more information about updating partials using ajax you can read this : https://octobercms.com/docs/ajax/update-partials

    if any doubt or question please comment.