<a href="{{ path('evaluation_page_edit', {id: page.id}) }}"
data-turbo-frame="modal_frame"
>
<div id="modal" class="hidden">
<div class"modal-content">
<turbo-frame id="modal_frame"
</turbo-frame>
</div>
</div>
<turbo-frame id="modal_frame">
<h2>{{ title }}</h2>
{# action must be full path.
form is rendered in context of different URL #}
{{ form(form) }}
</turbo-frame>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["frame"]
open() {
this.element.classList.remove("hidden")
}
close() {
this.element.classList.add("hidden")
if (this.hasFrameTarget) {
this.frameTarget.innerHTML = ""
}
}
onSubmit(event) {
if (!event.detail.success) return
// on error, we want to keep dialog open
// can not use data-turbo-frame="_top"
// server side can't break out of frame
if (event.detail.fetchResponse.response.redirected) {
this.close()
window.Turbo.visit(
event.detail.fetchResponse.response.url,
{ action: "replace" }
)
}
}
<div
id="modal"
class="hidden"
{{ stimulus_controller('modal') }}
{{ stimulus_action('modal', 'open', 'turbo:frame-load') |
stimulus_action('modal', 'onSubmit', 'turbo:submit-end')
}}
>
<div class="modal-overlay"
{{ stimulus_action('modal', 'close', 'click') }}
></div>
<div class"modal-content">
<turbo-frame id="modal_frame"
{{ stimulus_target('modal', 'frame') }}>
</turbo-frame>
</div>
</div>
<body
{{ turbo_stream_listen(
'evaluation-' ~ evaluation.id
) }}
>
<div id="evaluation-score-{{ evaluation.id }}">
{% include 'evaluation/score.html.twig'
with {'evaluation': evaluation} only %}
</div>
Twig\Environment $twig
Symfony\Component\Mercure\HubInterface $hub
... event listener for changes on an evaluation
public function sendUpdate(Evaluation $evaluation): void
{
$payload = $this->twig->render(
'evaluation/score.stream.html.twig', [
'evaluation' => $evaluation,
]
);
$update = new Update('evaluation-'.$evaluation->id, $payload);
$this->hub->publish($update);
}
<turbo-stream
action="update"
targets="#evaluation-score-{{ evaluation.id }}"
>
<template>
{% include 'evaluation/score.html.twig'
with {'evaluation': evaluation} only
%}
</template>
</turbo-stream>
import {Controller} from '@hotwired/stimulus';
export default class extends Controller {
static values = {
editUrl: String,
msgError: String,
}
...
updateNumeric(event) {
... find value and question from event.target
const editUrl = this.editUrlValue.replace(
'answerId',
question.dataset.questionId
)
fetch(editUrl), {
... http request to save value
}).then((response) => {
if (response.ok) {
console.log('saved numeric answer')
} else
... error handling - this.msgErrorValue
<div {{ stimulus_controller('evaluation-edit', {
'editUrl': path('evaluate_answer', {'id': 'answerId'}),
'msgError': 'evaluate_category.messages.error'|trans,
}) }}
>
<input
type="number"
value="{{ answer.value }}"
id="{{ answer.id }}"
{{ stimulus_action(
'evaluation-edit',
'updateNumeric',
'change'
) }}
>
#[AsEventListener]
final readonly class AnswerChangePublisher
{
public function __construct(private HubInterface $hub) {}
public function __invoke(AnswerHasBeenUpdated $event): void
{
$answer = $event->answer;
$topic = 'evaluation-'.$answer->evaluation->id;
$payload = json_encode([
'answers' => [
$answer->getId() => $answer,
],
], \JSON_THROW_ON_ERROR);
$this->hub->publish(new Update($topic, $payload));
}
}
import {Controller} from '@hotwired/stimulus';
export default class extends Controller {
static values = {
eventSrcUrl: String,
}
static targets = ['numericAnswer']
connect() {
this.eventSource = new EventSource(this.eventSrcUrlValue)
this.eventSource.onmessage = this.updateAnswersListener
}
disconnect() {
this.eventSource.close()
}
updateAnswersListener(event) {
const msg = JSON.parse(event.data)
if (!msg.hasOwnProperty('answers')) return
for (const [answerId, answer] of msg.answers.entries()) {
let input = document.getElementById(answerId)
if (!input) continue
switch (answer.questionType) {
case 'numeric':
input.value = answer.value
break
case 'boolean':
...
default:
console.log('Unexpected type '+answer.questionType)
}
}
}
<div
{{ stimulus_controller(
'evaluation-edit',
{
'eventSrcUrl':
mercure('evaluation-'~evaluation.id)
...
}
}}>
...
</div>
$update = new Update(
'topic-123',
json_encode(['status' => 'OutOfStock']),
private: true,
);
{{ 'eventSrcUrl' : mercure('topic-123', {
subscribe: 'topic-123'
}) }}
new EventSource(this.eventSrcUrlValue, {
withCredentials: true
})
Alternative: HTMX