http://mackhankins.com Lazy Loading with JsTree and Laravel
Jul 9, 2016  

I have had strep and pneumonia for the last week which gave me some time to knock a few things off the todo list for work. One of those things was to figure out lazy loading with JsTree which loads each subfolder with ajax only when requested. First, I had to rewrite the class I had been using to even get started. We're going to be relying on that heavily so check it out first.

Boilerplate

I installed a fresh copy of Laravel for this called jstree. I'll just list out some of the boilerplate setup that you can skip if you would like. I'm also pulling in font-awesome off a cdn, but we will get to that later.

cd ~/projects
laravel new jstree
cd ~/projects/jstree
npm install
php artisan make:controller TreeController
touch ~/projects/jstree/resources/views/tree.blade.php
cd ~/projects/jstree/storage/app
git clone https://github.com/msurguy/Laravel-wallpapers.git
Copied over JsTree.php to App/Support

Controller

The controller I'm setting up for the demo only needs two methods. One to render the view and one to fetch the json response. I've ommitted docblocks and settable options from the jstree class for brevity...

<?php

namespace App\Http\Controllers;

use App\Support\JsTree;
use Illuminate\Http\Request;
use App\Http\Requests;
use Illuminate\Support\Facades\Storage;

class TreeController extends Controller
{

    public function index()
    {
        return view('tree');
    }

    public function data(Request $request)
    {
        $id = 'Laravel-wallpapers';

        if ($request->has('id') and $request->id != '#') {
            $id = $request->id;
        }

        $nodes = array_merge(
            Storage::directories($id),
            Storage::files($id)
        );

        $tree = new JsTree($nodes, 'Laravel-wallpapers');
        $tree->fileIconClass = 'fa fa-file';
        $tree->setExcludedExtensions(['DS_Store', 'gitignore']);
        $tree->setExcludedPaths(['Laravel-wallpapers/.git']);
        $tree->setDisabledExtensions(['md', 'png']);

        return response()->json($tree->build());
    }
}

Routes

I named one of the routes for convenience, but you don't have to.

Route::get('tree', [
    'uses' => 'TreeController@index',
]);

Route::get('tree/data', [
    'as' => 'tree.data',
    'uses' => 'TreeController@data',
]);

View and Javascript

We have already created resources/views/tree.blade.php and now I'm just going to fill it with some generic html boilerplate. My assets are Jquery, Jstree, and Font-awesome.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
        <meta name="description" content="Page Description">
        <meta name="author" content="mackhankins">
        <title>JsTree Example</title>

        <!-- Font-Awesome -->
        <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet">

        <!--JsTree 3.3.1 -->
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.1/themes/default/style.min.css">

        <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
        <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
        <!--[if lt IE 9]>
          <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
          <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
        <![endif]-->
    </head>
    <body>
        <h1>JsTree Example</h1>
        <div id="jstree"></div>

        <!-- jQuery (necessary for JsTree) -->
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
        <!-- JsTree 3.3.1 -->
        <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.1/jstree.min.js"></script>

        <script type="text/javascript">
            $('#jstree').jstree({
                'core': {
                    'data': {
                        'url': '{!! route('tree.data') !!}',
                        },
                        'data': function (node) {
                            return {'id': node.id};
                        },
                        'error': function (data) {
                            $('#jstree').html('<p>We had an error...</p>');
                        }
                    }
                }
            });
        </script>
    </body>
</html>

You should have a working result on your screen at this point. If you look in chrome console you will notice that every time we click to open a folder we get a new ajax request and json response.

That's really it.

A Hard Base

One of the sacrifices I had to make was setting a hard base, but I can live with it.

//TreeController
$tree = new JsTree($nodes, 'Laravel-wallpapers');

//JsTree Class
'parent'  => ($this->base == dirname($node) ? '#' : dirname($node) ),

The reason is that top-level folders always need to have a parent of #and you can't deduce that in a dynamic class.

Children

'children' => ($file ? false : true),

That one line brings the whole lazy-loading functionality together, but one of the caveats are even if a folder is empty you can still try to expand it.

Font-Awesome

By default, JsTree doesn't have a file icon. To it everything is a node and It looks terrible. I always overwrite that with fa fa-file in the class options. Here is a side by side comparison.