Skip to content

Commit

Permalink
Merge branch 'develop' of https://github.com/michalsn/codeigniter-htmx
Browse files Browse the repository at this point in the history
…into develop
  • Loading branch information
michalsn committed Oct 17, 2024
2 parents 563de5a + 8ac11f8 commit 3bc7671
Show file tree
Hide file tree
Showing 22 changed files with 378 additions and 113 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ It also provides some additional help with **handling errors** and **Debug Toolb

composer require michalsn/codeigniter-htmx

Remember - you still need to include the `htmx` javascript library inside the `head` tag.
> [!NOTE]
> Remember - you still need to include the `htmx` javascript library inside the `head` tag.
## Docs

Expand Down
2 changes: 2 additions & 0 deletions docs/view_fragments.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ class Home extends BaseController

We can return multiple fragments at the same time. Just separate each fragment with a comma or assign an array
instead of a string.

If you declare the same fragment name multiple times in the view file, all these occurrences of the fragment will be returned.
2 changes: 2 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ parameters:
ignoreErrors:
- '#Variable \$testString might not be defined.#'
- '#Variable \$this might not be defined.#'
- '#Call to an undefined method CodeIgniter\\View\\RendererInterface::renderFragments\(\).#'
- '#Call to an undefined method CodeIgniter\\View\\RendererInterface::parseFragments\(\).#'
universalObjectCratesClasses:
- CodeIgniter\Entity
- CodeIgniter\Entity\Entity
Expand Down
4 changes: 1 addition & 3 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
use Rector\EarlyReturn\Rector\If_\RemoveAlwaysElseRector;
use Rector\EarlyReturn\Rector\Return_\PreparedValueToEarlyReturnRector;
use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector;
use Rector\Php73\Rector\FuncCall\JsonThrowOnErrorRector;
use Rector\Php73\Rector\FuncCall\StringifyStrNeedlesRector;
use Rector\PHPUnit\AnnotationsToAttributes\Rector\Class_\AnnotationWithValueToAttributeRector;
use Rector\PHPUnit\CodeQuality\Rector\Class_\YieldDataProviderRector;
Expand All @@ -48,7 +47,7 @@
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->sets([
SetList::DEAD_CODE,
LevelSetList::UP_TO_PHP_74,
LevelSetList::UP_TO_PHP_80,
PHPUnitSetList::PHPUNIT_CODE_QUALITY,
PHPUnitSetList::PHPUNIT_100,
]);
Expand Down Expand Up @@ -91,7 +90,6 @@
$rectorConfig->skip([
__DIR__ . '/src/Views',

JsonThrowOnErrorRector::class,
StringifyStrNeedlesRector::class,
YieldDataProviderRector::class,

Expand Down
2 changes: 1 addition & 1 deletion src/Common.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ function view_fragment(string $name, array|string $fragments, array $data = [],
? array_map('trim', explode(',', $fragments))
: $fragments;

return $renderer->setData($data, 'raw')->render($name, $options, $saveData);
return $renderer->setData($data, 'raw')->renderFragments($name, $options, $saveData);
}
}
208 changes: 121 additions & 87 deletions src/View/View.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use CodeIgniter\Debug\Toolbar\Collectors\Views;
use CodeIgniter\Filters\DebugToolbar;
use CodeIgniter\View\Exceptions\ViewException;
use CodeIgniter\View\RendererInterface;
use CodeIgniter\View\View as BaseView;
use Config\Toolbar;
use RuntimeException;
Expand All @@ -15,9 +16,9 @@
class View extends BaseView
{
/**
* Holds the sections and their data.
* Show fragments tags or not.
*/
protected array $fragments = [];
protected bool $showFragments = false;

/**
* The name of the current section being rendered,
Expand All @@ -27,6 +28,116 @@ class View extends BaseView
*/
protected array $fragmentStack = [];

/**
* Starts holds content for a fragment within the layout.
*
* @param string $name Fragment name
*/
public function fragment(string $name): void
{
$this->fragmentStack[] = $name;

if ($this->showFragments) {
echo sprintf('@[[fragmentStart="%s"]]', $name);
}
}

/**
* Captures the last fragment
*
* @throws RuntimeException
*/
public function endFragment(): void
{
if ($this->fragmentStack === []) {
ob_end_clean();

throw new RuntimeException('View themes, no current fragment.');
}

$name = array_pop($this->fragmentStack);

if ($this->showFragments) {
echo sprintf('@[[fragmentEnd="%s"]]', $name);
}
}

/**
* Whether we should display fragments tags or not.
*/
protected function showFragments(bool $display = true): RendererInterface
{
$this->showFragments = $display;

return $this;
}

/**
* Render fragments.
*/
public function renderFragments(string $name, ?array $options = null, ?bool $saveData = null): string
{
$fragments = $options['fragments'] ?? [];
$output = $this->showFragments()->render($name, $options, $saveData);

if ($fragments === []) {
return preg_replace('/@\[\[fragmentStart="[^"]+"\]\]|@\[\[fragmentEnd="[^"]+"\]\]/', '', $output);
}

$result = $this->showFragments(false)->parseFragments($output, $fragments);
$output = '';

foreach ($result as $contents) {
$output .= implode('', $contents);
}

return $output;
}

/**
* Parse output to retrieve fragments.
*/
protected function parseFragments(string $output, array $fragments): array
{
$results = [];
$stack = [];

// Match all fragment start and end tags at once
preg_match_all('/@\[\[fragmentStart="([^"]+)"\]\]|@\[\[fragmentEnd="([^"]+)"\]\]/', $output, $matches, PREG_OFFSET_CAPTURE);

// Return empty array if no matches
if ($matches[0] === []) {
return $results;
}

foreach ($matches[0] as $index => $match) {
$pos = $match[1];
$isStart = isset($matches[1][$index]) && $matches[1][$index][0] !== '';
$name = $isStart ? $matches[1][$index][0] : (isset($matches[2][$index]) ? $matches[2][$index][0] : '');

if ($isStart) {
$stack[] = ['name' => $name, 'start' => $pos];
} elseif ($stack !== [] && end($stack)['name'] === $name) {
$info = array_pop($stack);

// Calculate the position of the fragment content
$fragmentStart = $info['start'] + strlen($matches[0][array_search($info['name'], array_column($matches[1], 0), true)][0]);
$fragmentEnd = $pos;

// Extract the content between the tags
$content = substr($output, $fragmentStart, $fragmentEnd - $fragmentStart);
// Clean the fragment content by removing the tags
$content = preg_replace('/@\[\[fragmentStart="[^"]+"\]\]|@\[\[fragmentEnd="[^"]+"\]\]/', '', $content);

if (in_array($info['name'], $fragments, true)) {
$results[$info['name']][] = $content;
}
}
}

return $results;
}

/**
* Builds the output based upon a file name and any
* data that has already been set.
Expand All @@ -35,13 +146,13 @@ class View extends BaseView
* - cache Number of seconds to cache for
* - cache_name Name to use for cache
*
* @param string $view File name of the view source
* @param array|null $options Reserved for 3rd-party uses since
* it might be needed to pass additional info
* to other template engines.
* @param bool|null $saveData If true, saves data for subsequent calls,
* if false, cleans the data after displaying,
* if null, uses the config setting.
* @param string $view File name of the view source
* @param array<string, mixed>|null $options Reserved for 3rd-party uses since
* it might be needed to pass additional info
* to other template engines.
* @param bool|null $saveData If true, saves data for subsequent calls,
* if false, cleans the data after displaying,
* if null, uses the config setting.
*/
public function render(string $view, ?array $options = null, ?bool $saveData = null): string
{
Expand All @@ -58,7 +169,7 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n

// Was it cached?
if (isset($this->renderVars['options']['cache'])) {
$cacheName = $this->renderVars['options']['cache_name'] ?? str_replace('.php', '', $this->renderVars['view']);
$cacheName = $this->renderVars['options']['cache_name'] ?? str_replace('.php', '', $this->renderVars['view']) . (empty($this->renderVars['options']['fragments']) ? '' : implode('', $this->renderVars['options']['fragments']));
$cacheName = str_replace(['\\', '/'], '', $cacheName);

$this->renderVars['cacheName'] = $cacheName;
Expand Down Expand Up @@ -109,13 +220,6 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n
$output = $this->render($layoutView, $options, $saveData);
// Get back current vars
$this->renderVars = $renderVars;
} elseif (! empty($this->renderVars['options']['fragments']) && $this->fragmentStack === []) {
$output = '';

foreach ($this->renderVars['options']['fragments'] as $fragmentName) {
$output .= $this->renderFragment($fragmentName);
unset($this->fragments[$fragmentName]);
}
}

$output = $this->decorateOutput($output);
Expand Down Expand Up @@ -148,74 +252,4 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n

return $output;
}

/**
* Starts holds content for a fragment within the layout.
*
* @param string $name Fragment name
*
* @return void
*/
public function fragment(string $name)
{
$this->fragmentStack[] = $name;

ob_start();
}

/**
* Captures the last fragment
*
* @throws RuntimeException
*/
public function endFragment()
{
$contents = ob_get_clean();

if ($this->fragmentStack === []) {
throw new RuntimeException('View themes, no current fragment.');
}

$fragmentName = array_pop($this->fragmentStack);

// Ensure an array exists, so we can store multiple entries for this.
if (! array_key_exists($fragmentName, $this->fragments)) {
$this->fragments[$fragmentName] = [];
}

$this->fragments[$fragmentName][] = $contents;

echo $contents;
}

/**
* Renders a fragment's contents.
*/
protected function renderFragment(string $fragmentName)
{
if (! isset($this->fragments[$fragmentName])) {
return '';
}

foreach ($this->fragments[$fragmentName] as $contents) {
return $contents;
}
}

/**
* Used within layout views to include additional views.
*
* @param bool $saveData
*/
public function include(string $view, ?array $options = null, $saveData = true): string
{
if ($this->fragmentStack !== [] && ! empty($this->renderVars['options']['fragments'])) {
$options['fragments'] = $this->renderVars['options']['fragments'];
echo $this->render($view, $options, $saveData);

return '';
}

return $this->render($view, $options, $saveData);
}
}
66 changes: 66 additions & 0 deletions tests/CommonTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,70 @@ public function testViewFragmentFromLayout(): void

$this->assertSame($expected, view_fragment('with_fragment', 'sample0', $data));
}

