webchemistry / stimulus
Installs: 3 172
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 1
Forks: 0
Open Issues: 0
pkg:composer/webchemistry/stimulus
Requires
- php: >=8.0
- nette/finder: ^2.5 || ^3.0
- nette/php-generator: ^3.5.0 || ^4.0.0
- typertion/php: ^1.0
- utilitte/php: ^1.5
- webchemistry/simple-json: ^1.1.0
Requires (Dev)
- latte/latte: ^3.0.0
- nette/application: ^3.1
- nette/di: ^3.0
- nette/forms: ^3.1
- phpstan/phpstan: ^1.8
README
composer require webchemistry/stimulus
extensions:
- WebChemistry\Stimulus\DI\StimulusExtension
Set up extractor and generators
First, set up extractor and generator. Namespaced identifiers also supported.
Code with comments (copy-paste code is below):
require __DIR__ . '/vendor/autoload.php';
// directory with controllers, only *_controller.js and *-controller.js are extracted
$extractor = new JavascriptSourceExtractor(__DIR__ . '/js/stimulus/controllers');
// optional, we want UI namespace instead of Ui
$keywords = ['ui' => 'UI'];
// generate classes as Stimulus\*\_*Controller, these classes we don't edit
$originalClassNameConverter = new PrependClassNameConverter(
'Stimulus\\',
new AppendClassNameConverter(
new BaseClassNameConverter($keywords, fn (string $className) => '_' . $className),
'Controller',
),
);
// We want edit these classes
$emptyClassNameConverter = new PrependClassNameConverter(
'App\\Stimulus\\Controller\\',
new AppendClassNameConverter(
new BaseClassNameConverter($keywords),
'Controller',
),
);
// for autocomplete_controller.js these controllers are generated
// class Stimulus\_AutocompleteController
// class App\Stimulus\Controller\AutocompleteController extends Stimulus\_AutocompleteController
// for namespace/autocomplete_controller.js these controllers are generated
// class Stimulus\Namespace\_AutocompleteController
// class App\Stimulus\Controller\Namespace\AutocompleteController extends Stimulus\_AutocompleteController
// Generator generates static methods
$generator = new StaticClassStimulusControllerGenerator($extractor, $originalClassNameConverter);
// Generator generates class with empty body and extends original class (Stimulus\*\_*Controller)
$emptyGenerator = new EmptyClassStimulusControllerGenerator(
$extractor,
$emptyClassNameConverter,
$originalClassNameConverter,
);
// Files are written in app/generated/stimulus/*, Stimulus\ namespace have to be removed from path
$writer = new FilesystemWriter(__DIR__ . '/app/generated/stimulus', 'Stimulus\\');
// Files are written in app/src/Stimulus/Controller/*, App\Stimulus\Controller\ namespace have to be removed from path, if file exists don't rewrite it
$emptyWriter = new FilesystemWriter(__DIR__ . '/app/src/Stimulus/Controller', 'App\\Stimulus\\Controller\\', false);
foreach ($generator->generate() as $generated) {
$writer->write($generated);
}
foreach ($emptyGenerator->generate() as $generated) {
$emptyWriter->write($generated);
}
copy-paste code:
require __DIR__ . '/vendor/autoload.php';
$extractor = new JavascriptSourceExtractor(__DIR__ . '/js/stimulus/controllers');
$keywords = ['ui' => 'UI'];
$originalClassNameConverter = new PrependClassNameConverter(
'Stimulus\\',
new AppendClassNameConverter(
new BaseClassNameConverter($keywords, fn (string $className) => '_' . $className),
'Controller',
),
);
$emptyClassNameConverter = new PrependClassNameConverter(
'App\\Stimulus\\Controller\\',
new AppendClassNameConverter(
new BaseClassNameConverter($keywords),
'Controller',
),
);
$generator = new StaticClassStimulusControllerGenerator($extractor, $originalClassNameConverter);
$emptyGenerator = new EmptyClassStimulusControllerGenerator(
$extractor,
$emptyClassNameConverter,
$originalClassNameConverter,
);
$writer = new FilesystemWriter(__DIR__ . '/app/generated/stimulus', 'Stimulus\\');
$emptyWriter = new FilesystemWriter(__DIR__ . '/app/src/Stimulus/Controller', 'App\\Stimulus\\Controller\\', false);
foreach ($generator->generate() as $generated) {
$writer->write($generated);
}
foreach ($emptyGenerator->generate() as $generated) {
$emptyWriter->write($generated);
}
How files are generated
Class JavascriptSourceExtractor uses javascript comments for generating.
Each controller must be annotated with @controller, actions with @action and their parameters with @param, values, classes and targets with @property.
my_controller.js
/**
* @controller
*
* @property {String} stringValue
*
* @property {HTMLElement[]} itemTargets
* @property {HTMLElement} resultsTarget
*
* @property {String} activeClass
*/
export default class extends Controller {
static targets = ['results', 'item'];
static values = {
string: String,
};
static classes = ['active'];
/**
* @action
*/
switch() {
}
}
This PHP class is generated:
declare(strict_types = 1);
/**
* NOTE: This class is auto generated by file: my_controller.js
* Do not edit the class manually
*/
namespace Stimulus;
use WebChemistry\Stimulus\Type\StimulusAction;
use WebChemistry\Stimulus\Type\StimulusController;
use WebChemistry\Stimulus\Type\StimulusTarget;
abstract class _MyController
{
final public const identifier = 'my';
public static function construct(string $stringValue, string $activeClass): StimulusController
{
return new StimulusController(self::identifier, [
'stringValue' => $stringValue,
'activeClass' => $activeClass,
], []);
}
public static function itemTarget(): StimulusTarget
{
return new StimulusTarget(self::identifier, 'itemTarget');
}
public static function switchAction(): StimulusAction
{
return new StimulusAction(self::identifier, 'switch', []);
}
}
By default, each property is required, if we want to make optional we have several ways:
- add
?to the end of property name:@property {String} stringValue? - add
{ optional }to the 3rd section (options) of property:@property {String} stringValue {optional} - add hasser
@property {Boolean} hasStringValue
Javascript types and PHP types
webchemisty/stimulus introduces stricter environment for writting application. Nowadays, everyone use static analysis (at least they should) so correct types are crucial.
Library converts javascript types in the following way:
Number => int|float narrowing is achieved by options (3rd section) { number: int }
Array and Object => mixed[]
Boolean => bool
String => string
other => mixed
Arrays:
String[] => string[]
Number[] => array<int|float> narrowing: { number: float }
Bool[] => bool[]
other => mixed[]
Custom types
Sometimes we need overriding comment types @param ... and types method(... $type)
fot this there is options type and commentType e.g. { type: mixed, commentType: "array<string, resource>" }
Action parameters
Stimulus 3 introduced parameters for actions. For generating just use intersection type or just object type.
export default class extends Controller {
/**
* @action
* @param { { params: { value: String } } & PointerEvent} event
*/
switch(event) {
const { value } = event.params;
}
/**
* @action
* @param { { params: { value: String } } } event
*/
switchTwo(event) {
const { value } = event.params;
}
}
Usage in PHP
use WebChemistry\Stimulus\Renderer\HtmlRenderer;
$htmlAttributes = HtmlRenderer::render(
App\Stimulus\Controller\MyController::construct('string', 'activeClass'),
App\Stimulus\Controller\MyController::switchAction()->event('click'),
); // data-controller="my" data-my-string-value="string" data-my-active-class="active" data-action="click->my#switch"
// or as array [attribute => value]
HtmlRenderer::toArray(...);
Usage in Latte
<div n:stimulus="
App\Stimulus\Controller\MyController::construct('string', 'activeClass'),
App\Stimulus\Controller\MyController::switchAction()->event('click'),
"></div>
Controller as service
Sometimes we want to inject other services:
declare(strict_types = 1);
namespace App\Stimulus\Controller;
use Stimulus\_MyController as ParentController;
use WebChemistry\Stimulus\Type\StimulusController;
final class MyController extends ParentController
{
public function __construct(
private LinkGenerator $linkGenerator,
) {}
public function doConstruct(): StimulusController
{
return self::construct($this->linkGenerator->link('...'));
}
}