diff --git a/src/Vue/.gitattributes b/src/Vue/.gitattributes new file mode 100644 index 00000000000..a50917e5965 --- /dev/null +++ b/src/Vue/.gitattributes @@ -0,0 +1,7 @@ +/.gitattributes export-ignore +/.gitignore export-ignore +/.symfony.bundle.yaml export-ignore +/phpunit.xml.dist export-ignore +/Resources/assets/test export-ignore +/Resources/assets/jest.config.js export-ignore +/Tests export-ignore diff --git a/src/Vue/.gitignore b/src/Vue/.gitignore new file mode 100644 index 00000000000..30282084317 --- /dev/null +++ b/src/Vue/.gitignore @@ -0,0 +1,4 @@ +vendor +composer.lock +.php_cs.cache +.phpunit.result.cache diff --git a/src/Vue/.symfony.bundle.yaml b/src/Vue/.symfony.bundle.yaml new file mode 100644 index 00000000000..50b8d4a3040 --- /dev/null +++ b/src/Vue/.symfony.bundle.yaml @@ -0,0 +1,3 @@ +branches: ["2.x"] +maintained_branches: ["2.x"] +doc_dir: "Resources/doc" diff --git a/src/Vue/CHANGELOG.md b/src/Vue/CHANGELOG.md new file mode 100644 index 00000000000..9a6a98814bc --- /dev/null +++ b/src/Vue/CHANGELOG.md @@ -0,0 +1,5 @@ +# CHANGELOG + +## 2.4 + +- Component added diff --git a/src/Vue/DependencyInjection/VueExtension.php b/src/Vue/DependencyInjection/VueExtension.php new file mode 100644 index 00000000000..8465fe3f39b --- /dev/null +++ b/src/Vue/DependencyInjection/VueExtension.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Vue\DependencyInjection; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Symfony\UX\Vue\Twig\VueComponentExtension; + +/** + * @author Titouan Galopin + * @author Thibault RICHARD + * + * @internal + */ +class VueExtension extends Extension +{ + public function load(array $configs, ContainerBuilder $container) + { + $container + ->setDefinition('twig.extension.vue', new Definition(VueComponentExtension::class)) + ->setArgument(0, new Reference('webpack_encore.twig_stimulus_extension')) + ->addTag('twig.extension') + ->setPublic(false) + ; + } +} diff --git a/src/Vue/LICENSE b/src/Vue/LICENSE new file mode 100644 index 00000000000..406242ff285 --- /dev/null +++ b/src/Vue/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020-2022 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Vue/README.md b/src/Vue/README.md new file mode 100644 index 00000000000..048bc7de90c --- /dev/null +++ b/src/Vue/README.md @@ -0,0 +1,14 @@ +# Symfony UX Vue.js + +Symfony UX Vue integrates [Vue.js](https://vuejs.org/) into Symfony applications. +It provides tools to render Vue.js v3 components from Twig. + +**This repository is a READ-ONLY sub-tree split**. See +https://github.com/symfony/ux to create issues or submit pull requests. + +## Resources + +- [Documentation](https://symfony.com/bundles/ux-vue/current/index.html) +- [Report issues](https://github.com/symfony/ux/issues) and + [send Pull Requests](https://github.com/symfony/ux/pulls) + in the [main Symfony UX repository](https://github.com/symfony/ux) diff --git a/src/Vue/Resources/assets/dist/register_controller.js b/src/Vue/Resources/assets/dist/register_controller.js new file mode 100644 index 00000000000..0cd5f01d96a --- /dev/null +++ b/src/Vue/Resources/assets/dist/register_controller.js @@ -0,0 +1,16 @@ +function registerVueControllerComponents(context) { + const vueControllers = {}; + const importAllVueComponents = (r) => { + r.keys().forEach((key) => (vueControllers[key] = r(key).default)); + }; + importAllVueComponents(context); + window.resolveVueComponent = (name) => { + const component = vueControllers[`./${name}.vue`]; + if (typeof component === 'undefined') { + throw new Error('Vue controller "' + name + '" does not exist'); + } + return component; + }; +} + +export { registerVueControllerComponents }; diff --git a/src/Vue/Resources/assets/dist/render_controller.js b/src/Vue/Resources/assets/dist/render_controller.js new file mode 100644 index 00000000000..23de1abe7b5 --- /dev/null +++ b/src/Vue/Resources/assets/dist/render_controller.js @@ -0,0 +1,30 @@ +import { Controller } from '@hotwired/stimulus'; +import { createApp } from 'vue'; + +class default_1 extends Controller { + connect() { + var _a; + this.props = (_a = this.propsValue) !== null && _a !== void 0 ? _a : null; + this._dispatchEvent('vue:connect', { componentName: this.componentValue, props: this.props }); + const component = window.resolveVueComponent(this.componentValue); + this.app = createApp(component, this.props); + this.app.mount(this.element); + this._dispatchEvent('vue:mount', { componentName: this.componentValue, component: component, props: this.props }); + } + disconnect() { + this.app.unmount(); + this._dispatchEvent('vue:unmount', { + componentName: this.componentValue, + props: this.props, + }); + } + _dispatchEvent(name, payload) { + this.element.dispatchEvent(new CustomEvent(name, { detail: payload, bubbles: true })); + } +} +default_1.values = { + component: String, + props: Object, +}; + +export { default_1 as default }; diff --git a/src/Vue/Resources/assets/jest.config.js b/src/Vue/Resources/assets/jest.config.js new file mode 100644 index 00000000000..80f6d7c29b9 --- /dev/null +++ b/src/Vue/Resources/assets/jest.config.js @@ -0,0 +1,7 @@ +const { defaults } = require('jest-config'); +const jestConfig = require('../../../../jest.config.js'); + +jestConfig.moduleFileExtensions = [...defaults.moduleFileExtensions, 'vue']; +jestConfig.transform['^.+\\.vue$'] = ['@vue/vue3-jest']; + +module.exports = jestConfig; diff --git a/src/Vue/Resources/assets/package.json b/src/Vue/Resources/assets/package.json new file mode 100644 index 00000000000..85b33d8dfdb --- /dev/null +++ b/src/Vue/Resources/assets/package.json @@ -0,0 +1,28 @@ +{ + "name": "@symfony/ux-vue", + "description": "Integration of Vue.js in Symfony", + "license": "MIT", + "version": "1.0.0", + "main": "dist/register_controller.js", + "symfony": { + "controllers": { + "vue": { + "main": "dist/render_controller.js", + "webpackMode": "eager", + "fetch": "eager", + "enabled": true + } + } + }, + "peerDependencies": { + "@hotwired/stimulus": "^3.0.0", + "vue": "^3.0" + }, + "devDependencies": { + "@hotwired/stimulus": "^3.0.0", + "@types/webpack-env": "^1.16", + "@vue/vue3-jest": "^27.0.0", + "ts-jest": "^27.1.5", + "vue": "^3.0" + } +} diff --git a/src/Vue/Resources/assets/src/register_controller.ts b/src/Vue/Resources/assets/src/register_controller.ts new file mode 100644 index 00000000000..568ded7aca6 --- /dev/null +++ b/src/Vue/Resources/assets/src/register_controller.ts @@ -0,0 +1,30 @@ +/* + * 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'; + +export function registerVueControllerComponents(context: __WebpackModuleApi.RequireContext) { + const vueControllers: { [key: string]: object } = {}; + + const importAllVueComponents = (r: __WebpackModuleApi.RequireContext) => { + r.keys().forEach((key) => (vueControllers[key] = r(key).default)); + }; + + importAllVueComponents(context); + + // Expose a global Vue loader to allow rendering from the Stimulus controller + (window as any).resolveVueComponent = (name: string): object => { + const component = vueControllers[`./${name}.vue`]; + if (typeof component === 'undefined') { + throw new Error(`Vue controller "${name}" does not exist`); + } + + return component; + }; +} diff --git a/src/Vue/Resources/assets/src/render_controller.ts b/src/Vue/Resources/assets/src/render_controller.ts new file mode 100644 index 00000000000..f78e10df855 --- /dev/null +++ b/src/Vue/Resources/assets/src/render_controller.ts @@ -0,0 +1,60 @@ +/* + * 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 '@hotwired/stimulus'; +import { App, Component, createApp } from 'vue'; + +export default class extends Controller { + private props: Record | null; + private app: App; + readonly componentValue: string; + + readonly propsValue: Record | null | undefined; + static values = { + component: String, + props: Object, + }; + + connect() { + this.props = this.propsValue ?? null; + + this._dispatchEvent('vue:connect', { componentName: this.componentValue, props: this.props }); + + const component: Component = window.resolveVueComponent(this.componentValue); + + this.app = createApp(component, this.props); + + if (this.element.__vue_app__ !== undefined) { + this.element.__vue_app__.unmount(); + } + + this.app.mount(this.element); + + this._dispatchEvent('vue:mount', { + componentName: this.componentValue, + component: component, + props: this.props, + }); + } + + disconnect() { + this.app.unmount(); + + this._dispatchEvent('vue:unmount', { + componentName: this.componentValue, + props: this.props, + }); + } + + _dispatchEvent(name: string, payload: any) { + this.element.dispatchEvent(new CustomEvent(name, { detail: payload, bubbles: true })); + } +} diff --git a/src/Vue/Resources/assets/test/fixtures/Hello.vue b/src/Vue/Resources/assets/test/fixtures/Hello.vue new file mode 100644 index 00000000000..77301461ae9 --- /dev/null +++ b/src/Vue/Resources/assets/test/fixtures/Hello.vue @@ -0,0 +1,3 @@ + diff --git a/src/Vue/Resources/assets/test/register_controller.test.ts b/src/Vue/Resources/assets/test/register_controller.test.ts new file mode 100644 index 00000000000..4c7501a3502 --- /dev/null +++ b/src/Vue/Resources/assets/test/register_controller.test.ts @@ -0,0 +1,26 @@ +/* + * 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 {registerVueControllerComponents} from '../src/register_controller'; +import {createRequireContextPolyfill} from './util/require_context_poylfill'; +import Hello from './fixtures/Hello.vue' + +require.context = createRequireContextPolyfill(__dirname); + +describe('registerVueControllerComponents', () => { + it('test', () => { + registerVueControllerComponents(require.context('./fixtures', true, /\.vue$/)); + const resolveComponent = (window as any).resolveVueComponent; + + expect(resolveComponent).not.toBeUndefined(); + expect(resolveComponent('Hello')).toBe(Hello); + }); +}); diff --git a/src/Vue/Resources/assets/test/render_controller.test.ts b/src/Vue/Resources/assets/test/render_controller.test.ts new file mode 100644 index 00000000000..03a401369a9 --- /dev/null +++ b/src/Vue/Resources/assets/test/render_controller.test.ts @@ -0,0 +1,84 @@ +/* + * 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 '@hotwired/stimulus'; +import { getByTestId, waitFor } from '@testing-library/dom'; +import { clearDOM, mountDOM } from '@symfony/stimulus-testing'; +import VueController from '../src/render_controller'; + +// Controller used to check the actual controller was properly booted +class CheckController extends Controller { + connect() { + this.element.addEventListener('vue:connect', () => { + this.element.classList.add('connected'); + }); + + this.element.addEventListener('vue:mount', () => { + this.element.classList.add('mounted'); + }); + } +} + +const startStimulus = () => { + const application = Application.start(); + application.register('check', CheckController); + application.register('vue', VueController); +}; + +const Hello = { + template: "