public function testComplexLayout(): void
{
$data = ['foo' => 'FOO'];
$result = view_fragment('complex/view1', ['fragment1'], $data + ['bar' => 'BAR'])
. view_fragment('complex/view2', ['fragment2'], $data + ['baz' => 'BAZ'])
. view_fragment('complex/view3', ['fragment3'], $data + ['too' => 'TOO'])
. view_fragment('complex/include', ['include'], $data + ['inc' => 'INC']);

$expected = <<<'EOD'
<div>
<b>Fragment 2</b><br>
<b>foo: </b> YES<br>
<b>bar: </b> YES<br>
<b>baz: </b> YES<br>
<b>too: </b> NO<br>
<b>inc: </b> NO<br>
</div>
<div>
<b>Fragment 3</b><br>
<b>foo: </b> YES<br>
<b>bar: </b> YES<br>
<b>baz: </b> YES<br>
<b>too: </b> YES<br>
<b>inc: </b> NO<br>
</div>
<div>
<b>Include</b><br>
<b>foo: </b> YES<br>
<b>bar: </b> YES<br>
<b>baz: </b> YES<br>
<b>too: </b> YES<br>
<b>inc: </b> YES<br>
</div>

EOD;

$this->assertSame($expected, $result);
}

public function testManySameNameFragments()
{
$result = view_fragment('many/view1', ['fragment1']);

$expected = <<<'EOD'
<b>Fragment 1 (1)</b><br>
<b>Fragment 1 (3)</b><br>

EOD;

$this->assertSame($expected, $result);
}

public function testHugeView(): void
{
$result = view_fragment('huge/view', ['fragment_one']);

$expected = <<<'EOD'
Fragment one (from "huge/view")
Fragment one (from "huge/include")
Fragment one (from "huge/layout")

EOD;

$this->assertSame($expected, $result);
}
}
Loading

0 comments on commit 3bc7671

Please sign in to comment.