Skip to content

Commit e81aec8

Browse files
authored
Merge pull request #63 from michalsn/feat/decorators
feat: configurable view decorators
2 parents c6e2ed4 + dfe89d1 commit e81aec8

14 files changed

+224
-26
lines changed

composer.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
},
1818
"require-dev": {
1919
"codeigniter4/devkit": "^1.0",
20-
"codeigniter4/framework": "^4.1",
21-
"rector/rector": "0.18.5"
20+
"codeigniter4/framework": "^4.1"
2221
},
2322
"minimum-stability": "dev",
2423
"prefer-stable": true,

docs/configuration.md

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Configuration
2+
3+
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.
4+
5+
When we run:
6+
7+
php spark htmx:publish
8+
9+
We will get our copy ready for modifications.
10+
11+
---
12+
13+
Available options:
14+
15+
- [$toolbarDecorator](#$toolbarDecorator)
16+
- [$errorModalDecorator](#$errorModalDecorator)
17+
- [$skipViewDecoratorsString](#$skipViewDecoratorsString)
18+
19+
### $toolbarDecorator
20+
21+
This allows us to disable the `ToolbarDecorator` class. Please read [Debug Toolbar](debug_toolbar.md) page for more information.
22+
23+
### $errorModalDecorator
24+
25+
This allows us to disable the `ErrorModalDecorator` class. Please read [Error handling](error_handling.md) page for more information.
26+
27+
### $skipViewDecoratorsString
28+
29+
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.
30+
31+
You can change this string to whatever you want. Just remember to make it unique enough to not use it by accident.
32+
33+
This may be useful when we want to send an e-mail, which message is prepared via the View file.
34+
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.
35+
36+
We can add the defined string as an `id` or `class` to the html tag.
37+
38+
In the `production` environment these decorators are ignored by design. So this is useful only for the `development` mode.

docs/debug_toolbar.md

+2
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ use the `History` tab in the Toolbar.
77

88
If you're using the `head-support` extension then the Debug Toolbar rendering will not work for `htmx` requests.
99
You can still access the toolbar for a given request by checking the URL in the `debugbar-link` response header.
10+
11+
This feature can be disabled in the [Config](configuration.md) file.

docs/error_handling.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
# Error handling
22

33
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.
4+
5+
This feature can be disabled in the [Config](configuration.md) file.

docs/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ It also provides some additional help with **handling errors** and **Debug Toolb
1212
### Table of Contents
1313

1414
* [Installation](installation.md)
15+
* [Configuration](configuration.md)
1516
* [Error handling](error_handling.md)
1617
* [View fragments](view_fragments.md)
1718
* [IncomingReqeuest](incoming_request.md)

mkdocs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ extra_javascript:
4949
nav:
5050
- Home: index.md
5151
- Installation: installation.md
52+
- Configuration: configuration.md
5253
- Error handling: error_handling.md
5354
- View fragments: view_fragments.md
5455
- IncomingReqeuest: incoming_request.md

phpunit.xml.dist

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
</include>
2626
<exclude>
2727
<directory suffix=".php">./src/Config</directory>
28+
<file>./src/Commands/HtmxPublish.php</file>
2829
<file>./src/Debug/Toolbar.php</file>
2930
</exclude>
3031
<report>

rector.php

+30-24
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
<?php
22

33
use Rector\CodeQuality\Rector\BooleanAnd\SimplifyEmptyArrayCheckRector;
4+
use Rector\CodeQuality\Rector\Class_\CompleteDynamicPropertiesRector;
45
use Rector\CodeQuality\Rector\Expression\InlineIfToExplicitIfRector;
5-
use Rector\CodeQuality\Rector\For_\ForToForeachRector;
66
use Rector\CodeQuality\Rector\Foreach_\UnusedForeachValueToArrayKeysRector;
7-
use Rector\CodeQuality\Rector\FuncCall\AddPregQuoteDelimiterRector;
87
use Rector\CodeQuality\Rector\FuncCall\ChangeArrayPushToArrayAssignRector;
98
use Rector\CodeQuality\Rector\FuncCall\SimplifyRegexPatternRector;
109
use Rector\CodeQuality\Rector\FuncCall\SimplifyStrposLowerRector;
@@ -20,24 +19,30 @@
2019
use Rector\Config\RectorConfig;
2120
use Rector\Core\ValueObject\PhpVersion;
2221
use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPromotedPropertyRector;
23-
use Rector\DeadCode\Rector\MethodCall\RemoveEmptyMethodCallRector;
2422
use Rector\EarlyReturn\Rector\Foreach_\ChangeNestedForeachIfsToEarlyContinueRector;
2523
use Rector\EarlyReturn\Rector\If_\ChangeIfElseValueAssignToEarlyReturnRector;
2624
use Rector\EarlyReturn\Rector\If_\RemoveAlwaysElseRector;
2725
use Rector\EarlyReturn\Rector\Return_\PreparedValueToEarlyReturnRector;
2826
use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector;
29-
use Rector\Php56\Rector\FunctionLike\AddDefaultValueForUndefinedVariableRector;
3027
use Rector\Php73\Rector\FuncCall\JsonThrowOnErrorRector;
3128
use Rector\Php73\Rector\FuncCall\StringifyStrNeedlesRector;
32-
use Rector\PHPUnit\Set\PHPUnitLevelSetList;
29+
use Rector\PHPUnit\AnnotationsToAttributes\Rector\ClassMethod\DataProviderAnnotationToAttributeRector;
3330
use Rector\PHPUnit\Set\PHPUnitSetList;
34-
use Rector\PSR4\Rector\FileWithoutNamespace\NormalizeNamespaceByPSR4ComposerAutoloadRector;
31+
use Rector\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector;
3532
use Rector\Set\ValueObject\LevelSetList;
3633
use Rector\Set\ValueObject\SetList;
34+
use Rector\TypeDeclaration\Rector\Property\TypedPropertyFromAssignsRector;
3735

3836
return static function (RectorConfig $rectorConfig): void {
39-
$rectorConfig->sets([SetList::DEAD_CODE, LevelSetList::UP_TO_PHP_80, PHPUnitSetList::PHPUNIT_SPECIFIC_METHOD, PHPUnitLevelSetList::UP_TO_PHPUNIT_100]);
37+
$rectorConfig->sets([
38+
SetList::DEAD_CODE,
39+
LevelSetList::UP_TO_PHP_80,
40+
PHPUnitSetList::PHPUNIT_CODE_QUALITY,
41+
PHPUnitSetList::PHPUNIT_100,
42+
]);
43+
4044
$rectorConfig->parallel();
45+
4146
// The paths to refactor (can also be supplied with CLI arguments)
4247
$rectorConfig->paths([
4348
__DIR__ . '/src/',
@@ -59,7 +64,7 @@
5964
}
6065

6166
// Set the target version for refactoring
62-
$rectorConfig->phpVersion(PhpVersion::PHP_80);
67+
$rectorConfig->phpVersion(PhpVersion::PHP_81);
6368

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

77-
// Ignore tests that might make calls without a result
78-
RemoveEmptyMethodCallRector::class => [
79-
__DIR__ . '/tests',
80-
],
81-
82-
// Ignore files that should not be namespaced
83-
NormalizeNamespaceByPSR4ComposerAutoloadRector::class => [
84-
__DIR__ . '/src/Common.php',
85-
__DIR__ . '/tests/_support/Views/',
86-
],
87-
8882
// May load view files directly when detecting classes
8983
StringClassNameToClassConstantRector::class,
9084

91-
// May be uninitialized on purpose
92-
AddDefaultValueForUndefinedVariableRector::class,
85+
// Supported from PHPUnit 10
86+
DataProviderAnnotationToAttributeRector::class,
9387
]);
88+
89+
// auto import fully qualified class names
90+
$rectorConfig->importNames();
91+
9492
$rectorConfig->rule(SimplifyUselessVariableRector::class);
9593
$rectorConfig->rule(RemoveAlwaysElseRector::class);
9694
$rectorConfig->rule(CountArrayToEmptyArrayComparisonRector::class);
97-
$rectorConfig->rule(ForToForeachRector::class);
9895
$rectorConfig->rule(ChangeNestedForeachIfsToEarlyContinueRector::class);
9996
$rectorConfig->rule(ChangeIfElseValueAssignToEarlyReturnRector::class);
10097
$rectorConfig->rule(SimplifyStrposLowerRector::class);
@@ -107,10 +104,19 @@
107104
$rectorConfig->rule(UnusedForeachValueToArrayKeysRector::class);
108105
$rectorConfig->rule(ChangeArrayPushToArrayAssignRector::class);
109106
$rectorConfig->rule(UnnecessaryTernaryExpressionRector::class);
110-
$rectorConfig->rule(AddPregQuoteDelimiterRector::class);
111107
$rectorConfig->rule(SimplifyRegexPatternRector::class);
112108
$rectorConfig->rule(FuncGetArgsToVariadicParamRector::class);
113109
$rectorConfig->rule(MakeInheritedMethodVisibilitySameAsParentRector::class);
114110
$rectorConfig->rule(SimplifyEmptyArrayCheckRector::class);
115-
$rectorConfig->rule(NormalizeNamespaceByPSR4ComposerAutoloadRector::class);
111+
$rectorConfig
112+
->ruleWithConfiguration(TypedPropertyFromAssignsRector::class, [
113+
/**
114+
* 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
115+
* Set to true for projects that allow BC break
116+
*/
117+
TypedPropertyFromAssignsRector::INLINE_PUBLIC => false,
118+
]);
119+
$rectorConfig->rule(StringClassNameToClassConstantRector::class);
120+
$rectorConfig->rule(PrivatizeFinalClassPropertyRector::class);
121+
$rectorConfig->rule(CompleteDynamicPropertiesRector::class);
116122
};

src/Commands/HtmxPublish.php

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace Michalsn\CodeIgniterHtmx\Commands;
4+
5+
use CodeIgniter\CLI\BaseCommand;
6+
use CodeIgniter\CLI\CLI;
7+
use CodeIgniter\Publisher\Publisher;
8+
use Throwable;
9+
10+
class HtmxPublish extends BaseCommand
11+
{
12+
protected $group = 'Htmx';
13+
protected $name = 'htmx:publish';
14+
protected $description = 'Publish Htmx config file into the current application.';
15+
16+
/**
17+
* @return void
18+
*/
19+
public function run(array $params)
20+
{
21+
$source = service('autoloader')->getNamespace('Michalsn\\CodeIgniterHtmx')[0];
22+
23+
$publisher = new Publisher($source, APPPATH);
24+
25+
try {
26+
$publisher->addPaths([
27+
'Config/Htmx.php',
28+
])->merge(false);
29+
} catch (Throwable $e) {
30+
$this->showError($e);
31+
32+
return;
33+
}
34+
35+
foreach ($publisher->getPublished() as $file) {
36+
$contents = file_get_contents($file);
37+
$contents = str_replace('namespace Michalsn\\CodeIgniterHtmx\\Config', 'namespace Config', $contents);
38+
$contents = str_replace('use CodeIgniter\\Config\\BaseConfig', 'use Michalsn\\CodeIgniterHtmx\\Config\\Htmx as BaseHtmx', $contents);
39+
$contents = str_replace('class Htmx extends BaseConfig', 'class Htmx extends BaseHtmx', $contents);
40+
file_put_contents($file, $contents);
41+
}
42+
43+
CLI::write(CLI::color(' Published! ', 'green') . 'You can customize the configuration by editing the "app/Config/Htmx.php" file.');
44+
}
45+
}

src/Config/Htmx.php

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Michalsn\CodeIgniterHtmx\Config;
4+
5+
use CodeIgniter\Config\BaseConfig;
6+
7+
class Htmx extends BaseConfig
8+
{
9+
/**
10+
* Enable / disable ToolbarDecorator.
11+
*/
12+
public bool $toolbarDecorator = true;
13+
14+
/**
15+
* Enable / diable ErrorModalDecorator.
16+
*/
17+
public bool $errorModalDecorator = true;
18+
19+
/**
20+
* The appearance of this string in the view
21+
* content will skip the htmx decorators. Even
22+
* when they are enabled.
23+
*/
24+
public string $skipViewDecoratorsString = 'htmxSkipViewDecorators';
25+
}

src/View/ErrorModalDecorator.php

+2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ public static function decorate(string $html): string
1111
if (CI_DEBUG
1212
&& (! is_cli() || ENVIRONMENT === 'testing')
1313
&& ! service('request')->isHtmx()
14+
&& config('Htmx')->errorModalDecorator
1415
&& str_contains($html, '</head>')
16+
&& ! str_contains($html, config('Htmx')->skipViewDecoratorsString)
1517
&& ! str_contains($html, 'id="htmxErrorModalScript"')
1618
) {
1719
$script = sprintf(

src/View/ToolbarDecorator.php

+2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ public static function decorate(string $html): string
1111
if (CI_DEBUG
1212
&& (! is_cli() || ENVIRONMENT === 'testing')
1313
&& ! service('request')->isHtmx()
14+
&& config('Htmx')->toolbarDecorator
1415
&& str_contains($html, '</head>')
16+
&& ! str_contains($html, config('Htmx')->skipViewDecoratorsString)
1517
&& ! str_contains($html, 'id="htmxToolbarScript"')
1618
) {
1719
$script = sprintf(

tests/View/ErrorModelDecoratorTest.php

+37
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use CodeIgniter\Test\CIUnitTestCase;
99
use CodeIgniter\View\View;
1010
use Config\View as ViewConfig;
11+
use Michalsn\CodeIgniterHtmx\Config\Htmx as HtmxConfig;
1112
use Michalsn\CodeIgniterHtmx\View\ErrorModalDecorator;
1213

1314
/**
@@ -59,4 +60,40 @@ public function testDecoratorApply(): void
5960
$this->assertStringContainsString($expected1, $view->render('with_decorator'));
6061
$this->assertStringContainsString($expected2, $view->render('with_decorator'));
6162
}
63+
64+
public function testDecoratorDisabled(): void
65+
{
66+
$htmxConfig = new HtmxConfig();
67+
$htmxConfig->errorModalDecorator = false;
68+
Factories::injectMock('config', 'Htmx', $htmxConfig);
69+
70+
$config = $this->config;
71+
$config->decorators = [ErrorModalDecorator::class];
72+
Factories::injectMock('config', 'View', $config);
73+
74+
$view = new View($this->config, $this->viewsDir, $this->loader);
75+
76+
$view->setVar('testString', 'Hello World 1');
77+
$expected1 = '<h1>Hello World 1</h1>';
78+
$expected2 = 'id="htmxErrorModalScript"';
79+
80+
$this->assertStringContainsString($expected1, $view->render('without_decorator'));
81+
$this->assertStringNotContainsString($expected2, $view->render('without_decorator'));
82+
}
83+
84+
public function testDecoratorDisabledWithSkipDecoratorsString(): void
85+
{
86+
$config = $this->config;
87+
$config->decorators = [ErrorModalDecorator::class];
88+
Factories::injectMock('config', 'View', $config);
89+
90+
$view = new View($this->config, $this->viewsDir, $this->loader);
91+
92+
$view->setVar('testString', 'htmxSkipViewDecorators');
93+
$expected1 = '<h1>htmxSkipViewDecorators</h1>';
94+
$expected2 = 'id="htmxErrorModalScript"';
95+
96+
$this->assertStringContainsString($expected1, $view->render('without_decorator'));
97+
$this->assertStringNotContainsString($expected2, $view->render('without_decorator'));
98+
}
6299
}

tests/View/ToolbarDecoratorTest.php

+37
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use CodeIgniter\Test\CIUnitTestCase;
99
use CodeIgniter\View\View;
1010
use Config\View as ViewConfig;
11+
use Michalsn\CodeIgniterHtmx\Config\Htmx as HtmxConfig;
1112
use Michalsn\CodeIgniterHtmx\View\ToolbarDecorator;
1213

1314
/**
@@ -59,4 +60,40 @@ public function testDecoratorApply(): void
5960
$this->assertStringContainsString($expected1, $view->render('with_decorator'));
6061
$this->assertStringContainsString($expected2, $view->render('with_decorator'));
6162
}
63+
64+
public function testDecoratorDisabled(): void
65+
{
66+
$htmxConfig = new HtmxConfig();
67+
$htmxConfig->toolbarDecorator = false;
68+
Factories::injectMock('config', 'Htmx', $htmxConfig);
69+
70+
$config = $this->config;
71+
$config->decorators = [ToolbarDecorator::class];
72+
Factories::injectMock('config', 'View', $config);
73+
74+
$view = new View($this->config, $this->viewsDir, $this->loader);
75+
76+
$view->setVar('testString', 'Hello World');
77+
$expected1 = '<h1>Hello World</h1>';
78+
$expected2 = 'id="htmxToolbarScript"';
79+
80+
$this->assertStringContainsString($expected1, $view->render('without_decorator'));
81+
$this->assertStringNotContainsString($expected2, $view->render('without_decorator'));
82+
}
83+
84+
public function testDecoratorDisabledWithSkipDecoratorsString(): void
85+
{
86+
$config = $this->config;
87+
$config->decorators = [ToolbarDecorator::class];
88+
Factories::injectMock('config', 'View', $config);
89+
90+
$view = new View($this->config, $this->viewsDir, $this->loader);
91+
92+
$view->setVar('testString', 'htmxSkipViewDecorators');
93+
$expected1 = '<h1>htmxSkipViewDecorators</h1>';
94+
$expected2 = 'id="htmxToolbarScript"';
95+
96+
$this->assertStringContainsString($expected1, $view->render('without_decorator'));
97+
$this->assertStringNotContainsString($expected2, $view->render('without_decorator'));
98+
}
6299
}

0 commit comments

Comments
 (0)