From 22d94e9ebc2ede18f28f24fbe69e6e6389c263cf Mon Sep 17 00:00:00 2001 From: Michael Cramer Date: Sun, 13 Dec 2020 16:51:00 +0100 Subject: [PATCH 1/2] Add Leaflet.js package --- README.md | 2 + src/Leafletjs/.gitattributes | 5 + src/Leafletjs/.gitignore | 4 + src/Leafletjs/Builder/LeafletBuilder.php | 29 +++++ .../Builder/LeafletBuilderInterface.php | 25 ++++ .../LeafletjsExtension.php | 50 ++++++++ src/Leafletjs/LeafletjsBundle.php | 25 ++++ src/Leafletjs/Model/Map.php | 75 +++++++++++ src/Leafletjs/README.md | 120 ++++++++++++++++++ src/Leafletjs/Resources/assets/.babelrc | 4 + .../Resources/assets/dist/controller.js | 119 +++++++++++++++++ src/Leafletjs/Resources/assets/package.json | 45 +++++++ .../Resources/assets/src/controller.js | 57 +++++++++ .../Resources/assets/test/controller.test.js | 98 ++++++++++++++ src/Leafletjs/Resources/assets/test/setup.js | 13 ++ src/Leafletjs/Tests/Kernel/AppKernelTrait.php | 42 ++++++ src/Leafletjs/Tests/Kernel/EmptyAppKernel.php | 36 ++++++ .../Tests/Kernel/FrameworkAppKernel.php | 41 ++++++ src/Leafletjs/Tests/Kernel/TwigAppKernel.php | 46 +++++++ src/Leafletjs/Tests/LeafletjsBundleTest.php | 43 +++++++ .../Tests/Twig/LeafletExtensionTest.php | 57 +++++++++ src/Leafletjs/Twig/LeafletExtension.php | 63 +++++++++ src/Leafletjs/composer.json | 54 ++++++++ src/Leafletjs/phpunit.xml.dist | 35 +++++ 24 files changed, 1088 insertions(+) create mode 100644 src/Leafletjs/.gitattributes create mode 100644 src/Leafletjs/.gitignore create mode 100644 src/Leafletjs/Builder/LeafletBuilder.php create mode 100644 src/Leafletjs/Builder/LeafletBuilderInterface.php create mode 100644 src/Leafletjs/DependencyInjection/LeafletjsExtension.php create mode 100644 src/Leafletjs/LeafletjsBundle.php create mode 100644 src/Leafletjs/Model/Map.php create mode 100644 src/Leafletjs/README.md create mode 100644 src/Leafletjs/Resources/assets/.babelrc create mode 100644 src/Leafletjs/Resources/assets/dist/controller.js create mode 100644 src/Leafletjs/Resources/assets/package.json create mode 100644 src/Leafletjs/Resources/assets/src/controller.js create mode 100644 src/Leafletjs/Resources/assets/test/controller.test.js create mode 100644 src/Leafletjs/Resources/assets/test/setup.js create mode 100644 src/Leafletjs/Tests/Kernel/AppKernelTrait.php create mode 100644 src/Leafletjs/Tests/Kernel/EmptyAppKernel.php create mode 100644 src/Leafletjs/Tests/Kernel/FrameworkAppKernel.php create mode 100644 src/Leafletjs/Tests/Kernel/TwigAppKernel.php create mode 100644 src/Leafletjs/Tests/LeafletjsBundleTest.php create mode 100644 src/Leafletjs/Tests/Twig/LeafletExtensionTest.php create mode 100644 src/Leafletjs/Twig/LeafletExtension.php create mode 100644 src/Leafletjs/composer.json create mode 100644 src/Leafletjs/phpunit.xml.dist diff --git a/README.md b/README.md index 32c0e463492..5e4e462de2c 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,8 @@ build amazing User Experiences as quickly as we can now setup File input drag-and-drop zones for Symfony Forms - [UX LazyImage](https://github.com/symfony/ux-lazy-image): Improve image loading performances through lazy-loading and data-uri thumbnails +- [UX Leaflet.js](https://github.com/symfony/ux-leafletjs): + [Leaflet](https://leafletjs.com/) map library integration for Symfony - [UX Swup](https://github.com/symfony/ux-swup): [Swup](https://swup.js.org/) page transition library integration for Symfony diff --git a/src/Leafletjs/.gitattributes b/src/Leafletjs/.gitattributes new file mode 100644 index 00000000000..e59bc80d2fe --- /dev/null +++ b/src/Leafletjs/.gitattributes @@ -0,0 +1,5 @@ +/.gitattributes export-ignore +/.gitignore export-ignore +/phpunit.xml.dist export-ignore +/assets/test export-ignore +/tests export-ignore diff --git a/src/Leafletjs/.gitignore b/src/Leafletjs/.gitignore new file mode 100644 index 00000000000..bb17c3e124c --- /dev/null +++ b/src/Leafletjs/.gitignore @@ -0,0 +1,4 @@ +/vendor/ +.phpunit.result.cache +.php_cs.cache +composer.lock diff --git a/src/Leafletjs/Builder/LeafletBuilder.php b/src/Leafletjs/Builder/LeafletBuilder.php new file mode 100644 index 00000000000..9d1f7f724d4 --- /dev/null +++ b/src/Leafletjs/Builder/LeafletBuilder.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Leafletjs\Builder; + +use Symfony\UX\Leafletjs\Model\Map; + +/** + * @author Titouan Galopin + * @author Michael Cramer + * + * @final + * @experimental + */ +class LeafletBuilder implements LeafletBuilderInterface +{ + public function createMap(float $lon, float $lat, int $zoom = 10): Map + { + return new Map($lon, $lat, $zoom); + } +} diff --git a/src/Leafletjs/Builder/LeafletBuilderInterface.php b/src/Leafletjs/Builder/LeafletBuilderInterface.php new file mode 100644 index 00000000000..abd05592cd5 --- /dev/null +++ b/src/Leafletjs/Builder/LeafletBuilderInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Leafletjs\Builder; + +use Symfony\UX\Leafletjs\Model\Map; + +/** + * @author Titouan Galopin + * @author Michael Cramer + * + * @experimental + */ +interface LeafletBuilderInterface +{ + public function createMap(float $lon, float $lat, int $zoom = 10): Map; +} diff --git a/src/Leafletjs/DependencyInjection/LeafletjsExtension.php b/src/Leafletjs/DependencyInjection/LeafletjsExtension.php new file mode 100644 index 00000000000..556055322c4 --- /dev/null +++ b/src/Leafletjs/DependencyInjection/LeafletjsExtension.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Leafletjs\DependencyInjection; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Symfony\UX\Leafletjs\Builder\LeafletBuilder; +use Symfony\UX\Leafletjs\Builder\LeafletBuilderInterface; +use Symfony\UX\Leafletjs\Twig\LeafletExtension; +use Twig\Environment; + +/** + * @author Titouan Galopin + * @author Michael Cramer + * + * @internal + */ +class LeafletjsExtension extends Extension +{ + public function load(array $configs, ContainerBuilder $container) + { + $container + ->setDefinition('leafletjs.builder', new Definition(LeafletBuilder::class)) + ->setPublic(false) + ; + + $container + ->setAlias(LeafletBuilderInterface::class, 'leafletjs.builder') + ->setPublic(false) + ; + + if (class_exists(Environment::class)) { + $container + ->setDefinition('leafletjs.twig_extension', new Definition(LeafletExtension::class)) + ->addTag('twig.extension') + ->setPublic(false) + ; + } + } +} diff --git a/src/Leafletjs/LeafletjsBundle.php b/src/Leafletjs/LeafletjsBundle.php new file mode 100644 index 00000000000..1d4d105b9d0 --- /dev/null +++ b/src/Leafletjs/LeafletjsBundle.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Leafletjs; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +/** + * @author Titouan Galopin + * @author Michael Cramer + * + * @final + * @experimental + */ +class LeafletjsBundle extends Bundle +{ +} diff --git a/src/Leafletjs/Model/Map.php b/src/Leafletjs/Model/Map.php new file mode 100644 index 00000000000..d5e16cbc960 --- /dev/null +++ b/src/Leafletjs/Model/Map.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Leafletjs\Model; + +/** + * @author Titouan Galopin + * @author Michael Cramer + * + * @final + * @experimental + */ +class Map +{ + private $mapOptions = []; + private $attributes = []; + private $lon; + private $lat; + private $zoom; + + public function __construct(float $lon, float $lat, int $zoom) + { + $this->lon = $lon; + $this->lat = $lat; + $this->zoom = $zoom; + } + + public function setMapOptions(array $mapOptions): void + { + $this->mapOptions = $mapOptions; + } + + public function setAttributes(array $attributes): void + { + $this->attributes = $attributes; + } + + public function getDataController(): ?string + { + return $this->attributes['data-controller'] ?? null; + } + + public function getMapOptions(): array + { + return $this->mapOptions; + } + + public function getAttributes(): array + { + return $this->attributes; + } + + public function getLon(): float + { + return $this->lon; + } + + public function getLat(): float + { + return $this->lat; + } + + public function getZoom(): int + { + return $this->zoom; + } +} diff --git a/src/Leafletjs/README.md b/src/Leafletjs/README.md new file mode 100644 index 00000000000..4f309ad0d1d --- /dev/null +++ b/src/Leafletjs/README.md @@ -0,0 +1,120 @@ +# Symfony UX Leaflet.js + +Symfony UX Leaflet.js is a Symfony bundle integrating the [Leaflet](https://leafletjs.com/) +library in Symfony applications. It is part of [the Symfony UX initiative](https://symfony.com/ux). + +## Installation + +Symfony UX Leaflet requires PHP 7.2+ and Symfony 4.4+. + +Install this bundle using Composer and Symfony Flex: + +```sh +composer require symfony/ux-leafletjs + +# Don't forget to install the JavaScript dependencies as well and compile +yarn install --force +yarn encore dev +``` + +## Usage + +To use Symfony UX Leaflet.js, inject the `LeafletBuilderInterface` service and +create maps in PHP: + +```php +// ... +use Symfony\UX\Leafletjs\Builder\LeafletBuilderInterface; +use Symfony\UX\Leafletjs\Model\Map; + +class HomeController extends AbstractController +{ + /** + * @Route("/", name="homepage") + */ + public function index(LeafletBuilderInterface $mapBuilder): Response + { + $map = $mapBuilder->createMap(51.505, -0.09, 13); + $map->setMapOptions(['minZoom' => 1, 'maxZoom' => 8]); + + return $this->render('home/index.html.twig', [ + 'map' => $map, + ]); + } +} +``` + +Map options are provided as-is to Leaflet. You can read +[Leaflet documentation](https://leafletjs.com/reference-1.7.1.html#map-factory) to discover them all. + +Once created in PHP, a map can be displayed using Twig: + +```twig +{{ render_map(map) }} + +{# You can pass HTML attributes as a second argument to add them on the
tag #} +{{ render_map(map, {'class': 'my-map'}) }} +``` + +### Extend the default behavior + +Symfony UX Leaflet.js allows you to extend its default behavior using a custom Stimulus controller: + +```js +// mymap_controller.js + +import { Controller } from 'stimulus'; + +export default class extends Controller { + connect() { + this.element.addEventListener('leafletjs:connect', this._onConnect); + } + + disconnect() { + // You should always remove listeners when the controller is disconnected to avoid side effects + this.element.removeEventListener('leafletjs:connect', this._onConnect); + } + + _onConnect(event) { + // The map was just created + console.log(event.detail.map); // You can access the map instance using the event details + console.log(event.detail.layer); // You can access the default layer instance using the event details + + // For instance you can add additional markers to the map + new LLMarker([lat, lon]).addTo(event.detail.map); + + // Changing the default URL for the default layer + event.detail.layer.setUrl('http://{s}.somedomain.com/blabla/{z}/{x}/{y}{r}.png'); + } +} +``` + +Then in your render call, add your controller as an HTML attribute: + +```twig +{{ render_map(map, {'data-controller': 'mymap'}) }} +``` + +## Backward Compatibility promise + +This bundle aims at following the same Backward Compatibility promise as the Symfony framework: +[https://symfony.com/doc/current/contributing/code/bc.html](https://symfony.com/doc/current/contributing/code/bc.html) + +However it is currently considered +[**experimental**](https://symfony.com/doc/current/contributing/code/experimental.html), +meaning it is not bound to Symfony's BC policy for the moment. + +## Run tests + +### PHP tests + +```sh +php vendor/bin/phpunit +``` + +### JavaScript tests + +```sh +cd Resources/assets +yarn test +``` diff --git a/src/Leafletjs/Resources/assets/.babelrc b/src/Leafletjs/Resources/assets/.babelrc new file mode 100644 index 00000000000..77f182d2ca8 --- /dev/null +++ b/src/Leafletjs/Resources/assets/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["@babel/env"], + "plugins": ["@babel/plugin-proposal-class-properties"] +} diff --git a/src/Leafletjs/Resources/assets/dist/controller.js b/src/Leafletjs/Resources/assets/dist/controller.js new file mode 100644 index 00000000000..7881b83794c --- /dev/null +++ b/src/Leafletjs/Resources/assets/dist/controller.js @@ -0,0 +1,119 @@ +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +'use strict'; + +function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; + +var _stimulus = require("stimulus"); + +function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } + +function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } + +function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } + +function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } + +function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } + +function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } } + +function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +var _default = /*#__PURE__*/function (_Controller) { + _inherits(_default, _Controller); + + var _super = _createSuper(_default); + + function _default() { + _classCallCheck(this, _default); + + return _super.apply(this, arguments); + } + + _createClass(_default, [{ + key: "connect", + value: function connect() { + var _this = this; + + Promise.resolve().then(function () { + return _interopRequireWildcard(require("leaflet")); + }).then(function (L) { + var mapOptions = _this._extractOptions('map-options'); + + _this.map = L.map(_this.placeholderTarget, mapOptions).setView(_this._coordinates(), _this.data.get('zoom')); + var layer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }); + layer.addTo(_this.map); + var map = _this.map; + + _this._dispatchEvent('leafletjs:connect', { + map: map, + layer: layer + }); + }); + } + }, { + key: "disconnect", + value: function disconnect() { + this.map.off(); + this.map.remove(); + } + }, { + key: "_extractOptions", + value: function _extractOptions(dataName) { + var options = JSON.parse(this.data.get(dataName)); + + if (Array.isArray(options) && 0 === options.length) { + options = {}; + } + + return options; + } + }, { + key: "_coordinates", + value: function _coordinates() { + return [this.data.get("latitude"), this.data.get("longitude")]; + } + }, { + key: "_dispatchEvent", + value: function _dispatchEvent(name) { + var payload = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; + var canBubble = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + var cancelable = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; + var userEvent = document.createEvent('CustomEvent'); + userEvent.initCustomEvent(name, canBubble, cancelable, payload); + this.element.dispatchEvent(userEvent); + } + }]); + + return _default; +}(_stimulus.Controller); + +exports["default"] = _default; + +_defineProperty(_default, "targets", ["placeholder"]); \ No newline at end of file diff --git a/src/Leafletjs/Resources/assets/package.json b/src/Leafletjs/Resources/assets/package.json new file mode 100644 index 00000000000..4501999d05d --- /dev/null +++ b/src/Leafletjs/Resources/assets/package.json @@ -0,0 +1,45 @@ +{ + "name": "@symfony/ux-leafletjs", + "description": "Leaflet integration for Symfony", + "license": "MIT", + "version": "1.1.0", + "symfony": { + "controllers": { + "map": { + "main": "dist/controller.js", + "webpackMode": "eager", + "enabled": true, + "autoimport": { + "leaflet/dist/leaflet.css": true + } + } + } + }, + "scripts": { + "build": "babel src -d dist", + "test": "babel src -d dist && jest", + "lint": "eslint src test" + }, + "dependencies": { + "leaflet": "^1.7.1" + }, + "peerDependencies": { + "stimulus": "^2.0.0" + }, + "devDependencies": { + "@babel/cli": "^7.12.1", + "@babel/core": "^7.12.3", + "@babel/plugin-proposal-class-properties": "^7.12.1", + "@babel/preset-env": "^7.12.7", + "@symfony/stimulus-testing": "^1.1.0", + "jest-canvas-mock": "^2.3.0", + "stimulus": "^2.0.0" + }, + "jest": { + "testRegex": "test/.*\\.test.js", + "setupFilesAfterEnv": [ + "./test/setup.js" + ] + } +} + diff --git a/src/Leafletjs/Resources/assets/src/controller.js b/src/Leafletjs/Resources/assets/src/controller.js new file mode 100644 index 00000000000..c81480013fd --- /dev/null +++ b/src/Leafletjs/Resources/assets/src/controller.js @@ -0,0 +1,57 @@ +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +'use strict'; + +import { Controller } from 'stimulus'; + +export default class extends Controller { + static targets = ['placeholder']; + + connect() { + import('leaflet').then((L) => { + let mapOptions = this._extractOptions('map-options'); + + this.map = L.map(this.placeholderTarget, mapOptions).setView(this._coordinates(), this.data.get('zoom')); + + let layer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', + }); + + layer.addTo(this.map); + + const map = this.map; + this._dispatchEvent('leafletjs:connect', { map: map, layer: layer }); + }); + } + + disconnect() { + this.map.off(); + this.map.remove(); + } + + _extractOptions(dataName) { + let options = JSON.parse(this.data.get(dataName)); + if (Array.isArray(options) && 0 === options.length) { + options = {}; + } + return options; + } + + _coordinates() { + return [this.data.get('latitude'), this.data.get('longitude')]; + } + + _dispatchEvent(name, payload = null, canBubble = false, cancelable = false) { + const userEvent = document.createEvent('CustomEvent'); + userEvent.initCustomEvent(name, canBubble, cancelable, payload); + + this.element.dispatchEvent(userEvent); + } +} diff --git a/src/Leafletjs/Resources/assets/test/controller.test.js b/src/Leafletjs/Resources/assets/test/controller.test.js new file mode 100644 index 00000000000..b4a95161483 --- /dev/null +++ b/src/Leafletjs/Resources/assets/test/controller.test.js @@ -0,0 +1,98 @@ +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +'use strict'; + +import { Application, Controller } from 'stimulus'; +import { getByTestId, waitFor } from '@testing-library/dom'; +import { clearDOM, mountDOM } from '@symfony/stimulus-testing'; +// import LeafletjsController from '../dist/controller'; +import LeafletjsController from '../src/controller'; + +// Controller used to check the actual controller was properly booted +class CheckController extends Controller { + connect() { + this.element.addEventListener('leafletjs:connect', (event) => { + this.element.classList.add('connected'); + this.element.map = event.detail.map; + this.element.layer = event.detail.layer; + }); + } +} + +const startStimulus = () => { + const application = Application.start(); + application.register('check', CheckController); + application.register('leafletjs', LeafletjsController); + return application; +}; + +describe('LeafletjsController', () => { + let container; + + afterEach(() => { + clearDOM(); + }); + + it('connect without options', async () => { + container = mountDOM(` +
+ `); + + expect(getByTestId(container, 'map')).not.toHaveClass('connected'); + + let stimulus = startStimulus(); + await waitFor(() => expect(getByTestId(container, 'map')).toHaveClass('connected')); + + let byTestId = getByTestId(container, 'map'); + expect(byTestId.layer).toBeDefined(); + expect(byTestId.layer._url).toBe('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'); + + expect(byTestId.map).toBeDefined(); + expect(byTestId.map._loaded).toBe(true); + expect(byTestId.map._zoom).toBe(10); + expect(byTestId.map._lastCenter.lat).toBe(51.505); + expect(byTestId.map._lastCenter.lng).toBe(-0.09); + + stimulus.stop(); + }); + + it('connect with options', async () => { + container = mountDOM(` +
+ `); + + expect(getByTestId(container, 'map')).not.toHaveClass('connected'); + + let stimulus = startStimulus(); + await waitFor(() => expect(getByTestId(container, 'map')).toHaveClass('connected')); + + let byTestId = getByTestId(container, 'map'); + expect(byTestId.map).toBeDefined(); + expect(byTestId.map.options.minZoom).toBe(1); + expect(byTestId.map.options.maxZoom).toBe(5); + + stimulus.stop(); + }); +}); diff --git a/src/Leafletjs/Resources/assets/test/setup.js b/src/Leafletjs/Resources/assets/test/setup.js new file mode 100644 index 00000000000..da38f5b52ea --- /dev/null +++ b/src/Leafletjs/Resources/assets/test/setup.js @@ -0,0 +1,13 @@ +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +'use strict'; + +import '@symfony/stimulus-testing/setup'; +import 'jest-canvas-mock'; diff --git a/src/Leafletjs/Tests/Kernel/AppKernelTrait.php b/src/Leafletjs/Tests/Kernel/AppKernelTrait.php new file mode 100644 index 00000000000..1781d968749 --- /dev/null +++ b/src/Leafletjs/Tests/Kernel/AppKernelTrait.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Leafletjs\Tests\Kernel; + +/** + * @author Titouan Galopin + * @author Michael Cramer + * + * @internal + */ +trait AppKernelTrait +{ + public function getCacheDir() + { + return $this->createTmpDir('cache'); + } + + public function getLogDir() + { + return $this->createTmpDir('logs'); + } + + private function createTmpDir(string $type): string + { + $dir = sys_get_temp_dir().'/leafletjs_bundle/'.uniqid($type.'_', true); + + if (!file_exists($dir)) { + mkdir($dir, 0777, true); + } + + return $dir; + } +} diff --git a/src/Leafletjs/Tests/Kernel/EmptyAppKernel.php b/src/Leafletjs/Tests/Kernel/EmptyAppKernel.php new file mode 100644 index 00000000000..68bb5974d95 --- /dev/null +++ b/src/Leafletjs/Tests/Kernel/EmptyAppKernel.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Leafletjs\Tests\Kernel; + +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\HttpKernel\Kernel; +use Symfony\UX\Leafletjs\LeafletjsBundle; + +/** + * @author Titouan Galopin + * @author Michael Cramer + * + * @internal + */ +class EmptyAppKernel extends Kernel +{ + use AppKernelTrait; + + public function registerBundles() + { + return [new LeafletjsBundle()]; + } + + public function registerContainerConfiguration(LoaderInterface $loader) + { + } +} diff --git a/src/Leafletjs/Tests/Kernel/FrameworkAppKernel.php b/src/Leafletjs/Tests/Kernel/FrameworkAppKernel.php new file mode 100644 index 00000000000..b6375065ab9 --- /dev/null +++ b/src/Leafletjs/Tests/Kernel/FrameworkAppKernel.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Leafletjs\Tests\Kernel; + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Kernel; +use Symfony\UX\Leafletjs\LeafletjsBundle; + +/** + * @author Titouan Galopin + * @author Michael Cramer + * + * @internal + */ +class FrameworkAppKernel extends Kernel +{ + use AppKernelTrait; + + public function registerBundles() + { + return [new FrameworkBundle(), new LeafletjsBundle()]; + } + + public function registerContainerConfiguration(LoaderInterface $loader) + { + $loader->load(function (ContainerBuilder $container) { + $container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true]); + }); + } +} diff --git a/src/Leafletjs/Tests/Kernel/TwigAppKernel.php b/src/Leafletjs/Tests/Kernel/TwigAppKernel.php new file mode 100644 index 00000000000..641ec8a4f5d --- /dev/null +++ b/src/Leafletjs/Tests/Kernel/TwigAppKernel.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Leafletjs\Tests\Kernel; + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\TwigBundle\TwigBundle; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Kernel; +use Symfony\UX\Leafletjs\LeafletjsBundle; + +/** + * @author Titouan Galopin + * @author Michael Cramer + * + * @internal + */ +class TwigAppKernel extends Kernel +{ + use AppKernelTrait; + + public function registerBundles() + { + return [new FrameworkBundle(), new TwigBundle(), new LeafletjsBundle()]; + } + + public function registerContainerConfiguration(LoaderInterface $loader) + { + $loader->load(function (ContainerBuilder $container) { + $container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true]); + $container->loadFromExtension('twig', ['default_path' => __DIR__.'/templates', 'strict_variables' => true, 'exception_controller' => null]); + + $container->setAlias('test.leafletjs.builder', 'leafletjs.builder')->setPublic(true); + $container->setAlias('test.leafletjs.twig_extension', 'leafletjs.twig_extension')->setPublic(true); + }); + } +} diff --git a/src/Leafletjs/Tests/LeafletjsBundleTest.php b/src/Leafletjs/Tests/LeafletjsBundleTest.php new file mode 100644 index 00000000000..c3be2d1b4bc --- /dev/null +++ b/src/Leafletjs/Tests/LeafletjsBundleTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Leafletjs\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpKernel\Kernel; +use Symfony\UX\Leafletjs\Tests\Kernel\EmptyAppKernel; +use Symfony\UX\Leafletjs\Tests\Kernel\FrameworkAppKernel; +use Symfony\UX\Leafletjs\Tests\Kernel\TwigAppKernel; + +/** + * @author Titouan Galopin + * @author Michael Cramer + * + * @internal + */ +class LeafletjsBundleTest extends TestCase +{ + public function provideKernels() + { + yield 'empty' => [new EmptyAppKernel('test', true)]; + yield 'framework' => [new FrameworkAppKernel('test', true)]; + yield 'twig' => [new TwigAppKernel('test', true)]; + } + + /** + * @dataProvider provideKernels + */ + public function testBootKernel(Kernel $kernel) + { + $kernel->boot(); + $this->assertArrayHasKey('LeafletjsBundle', $kernel->getBundles()); + } +} diff --git a/src/Leafletjs/Tests/Twig/LeafletExtensionTest.php b/src/Leafletjs/Tests/Twig/LeafletExtensionTest.php new file mode 100644 index 00000000000..bead485a504 --- /dev/null +++ b/src/Leafletjs/Tests/Twig/LeafletExtensionTest.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Leafletjs\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Leafletjs\Builder\LeafletBuilderInterface; +use Symfony\UX\Leafletjs\Tests\Kernel\TwigAppKernel; +use Twig\Environment; + +/** + * @author Titouan Galopin + * @author Michael Cramer + * + * @internal + */ +class LeafletExtensionTest extends TestCase +{ + public function testRenderMap() + { + $kernel = new TwigAppKernel('test', true); + $kernel->boot(); + $container = $kernel->getContainer()->get('test.service_container'); + + /** @var LeafletBuilderInterface $builder */ + $builder = $container->get('test.leafletjs.builder'); + + $map = $builder->createMap(50, 0); + $map->setMapOptions(['minZoom' => 1, 'maxZoom' => 8]); + + $rendered = $container->get('test.leafletjs.twig_extension')->renderMap( + $container->get(Environment::class), + $map, + ['data-controller' => 'mycontroller', 'class' => 'myclass'] + ); + + $this->assertSame( + '
', + $rendered + ); + } +} diff --git a/src/Leafletjs/Twig/LeafletExtension.php b/src/Leafletjs/Twig/LeafletExtension.php new file mode 100644 index 00000000000..989f7c58cd2 --- /dev/null +++ b/src/Leafletjs/Twig/LeafletExtension.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Leafletjs\Twig; + +use Symfony\UX\Leafletjs\Model\Map; +use Twig\Environment; +use Twig\Extension\AbstractExtension; +use Twig\TwigFunction; + +/** + * @author Titouan Galopin + * @author Michael Cramer + * + * @final + * @experimental + */ +class LeafletExtension extends AbstractExtension +{ + public function getFunctions(): array + { + return [ + new TwigFunction('render_map', [$this, 'renderMap'], ['needs_environment' => true, 'is_safe' => ['html']]), + ]; + } + + public function renderMap(Environment $env, Map $map, array $attributes = []): string + { + $map->setAttributes(array_merge($map->getAttributes(), $attributes)); + + $html = ' +
getAttributes() as $name => $value) { + if ('data-controller' === $name) { + continue; + } + + if (true === $value) { + $html .= $name . '="' . $name . '" '; + } elseif (false !== $value) { + $html .= $name . '="' . $value . '" '; + } + } + + return trim($html) . '>
'; + } +} diff --git a/src/Leafletjs/composer.json b/src/Leafletjs/composer.json new file mode 100644 index 00000000000..04061e8341a --- /dev/null +++ b/src/Leafletjs/composer.json @@ -0,0 +1,54 @@ +{ + "name": "symfony/ux-leafletjs", + "type": "symfony-bundle", + "description": "Leaflet integration for Symfony", + "keywords": [ + "symfony-ux" + ], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Michael Cramer", + "email": "michael@bigmichi1.de" + }, + { + "name": "Titouan Galopin", + "email": "galopintitouan@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "autoload": { + "psr-4": { + "Symfony\\UX\\Leafletjs\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "require": { + "php": ">=7.2.5", + "symfony/config": "^4.4.17|^5.0", + "symfony/dependency-injection": "^4.4.17|^5.0", + "symfony/http-kernel": "^4.4.17|^5.0" + }, + "require-dev": { + "symfony/framework-bundle": "^4.4.17|^5.0", + "symfony/phpunit-bridge": "^5.2", + "symfony/twig-bundle": "^4.4.17|^5.0", + "symfony/var-dumper": "^4.4.17|^5.0" + }, + "extra": { + "branch-alias": { + "dev-main": "1.1-dev" + }, + "thanks": { + "name": "symfony/ux", + "url": "https://github.com/symfony/ux" + } + }, + "minimum-stability": "dev" +} diff --git a/src/Leafletjs/phpunit.xml.dist b/src/Leafletjs/phpunit.xml.dist new file mode 100644 index 00000000000..17c07af5582 --- /dev/null +++ b/src/Leafletjs/phpunit.xml.dist @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + Tests + + + + + + . + + ./Tests + ./Resources + ./vendor + + + + From 213fdd30a50c84d6096d681e79bd686726b117f6 Mon Sep 17 00:00:00 2001 From: Michael Cramer Date: Sun, 13 Dec 2020 17:03:16 +0100 Subject: [PATCH 2/2] fix spacing --- src/Leafletjs/Twig/LeafletExtension.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Leafletjs/Twig/LeafletExtension.php b/src/Leafletjs/Twig/LeafletExtension.php index 989f7c58cd2..b3313cf81d0 100644 --- a/src/Leafletjs/Twig/LeafletExtension.php +++ b/src/Leafletjs/Twig/LeafletExtension.php @@ -38,12 +38,12 @@ public function renderMap(Environment $env, Map $map, array $attributes = []): s $html = '
getAttributes() as $name => $value) { @@ -52,12 +52,12 @@ public function renderMap(Environment $env, Map $map, array $attributes = []): s } if (true === $value) { - $html .= $name . '="' . $name . '" '; + $html .= $name.'="'.$name.'" '; } elseif (false !== $value) { - $html .= $name . '="' . $value . '" '; + $html .= $name.'="'.$value.'" '; } } - return trim($html) . '>
'; + return trim($html).'>
'; } }