Skip to content

Commit

Permalink
Merge pull request #63 from michalsn/feat/decorators
Browse files Browse the repository at this point in the history
feat: configurable view decorators
  • Loading branch information
michalsn authored Nov 17, 2023
2 parents c6e2ed4 + dfe89d1 commit e81aec8
Show file tree
Hide file tree
Showing 14 changed files with 224 additions and 26 deletions.
3 changes: 1 addition & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
},
"require-dev": {
"codeigniter4/devkit": "^1.0",
"codeigniter4/framework": "^4.1",
"rector/rector": "0.18.5"
"codeigniter4/framework": "^4.1"
},
"minimum-stability": "dev",
"prefer-stable": true,
Expand Down
38 changes: 38 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Configuration

To make changes to the config file, we have to have our copy in the `app/Config/Htmx.php`. Luckily, this package comes with handy command that will make this easy.

When we run:

php spark htmx:publish

We will get our copy ready for modifications.

---

Available options:

- [$toolbarDecorator](#$toolbarDecorator)
- [$errorModalDecorator](#$errorModalDecorator)
- [$skipViewDecoratorsString](#$skipViewDecoratorsString)

### $toolbarDecorator

This allows us to disable the `ToolbarDecorator` class. Please read [Debug Toolbar](debug_toolbar.md) page for more information.

### $errorModalDecorator

This allows us to disable the `ErrorModalDecorator` class. Please read [Error handling](error_handling.md) page for more information.

### $skipViewDecoratorsString

If this string appears in the content of the file, it will prevent CodeIgniter from using both View Decorator classes above - even if they are enabled.

You can change this string to whatever you want. Just remember to make it unique enough to not use it by accident.

This may be useful when we want to send an e-mail, which message is prepared via the View file.
Since these decorators are used automatically in the `development` mode (or to be more strict - when `CI_DEBUG` is enabled), we may want to disable all the scripts for the e-mail messages.

We can add the defined string as an `id` or `class` to the html tag.

In the `production` environment these decorators are ignored by design. So this is useful only for the `development` mode.
2 changes: 2 additions & 0 deletions docs/debug_toolbar.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ use the `History` tab in the Toolbar.

If you're using the `head-support` extension then the Debug Toolbar rendering will not work for `htmx` requests.
You can still access the toolbar for a given request by checking the URL in the `debugbar-link` response header.

This feature can be disabled in the [Config](configuration.md) file.
2 changes: 2 additions & 0 deletions docs/error_handling.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Error handling

By default, when an HTTP error response occurs, htmx is not displaying the error. This library changes it so that in the development mode, errors are displayed in a modal window.

This feature can be disabled in the [Config](configuration.md) file.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ It also provides some additional help with **handling errors** and **Debug Toolb
### Table of Contents

* [Installation](installation.md)
* [Configuration](configuration.md)
* [Error handling](error_handling.md)
* [View fragments](view_fragments.md)
* [IncomingReqeuest](incoming_request.md)
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ extra_javascript:
nav:
- Home: index.md
- Installation: installation.md
- Configuration: configuration.md
- Error handling: error_handling.md
- View fragments: view_fragments.md
- IncomingReqeuest: incoming_request.md
Expand Down
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
</include>
<exclude>
<directory suffix=".php">./src/Config</directory>
<file>./src/Commands/HtmxPublish.php</file>
<file>./src/Debug/Toolbar.php</file>
</exclude>
<report>
Expand Down
54 changes: 30 additions & 24 deletions rector.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
<?php

use Rector\CodeQuality\Rector\BooleanAnd\SimplifyEmptyArrayCheckRector;
use Rector\CodeQuality\Rector\Class_\CompleteDynamicPropertiesRector;
use Rector\CodeQuality\Rector\Expression\InlineIfToExplicitIfRector;
use Rector\CodeQuality\Rector\For_\ForToForeachRector;
use Rector\CodeQuality\Rector\Foreach_\UnusedForeachValueToArrayKeysRector;
use Rector\CodeQuality\Rector\FuncCall\AddPregQuoteDelimiterRector;
use Rector\CodeQuality\Rector\FuncCall\ChangeArrayPushToArrayAssignRector;
use Rector\CodeQuality\Rector\FuncCall\SimplifyRegexPatternRector;
use Rector\CodeQuality\Rector\FuncCall\SimplifyStrposLowerRector;
Expand All @@ -20,24 +19,30 @@
use Rector\Config\RectorConfig;
use Rector\Core\ValueObject\PhpVersion;
use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPromotedPropertyRector;
use Rector\DeadCode\Rector\MethodCall\RemoveEmptyMethodCallRector;
use Rector\EarlyReturn\Rector\Foreach_\ChangeNestedForeachIfsToEarlyContinueRector;
use Rector\EarlyReturn\Rector\If_\ChangeIfElseValueAssignToEarlyReturnRector;
use Rector\EarlyReturn\Rector\If_\RemoveAlwaysElseRector;
use Rector\EarlyReturn\Rector\Return_\PreparedValueToEarlyReturnRector;
use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector;
use Rector\Php56\Rector\FunctionLike\AddDefaultValueForUndefinedVariableRector;
use Rector\Php73\Rector\FuncCall\JsonThrowOnErrorRector;
use Rector\Php73\Rector\FuncCall\StringifyStrNeedlesRector;
use Rector\PHPUnit\Set\PHPUnitLevelSetList;
use Rector\PHPUnit\AnnotationsToAttributes\Rector\ClassMethod\DataProviderAnnotationToAttributeRector;
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\PSR4\Rector\FileWithoutNamespace\NormalizeNamespaceByPSR4ComposerAutoloadRector;
use Rector\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;
use Rector\TypeDeclaration\Rector\Property\TypedPropertyFromAssignsRector;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->sets([SetList::DEAD_CODE, LevelSetList::UP_TO_PHP_80, PHPUnitSetList::PHPUNIT_SPECIFIC_METHOD, PHPUnitLevelSetList::UP_TO_PHPUNIT_100]);
$rectorConfig->sets([
SetList::DEAD_CODE,
LevelSetList::UP_TO_PHP_80,
PHPUnitSetList::PHPUNIT_CODE_QUALITY,
PHPUnitSetList::PHPUNIT_100,
]);

$rectorConfig->parallel();

// The paths to refactor (can also be supplied with CLI arguments)
$rectorConfig->paths([
__DIR__ . '/src/',
Expand All @@ -59,7 +64,7 @@
}

// Set the target version for refactoring
$rectorConfig->phpVersion(PhpVersion::PHP_80);
$rectorConfig->phpVersion(PhpVersion::PHP_81);

// Auto-import fully qualified class names
$rectorConfig->importNames();
Expand All @@ -74,27 +79,19 @@
// Note: requires php 8
RemoveUnusedPromotedPropertyRector::class,

// Ignore tests that might make calls without a result
RemoveEmptyMethodCallRector::class => [
__DIR__ . '/tests',
],

// Ignore files that should not be namespaced
NormalizeNamespaceByPSR4ComposerAutoloadRector::class => [
__DIR__ . '/src/Common.php',
__DIR__ . '/tests/_support/Views/',
],

// May load view files directly when detecting classes
StringClassNameToClassConstantRector::class,

// May be uninitialized on purpose
AddDefaultValueForUndefinedVariableRector::class,
// Supported from PHPUnit 10
DataProviderAnnotationToAttributeRector::class,
]);

// auto import fully qualified class names
$rectorConfig->importNames();

$rectorConfig->rule(SimplifyUselessVariableRector::class);
$rectorConfig->rule(RemoveAlwaysElseRector::class);
$rectorConfig->rule(CountArrayToEmptyArrayComparisonRector::class);
$rectorConfig->rule(ForToForeachRector::class);
$rectorConfig->rule(ChangeNestedForeachIfsToEarlyContinueRector::class);
$rectorConfig->rule(ChangeIfElseValueAssignToEarlyReturnRector::class);
$rectorConfig->rule(SimplifyStrposLowerRector::class);
Expand All @@ -107,10 +104,19 @@
$rectorConfig->rule(UnusedForeachValueToArrayKeysRector::class);
$rectorConfig->rule(ChangeArrayPushToArrayAssignRector::class);
$rectorConfig->rule(UnnecessaryTernaryExpressionRector::class);
$rectorConfig->rule(AddPregQuoteDelimiterRector::class);
$rectorConfig->rule(SimplifyRegexPatternRector::class);
$rectorConfig->rule(FuncGetArgsToVariadicParamRector::class);
$rectorConfig->rule(MakeInheritedMethodVisibilitySameAsParentRector::class);
$rectorConfig->rule(SimplifyEmptyArrayCheckRector::class);
$rectorConfig->rule(NormalizeNamespaceByPSR4ComposerAutoloadRector::class);
$rectorConfig
->ruleWithConfiguration(TypedPropertyFromAssignsRector::class, [
/**
* The INLINE_PUBLIC value is default to false to avoid BC break, if you use for libraries and want to preserve BC break, you don't need to configure it, as it included in LevelSetList::UP_TO_PHP_74
* Set to true for projects that allow BC break
*/
TypedPropertyFromAssignsRector::INLINE_PUBLIC => false,
]);
$rectorConfig->rule(StringClassNameToClassConstantRector::class);
$rectorConfig->rule(PrivatizeFinalClassPropertyRector::class);
$rectorConfig->rule(CompleteDynamicPropertiesRector::class);
};
45 changes: 45 additions & 0 deletions src/Commands/HtmxPublish.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace Michalsn\CodeIgniterHtmx\Commands;

use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Publisher\Publisher;
use Throwable;

class HtmxPublish extends BaseCommand
{
protected $group = 'Htmx';
protected $name = 'htmx:publish';
protected $description = 'Publish Htmx config file into the current application.';

/**
* @return void
*/
public function run(array $params)
{
$source = service('autoloader')->getNamespace('Michalsn\\CodeIgniterHtmx')[0];

$publisher = new Publisher($source, APPPATH);

try {
$publisher->addPaths([
'Config/Htmx.php',
])->merge(false);
} catch (Throwable $e) {
$this->showError($e);

return;
}

foreach ($publisher->getPublished() as $file) {
$contents = file_get_contents($file);
$contents = str_replace('namespace Michalsn\\CodeIgniterHtmx\\Config', 'namespace Config', $contents);
$contents = str_replace('use CodeIgniter\\Config\\BaseConfig', 'use Michalsn\\CodeIgniterHtmx\\Config\\Htmx as BaseHtmx', $contents);
$contents = str_replace('class Htmx extends BaseConfig', 'class Htmx extends BaseHtmx', $contents);
file_put_contents($file, $contents);
}

CLI::write(CLI::color(' Published! ', 'green') . 'You can customize the configuration by editing the "app/Config/Htmx.php" file.');
}
}
25 changes: 25 additions & 0 deletions src/Config/Htmx.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Michalsn\CodeIgniterHtmx\Config;

use CodeIgniter\Config\BaseConfig;

class Htmx extends BaseConfig
{
/**
* Enable / disable ToolbarDecorator.
*/
public bool $toolbarDecorator = true;

/**
* Enable / diable ErrorModalDecorator.
*/
public bool $errorModalDecorator = true;

/**
* The appearance of this string in the view
* content will skip the htmx decorators. Even
* when they are enabled.
*/
public string $skipViewDecoratorsString = 'htmxSkipViewDecorators';
}
2 changes: 2 additions & 0 deletions src/View/ErrorModalDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ public static function decorate(string $html): string
if (CI_DEBUG
&& (! is_cli() || ENVIRONMENT === 'testing')
&& ! service('request')->isHtmx()
&& config('Htmx')->errorModalDecorator
&& str_contains($html, '</head>')
&& ! str_contains($html, config('Htmx')->skipViewDecoratorsString)
&& ! str_contains($html, 'id="htmxErrorModalScript"')
) {
$script = sprintf(
Expand Down
2 changes: 2 additions & 0 deletions src/View/ToolbarDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ public static function decorate(string $html): string
if (CI_DEBUG
&& (! is_cli() || ENVIRONMENT === 'testing')
&& ! service('request')->isHtmx()
&& config('Htmx')->toolbarDecorator
&& str_contains($html, '</head>')
&& ! str_contains($html, config('Htmx')->skipViewDecoratorsString)
&& ! str_contains($html, 'id="htmxToolbarScript"')
) {
$script = sprintf(
Expand Down
37 changes: 37 additions & 0 deletions tests/View/ErrorModelDecoratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\View\View;
use Config\View as ViewConfig;
use Michalsn\CodeIgniterHtmx\Config\Htmx as HtmxConfig;
use Michalsn\CodeIgniterHtmx\View\ErrorModalDecorator;

/**
Expand Down Expand Up @@ -59,4 +60,40 @@ public function testDecoratorApply(): void
$this->assertStringContainsString($expected1, $view->render('with_decorator'));
$this->assertStringContainsString($expected2, $view->render('with_decorator'));
}

public function testDecoratorDisabled(): void
{
$htmxConfig = new HtmxConfig();
$htmxConfig->errorModalDecorator = false;
Factories::injectMock('config', 'Htmx', $htmxConfig);

$config = $this->config;
$config->decorators = [ErrorModalDecorator::class];
Factories::injectMock('config', 'View', $config);

$view = new View($this->config, $this->viewsDir, $this->loader);

$view->setVar('testString', 'Hello World 1');
$expected1 = '<h1>Hello World 1</h1>';
$expected2 = 'id="htmxErrorModalScript"';

$this->assertStringContainsString($expected1, $view->render('without_decorator'));
$this->assertStringNotContainsString($expected2, $view->render('without_decorator'));
}

public function testDecoratorDisabledWithSkipDecoratorsString(): void
{
$config = $this->config;
$config->decorators = [ErrorModalDecorator::class];
Factories::injectMock('config', 'View', $config);

$view = new View($this->config, $this->viewsDir, $this->loader);

$view->setVar('testString', 'htmxSkipViewDecorators');
$expected1 = '<h1>htmxSkipViewDecorators</h1>';
$expected2 = 'id="htmxErrorModalScript"';

$this->assertStringContainsString($expected1, $view->render('without_decorator'));
$this->assertStringNotContainsString($expected2, $view->render('without_decorator'));
}
}
37 changes: 37 additions & 0 deletions tests/View/ToolbarDecoratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\View\View;
use Config\View as ViewConfig;
use Michalsn\CodeIgniterHtmx\Config\Htmx as HtmxConfig;
use Michalsn\CodeIgniterHtmx\View\ToolbarDecorator;

/**
Expand Down Expand Up @@ -59,4 +60,40 @@ public function testDecoratorApply(): void
$this->assertStringContainsString($expected1, $view->render('with_decorator'));
$this->assertStringContainsString($expected2, $view->render('with_decorator'));
}

public function testDecoratorDisabled(): void
{
$htmxConfig = new HtmxConfig();
$htmxConfig->toolbarDecorator = false;
Factories::injectMock('config', 'Htmx', $htmxConfig);

$config = $this->config;
$config->decorators = [ToolbarDecorator::class];
Factories::injectMock('config', 'View', $config);

$view = new View($this->config, $this->viewsDir, $this->loader);

$view->setVar('testString', 'Hello World');
$expected1 = '<h1>Hello World</h1>';
$expected2 = 'id="htmxToolbarScript"';

$this->assertStringContainsString($expected1, $view->render('without_decorator'));
$this->assertStringNotContainsString($expected2, $view->render('without_decorator'));
}

public function testDecoratorDisabledWithSkipDecoratorsString(): void
{
$config = $this->config;
$config->decorators = [ToolbarDecorator::class];
Factories::injectMock('config', 'View', $config);

$view = new View($this->config, $this->viewsDir, $this->loader);

$view->setVar('testString', 'htmxSkipViewDecorators');
$expected1 = '<h1>htmxSkipViewDecorators</h1>';
$expected2 = 'id="htmxToolbarScript"';

$this->assertStringContainsString($expected1, $view->render('without_decorator'));
$this->assertStringNotContainsString($expected2, $view->render('without_decorator'));
}
}

0 comments on commit e81aec8

Please sign in to comment.