Skip to content

Commit 71cfda2

Browse files
committed
[Vue] Introduce Vue UX component
1 parent ebe425c commit 71cfda2

26 files changed

+806
-0
lines changed

src/Vue/.gitattributes

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/.gitattributes export-ignore
2+
/.gitignore export-ignore
3+
/.symfony.bundle.yaml export-ignore
4+
/phpunit.xml.dist export-ignore
5+
/Resources/assets/test export-ignore
6+
/Resources/assets/jest.config.js export-ignore
7+
/Tests export-ignore

src/Vue/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
vendor
2+
composer.lock
3+
.php_cs.cache
4+
.phpunit.result.cache

src/Vue/.symfony.bundle.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
branches: ["2.x"]
2+
maintained_branches: ["2.x"]
3+
doc_dir: "Resources/doc"

src/Vue/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# CHANGELOG
2+
3+
## 2.4
4+
5+
- Component added
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Vue\DependencyInjection;
13+
14+
use Symfony\Component\DependencyInjection\ContainerBuilder;
15+
use Symfony\Component\DependencyInjection\Definition;
16+
use Symfony\Component\DependencyInjection\Reference;
17+
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
18+
use Symfony\UX\Vue\Twig\VueComponentExtension;
19+
20+
/**
21+
* @author Titouan Galopin <[email protected]>
22+
* @author Thibault RICHARD <[email protected]>
23+
*
24+
* @internal
25+
*/
26+
class VueExtension extends Extension
27+
{
28+
public function load(array $configs, ContainerBuilder $container)
29+
{
30+
$container
31+
->setDefinition('twig.extension.vue', new Definition(VueComponentExtension::class))
32+
->setArgument(0, new Reference('webpack_encore.twig_stimulus_extension'))
33+
->addTag('twig.extension')
34+
->setPublic(false)
35+
;
36+
}
37+
}

src/Vue/LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2020-2022 Fabien Potencier
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.