Hello {{ name ?? 'world' }}

", + props: ['name'] +}; + +(window as any).resolveVueComponent = () => { + return Hello; +}; + +describe('VueController', () => { + it('connect with props', async () => { + const container = mountDOM(` +
+ `); + + const component = getByTestId(container, 'component'); + expect(component).not.toHaveClass('connected'); + expect(component).not.toHaveClass('mounted'); + + startStimulus(); + await waitFor(() => expect(component).toHaveClass('connected')); + await waitFor(() => expect(component).toHaveClass('mounted')); + await waitFor(() => expect(component.innerHTML).toEqual('

Hello Thibault Richard

')); + + clearDOM(); + }); + + it('connect without props', async () => { + const container = mountDOM(` +
+ `); + + const component = getByTestId(container, 'component'); + expect(component).not.toHaveClass('connected'); + expect(component).not.toHaveClass('mounted'); + + startStimulus(); + await waitFor(() => expect(component).toHaveClass('connected')); + await waitFor(() => expect(component).toHaveClass('mounted')); + await waitFor(() => expect(component.innerHTML).toEqual('

Hello world

')); + + clearDOM(); + }); +}); diff --git a/src/Vue/Resources/assets/test/util/require_context_poylfill.ts b/src/Vue/Resources/assets/test/util/require_context_poylfill.ts new file mode 100644 index 00000000000..376746d8f08 --- /dev/null +++ b/src/Vue/Resources/assets/test/util/require_context_poylfill.ts @@ -0,0 +1,39 @@ +import fs from 'fs'; +import path from 'path'; + +export function createRequireContextPolyfill (rootDir: string) { + return (base: string, deep: boolean, filter: RegExp): __WebpackModuleApi.RequireContext => { + const basePrefix = path.resolve(rootDir, base); + const files: { [key: string]: boolean } = {}; + + function readDirectory(directory: string) { + fs.readdirSync(directory).forEach((file: string) => { + const fullPath = path.resolve(directory, file); + + if (fs.statSync(fullPath).isDirectory()) { + if (deep) { + readDirectory(fullPath); + } + + return; + } + + if (!filter.test(fullPath)) { + return; + } + + files[fullPath.replace(basePrefix, '.')] = true; + }); + } + + readDirectory(path.resolve(rootDir, base)); + + function Module(file: string) { + return require(basePrefix + '/' + file); + } + + Module.keys = () => Object.keys(files); + + return (Module as __WebpackModuleApi.RequireContext); + }; +} diff --git a/src/Vue/Resources/doc/index.rst b/src/Vue/Resources/doc/index.rst new file mode 100644 index 00000000000..c1072048e5f --- /dev/null +++ b/src/Vue/Resources/doc/index.rst @@ -0,0 +1,101 @@ +Symfony UX Vue.js +================= + +Symfony UX Vue.js is a Symfony bundle integrating `Vue.js`_ in +Symfony applications. It is part of `the Symfony UX initiative`_. + +Vue.js is a JavaScript framework for building user interfaces. +Symfony UX Vue.js provides tools to render Vue components from Twig, +handling rendering and data transfers. + +Symfony UX Vue.js supports Vue.js v3 only. + +Installation +------------ + +Before you start, make sure you have `Symfony UX configured in your app`_. + +Then install the bundle using Composer and Symfony Flex: + +.. code-block:: terminal + + $ composer require symfony/ux-vue + + # Don't forget to install the JavaScript dependencies as well and compile + $ npm install --force + $ npm run watch + + # or use yarn + $ yarn install --force + $ yarn watch + +You also need to add the following lines at the end to your ``assets/app.js`` file: + +.. code-block:: javascript + + // assets/app.js + import { registerVueControllerComponents } from '@symfony/ux-vue'; + + // Registers Vue.js controller components to allow loading them from Twig + // + // Vue.js controller components are components that are meant to be rendered + // from Twig. These component can then rely on other components that won't be + // called directly from Twig. + // + // By putting only controller components in `vue/controllers`, you ensure that + // internal components won't be automatically included in your JS built file if + // they are not necessary. + registerVueControllerComponents(require.context('./vue/controllers', true, /\.vue$/)); + + +Usage +----- + +UX Vue.js works by using a system of **Vue.js controller components**: Vue.js components that +are registered using ``registerVueControllerComponents`` and that are meant to be rendered +from Twig. + +When using the ``registerVueControllerComponents`` configuration shown previously, all +Vue.js components located in the directory ``assets/vue/controllers`` are registered as +Vue.js controller components. + +To make sure those components can be loaded by Webpack Encore, you need to configure +it by following the instructions in `the related section of the documentation`_. + +You can then render any Vue.js controller component in Twig using the ``vue_component``. +For example: + +.. code-block:: javascript + + // assets/vue/controllers/MyComponent.vue + + + + +.. code-block:: twig + + {# templates/home.html.twig #} + +
+ +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 + +However it is currently considered `experimental`_, +meaning it is not bound to Symfony's BC policy for the moment. + +.. _`Vue.js`: https://vuejs.org/ +.. _`the Symfony UX initiative`: https://symfony.com/ux +.. _ `the related section of the documentation`: https://symfony.com/doc/current/frontend/encore/vuejs.html +.. _`experimental`: https://symfony.com/doc/current/contributing/code/experimental.html +.. _`Symfony UX configured in your app`: https://symfony.com/doc/current/frontend/ux.html diff --git a/src/Vue/Tests/Kernel/AppKernelTrait.php b/src/Vue/Tests/Kernel/AppKernelTrait.php new file mode 100644 index 00000000000..e901df1bfad --- /dev/null +++ b/src/Vue/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\Vue\Tests\Kernel; + +/** + * @author Titouan Galopin + * @author Thibault RICHARD + * + * @internal + */ +trait AppKernelTrait +{ + public function getCacheDir(): string + { + return $this->createTmpDir('cache'); + } + + public function getLogDir(): string + { + return $this->createTmpDir('logs'); + } + + private function createTmpDir(string $type): string + { + $dir = sys_get_temp_dir().'/vue_bundle/'.uniqid($type.'_', true); + + if (!file_exists($dir)) { + mkdir($dir, 0777, true); + } + + return $dir; + } +} diff --git a/src/Vue/Tests/Kernel/FrameworkAppKernel.php b/src/Vue/Tests/Kernel/FrameworkAppKernel.php new file mode 100644 index 00000000000..99a280ac95c --- /dev/null +++ b/src/Vue/Tests/Kernel/FrameworkAppKernel.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\Vue\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\Vue\VueBundle; +use Symfony\WebpackEncoreBundle\WebpackEncoreBundle; + +/** + * @author Titouan Galopin + * @author Thibault RICHARD + * + * @internal + */ +class FrameworkAppKernel extends Kernel +{ + use AppKernelTrait; + + public function registerBundles(): iterable + { + return [new WebpackEncoreBundle(), new FrameworkBundle(), new VueBundle()]; + } + + public function registerContainerConfiguration(LoaderInterface $loader) + { + $loader->load(function (ContainerBuilder $container) { + $container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true]); + $container->loadFromExtension('webpack_encore', ['output_path' => '%kernel.project_dir%/public/build']); + }); + } +} diff --git a/src/Vue/Tests/Kernel/TwigAppKernel.php b/src/Vue/Tests/Kernel/TwigAppKernel.php new file mode 100644 index 00000000000..0e805f0ece4 --- /dev/null +++ b/src/Vue/Tests/Kernel/TwigAppKernel.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Vue\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\Vue\VueBundle; +use Symfony\WebpackEncoreBundle\WebpackEncoreBundle; + +/** + * @author Titouan Galopin + * @author Thibault RICHARD + * + * @internal + */ +class TwigAppKernel extends Kernel +{ + use AppKernelTrait; + + public function registerBundles(): iterable + { + return [new WebpackEncoreBundle(), new FrameworkBundle(), new TwigBundle(), new VueBundle()]; + } + + public function registerContainerConfiguration(LoaderInterface $loader) + { + $loader->load(function (ContainerBuilder $container) { + $container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true]); + $container->loadFromExtension('webpack_encore', ['output_path' => '%kernel.project_dir%/public/build']); + $container->loadFromExtension('twig', ['default_path' => __DIR__.'/templates', 'strict_variables' => true, 'exception_controller' => null]); + + $container->setAlias('test.twig', 'twig')->setPublic(true); + $container->setAlias('test.twig.extension.vue', 'twig.extension.vue')->setPublic(true); + }); + } +} diff --git a/src/Vue/Tests/Twig/VueComponentExtensionTest.php b/src/Vue/Tests/Twig/VueComponentExtensionTest.php new file mode 100644 index 00000000000..1bbba1b89eb --- /dev/null +++ b/src/Vue/Tests/Twig/VueComponentExtensionTest.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Vue\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Vue\Tests\Kernel\TwigAppKernel; +use Symfony\UX\Vue\Twig\VueComponentExtension; + +/** + * @author Titouan Galopin + * @author Thibault RICHARD + * + * @internal + */ +class VueComponentExtensionTest extends TestCase +{ + public function testRenderComponent() + { + $kernel = new TwigAppKernel('test', true); + $kernel->boot(); + + /** @var VueComponentExtension $extension */ + $extension = $kernel->getContainer()->get('test.twig.extension.vue'); + + $rendered = $extension->renderVueComponent( + $kernel->getContainer()->get('test.twig'), + 'SubDir/MyComponent', + ['fullName' => 'Titouan Galopin'] + ); + + $this->assertSame( + 'data-controller="symfony--ux-vue--vue" data-symfony--ux-vue--vue-component-value="SubDir/MyComponent" data-symfony--ux-vue--vue-props-value="{"fullName":"Titouan Galopin"}"', + $rendered + ); + } + + public function testRenderComponentWithoutProps() + { + $kernel = new TwigAppKernel('test', true); + $kernel->boot(); + + /** @var VueComponentExtension $extension */ + $extension = $kernel->getContainer()->get('test.twig.extension.vue'); + + $rendered = $extension->renderVueComponent($kernel->getContainer()->get('test.twig'), 'SubDir/MyComponent'); + + $this->assertSame( + 'data-controller="symfony--ux-vue--vue" data-symfony--ux-vue--vue-component-value="SubDir/MyComponent"', + $rendered + ); + } +} diff --git a/src/Vue/Tests/VueBundleTest.php b/src/Vue/Tests/VueBundleTest.php new file mode 100644 index 00000000000..9e36d101708 --- /dev/null +++ b/src/Vue/Tests/VueBundleTest.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\Vue\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpKernel\Kernel; +use Symfony\UX\Vue\Tests\Kernel\FrameworkAppKernel; +use Symfony\UX\Vue\Tests\Kernel\TwigAppKernel; + +/** + * @author Titouan Galopin + * @author Thibault RICHARD + * + * @internal + */ +class VueBundleTest extends TestCase +{ + public function provideKernels() + { + yield 'framework' => [new FrameworkAppKernel('test', true)]; + yield 'twig' => [new TwigAppKernel('test', true)]; + } + + /** + * @dataProvider provideKernels + */ + public function testBootKernel(Kernel $kernel) + { + $kernel->boot(); + $this->assertArrayHasKey('VueBundle', $kernel->getBundles()); + } +} diff --git a/src/Vue/Twig/VueComponentExtension.php b/src/Vue/Twig/VueComponentExtension.php new file mode 100644 index 00000000000..32a27412ca9 --- /dev/null +++ b/src/Vue/Twig/VueComponentExtension.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Vue\Twig; + +use Symfony\WebpackEncoreBundle\Twig\StimulusTwigExtension; +use Twig\Environment; +use Twig\Extension\AbstractExtension; +use Twig\TwigFunction; + +/** + * @author Titouan Galopin + * @author Thibault RICHARD + * + * @final + * @experimental + */ +class VueComponentExtension extends AbstractExtension +{ + private $stimulusExtension; + + public function __construct(StimulusTwigExtension $stimulusExtension) + { + $this->stimulusExtension = $stimulusExtension; + } + + public function getFunctions(): array + { + return [ + new TwigFunction('vue_component', [$this, 'renderVueComponent'], ['needs_environment' => true, 'is_safe' => ['html_attr']]), + ]; + } + + public function renderVueComponent(Environment $env, string $componentName, array $props = []): string + { + $params = ['component' => $componentName]; + if ($props) { + $params['props'] = $props; + } + + return $this->stimulusExtension->renderStimulusController($env, '@symfony/ux-vue/vue', $params); + } +} diff --git a/src/Vue/VueBundle.php b/src/Vue/VueBundle.php new file mode 100644 index 00000000000..1d4525139a8 --- /dev/null +++ b/src/Vue/VueBundle.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\Vue; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +/** + * @author Titouan Galopin + * @author Thibault RICHARD + * + * @final + * @experimental + */ +class VueBundle extends Bundle +{ +} diff --git a/src/Vue/composer.json b/src/Vue/composer.json new file mode 100644 index 00000000000..9c94849d88b --- /dev/null +++ b/src/Vue/composer.json @@ -0,0 +1,48 @@ +{ + "name": "symfony/ux-vue", + "type": "symfony-bundle", + "description": "Integration of Vue.js in Symfony", + "keywords": [ + "symfony-ux" + ], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Titouan Galopin", + "email": "galopintitouan@gmail.com" + }, + { + "name": "Thibault Richard", + "email": "thibault.richard62@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "autoload": { + "psr-4": { + "Symfony\\UX\\Vue\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "require": { + "symfony/webpack-encore-bundle": "^1.11" + }, + "require-dev": { + "symfony/framework-bundle": "^4.4|^5.0|^6.0", + "symfony/phpunit-bridge": "^5.2|^6.0", + "symfony/twig-bundle": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" + }, + "extra": { + "thanks": { + "name": "symfony/ux", + "url": "https://github.com/symfony/ux" + } + }, + "minimum-stability": "dev" +} diff --git a/src/Vue/phpunit.xml.dist b/src/Vue/phpunit.xml.dist new file mode 100644 index 00000000000..17c07af5582 --- /dev/null +++ b/src/Vue/phpunit.xml.dist @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + Tests + + + + + + . + + ./Tests + ./Resources + ./vendor + + + +