webchemistry/stimulus

There is no license information available for the latest version (v3.0.4) of this package.

v3.0.4 2024-09-02 13:15 UTC

This package is auto-updated.

Last update: 2024-11-02 13:42:12 UTC


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:

  1. add ? to the end of property name: @property {String} stringValue?
  2. add { optional } to the 3rd section (options) of property: @property {String} stringValue {optional}
  3. 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('...'));
    }

}