Skip to content

Commit

Permalink
Merge pull request #15 from Fohn-Group/feature-tabs
Browse files Browse the repository at this point in the history
Tabs Component
  • Loading branch information
ibelar authored Aug 4, 2023
2 parents 9639071 + 075c31d commit dbfcf19
Show file tree
Hide file tree
Showing 18 changed files with 521 additions and 21 deletions.
2 changes: 2 additions & 0 deletions app-test/_includes/AppTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ private static function getNavigationGroup(string $baseUrl = '/app-test/'): arra
'icon' => 'bi bi-chat-left',
'url' => $baseUrl . 'interactive/virtual.php',
'items' => [
new Item(['name' => 'Tabs', 'url' => $baseUrl . 'interactive/tabs.php']),
new Item(['name' => 'Tabs Menu', 'url' => $baseUrl . 'interactive/tabs-menu.php']),
new Item(['name' => 'Modal', 'url' => $baseUrl . 'interactive/modal.php']),
new Item(['name' => 'Toast', 'url' => $baseUrl . 'interactive/toast.php']),
new Item(['name' => 'Virtual Page', 'url' => $baseUrl . 'interactive/virtual.php']),
Expand Down
37 changes: 30 additions & 7 deletions app-test/basic/button.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,37 @@
require_once __DIR__ . '/../init-ui.php';

$bar = View::addTo(Ui::layout(), ['defaultTailwind' => ['inline-block, my-4']]);
Button::addTo($bar, ['label' => 'Link', 'type' => 'link']);

Button::addTo($bar, ['label' => 'Primary']);
Button::addTo($bar, ['label' => 'Secondary', 'color' => 'secondary', 'type' => 'outline']);
Button::addTo($bar, ['label' => 'Info', 'color' => 'info', 'type' => 'link']);
Button::addTo($bar, ['label' => 'Success', 'color' => 'success'])->setType('link');
Button::addTo($bar, ['label' => 'Warning', 'color' => 'warning']);
Button::addTo($bar, ['label' => 'Error', 'color' => 'error']);
Button::addTo($bar, ['label' => 'Neutral', 'color' => 'neutral']);
$type = 'contained';
$bar = View::addTo(Ui::layout(), ['defaultTailwind' => ['inline-block, my-4']]);
Button::addTo($bar, ['label' => 'Primary', 'type' => $type]);
Button::addTo($bar, ['label' => 'Secondary', 'color' => 'secondary', 'type' => $type]);
Button::addTo($bar, ['label' => 'Info', 'color' => 'info', 'type' => $type]);
Button::addTo($bar, ['label' => 'Success', 'color' => 'success'])->setType($type);
Button::addTo($bar, ['label' => 'Warning', 'color' => 'warning', 'type' => $type]);
Button::addTo($bar, ['label' => 'Error', 'color' => 'error', 'type' => $type]);
Button::addTo($bar, ['label' => 'Neutral', 'color' => 'neutral', 'type' => $type]);

$type = 'outline';
$bar = View::addTo(Ui::layout(), ['defaultTailwind' => ['inline-block, my-4']]);
Button::addTo($bar, ['label' => 'Primary', 'type' => $type]);
Button::addTo($bar, ['label' => 'Secondary', 'color' => 'secondary', 'type' => $type]);
Button::addTo($bar, ['label' => 'Info', 'color' => 'info', 'type' => $type]);
Button::addTo($bar, ['label' => 'Success', 'color' => 'success'])->setType($type);
Button::addTo($bar, ['label' => 'Warning', 'color' => 'warning', 'type' => $type]);
Button::addTo($bar, ['label' => 'Error', 'color' => 'error', 'type' => $type]);
Button::addTo($bar, ['label' => 'Neutral', 'color' => 'neutral', 'type' => $type]);

$type = 'text';
$bar = View::addTo(Ui::layout(), ['defaultTailwind' => ['inline-block, my-4']]);
Button::addTo($bar, ['label' => 'Primary', 'type' => $type]);
Button::addTo($bar, ['label' => 'Secondary', 'color' => 'secondary', 'type' => $type]);
Button::addTo($bar, ['label' => 'Info', 'color' => 'info', 'type' => $type]);
Button::addTo($bar, ['label' => 'Success', 'color' => 'success'])->setType($type);
Button::addTo($bar, ['label' => 'Warning', 'color' => 'warning', 'type' => $type]);
Button::addTo($bar, ['label' => 'Error', 'color' => 'error', 'type' => $type]);
Button::addTo($bar, ['label' => 'Neutral', 'color' => 'neutral', 'type' => $type]);

$bar = View::addTo(Ui::layout(), ['defaultTailwind' => ['inline-block, my-4']]);
Button::addTo($bar, ['label' => 'Home'])->addIcon(new Icon(['iconName' => 'bi-house-fill']));
Expand Down
5 changes: 3 additions & 2 deletions app-test/form/form-model-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,13 @@
)
), 500);

