ublaboo/datagrid: mocný, rychlý, rozšiřitelný, hezký, anglicky dokumentovaný datagrid
- Pavel Janda
- Člen | 977
ublaboo/datagrid
- Dokumentace: https://contributte.org/…te/datagrid/
- Demo: https://examples.contributte.org/…es/datagrid/
- Componette: https://componette.org/…te/datagrid/
Instalace
composer require ublaboo/datagrid
Roadmap
- Vylepsovat verzi 6.0.
- Nahrazeni nette.ajax.js
- Vycleneni datasources do vlastnich balicku.
Verze
State | Version | Branch | Nette | PHP |
---|---|---|---|---|
dev | ^6.2.0 |
master |
>=3.0 | ^7.2 |
stable | ^6.1.0 |
master |
>=3.0 | ^7.2 |
stable | ^5.7.1 |
v5.x |
>=2.3 | ^5.6 |
stable | ^4.4.22 |
v5.x |
>=2.3 | ^5.6 |
stable | ^3.3.1 |
v5.x |
>=2.3 | ^5.3 |
Features
Vypíchnu některé věci, které jsou třeba zajímavější a nedostupné u jiných datagridů (všechny ostatní filtrování, datasourcy, sortování, reorderování, inline editace (malá i velká), ajax stavy, url refresh přes history api a podobně máme samozřejmě také!):
- Ajaxově načítaný detail položky (viz očíčko na webu)
- Zajímavější filtry, sloupce (filter range, date range, column link)
- Možnost skryvání sloupců (s interaktivním přhledem skrytých a zobrazených)
- Možné custom šablony filtrů/sloupců/dalších bloků
- Možnost definovat sloupec i rendererem, šablonou, nebo latte
{block}
em - Tree view s ajaxově načítanými potomky
- Vícelevelové hromadné akce
- Column renderer s možností definovat podmínku, při které se použije
- Při ajaxových změnách položek (viz např. změna statusu na webu) není třeba překreslovat celý grid, ale jen jeden řádek
- Filtrované/nefiltrované exporty, csv ready (opět filtered/non filtered)
- JEDINÝ grid, který umí pomocí doplňku
ublaboo/datagrid-nette-database-data-source
použít jako data source i čistou query, kterou předáváte metoděNette\Database\Context::query()
– více zde. NDBT umí datagrid samozřejmě také. - SUM vybraných sloupců na konci tabulky
Prologue
Pustil jsem se do tohoto datagridu proto, že jsem chtěl především snadno rozšiřitelné a upravitelné šablony (u ostatních datagridů frontenďáci skuhrají a mají proč). Potřebuji mobilní verzi výpisu dat? S tímto datagridem hračka. Poextendím šablonu, definuji block data (nebo nastavím rovnou svojí šablonu – tím ale vypadne stránkování, filtry a pod), a iteraci prvků nebudu dělat v tabulce přes řádky ale pomocí divů. Opět – více info v docu.
Editoval Pavel Janda (14. 1. 2018 22:21)
- CZechBoY
- Člen | 3608
Pěkný. Líbí se mi možnost práce se skupinou zatržených řádků.
Plánuješ do budoucna přidat možnost seskupení řádků?
Např. mám stromové zobrazení kategorií – takže se mi na začátku
ukážou všechny nadřazené kategorie, po rozkliknutí třeba „+“ se mi
pod tou nadřazenou kategorií zobrazí podřazený kategorie.
Případně bych si to nakodil znova sám pokud to půjde nějak jednoduše jako
třeba v o5/gridu.
- Oli
- Člen | 1215
@CZechBoY myslíš něco jako http://ublaboo.paveljanda.com/…id/tree-view
btw. jak to děláš v grido? Zatím to asi nepotřebuju, jen mě to zajímá. Nativní podporu to nemá, nebo jo?
Editoval Oli (1. 2. 2016 0:28)
- Myiyk
- Člen | 321
👍 Hezké.
Dávám velké + za propracovanou dokumentaci.
U http://ublaboo.paveljanda.com/…id/tree-view jsem našel
chybku.
Jde vyjmout potomka z rodiče, ale když vyjmu potomky všechny. Tak už je tam
nejde vložit.
- Pavel Janda
- Člen | 977
@Myiyk Díky. Ano, vím o tom. Má to nyní prioritu number one. Bohužel budu muset asi přepsat šablonu tree view, protože knihovnička jquery ui sortable, kterou v datagridu používám, má pěkně zbastlené api a pokud náhodou metodou pokus-omyl nenajde člověk něco složitějšího, co hledá, nemá šanci docílit kýženého efektu. Dnes/zítra se tomu budu věnovat.
@CZechBoY Myslíš v tom tree view? Tedy, pokud by podkategorie
některé kategorie měly > X záznamů, zobrazil by se u nich
paginator?
@Oli Nope, nemá. Jeden z důvodů, proč začal vznikat vlastní.
Editoval Pavel Janda (1. 2. 2016 8:51)
- Pavel Janda
- Člen | 977
@CZechBoY Zajímavý nápad. Dávám to do issue jako enhancement.
Editoval Pavel Janda (1. 2. 2016 14:43)
- Pavel Kravčík
- Člen | 1195
Opravdu hezká dokumentace.
Možná to sem nepatří. Ale kdyby to nativně řešilo „zaškrtnutí všech“ s výjimkou těch, které nesplňují nějaké podmínky bylo by to famózní. :) Tj. seznam 100 řádků a já chci vybrat třeba, jen „ženské“.
- Pavel Janda
- Člen | 977
@Felix Když já bych měl nejradši takto co nejméně textu, aby ten link „here“ byl co neviditelnější.. Když budu cpát „kousky“ dokumentace do Readme, tak si člověk řekne „hm, to toho moc neumí, to se moc nepředvedl“. Kdežto teď rovnou skočí k plné dokumentaci a ukázkám.. Chápu, že toho není moc vidět z componette webu. Ale odkaz na dokumentaci tam je.. nějaký jiný nápad, co tam přidat, krom zkrácené verze dokumentace?
@PavelKravčík Díky.
Edit Aha, blbě jsem to pochopil. :D Zatím mě taky nenapadá jiná
možnost než vyfiltrovat ženské a zaškrtnout vše..
Editoval Pavel Janda (1. 2. 2016 15:01)
- Felix
- Nette Core | 1196
@PavelJanda Premyslim o nejakem informacnim souboru jenom pro Nette/Componette, ber to prosim hodne teoreticky.
V repozitari by jsi mel soubor .nette, ktery by byl JSON. Byly by tam treba tagy a take odkaz na prislusnou dokumentaci.
{
tags: ["grid", "foo", "bar"],
docs: [
"/docs/index.md",
"/docs/sub-page.md"
],
}
Docs by mohlo byt pole, s tim, ze componette by pak umelo udelat nejake menu ze stranek.
Mam to spis v hlave, nevim jak by se na to tvarili ostatni.
- Pavel Kravčík
- Člen | 1195
@PavelJanda: Omlouvám se, špatně jsem se vyjádřil. A příklad by ještě horší. Tohle je samozřejmě super a chápu to. Myslel jsem omezit možnost group-action pro některé řádky nezávisle na filtrech. Tj. uživatel může vidět vše, ale třeba jen tohle může smazat. Teď to řeším složitě přes JS.
Např. něco takového:
$dataGrid->RowCallback = [$this, 'allowRows'];
...
public function allowRows($row)
{
if($row->foo == 'bar')
{
$row->setDisabledGroupAction();
}
return $row;
}
Ale třeba je to blbost řešit každou eventualitu. :)
- Pavel Janda
- Člen | 977
@Felix To zní náhodou moc dobře, spravovat componette stránku z repozitáře projektu.
@PavelKravčík Teď mě mrzí, že jsem nepojal rendering datagridu takto přes vlastní třídu Row. Ale díky za nápad, asi to nezapadne, na to se mi to moc líbí. :)
Co se týče samotného problému, určitě bych také našel uplatnění. Píšu si issue!
- Pavel Janda
- Člen | 977
@Antik Haha, vypnutý JavaScript. Díky, nějak mě nenapadlo použít datagrid bez toho. Ale musí to fungovat, to je jasnačka. večer bude fucnuté.
- Stinky
- Člen | 7
@PavelJanda Jak vyřešit Many-To-One renderování v tabulce.
Zkoušel jsem to takto –
$queryBuilder = $queryBuilder->select('p, c.code')
->from(System\Shipping\PickupPlaces\PickupPlace::class, 'p')
->leftJoin('p.currency', 'c')
->where('p.transportCompany = 1');
Výstup pak vypadá takto. Tedy hlavní entita (p) se hodí do dalšího pole. Jde to vyřešit nějak čistě?
Jinak díky za parádní doplněk! Přišel v tu nejvíc vhodnou chvíli kdy chystám nový projekt a vypadá zatím bezvadně!
- Jakub Kontra
- Člen | 30
Stinky napsal(a):
@PavelJanda Jak vyřešit Many-To-One renderování v tabulce.
Zkoušel jsem to takto –
$queryBuilder = $queryBuilder->select('p, c.code') ->from(System\Shipping\PickupPlaces\PickupPlace::class, 'p') ->leftJoin('p.currency', 'c') ->where('p.transportCompany = 1');
Výstup pak vypadá takto. Tedy hlavní entita (p) se hodí do dalšího pole. Jde to vyřešit nějak čistě?
Jinak díky za parádní doplněk! Přišel v tu nejvíc vhodnou chvíli kdy chystám nový projekt a vypadá zatím bezvadně!
Podivam se na to, Doctrine mam na starosti ja :)
- jiri.pudil
- Nette Blogger | 1029
Tohle je imo fakt vlastnost doctrine. Buďto použij
select('p, c')
, nebo select('p, PARTIAL c.{code}')
,
pokud ti stačí jen ten kód. Doctrine by ti pak měla vrátit pole PickupPlace
s hydratovanou asociací na Currency.
- Pavel Janda
- Člen | 977
Nová verze – v1.1.0
- Přidány podmínky pro výpis akcí a hromadných akcí (ukázka). Ping @PavelKravčík.
- Updaty JS, stromový výpis konečně zanořuje nové potomky do rodičů.
- Možnost definování nadpisu sloupce blokem
{block col-name-header}
. - Opravy
Editoval Pavel Janda (4. 2. 2016 0:14)
- Pavel Janda
- Člen | 977
@fizzy Ano. Viz github issue: https://github.com/…id/issues/42. Prosím o strpení do večera. Zatím je to v masteru. Jakmile k tomu připíši testy, releasnu verzi 1.2.0.
Editoval Pavel Janda (3. 2. 2016 16:10)
- Pavel Janda
- Člen | 977
Nová verze – v1.2.0
- Data z Doctriny nejsou již předávána v poli, ale klasicky jako entity.
- Třída
Row
, která obaluje vypisované položky, umí přistupovat k „properties“ entit. Dohromady to znamená, že (na pozadí za pomoci balíčku symfony/property-access, pokud doctrine) můžete jednoduše zobrazovat „related“ entity a jejich data. Příklad: mám tříduUser
a ta má property$name
. Dále má property$grandma
– to je babička uživatel, též instance třídyUser
. Pokud chci vypsat uživatele a vedle něj jméno jeho babičky, docílím toho jednoduše takto:
$grid->addColumnText('name', 'Name', 'name');
$grid->addColumnText('grandma_name', 'Grandma', 'grandma.name');
- Ještě malinká změna v JS – sorting: Neposílá se v url parametr ID, ale ITEM_ID – více info v zavřeném github PR#46. Díky za fix @DavidZadražil.
@castamir též díky za inspiraci na poslední PoSobotě.
Editoval Pavel Janda (4. 2. 2016 1:07)
- Pavel Janda
- Člen | 977
@fizzy Pokud tomu správně rozumím, tak chceš toto? Příklad: User nemá jen jednu babičku, ale víc babiček. Chci vypsat v gridu usery a k nim jejich babičky – buď jména oddělené čárkami, nebo jen jednu babičku – tu nejstarší. Příklad řešení „babičky oddělené čárkami“ pomocí orm (řešil bych to normálně přes group_concat, ale nevím, jak se to dělá v Doctrině, takže orm-ově):
class FooPresenter
{
public function createComponentGrandmaGrid
{
# ...
$grid->addColumnText('username', 'Username');
$grid->addColumnText('grandmas', 'Grandmas', 'names_of_grandmas');
}
}
class User
{
/**
* one to many blabla
*/
private $grandma;
public function getNamesOfGrandmas()
{
$return = [];
foreach ($this->grandmas as $grandma) {
$return[] = $grandma->name;
}
return implode(', ', $return);
}
}
Opět – nepoužívám doctrinu, takže nevím, jak efektivně vytáhnout
jednu nejstarší babičku. V LeanMapperu bych zavolal například
getOldestGrandma()
, to by mi vrátilo nejstarší babičku jako
instanci Usera. Potom bych přidal textový sloupec:
$grid->addColumnText('grandmas', 'Grandmas', 'oldest_grandma.name');
Nutno zmínit, že pokud by babička neexistovala, nemá se odkud vytáhnout
property name
té neexistující instance. Datagrid s tím
počítá. Vrací v takovém případě NULL. Pokud datagridu změním property
$strict_entity_property
na TRUE, tak bude křičet, že instance,
odkud má brát property name
, neexistuje.
Editoval Pavel Janda (4. 2. 2016 10:21)
- Pavel Janda
- Člen | 977
@fizzy Koukl jsem se to dokumentace doctriny a já bych to řešil třeba takto:
1, Entita Pricing:
class pricing
{
private $price;
private $currency;
/**
* @ManyToOne(targetEntity="Product", inversedBy="pricings")
* @JoinColumn(name="product_id", referencedColumnName="id")
*/
private $product;
public function getPrice()
{
return $this->price;
}
}
2, Entita Product:
class Product
{
private $name;
/**
* @ORM\OneToMany(targetEntity="Pricing", mappedBy="product")
*/
private $pricings;
public function getName()
{
return $this->name;
}
public function getMainPricing()
{
$criteria = Doctrine\Common\Collections\Criteria::create()
->where(Criteria::expr()->eq("currency", "CZK"))
->setFirstResult(0)
->setMaxResults(1);
$pricing = $this->pricings->matching($criteria);
return !empty($pricing) ? reset($pricing) : NULL;
}
}
3, V gridu vypíšu požadovanou měnu:
$grid->addColumnText('price', 'Price', 'main_pricing.price');
Jak říkám, s doctrine neumím a tohle jsem teď vykoukal z dokumentace. Ale snad jsem nastínil svůj myšlenkový pochod..
Editoval Pavel Janda (4. 2. 2016 11:10)
- chap
- Člen | 81
Ahoj, začal jsem si hrát s tvým Datagridem a narazil jsem na následující:
$grid = new DataGrid();
$grid->addColumnText("name", "Jméno")->setSortable();
$grid->addFilterText('name', 'Search');`
Používám Kdyby/Doctrine a při filtrování mi to vytvoří následující chybný DQL:
SELECT u FROM App\User u WHERE name LIKE '%karel%'
místo
SELECT u FROM App\User u WHERE u.name LIKE '%karel%'
Nebo dělám něco chybně? Jinak super práce a díky za alternativní Grid!!
- Pavel Janda
- Člen | 977
@chap
Zkus toto:
$grid->addColumnText("name", "Jméno", "u.name")->setSortable();
$grid->addFilterText('name', 'Search', 'u.name'); // Nebo ['u.name', 'u.address', ...] pro vyhledávání nad více sloupci
- chap
- Člen | 81
Pavel Janda napsal(a):
@chap
Zkus toto:
$grid->addColumnText("name", "Jméno", "u.name")->setSortable(); $grid->addFilterText('name', 'Search', 'u.name'); // Nebo ['u.name', 'u.address', ...] pro vyhledávání nad více sloupci
To jsem zkoušel, ale psalo mi chybu Cannot read an undeclared property App\User::$u
Řešením nakonec je přidání aliasu do filtru :)
$grid->addColumnText("name", "Jméno");
$grid->addFilterText('name', 'Search', 'u.name');
Díky
- Pavel Kravčík
- Člen | 1195
@PavelJanda: Zkoušel si „item detail“ pro ArrayDataSource?
V případě, že vyzkouším tohle na jednoduchý datagrid (1 textColumn):
$grid->setItemsDetail(function() { return 'Lorem Ipsum'; });
Argument 2 passed to Ublaboo\DataGrid\DataSource\ArrayDataSource::applyFilter() must be an instance of Ublaboo\DataGrid\Filter\Filter, array given
–
nemělo by to být těžké zduplikovat.
Zkoušel jsem definovat primaryKey a nastavit setRemmeberState na FALSE pro jistotu.
Tenhle příklad by měl být funkční nebo něco dělám špatně? :) Díky.
$grid = new \Ublaboo\DataGrid\DataGrid($this, $name);
$grid->setPrimaryKey('id');
$grid->setDataSource([['id' => 1, 'name' => 'Pavel']]);
$grid->addColumnText('name', 'Jméno');
$grid->setItemsDetail(function() { return 'Lorem Ipsum'; });
- Pavel Janda
- Člen | 977
@PavelKravčík v1.2.6 :D
Dík
To jsou přesně ty testy, ale budou, budou..
Editoval Pavel Janda (8. 2. 2016 12:19)
- CZechBoY
- Člen | 3608
@PavelJanda
Je v datagridu možnost custom edit control?
Zobrazí se mi validační errory při ajaxovým updatu?
Editoval CZechBoY (9. 2. 2016 18:16)
- Pavel Janda
- Člen | 977
@CZechBoY Není. Stále váhám, jestli nechat takto jednoduchou inline editaci, nebo přidat velký kontejner, který bude obalovat celý řádek. Už jsem psal do některé issue, proč jsem se zatím nepustil do „velké“ inline editace a přidávání záznamů. Pokud ale přijdou pořádné argumenty, tak se do toho budu muset pustit. :)
Jj, vím o tom. Budu muset asi lehce překopat celý flex box, pomocí kterého je tvořen tree view.
- Pavel Janda
- Člen | 977
Nová verze – v1.3.0
- Přidáno skrývání sloupců. (demo). Po najetí na hlavičku sloupce se zobrazí dropdownek – tím je možné skrýt sloupec. Reset se provede nahoře vpravo v tom nastavovátku.
- V nějaké rozumné míře se tree view chová jako tabulka, respektive jeho pravá část. Sloupce se tedy snaží být stejně široké, aby to nějak vypadalo.
- V treeview jsou nyní popisky sloupců (opět se snaží být stejně široké jako datové sloupce)
- Je možné upravovat třídu html elementu
<tr>
. - Další maličkosti
Díky za spolupráci všem, kdo posílají PR a přidávají issues.
- Hurass
- Člen | 114
Šel by nějak jednoduše použít Kdyby/Translation, (využívam členění pomocí teček např. customers.list.email) pro základní texty které jsou v datagridu? Líbí se mi řešení, že pak nemusím překládat zvlášť název každého sloupce, akce atd.
Je možné skrýt select box s výběrem počtu záznamů na stránku? Nerad bych to řešil v css, prostě bych chtěl, aby se to vůbec nevykreslovalo.
Editoval Hurass (18. 2. 2016 20:42)
- Pavel Janda
- Člen | 977
@Hurass
1, O víkendu bych chtěl zapracovat na issues do verze 2, tečková notace textů k překladu je v jednom z nich. Takže o víkendu. :)
2, Přidávám do milestone – v2.
- Hurass
- Člen | 114
@PavelJanda zle nějakým stylem ovlivnit název, ikonku a třídu akce pouze pro některé řádky? Případně zda je možné ovlivnit výpis celé akce pouze pro určité řádky.
Konkrétně mám tento kód:
$grid->addAction("toggleActive", "Block")
->setIcon("lock")
->setClass("btn btn-sm btn-warning");
A potřeboval bych, aby to šlo použít např. nějak takto:
$grid->addAction("toggleActive")
->setIcon(function($item) {
return $item->active ? "lock" : "unlock";
}
->setClass(function($item) {
return $item->active ? "btn btn-sm btn-danger" : "btn btn-sm btn-warning";
};
A ještě jeden dotaz, mám komponenty v oddělěných souborech. Např. komponenta App\Components\Grid. Tu volám v presenteru (např. App\Presenter\HomepagePresenter). Při vytváření akce to vytváří link na metodu handleAciton ve třídě App\Components\Grid. Já bych ale potřeboval, aby to vytvářelo link na metodu handleAciton v presenteru. Jde to nějak vyřešit?
Editoval Hurass (20. 2. 2016 18:38)
- Pavel Janda
- Člen | 977
@Hurass Takhle přesně asi ne, ale je to dobrý nápad, přidám to. Ale, lze například vytvořit dvě různé akce a jedněm položkám to jednu akci zakázat a některým zakázat tu druhou (docu zde). Nebo lze na základě dat položky ovlivnit třídu celé row (tedy třeba přes css skrýt dané akce (docu tamtéž, jen kousek dole).
Ale díky za nápad.
Taky mě napadlo, jestli nechceš něco jako mám v ukázkách ten status. To je prostě sloupec s vlastní šablonou. Taky akce má svoji šablonu a můžeš ji nastavit na jinou – svoji.
- Hurass
- Člen | 114
@PavelJanda díky, row conditions je dostačující.
Ještě mám ale jednu věc. Datagrid vytvářím jako komponentu v jiném souboru, než v presenteru. Hodně podobně jako to má Martin na svém blogu. Link akce se ale vždy vytvoří na ten soubor, ve kterém je komponenta vytvořená. Našel jsem, že se to děje tady. Kousek nad tím, že nastavuje $parent. Pokud se $parent předá $this->grid->getPresenter(), tak to funguje v pořádku. Napadá mě, že by se dal tříde Action předat nějaký parametr, podle kterého by se mohl zavolat právě $this->grid->getPresenter(). Klidně pošlu pull request, pokud to nemá jiné řešení.
Napadlo mě, že by to šlo obejít vlastní šablonou, nicméně líbí se mi to řešení, že bych nemusel vytvářet šablonu a nastavil to rovnou v definici akce.
- Pavel Janda
- Člen | 977
@Hurass Aj, na to jsem zapomněl odpovědět. Z principu by to jít nemělo, protože komponenta odkazuje vždy jen sama na sebe a své subkomponenty. Takže je vhodné to řešit právě šablonou. Nicméně, ještě se nad tím zamyslím.
Taky to můžeš řešit v rendereru:
$grid = $this;
$grid->addColumnText('a', 'A')
->setRenderer(function($item) use ($grid) {
return Html::el('a')->href($grid->getPresenter()->link('this'))
->setText('Klikni');
})