I’m working on a Symfony project using EasyAdmin. I have a Page entity that contains a collection of Step entities. Each Step has a field called intro where I want to store and display a YouTube iframe embed code.
In my EasyAdmin CRUD controller, I use this configuration:
yield CollectionField::new('steps', 'Steps')
->setEntryType(IntroStepType::class)
->allowAdd()
->allowDelete()
->renderExpanded()
->addJsFiles(Asset::fromEasyAdminAssetPackage('field-text-editor.js')->onlyOnForms())
->addCssFiles(Asset::fromEasyAdminAssetPackage('field-text-editor.css')->onlyOnForms());
And my custom form type for ‘steps’ looks like this:
class IntroStepType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('title', TextType::class, [
'label' => 'Title',
])
->add('element', TextType::class, [
'label' => 'Element',
])
->add('intro', TextEditorType::class, [
'label' => 'Instructions',
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Step::class,
]);
}
}
Problem:
I can enter YouTube iframe embed code into the intro field via the text editor in easyadmin, but when viewing the detail page, the iframe is not rendered as HTML. Instead, it shows as plain text.
This is my Stimulus controller where i load the data in:
import { Controller } from '@hotwired/stimulus';
import introJs from 'intro.js';
import '../vendor/intro.js/introjs.css';
export default class extends Controller {
async connect() {
this.intro = introJs();
this.baseOptions = {
showBullets: false,
showPrevButton: true,
nextLabel: 'Next',
prevLabel: 'Previous',
};
this.intro.setOptions(this.baseOptions);
const params = new URLSearchParams(window.location.search);
if ('true' === params.get('tutorial')) {
await this.startTutorial();
}
}
async startTutorial() {
const path = encodeURIComponent(window.location.pathname);
try {
const response = await fetch(`/tutorial?path=${path}`);
if (!response.ok) {
console.error('Error loading tutorial data:', response.status, response.statusText);
return;
}
const data = await response.json();
if (!data.steps?.length) return;
const steps = data.steps.filter(step => !step.element || document.querySelector(step.element));
if (steps.length === 0) return;
const options = {
...this.baseOptions,
steps,
doneLabel: data.next ? 'Next' : 'Done',
};
this.intro.setOptions(options);
this.intro.oncomplete(() => {
if (data.next) {
const nextUrl = new URL(window.location.origin + data.next);
nextUrl.searchParams.set('tutorial', 'true');
window.location.href = nextUrl.toString();
} else {
this.removeTutorialQueryParam();
}
});
setTimeout(() => this.intro.start(), 100);
} catch (error) {
console.error('Network error:', error);
}
}
removeTutorialQueryParam() {
const url = new URL(window.location);
url.searchParams.delete('tutorial');
window.history.replaceState({}, document.title, url);
}
}
Any advice or examples are much appreciated.