$c = Form\Control\Input::addTo($formContainer, ['controlName' => 'std1'])->setValue('test');
$c->onChange(JsFunction::anonymous()
$c1 = Form\Control\Input::addTo($formContainer, ['controlName' => 'std1'])->setValue('test');
$c1->onChange(JsFunction::anonymous()
->execute(
JsToast::notifyWithJs(
Jquery::withSelector('input[name=\'std1\']')->val()
)
), 500);

Ui::viewDump($f, 'form');
Ui::viewDump($c, 'ctl');
45 changes: 45 additions & 0 deletions app-test/interactive/tabs-menu.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

use Fohn\Ui\Component\Tabs;
use Fohn\Ui\Component\Tabs\Tab;
use Fohn\Ui\Service\Ui;
use Fohn\Ui\View;

require_once __DIR__ . '/../init-ui.php';

$tabs = Tabs::addTo(Ui::layout());
$tabs->setTabsMenuTemplate(Ui::templateFromFile(__DIR__ . '/template/tabs-menu.html'));

$homeTab = $tabs->addTab(new Tab(['name' => 'home']));
View::addTo($homeTab)->setTextContent('This is Home content.');

$profileTab = $tabs->addTab(new Tab(['name' => 'profile']));
View::addTo($profileTab)->setTextContent('This is Profile content.');

$userTab = $tabs->addTab(new Tab(['name' => 'preferences']));
View::addTo($userTab)->setTextContent('This is Preferences content.');

$adminTab = $tabs->addTab(new Tab(['name' => 'admin']))->disabled();
View::addTo($adminTab)->setTextContent('This is Admin content.');

View\Divider::addTo(Ui::layout(), ['verticalSpace' => '12']);

View::addTo(Ui::layout())->setTextContent('Show how a Vue property, like an icon name, can be added to Tab component and be available
within the menu template.');

$tabs = Tabs::addTo(Ui::layout());
$tabs->setTabsMenuTemplate(Ui::templateFromFile(__DIR__ . '/template/tabs-menu-icon.html'));

$homeTab = $tabs->addTab(new Tab(['name' => 'home']))->addProperty('icon', 'bi-house-fill');
View::addTo($homeTab)->setTextContent('This is Home content.');

$profileTab = $tabs->addTab(new Tab(['name' => 'profile']))->addProperty('icon', 'bi-person-fill');
View::addTo($profileTab)->setTextContent('This is Profile content.');

$userTab = $tabs->addTab(new Tab(['name' => 'preferences']))->addProperty('icon', 'bi-gear-fill');
View::addTo($userTab)->setTextContent('This is Preferences content.');

$adminTab = $tabs->addTab(new Tab(['name' => 'admin']))->disabled()->addProperty('icon', 'bi-person-fill-lock');
View::addTo($adminTab)->setTextContent('This is Admin content.');
63 changes: 63 additions & 0 deletions app-test/interactive/tabs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

use Fohn\Ui\AppTest\Model\Country;
use Fohn\Ui\Component\Form;
use Fohn\Ui\Component\Tabs;
use Fohn\Ui\Component\Tabs\Tab;
use Fohn\Ui\Js\Jquery;
use Fohn\Ui\Js\JsReload;
use Fohn\Ui\Js\JsToast;
use Fohn\Ui\Service\Atk\FormModelController;
use Fohn\Ui\Service\Data;
use Fohn\Ui\Service\Ui;
use Fohn\Ui\View;
use Fohn\Ui\View\Button;

require_once __DIR__ . '/../init-ui.php';

$modelCtrl = new FormModelController(new Country(Data::db()));
$id = (string) $modelCtrl->getModel()->tryLoadAny()->get('id');

$btnGoTo = Button::addTo(Ui::layout(), ['label' => 'Go to country tab', 'type' => 'text']);
$btnEnableUser = Button::addTo(Ui::layout(), ['label' => 'Enable User Tab', 'type' => 'text']);
$btnDisableUser = Button::addTo(Ui::layout(), ['label' => 'Disable User Tab', 'type' => 'text']);

$tabs = Tabs::addTo(Ui::layout());
Jquery::addEventTo($btnGoTo, 'click')->execute($tabs->jsActivateTabName('country'));
Jquery::addEventTo($btnEnableUser, 'click')->execute($tabs->jsEnableTabName('user'));
Jquery::addEventTo($btnDisableUser, 'click')->execute($tabs->jsDisableTabName('user'));

$homeTab = $tabs->addTab(new Tab(['name' => 'home']));
$fn = $homeTab->jsOnInitTab(\Fohn\Ui\Js\JsFunction::arrow());
$fn->execute(\Fohn\Ui\Js\Js::from('console.log(\'homeTab on init\')'));

$fn = $homeTab->jsOnShowTab(\Fohn\Ui\Js\JsFunction::arrow());
$fn->execute(\Fohn\Ui\Js\Js::from('console.log(\'homeTab on show\')'));

$fn = $homeTab->jsOnHideTab(\Fohn\Ui\Js\JsFunction::arrow());
$fn->execute(\Fohn\Ui\Js\Js::from('console.log(\'homeTab on hide\')'));

View::addTo($homeTab)->setTextContent('This is home tab content.');

$profileTab = $tabs->addTab(new Tab(['name' => 'country']));

$form = Form::addTo($profileTab);
$form->addControls($modelCtrl->factoryFormControls($id));
$form->onSubmit(function (Form $f) use ($modelCtrl, $id) {
if ($errors = $modelCtrl->saveModelUsingForm($id, $f->getControls())) {
$f->addValidationErrors($errors);
}

return JsToast::success('Saved!');
});

$userTab = $tabs->addTab(new Tab(['name' => 'user']));
View::addTo($userTab)->setTextContent('This is user tab content.');
$b = Button::addTo($userTab, ['label' => 'Reload ' . ($_GET['test'] ?? 0), 'type' => 'text', 'size' => 'small']);
Jquery::addEventTo($b, 'click')->execute(JsReload::view($b, ['test ' => random_int(0, 100)]));

$tabs->activateTabName('user');

Ui::viewDump($tabs, 'tab');
16 changes: 16 additions & 0 deletions app-test/interactive/template/tabs-menu-icon.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<div class="border-b border-gray-200 mb-8">
<ul class="flex flex-wrap -mb-px">
<template v-for="(tab, index) in tabs" :key="index">
<li role="presentation" class="mr-4">
<button
@click="activate(index)"
class="inline-block p-4 border-b-2 rounded-t-lg disabled:text-gray-400 disabled:cursor-not-allowed"
:class="{'border-blue-500 text-blue-500': index === currentIndex, 'border-transparent hover:text-gray-600 hover:border-gray-300': index !== currentIndex}"
:data-active="index === currentIndex"
:data-name="tab.name"
:disabled="tab.disabled"
><i class="mx-2" :class="tab.icon"></i>{{tab.caption}}</button>
</li>
</template>
</ul>
</div>
16 changes: 16 additions & 0 deletions app-test/interactive/template/tabs-menu.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<div class="mb-8 border-b border-gray-200">
<ul class="flex flex-wrap text-sm font-medium text-center text-gray-500">
<template v-for="(tab, index) in tabs" :key="index">
<li role="presentation" class="mr-4">
<button
@click="activate(index)"
class="inline-block p-4 rounded-t-lg disabled:text-gray-400 disabled:cursor-not-allowed"
:class="{'bg-gray-300 text-blue-500': index === currentIndex, 'hover:bg-gray-100': index !== currentIndex}"
:data-active="index === currentIndex"
:data-name="tab.name"
:disabled="tab.disabled"
>{{tab.caption}}</button>
</li>
</template>
</ul>
</div>
4 changes: 2 additions & 2 deletions src/Component/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ public function addControl(Form\Control $control, string $layoutName = self::MAI

protected function registerControl(Control $control): void
{
$this->assertControlAsName($control->getControlName());
$this->assertControlHasName($control->getControlName());
$this->assertControlIsUnique($control->getControlName());
$control->formStoreId = $this->getPiniaStoreId(self::PINIA_PREFIX);
$this->controls[$control->getControlName()] = $control;
Expand Down Expand Up @@ -282,7 +282,7 @@ private function assertControlIsUnique(string $controlName): void
}
}

private function assertControlAsName(string $controlName): void
private function assertControlHasName(string $controlName): void
{
if (!$controlName) {
throw new Exception('Trying to add a form control without name.');
Expand Down
136 changes: 136 additions & 0 deletions src/Component/Tabs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php

declare(strict_types=1);
/**
* Tabs component.
*/

namespace Fohn\Ui\Component;

use Fohn\Ui\Component\Tabs\Tab;
use Fohn\Ui\Core\Exception;
use Fohn\Ui\HtmlTemplate;
use Fohn\Ui\Js\JsRenderInterface;
use Fohn\Ui\Js\Type\Type;
use Fohn\Ui\View;

class Tabs extends View implements VueInterface
{
use VueTrait;

protected const COMP_NAME = 'fohn-tabs';
protected const TAB_REGION_NAME = 'tabs';

private const PINIA_PREFIX = '__tabs_';

public string $defaultTemplate = 'vue-component/tabs.html';

/** @var array<Tab> */
protected array $tabs = [];

protected string $activeTabName = '';

protected function initRenderTree(): void
{
parent::initRenderTree();
}

public function addTab(Tab $tab): Tab
{
$this->registerTab($tab);
$this->addView($tab, self::TAB_REGION_NAME);

return $tab;
}

public function activateTabName(string $name): self
{
$this->activeTabName = $name;

return $this;
}

public function jsActivateTabName(string $name): JsRenderInterface
{
// @phpstan-ignore-next-line
return $this->jsGetStore(self::PINIA_PREFIX)->activateByName($name);
}

public function jsEnableTabName(string $name): JsRenderInterface
{
// @phpstan-ignore-next-line
return $this->jsGetStore(self::PINIA_PREFIX)->enableByName($name);
}

public function jsDisableTabName(string $name): JsRenderInterface
{
// @phpstan-ignore-next-line
return $this->jsGetStore(self::PINIA_PREFIX)->disableByName($name);
}

/**
* Replace Tabs menu template.
* The template must use Tab component template props:
* - tabs, an array of {name: '', caption: '', disabled: false...}
* - currentIndex, an integer value representing the current activated tab.
* - activate, a function with an index argument that activate the tab.
*/
public function setTabsMenuTemplate(HtmlTemplate $template, string $region = 'TabMenu'): self
{
$this->getTemplate()->dangerouslySetHtml($region, $template->renderToHtml());

return $this;
}

protected function registerTab(Tab $tab): void
{
$this->assertTabHasName($tab->getName());
$this->assertTabIsUnique($tab->getName());

if (!$tab->getCaption()) {
$tab->setCaption(ucfirst($tab->getName()));
}

$tab->tabStoreId = $this->getPiniaStoreId(self::PINIA_PREFIX);
$this->tabs[$tab->getName()] = $tab;
}

private function assertTabHasName(string $tabName): void
{
if (!$tabName) {
throw new Exception('Tab must have a name.');
}
}

private function assertTabIsUnique(string $tabName): void
{
if (array_key_exists($tabName, $this->tabs)) {
throw (new Exception('This tab name is already added.'))
->addMoreInfo('Tab name:', $tabName);
}
}

protected function setTemplateProps(): void
{
$props['storeId'] = $this->getPiniaStoreId(self::PINIA_PREFIX);

$tabList = [];
foreach ($this->tabs as $tab) {
$tabList[] = $tab->getProperties();
}
$props['tabList'] = $tabList;
$props['activeTabIdx'] = array_search($this->activeTabName, array_keys($this->tabs), true) ?: 0;

foreach ($props as $key => $value) {
$this->getTemplate()->setJs($key, Type::factory($value));
}
}

protected function beforeHtmlRender(): void
{
$this->setTemplateProps();

$this->createVueApp(self::COMP_NAME, [], $this->getDefaultSelector());
parent::beforeHtmlRender();
}
}
Loading

0 comments on commit dbfcf19

Please sign in to comment.