Merhaba,
Bu yazıda https://www.jstree.com/ adresindeki JS bileşenini kullanarak Client tarafında Javascript ile; ağaç yapısında (Nested Set) kategorilerimizi göstermeyi ve aynı zamanda https://github.com/lazychaser/laravel-nestedset adresindeki Laravel Nested Set paketini kullanarak Laravel'de bu sınırsız kategorileri okuyup yazmayı anlatacağım. Bu yöntemin çalıştığını biliyorum ancak daha iyi yöntemleri belirterek geliştirme yapacak arkadaşlar için teşekkürler.
www.jstree.com adresinden jsTree bileşenini yükledikten sonra jstree için gerekli CSS'i Blade'imize çağıralım.
<link rel="stylesheet" href="dist/themes/default/style.min.css" />
Blade sayfamızda bir DIV oluşturalım. jsTree bileşeni; oluşturacağı ağaç yapısını bu DIV'in içerisine oluşturacak.
İsmini (ID'sini jstree yapıyoruz)
<div id="jstree"></div>
Blade sayfamızda jQuery'yi ve jstree.js dosyalarını da çağıralım.
<script src="dist/jquery.min.js"></script>
<script src="dist/jstree.min.js"></script>
Şimdi javascript bloğu içinde jsTree'yi başlatıp, konfigüre etmemiz gerekiyor. Ben şöyle yaptım.
<script>
$('#jstree').jstree({
//Aynı anda sadece 1 tane seçim olmasını istediğim için three_state'i false olarak atadım
"checkbox": {
keep_selected_style: true,
three_state: false,
},
//seçilince tüm satırın seçili olmasını, her bir öğede bir checkbox olmasını ve arama fonksiyonu kullanacağımızı belirtiyoruz. Bunlar jsTree'nin plugin'leri. Diğer plugin'leri incelemek için https://www.jstree.com/plugins/ adresine bakabilirsiniz.
plugins: ["wholerow", "checkbox", "search"],
core: {
//Yeni kategori ekleyince, New Node yazısı yerine "Yeni Kategori" yazsın.
strings : {
'New node': 'Yeni Kategori'
},
//Çoklu seçim yapılmasını istemiyorum, aynı anda tek seçim yapılsın
//responsive olsun ve zebra çizgili olsun (stripes)
multiple: false,
themes: {
name:"default",
variant: "large",
responsive: true,
stripes: true,
},
//jsTree'nin Ajax ie otomatik doldurulmasını istiyoruz.
//Bu fonksiyon (getProductCategories) aşağıda bulunuyor.
data: {
"url": "{{Route("productCategories.getProductCategories")}}"
},
}
});
</script>
jsTree bileşenine ekleme, çıkarma ve düzenlemeyi Client üzerinde javaScript yaptıracak ve işlemler bittiğinde tüm data'yı controllar'a göndererek database'e kayıt edeceğiz. O nedenle blade'imize 4 tane de tuş koyalım. Bu tuşlar EKLEME, SİLME ve DÜZENLEME ve KAYDETME işlemlerini yapacaklar. Aynı zamanda bir de TEXTBOX ekleyelim, bu sayede oluşturulmuş olan ağaç yapısı üzerinde JS ile ARAMA işlemini de yapabileceğiz.
Yaklaşık olarak şöyle bir ekran yapacağız :
Blade'deki tuşlarımız ve arama kutumuz için kodlarım şu şekilde :
<button class="btn btn-success float-right ml-1" type="button" onclick="jstree_save();">
<i class="fas fa-check"></i><strong> Kaydet</strong>
</button>
<div class="col-md-4 text-center">
<input class="form-control" type="text" id="search_box" placeholder="Ara..." autocomplete="off">
</div>
<div class="text-center">
<a href="#" class="btn btn-success text-left" onclick="jstree_create();">
<i class="fas fa-plus"></i><strong>Yeni Kategori</strong>
</a>
<a href="#" class="btn btn-warning text-left" onclick="jstree_rename();">
<i class="fas fa-edit"></i><strong>Kategoriyi Değiştir</strong>
</a>
<a href="#" class="btn btn-danger text-left" onclick="jstree_delete();">
<i class="fas fa-trash"></i><strong>Kategoriyi Sil</strong>
</a>
</div>
Aynı dosyada yukarıdaki 4 tuşun vazifesini yapmak için şu script'leri ekleyelim. Burada arama işlemi için ve kayıt etme işlemi için de fonksiyonumuz da burada bulunuyor.
jstree_save ile tüm datayı alıyoruz ve Ajax ile Controller'ımıza gönderiyoruz.
jstree_create ile yeni bir öğe oluşturuyoruz. Seçili öğe varsa alt öğe, seçili öğe yoksa ana öğe oluşturuyoruz.
jstree_rename ile seçili olan öğenin ismini değiştiriyoruz.
jstree_delete seçili olan öğeyi siliyoruz.
function jstree_save () {
//Ağaç yapısının şu andaki halini get_json ile alalım. (attr , li özellikleri ve state özelliği bana gerekmiyor.)
var treeData =$("#jstree").jstree(true).get_json('#',{no_a_attr:true,no_li_attr:true,no_state:true});
$.ajax({
url: '{{route('productCategories.store')}}',
type: 'POST',
headers: { 'X-CSRF-TOKEN': $('meta[name="csrf_token"]').attr('content') },
data: { data: treeData },
});
}
function jstree_create() {
var ref = $('#jstree').jstree(true),
sel = ref.get_selected();
if (!sel.length) sel = "#";
sel = sel[0];
sel = ref.create_node(sel);
if (sel) {
ref.edit(sel);
}
}
function jstree_rename() {
var ref = $('#jstree').jstree(true),
sel = ref.get_selected();
if (!sel.length) {
alertify.alert('Bilgi', 'Değiştirmek için bir grup seçin.');
return false;
}
sel = sel[0];
ref.edit(sel);
}
function jstree_delete() {
var ref = $('#jstree').jstree(true),
sel = ref.get_selected();
if (!sel.length) {
alertify.alert('Bilgi', 'Silmek için bir grup seçin.');
return false;
}
alertify.confirm('Emin misiniz ?', 'Seçili olan grup silinecektir. Emin misiniz ?', function () {
ref.delete_node(sel);
alertify.success('Grup Silindi.')
}
, function () {
alertify.error('İşlem iptal edildi.')
});
}
var to = false;
$('#search_box').keyup(function () {
if (to) {
clearTimeout(to);
}
to = setTimeout(function () {
var v = $('#search_box').val();
$('#jstree').jstree(true).search(v);
}, 250);
})
Şimdilik Blade tarafı tamam. Artık https://github.com/lazychaser/laravel-nestedset paketini yükleyebiliriz.
Nasıl yüklendiği ve detaylar ilgili sayfada var. Yine de kısaca anlatalım.
Paketi şu şekilde kuralım :
composer require kalnoy/nestedset
Benim modelimin adı ProductCategory. Oluşturacağınız migration dosyanıza şu satırları ekleyelim :
public function up()
{
Schema::create('product_categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->nestedSet();
$table->timestamps();
});
}
Veritabanında kategori isminin tutulduğu alanı "name" yapalım.
Migrasyon işlemini tamamlayıp çalıştırdığımızda artık benim tablomda şu alanlar vardı :
id
name
_lft
_rgt
parent_id
created_at
updated_at
Model dosyasına şu satırı ekleyelim.
use Kalnoy\Nestedset\NodeTrait;
Devamında yine model dosyasındaki class içerisine şunu ekleyelim :
protected $fillable = ['name','id'];
use NodeTrait;
Artık Controller dosyamızdan kategorileri çekip, sayfamıza gönderebiliriz.
public function getProductCategories()
{
$productCategories = productCategory::defaultOrder()->withDepth()->get()->linkNodes();
$result = [];
foreach ($productCategories as $productCategory) {
$parent = $productCategory->parent_id ?: '#';
$node = [
'id' => $productCategory->id,
'parent' => $parent,
'text' => $productCategory->name,
];
array_push($result, $node);
}
return response()->json($result);
}
Controller dosyamız hazır açıkken "store" fonksiyonumuzu da yazalım.
Blade tarafında Kaydet tuşuna basıldığında bu fonksiyon çağrılacak.
Nested Set paketinin rebuildTree dokümanını https://github.com/lazychaser/laravel-nestedset#inserting-nodes adresinden okuyabilirsiniz. Burada önemli olan bu fonksiyonun "tüm ağaç yapısını" yeniden oluşturuyor olmasıdır.
Bu nedenle sizin Nested kategori mantığında bulunan "left" ve "right" değerlerini hesaplamak için bir efor sarfetmeniz gerekmiyor. rebuildTree fonksiyonunun ikinci parametresinin true olduğuna dikkat etmişsinizdir. Ben js tarafında silinen öğelerin veritabanında da silinmesini istedim. Siz istemiyorsanuz bu parametreyi false yapabilirsiniz.
public function store(Request $request)
{
$data = $request->data;
$result = productCategory::rebuildTree($data,true);
}
jsTree bileşeni, ağaç yapısını oluşturmak için ilgili kategorinin adını text değişkeninde isterken, Nested Set bu ağaç yapısını kayıt etmek için name isimli bir değişken istiyor. Ayrıca jsTree'den get_json ile veriler çekildiğinde, yeni eklenen kategorilerin ID'si j1_1 veya j1_2 gibi bir ID olarak dönüyor. Oysa Nested Set paketi yeni eklenen kategorilerin ID'sinin boş olmasını istiyor.
Not : Eğer Nested Set'e gönderilen kategorinin ID değişkeni dolu ise düzenleme yapılıyor, ID değişkeni var ama boş ise yeni kayıt yapılıyor, ID değişkeni yok ama veri tabanında var ise silme yapılıyor.
Bunları da eşitlemek için jstree.js dosyasındaki get_json dosyasında bazı değişiklikler yapmak durumundayım :
Açıklama satırı bulunan satırlar değiştirilmiş satırlardır.
get_json : function (obj, options, flat) {
obj = this.get_node(obj || $.jstree.root);
if(!obj) { return false; }
if(options && options.flat && !flat) { flat = []; }
**//Eğer ilgili kategorinin ID'si "j" ile başlıyorsa, uyumlu olması için ID'yi boş string olarak atayalım.**
**var id=obj.id;**
**if ((id).startsWith("j")) id=id="";**
var tmp = {
//id değişkenini atayalım
'id' : id,
//"text" değişken ismini "name" olarak değiştirelim.
'name' : obj.text,
//'icon' : this.get_icon(obj),
'li_attr' : $.extend(true, {}, obj.li_attr),
'a_attr' : $.extend(true, {}, obj.a_attr),
'state' : {},
'data' : options && options.no_data ? false : $.extend(true, $.isArray(obj.data)?[]:{}, obj.data)
//( this.get_node(obj, true).length ? this.get_node(obj, true).data() : obj.data ),
}, i, j;
if(options && options.flat) {
tmp.parent = obj.parent;
}
else {
tmp.children = [];
}
if(!options || !options.no_state) {
for(i in obj.state) {
if(obj.state.hasOwnProperty(i)) {
tmp.state[i] = obj.state[i];
}
}
} else {
delete tmp.state;
}
if(options && options.no_li_attr) {
delete tmp.li_attr;
}
if(options && options.no_a_attr) {
delete tmp.a_attr;
}
if(options && options.no_id) {
delete tmp.id;
if(tmp.li_attr && tmp.li_attr.id) {
delete tmp.li_attr.id;
}
if(tmp.a_attr && tmp.a_attr.id) {
delete tmp.a_attr.id;
}
}
if(options && options.flat && obj.id !== $.jstree.root) {
flat.push(tmp);
}
if(!options || !options.no_children) {
for(i = 0, j = obj.children.length; i < j; i++) {
if(options && options.flat) {
this.get_json(obj.children[i], options, flat);
}
else {
tmp.children.push(this.get_json(obj.children[i], options));
}
}
}
//Yeni oluşturulan id değişkenini kullanılıyor
return options && options.flat ? flat : (id === $.jstree.root ? tmp.children : tmp);
}
Bu işlemler neticesinde Blade tarafında DB'deki Nested Sınırsız Kategorileri göstermiş olduk, kullanıcının düzenleme, ekleme, çıkarma gibi işlemleri yapmasını sağlamış olduk. Son olarak Kaydet tuşu ile de tüm datayı Controller'a göndererek son halini kayıt etmiş olduk.