src/Vue/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Symfony UX Vue.js
2+
3+
Symfony UX Vue integrates [Vue.js](https://vuejs.org/) into Symfony applications.
4+
It provides tools to render Vue.js components from Twig.
5+
6+
**This repository is a READ-ONLY sub-tree split**. See
7+
https://github.com/symfony/ux to create issues or submit pull requests.
8+
9+
## Resources
10+
11+
- [Documentation](https://symfony.com/bundles/ux-vue/current/index.html)
12+
- [Report issues](https://github.com/symfony/ux/issues) and
13+
[send Pull Requests](https://github.com/symfony/ux/pulls)
14+
in the [main Symfony UX repository](https://github.com/symfony/ux)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
function registerVueControllerComponents(context) {
2+
const vueControllers = {};
3+
const importAllVueComponents = (r) => {
4+
r.keys().forEach((key) => (vueControllers[key] = r(key).default));
5+
};
6+
importAllVueComponents(context);
7+
window.resolveVueComponent = (name) => {
8+
const component = vueControllers[`./${name}.vue`];
9+
if (typeof component === 'undefined') {
10+
throw new Error('Vue controller "' + name + '" does not exist');
11+
}
12+
return component;
13+
};
14+
}
15+
16+
export { registerVueControllerComponents };
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Controller } from '@hotwired/stimulus';
2+
import { createApp } from 'vue';
3+
4+
class default_1 extends Controller {
5+
connect() {
6+
var _a;
7+
this.props = (_a = this.propsValue) !== null && _a !== void 0 ? _a : null;
8+
this._dispatchEvent('vue:connect', { componentName: this.componentValue, props: this.props });
9+
const component = window.resolveVueComponent(this.componentValue);
10+
this.app = createApp(component, this.props);
11+
this.app.mount(this.element);
12+
this._dispatchEvent('vue:mount', { componentName: this.componentValue, component: component, props: this.props });
13+
}
14+
disconnect() {
15+
this.app.unmount();
16+
this._dispatchEvent('vue:unmount', {
17+
componentName: this.componentValue,
18+
props: this.props,
19+
});
20+
}
21+
_dispatchEvent(name, payload) {
22+
this.element.dispatchEvent(new CustomEvent(name, { detail: payload, bubbles: true }));
23+
}
24+
}
25+
default_1.values = {
26+
component: String,
27+
props: Object,
28+
};
29+
30+
export { default_1 as default };
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const { defaults } = require('jest-config');
2+
const jestConfig = require('../../../../jest.config.js');
3+
4+
jestConfig.moduleFileExtensions = [...defaults.moduleFileExtensions, 'vue'];
5+
jestConfig.transform['^.+\\.vue$'] = ['@vue/vue3-jest'];
6+
7+
module.exports = jestConfig;

src/Vue/Resources/assets/package.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "@symfony/ux-vue",
3+
"description": "Integration of Vue.js in Symfony",
4+
"license": "MIT",
5+
"version": "1.0.0",
6+
"main": "dist/register_controller.js",
7+
"symfony": {
8+
"controllers": {
9+
"vue": {
10+
"main": "dist/render_controller.js",
11+
"webpackMode": "eager",
12+
"fetch": "eager",
13+
"enabled": true
14+
}
15+
}
16+
},
17+
"peerDependencies": {
18+
"@hotwired/stimulus": "^3.0.0",
19+
"vue": "^3.0"
20+
},
21+
"devDependencies": {
22+
"@hotwired/stimulus": "^3.0.0",
23+
"@types/webpack-env": "^1.16",
24+
"@vue/vue3-jest": "27",
25+
"vue": "^3.0"
26+
},
27+
"dependencies": {}
28+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* This file is part of the Symfony package.
3+
*
4+
* (c) Fabien Potencier <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
'use strict';
11+
12+
export function registerVueControllerComponents(context: __WebpackModuleApi.RequireContext) {
13+
const vueControllers: { [key: string]: object } = {};
14+
15+
const importAllVueComponents = (r: __WebpackModuleApi.RequireContext) => {
16+
r.keys().forEach((key) => (vueControllers[key] = r(key).default));
17+
};
18+
19+
importAllVueComponents(context);
20+
21+
// Expose a global Vue loader to allow rendering from the Stimulus controller
22+
(window as any).resolveVueComponent = (name: string): object => {
23+
const component = vueControllers[`./${name}.vue`];
24+
if (typeof component === 'undefined') {
25+
throw new Error('Vue controller "' + name + '" does not exist');
26+
}
27+
28+
return component;
29+
};
30+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* This file is part of the Symfony package.
3+
*
4+
* (c) Fabien Potencier <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
'use strict';
11+
12+
import { Controller } from '@hotwired/stimulus';
13+
import { App, Component, createApp } from 'vue';
14+
15+
export default class extends Controller {
16+
private props: Record<string, unknown> | null;
17+
private app: App<Element>;
18+
readonly componentValue: string;
19+
20+
readonly propsValue: Record<string, unknown> | null | undefined;
21+
static values = {
22+
component: String,
23+
props: Object,
24+
};
25+
26+
connect() {
27+
this.props = this.propsValue ?? null;
28+
29+
this._dispatchEvent('vue:connect', { componentName: this.componentValue, props: this.props });
30+
31+
const component: Component = window.resolveVueComponent(this.componentValue);
32+
33+
this.app = createApp(component, this.props);
34+
35+
if (this.element.__vue_app__ !== undefined) {
36+
this.element.__vue_app__.unmount();
37+
}
38+
39+
this.app.mount(this.element);
40+
41+
this._dispatchEvent('vue:mount', {
42+
componentName: this.componentValue,
43+
component: component,
44+
props: this.props,
45+
});
46+
}
47+
48+
disconnect() {
49+
this.app.unmount();
50+
51+
this._dispatchEvent('vue:unmount', {
52+
componentName: this.componentValue,
53+
props: this.props,
54+
});
55+
}
56+
57+
_dispatchEvent(name: string, payload: any) {
58+
this.element.dispatchEvent(new CustomEvent(name, { detail: payload, bubbles: true }));
59+
}
60+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
<h1>Hello {{ name }}</h1>
3+
</template>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* This file is part of the Symfony package.
3+
*
4+
* (c) Fabien Potencier <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
'use strict';
11+
12+
import {registerVueControllerComponents} from '../src/register_controller';
13+
import {createRequireContextPolyfill} from './util/require_context_poylfill';
14+
import Hello from './fixtures/Hello.vue'
15+
16+
require.context = createRequireContextPolyfill(__dirname);
17+
18+
describe('registerVueControllerComponents', () => {
19+
it('test', () => {
20+
registerVueControllerComponents(require.context('./fixtures', true, /\.vue$/));
21+
const resolveComponent = (window as any).resolveVueComponent;
22+
23+
expect(resolveComponent).not.toBeUndefined();
24+
expect(resolveComponent('Hello')).toBe(Hello);
25+
});
26+
});
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* This file is part of the Symfony package.
3+
*
4+
* (c) Fabien Potencier <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
'use strict';
11+
12+
import { Application, Controller } from '@hotwired/stimulus';
13+
import { getByTestId, waitFor } from '@testing-library/dom';
14+
import { clearDOM, mountDOM } from '@symfony/stimulus-testing';
15+
import VueController from '../src/render_controller';
16+
17+
// Controller used to check the actual controller was properly booted
18+
class CheckController extends Controller {
19+
connect() {
20+
this.element.addEventListener('vue:connect', () => {
21+
this.element.classList.add('connected');
22+
});
23+
24+
this.element.addEventListener('vue:mount', () => {
25+
this.element.classList.add('mounted');
26+
});
27+
}
28+
}
29+
30+
const startStimulus = () => {
31+
const application = Application.start();
32+
application.register('check', CheckController);
33+
application.register('vue', VueController);
34+
};
35+
36+
const Hello = {
37+
template: "<h1>Hello {{ name ?? 'world' }}</h1>",
38+
props: ['name']
39+
};
40+
41+
(window as any).resolveVueComponent = () => {
42+
return Hello;
43+
};
44+
45+
describe('VueController', () => {
46+
it('connect with props', async () => {
47+
const container = mountDOM(`
48+
<div data-testid="component"
49+
data-controller="check vue"
50+
data-vue-component-value="Hello"
51+
data-vue-props-value="{&quot;name&quot;: &quot;Thibault Richard&quot;}" />
52+
`);
53+
54+
const component = getByTestId(container, 'component');
55+
expect(component).not.toHaveClass('connected');
56+
expect(component).not.toHaveClass('mounted');
57+
58+
startStimulus();
59+
await waitFor(() => expect(component).toHaveClass('connected'));
60+
await waitFor(() => expect(component).toHaveClass('mounted'));
61+
await waitFor(() => expect(component.innerHTML).toEqual('<h1>Hello Thibault Richard</h1>'));
62+
63+
clearDOM();
64+
});
65+
66+
it('connect without props', async () => {
67+
const container = mountDOM(`
68+
<div data-testid="component" id="container-2"
69+
data-controller="check vue"
70+
data-vue-component-value="Hello" />
71+
`);
72+
73+
const component = getByTestId(container, 'component');
74+
expect(component).not.toHaveClass('connected');
75+
expect(component).not.toHaveClass('mounted');
76+
77+
startStimulus();
78+
await waitFor(() => expect(component).toHaveClass('connected'));
79+
await waitFor(() => expect(component).toHaveClass('mounted'));
80+
await waitFor(() => expect(component.innerHTML).toEqual('<h1>Hello world</h1>'));
81+
82+
clearDOM();
83+
});
84+
});

0 commit comments

Comments
 (0)