Commit ba0ea875 authored by Benjamin Franzke's avatar Benjamin Franzke Committed by Benni Mack
Browse files

[TASK] Revert "[FEATURE] Introduce client-side templating engine"

This reverts commit 6558a6bd.

The client side template engine introduced in #91810 is inspired
by lit-html. It was introduced because ES6 modules like lit-html could
not be included in core AMD builds.
With the help of rollup, lit-html can be converted to AMD within
our build process. Therefore our own implementation is removed
in order for lit-html to be introduced afterwards.

Resolves: #93058
Reverts: #91810
Releases: master
Change-Id: I2d5d20a6f9e0c8c4c683dafb6e36500f1966bd35
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/67095

Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Oliver Hader's avatarOliver Hader <oliver.hader@typo3.org>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent 2f129cb1
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
import SecurityUtility = require('TYPO3/CMS/Core/SecurityUtility');
/**
* Module: TYPO3/CMS/Backend/Element/Template
*
* @example
* const value = 'Hello World';
* const template = html`<div>${value}</div>`;
* console.log(template.content);
*/
export class Template {
private readonly securityUtility: SecurityUtility;
private readonly strings: TemplateStringsArray;
private readonly values: any[];
private readonly unsafe: boolean;
private closures: Map<string, Function>;
public constructor(unsafe: boolean, strings: TemplateStringsArray, ...values: any[]) {
this.securityUtility = new SecurityUtility();
this.unsafe = unsafe;
this.strings = strings;
this.values = values;
this.closures = new Map<string, Function>();
}
public getHtml(parentScope: Template = null): string {
if (parentScope === null) {
parentScope = this;
}
return this.strings
.map((string: string, index: number) => {
if (this.values[index] === undefined) {
return string;
}
return string + this.getValue(this.values[index], parentScope);
})
.join('');
}
public getElement(): HTMLTemplateElement {
const template = document.createElement('template');
template.innerHTML = this.getHtml();
return template;
}
public mountTo(renderRoot: HTMLElement | ShadowRoot, clear: boolean = false): void {
if (clear) {
renderRoot.innerHTML = '';
}
const fragment = this.getElement().content;
const target = fragment.cloneNode(true) as DocumentFragment;
const closurePattern = new RegExp('^@closure:(.+)$');
target.querySelectorAll('[\\@click]').forEach((element: HTMLElement) => {
const pointer = element.getAttribute('@click');
const matches = closurePattern.exec(pointer);
const closure = this.closures.get(matches[1]);
if (matches === null || closure === null) {
return;
}
element.removeAttribute('@click');
element.addEventListener('click', (evt: Event) => closure.call(null, evt));
});
renderRoot.appendChild(target)
}
private getValue(value: any, parentScope: Template): string {
if (value instanceof Array) {
return value
.map((item: any) => this.getValue(item, parentScope))
.filter((item: string) => item !== '')
.join('');
}
if (value instanceof Function) {
const identifier = this.securityUtility.getRandomHexValue(20);
parentScope.closures.set(identifier, value);
return '@closure:' + identifier;
}
// @todo `value instanceof Template` is removed somewhere during minification
if (value instanceof Template || value instanceof Object && value.constructor === this.constructor) {
return value.getHtml(parentScope);
}
if (value instanceof Object) {
return JSON.stringify(value);
}
if (this.unsafe) {
return (value + '').trim();
}
return this.securityUtility.encodeHtml(value).trim();
}
}
export const html = (strings: TemplateStringsArray, ...values: any[]): Template => {
return new Template(false, strings, ...values);
}
export const unsafe = (strings: TemplateStringsArray, ...values: any[]): Template => {
return new Template(true, strings, ...values);
}
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports","TYPO3/CMS/Core/SecurityUtility"],(function(t,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.unsafe=e.html=e.Template=void 0;class n{constructor(t,e,...n){this.securityUtility=new i,this.unsafe=t,this.strings=e,this.values=n,this.closures=new Map}getHtml(t=null){return null===t&&(t=this),this.strings.map((e,i)=>void 0===this.values[i]?e:e+this.getValue(this.values[i],t)).join("")}getElement(){const t=document.createElement("template");return t.innerHTML=this.getHtml(),t}mountTo(t,e=!1){e&&(t.innerHTML="");const i=this.getElement().content.cloneNode(!0),n=new RegExp("^@closure:(.+)$");i.querySelectorAll("[\\@click]").forEach(t=>{const e=t.getAttribute("@click"),i=n.exec(e),s=this.closures.get(i[1]);null!==i&&null!==s&&(t.removeAttribute("@click"),t.addEventListener("click",t=>s.call(null,t)))}),t.appendChild(i)}getValue(t,e){if(t instanceof Array)return t.map(t=>this.getValue(t,e)).filter(t=>""!==t).join("");if(t instanceof Function){const i=this.securityUtility.getRandomHexValue(20);return e.closures.set(i,t),"@closure:"+i}return t instanceof n||t instanceof Object&&t.constructor===this.constructor?t.getHtml(e):t instanceof Object?JSON.stringify(t):this.unsafe?(t+"").trim():this.securityUtility.encodeHtml(t).trim()}}e.Template=n,e.html=(t,...e)=>new n(!1,t,...e),e.unsafe=(t,...e)=>new n(!0,t,...e)}));
\ No newline at end of file
.. include:: ../../Includes.txt
=========================================================
Feature: #91810 - Introduce client-side templating engine
=========================================================
See :issue:`91810`
Description
===========
To avoid custom jQuery template building a new slim client-side templating
engine is introduced. The functionality has been inspired by `lit-html`_ -
however it is actually not the same. As long as RequireJS and AMD-based
JavaScript modules are in place `lit-html` cannot be used directly, since
it requires native ES6-module support.
This templating engine is very simplistic and does not yet support virtual
DOM, any kind of data-binding or mutation/change detection mechanism. However
it does support conditions, iterations and simple default events in templates.
.. _lit-html: https://lit-html.polymer-project.org/
Impact
======
Individual client-side templates can be processed in JavaScript directly
using moder web technologies like template-strings_ and template-elements_.
.. _template-strings: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
.. _template-elements: https://developer.mozilla.org/de/docs/Web/HTML/Element/template
Rendering is handled by AMD-module `TYPO3/CMS/Backend/Element/Template`:
* :js:`constructor(unsafe: boolean, strings: TemplateStringsArray, ...values: any[])`
is most probably invoked by template tag functions `html` and `unsafe` only
* :js:`getHtml(parentScope: Template = null): string`
renders and returns inner HTML
* :js:`getElement(): HTMLTemplateElement`
renders and returns HTML `template` element
* :js:`mountTo(renderRoot: HTMLElement | ShadowRoot, clear: boolean = false): void`
renders and mounts result to existing HTML element
+ :js:`renderRoot` can be a regular HTML element or root node of a Shadow DOM
+ :js:`clear` instructs to clear all exiting child elements in :js:`renderRoot`
Invocation usually happens using static template tag functions:
* :js:`html = (strings: TemplateStringsArray, ...values: any[]): Template`
processes templates and ensures values are encoded for HTML
* :js:`unsafe = (strings: TemplateStringsArray, ...values: any[]): Template`
processes templates and skips encoding values for HTML - when using this
function, user submitted values need be encoded manually to avoid XSS
Examples
========
Variable assignment
-------------------
.. code-block:: ts
import {Template, html, unsafe} from 'TYPO3/CMS/Backend/Element/Template';
const value = 'World';
const target = document.getElementById('target');
const template = html`<div>Hello ${value}!</div>`;
template.mountTo(target, true);
.. code-block:: html
<div>Hello World!</div>
Unsafe tags would have been encoded (e.g. :html:`<b>World</b>`
as :html:`&lt;b&gt;World&lt;/b&gt;`).
Condition and iteration
-----------------------
.. code-block:: ts
import {Template, html, unsafe} from 'TYPO3/CMS/Backend/Element/Template';
const items = ['a', 'b', 'c']
const addClass = true;
const target = document.getElementById('target');
const template = html`
<ul ${addClass ? 'class="list"' : ''}>
${items.map((item: string, index: number): string => {
return html`<li>#${index+1}: ${item}</li>`
})}
</ul>
`;
template.mountTo(target, true);
.. code-block:: html
<ul class="list">
<li>#1: a</li>
<li>#2: b</li>
<li>#3: c</li>
</ul>
The :js:`${...}` literal used in template tags can basically contain any
JavaScript instruction - as long as their result can be casted to `string`
again or is of type `TYPO3/CMS/Backend/Element/Template`. This allows to
make use of custom conditions as well as iterations:
* condition: :js:`${condition ? thenReturn : elseReturn}`
* iteration: :js:`${array.map((item) => { return item; })}`
Events
------
Currently only `click` events are supported using :html:`@click="${handler}"`.
.. code-block:: ts
import {Template, html, unsafe} from 'TYPO3/CMS/Backend/Element/Template';
const value = 'World';
const target = document.getElementById('target');
const template = html`
<div @click="${(evt: Event): void => { console.log(value); })}">
Hello ${value}!
</div>
`;
template.mountTo(target, true);
The result won't look much different than the first example - however the
custom attribute :html:`@click` will be transformed into an according event
listener bound to the element where it has been declared.
.. index:: Backend, JavaScript, ext:backend
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment