Skip to content

Commit 7bf195c

Browse files
committed
[WIP] LiveUrl
1 parent e653f48 commit 7bf195c

File tree

7 files changed

+171
-0
lines changed

7 files changed

+171
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export default class {
22
response: Response;
33
private body;
4+
private liveUrl;
45
constructor(response: Response);
56
getBody(): Promise<string>;
7+
getLiveUrl(): Promise<string | null>;
68
}

Diff for: src/LiveComponent/assets/dist/live_controller.js

+11
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class RequestBuilder {
3333
fetchOptions.headers = {
3434
Accept: 'application/vnd.live-component+html',
3535
'X-Requested-With': 'XMLHttpRequest',
36+
'X-Live-Url': window.location.href
3637
};
3738
const totalFiles = Object.entries(files).reduce((total, current) => total + current.length, 0);
3839
const hasFingerprints = Object.keys(children).length > 0;
@@ -111,6 +112,12 @@ class BackendResponse {
111112
}
112113
return this.body;
113114
}
115+
async getLiveUrl() {
116+
if (undefined === this.liveUrl) {
117+
this.liveUrl = await this.response.headers.get('X-Live-Url');
118+
}
119+
return this.liveUrl;
120+
}
114121
}
115122

116123
function getElementAsTagText(element) {
@@ -2137,6 +2144,10 @@ class Component {
21372144
return response;
21382145
}
21392146
this.processRerender(html, backendResponse);
2147+
const liveUrl = await backendResponse.getLiveUrl();
2148+
if (liveUrl) {
2149+
HistoryStrategy.replace(new UrlUtils(liveUrl));
2150+
}
21402151
this.backendRequest = null;
21412152
thisPromiseResolve(backendResponse);
21422153
if (this.isRequestPending) {

Diff for: src/LiveComponent/assets/src/Backend/BackendResponse.ts

+9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export default class {
22
response: Response;
33
private body: string;
4+
private liveUrl: string | null;
45

56
constructor(response: Response) {
67
this.response = response;
@@ -13,4 +14,12 @@ export default class {
1314

1415
return this.body;
1516
}
17+
18+
async getLiveUrl(): Promise<string | null> {
19+
if (undefined === this.liveUrl) {
20+
this.liveUrl = await this.response.headers.get('X-Live-Url');
21+
}
22+
23+
return this.liveUrl;
24+
}
1625
}

Diff for: src/LiveComponent/assets/src/Backend/RequestBuilder.ts

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export default class {
2626
fetchOptions.headers = {
2727
Accept: 'application/vnd.live-component+html',
2828
'X-Requested-With': 'XMLHttpRequest',
29+
'X-Live-Url' : window.location.href
2930
};
3031

3132
const totalFiles = Object.entries(files).reduce((total, current) => total + current.length, 0);

Diff for: src/LiveComponent/assets/src/Component/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { ElementDriver } from './ElementDriver';
1111
import UnsyncedInputsTracker from './UnsyncedInputsTracker';
1212
import ValueStore from './ValueStore';
1313
import type { PluginInterface } from './plugins/PluginInterface';
14+
import {HistoryStrategy, UrlUtils} from "../url_utils";
1415

1516
declare const Turbo: any;
1617

@@ -328,6 +329,10 @@ export default class Component {
328329
}
329330

330331
this.processRerender(html, backendResponse);
332+
const liveUrl = await backendResponse.getLiveUrl();
333+
if (liveUrl) {
334+
HistoryStrategy.replace(new UrlUtils(liveUrl));
335+
}
331336

332337
// finally resolve this promise
333338
this.backendRequest = null;

Diff for: src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php

+9
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
use Symfony\UX\LiveComponent\EventListener\DeferLiveComponentSubscriber;
3434
use Symfony\UX\LiveComponent\EventListener\InterceptChildComponentRenderSubscriber;
3535
use Symfony\UX\LiveComponent\EventListener\LiveComponentSubscriber;
36+
use Symfony\UX\LiveComponent\EventListener\LiveUrlSubscriber;
3637
use Symfony\UX\LiveComponent\EventListener\QueryStringInitializeSubscriber;
3738
use Symfony\UX\LiveComponent\EventListener\ResetDeterministicIdSubscriber;
3839
use Symfony\UX\LiveComponent\Form\Type\LiveCollectionType;
@@ -135,6 +136,14 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
135136
->addTag('container.service_subscriber', ['key' => LiveComponentMetadataFactory::class, 'id' => 'ux.live_component.metadata_factory'])
136137
;
137138

139+
$container->register('ux.live_component.live_url_subscriber', LiveUrlSubscriber::class)
140+
->setArguments([
141+
new Reference('router'),
142+
new Reference('ux.live_component.metadata_factory'),
143+
])
144+
->addTag('kernel.event_subscriber')
145+
;
146+
138147
$container->register('ux.live_component.live_responder', LiveResponder::class);
139148
$container->setAlias(LiveResponder::class, 'ux.live_component.live_responder');
140149

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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\LiveComponent\EventListener;
13+
14+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpKernel\Event\ResponseEvent;
17+
use Symfony\Component\HttpKernel\KernelEvents;
18+
use Symfony\Component\Routing\RouterInterface;
19+
use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory;
20+
21+
class LiveUrlSubscriber implements EventSubscriberInterface
22+
{
23+
private const string URL_HEADER = 'X-Live-Url';
24+
25+
public function __construct(
26+
private readonly RouterInterface $router,
27+
private readonly LiveComponentMetadataFactory $metadataFactory,
28+
) {
29+
}
30+
31+
public function onKernelResponse(ResponseEvent $event): void
32+
{
33+
if (!$this->isLiveComponentRequest($request = $event->getRequest())) {
34+
return;
35+
}
36+
if (!$event->isMainRequest()) {
37+
return;
38+
}
39+
40+
if ($previousLocation = $request->headers->get(self::URL_HEADER)) {
41+
$newUrl = $this->computeNewUrl(
42+
$previousLocation,
43+
$this->getUrlLiveProps($request)
44+
);
45+
if ($newUrl) {
46+
$event->getResponse()->headers->set(
47+
self::URL_HEADER,
48+
$this->computeNewUrl(
49+
$previousLocation,
50+
$this->getUrlLiveProps($request)
51+
)
52+
);
53+
}
54+
}
55+
}
56+
57+
public static function getSubscribedEvents(): array
58+
{
59+
return [
60+
KernelEvents::RESPONSE => 'onKernelResponse',
61+
];
62+
}
63+
64+
private function getUrlLiveProps(Request $request): array
65+
{
66+
$componentName = $request->attributes->get('_live_component');
67+
$component = $request->attributes->get('_mounted_component');
68+
$metadata = $this->metadataFactory->getMetadata($componentName);
69+
70+
$liveData = $request->attributes->get('_live_request_data') ?? [];
71+
$values = array_merge($liveData['props'] ?? [], $liveData['updated'] ?? []);
72+
73+
$urlLiveProps = [];
74+
foreach ($metadata->getAllLivePropsMetadata($component) as $liveProp) {
75+
$name = $liveProp->getName();
76+
$urlMapping = $liveProp->urlMapping();
77+
if (isset($values[$name]) && $urlMapping) {
78+
$urlLiveProps[$urlMapping->as ?? $name] = $values[$name];
79+
}
80+
}
81+
82+
return $urlLiveProps;
83+
}
84+
85+
// @todo use requestStack ?
86+
private function computeNewUrl(string $previousUrl, array $newProps): string
87+
{
88+
$parsed = parse_url($previousUrl);
89+
$baseUrl = $parsed['scheme'].'://';
90+
if (isset($parsed['user'])) {
91+
$baseUrl .= $parsed['user'];
92+
if (isset($parsed['pass'])) {
93+
$baseUrl .= ':'.$parsed['pass'];
94+
}
95+
$baseUrl .= '@';
96+
}
97+
$baseUrl .= $parsed['host'];
98+
if (isset($parsed['port'])) {
99+
$baseUrl .= ':'.$parsed['port'];
100+
}
101+
102+
$path = $parsed['path'] ?? '';
103+
if (isset($parsed['query'])) {
104+
$path .= '?'.$parsed['query'];
105+
}
106+
$match = $this->router->match($path);
107+
$newUrl = $this->router->generate(
108+
$match['_route'],
109+
$newProps
110+
);
111+
112+
$fragment = $parsed['fragment'] ?? '';
113+
114+
return $baseUrl.$newUrl.$fragment;
115+
}
116+
117+
/**
118+
* copied from LiveComponentSubscriber.
119+
*/
120+
private function isLiveComponentRequest(Request $request): bool
121+
{
122+
if (!$request->attributes->has('_live_component')) {
123+
return false;
124+
}
125+
126+
// if ($this->testMode) {
127+
// return true;
128+
// }
129+
130+
// Except when testing, require the correct content-type in the Accept header.
131+
// This also acts as a CSRF protection since this can only be set in accordance with same-origin/CORS policies.
132+
return \in_array('application/vnd.live-component+html', $request->getAcceptableContentTypes(), true);
133+
}
134+
}

0 commit comments

Comments
 (0)