Use lit-html to render form fields

This commit is contained in:
JC Brand 2020-12-28 17:48:25 +01:00
parent 62dbb1062f
commit da131715ba
26 changed files with 176 additions and 159 deletions

View File

@ -1614,12 +1614,9 @@ describe("Groupchats", function () {
.c('value').t('cauldronburn');
_converse.connection._dataRecv(mock.createRequest(config_stanza));
const form = await u.waitUntil(() => view.el.querySelector('.muc-config-form'));
expect(form.querySelectorAll('fieldset').length).toBe(2);
const membersonly = view.el.querySelectorAll('input[name="muc#roomconfig_membersonly"]');
expect(membersonly.length).toBe(1);
expect(membersonly[0].getAttribute('type')).toBe('checkbox');
membersonly[0].checked = true;
const membersonly = await u.waitUntil(() => view.el.querySelector('input[name="muc#roomconfig_membersonly"]'));
expect(membersonly.getAttribute('type')).toBe('checkbox');
membersonly.checked = true;
const moderated = view.el.querySelectorAll('input[name="muc#roomconfig_moderatedroom"]');
expect(moderated.length).toBe(1);

View File

@ -353,7 +353,7 @@ describe("The Registration Panel", function () {
</iq>`);
_converse.connection._dataRecv(mock.createRequest(stanza));
expect(registerview.form_type).toBe('xform');
expect(registerview.el.querySelectorAll('#converse-register input[required="required"]').length).toBe(3);
expect(registerview.el.querySelectorAll('#converse-register input[required]').length).toBe(3);
// Hide the controlbox so that we can see whether the test
// passed or failed
u.addClass('hidden', _converse.chatboxviews.get('controlbox').el);

View File

@ -4,7 +4,6 @@ import { CustomElement } from './element.js';
import { __ } from '../i18n';
import { api, converse } from "@converse/headless/core";
import { html } from "lit-html";
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
const { Strophe, $iq, sizzle } = converse.env;
const u = converse.env.utils;
@ -21,8 +20,7 @@ const tpl_command_form = (o, command) => {
<input type="hidden" name="command_jid" value="${command.jid}"/>
<p class="form-help">${command.instructions}</p>
<!-- Fields are generated internally, with xForm2webForm -->
${ command.fields.map(field => unsafeHTML(field)) }
${ command.fields }
</fieldset>
<fieldset>
<input type="submit" class="btn btn-primary" value="${i18n_run}">

View File

@ -57,7 +57,7 @@ export default BootstrapModal.extend({
command.fields;
try {
const iq = await api.sendIQ(stanza);
command.fields = sizzle('field', iq).map(f => u.xForm2webForm(f, iq))
command.fields = sizzle('field', iq).map(f => u.xForm2TemplateResult(f, iq))
} catch (e) {
if (e === null) {
log.error(`Error: timeout while trying to execute command for ${jid}`);
@ -83,7 +83,5 @@ export default BootstrapModal.extend({
</command>
</iq>
*/
}
});

View File

@ -32,7 +32,7 @@ const MUCConfigForm = View.extend({
};
return tpl_muc_config_form({
'closeConfigForm': ev => this.closeConfigForm(ev),
'fields': fields.map(f => u.xForm2webForm(f, stanza, options)),
'fields': fields.map(f => u.xForm2TemplateResult(f, stanza, options)),
'instructions': stanza.querySelector('instructions')?.textContent,
'submitConfigForm': ev => this.submitConfigForm(ev),
'title': stanza.querySelector('title')?.textContent

View File

@ -8,15 +8,16 @@
*/
import "./controlbox/index.js";
import log from "@converse/headless/log";
import tpl_form_input from "../templates/form_input.html";
import tpl_form_username from "../templates/form_username.html";
import tpl_form_input from "../templates/form_input.js";
import tpl_form_url from "../templates/form_url.js";
import tpl_form_username from "../templates/form_username.js";
import tpl_register_panel from "../templates/register_panel.html";
import tpl_registration_form from "../templates/registration_form.html";
import tpl_registration_form from "../templates/registration_form.js";
import tpl_registration_request from "../templates/registration_request.html";
import tpl_spinner from "../templates/spinner.js";
import utils from "@converse/headless/utils/form";
import { View } from "@converse/skeletor/src/view";
import { __ } from '../i18n';
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
import { pick } from "lodash-es";
import { render } from 'lit-html';
@ -403,24 +404,19 @@ converse.plugins.add('converse-register', {
}
},
renderLegacyRegistrationForm (form) {
Object.keys(this.fields).forEach(key => {
getLegacyFormFields () {
const input_fields = Object.keys(this.fields).map(key => {
if (key === "username") {
form.insertAdjacentHTML(
'beforeend',
tpl_form_username({
return tpl_form_username({
'domain': ` @${this.domain}`,
'name': key,
'type': "text",
'label': key,
'value': '',
'required': true
})
);
});
} else {
form.insertAdjacentHTML(
'beforeend',
tpl_form_input({
return tpl_form_input({
'label': key,
'name': key,
'placeholder': key,
@ -428,14 +424,20 @@ converse.plugins.add('converse-register', {
'type': (key === 'password' || key === 'email') ? key : "text",
'value': ''
})
);
}
});
// Show urls
this.urls.forEach(u => form.insertAdjacentHTML(
'afterend',
'<a target="blank" rel="noopener" href="'+u+'">'+u+'</a>'
));
const urls = this.urls.map(u => tpl_form_url({'label': '', 'value': u}));
return [...input_fields, ...urls];
},
getFormFields (stanza) {
if (this.form_type === 'xform') {
return Array.from(stanza.querySelectorAll('field')).map(field =>
utils.xForm2TemplateResult(field, stanza, {'domain': this.domain})
);
} else {
return this.getLegacyFormFields();
}
},
/**
@ -447,28 +449,14 @@ converse.plugins.add('converse-register', {
*/
renderRegistrationForm (stanza) {
const form = this.el.querySelector('form');
form.innerHTML = tpl_registration_form({
'__': __,
const tpl = tpl_registration_form({
'domain': this.domain,
'title': this.title,
'instructions': this.instructions,
'registration_domain': api.settings.get('registration_domain')
'fields': this.fields,
'form_fields': this.getFormFields(stanza)
});
const buttons = form.querySelector('fieldset.buttons');
if (this.form_type === 'xform') {
stanza.querySelectorAll('field').forEach(field => {
buttons.insertAdjacentHTML(
'beforebegin',
utils.xForm2webForm(field, stanza, {'domain': this.domain})
);
});
} else {
this.renderLegacyRegistrationForm(form);
}
if (!this.fields) {
form.querySelector('.button-primary').classList.add('hidden');
}
render(tpl, form);
form.classList.remove('hidden');
this.model.set('registration_form_rendered', true);
},

View File

@ -1,9 +0,0 @@
{[ if (o.label) { ]}
<label>
{{{o.label}}}
</label>
{[ } ]}
<img src="data:{{{o.type}}};base64,{{{o.data}}}">
<input name="{{{o.name}}}" type="text" {[ if (o.required) { ]} required="required" {[ } ]} />

View File

@ -0,0 +1,9 @@
import { html } from "lit-html";
export default (o) => html`
<fieldset class="form-group">
${o.label ? html`<label>${o.label}</label>` : '' }
<img src="data:${o.type};base64,${o.data}">
<input name="${o.name}" type="text" ?required="${o.required}" />
</fieldset>
`;

View File

@ -1,4 +0,0 @@
<div class="form-group">
<input id="{{{o.id}}}" name="{{{o.name}}}" type="checkbox" {{{o.checked}}} {[ if (o.required) { ]} required {[ } ]} />
<label class="form-check-label" for="{{{o.id}}}">{{{o.label}}}</label>
</div>

View File

@ -0,0 +1,7 @@
import { html } from "lit-html";
export default (o) => html`
<fieldset class="form-group">
<input id="${o.id}" name="${o.name}" type="checkbox" ?checked=${o.checked} ?required=${o.required} />
<label class="form-check-label" for="${o.id}">${o.label}</label>
</fieldset>`;

View File

@ -0,0 +1,3 @@
import { html } from "lit-html";
export default (o) => html`<p class="form-help">${o.text}</p>`;

View File

@ -1,16 +0,0 @@
<div class="form-group">
{[ if (o.type !== 'hidden') { ]}
<label for="{{{o.id}}}">{{{o.label}}}</label>
{[ } ]}
{[ if (o.type === 'password' && o.fixed_username) { ]}
<!-- This is a hack to prevent Chrome from auto-filling the username in
any of the other input fields in the MUC configuration form. -->
<input class="hidden-username" type="text" autocomplete="username" value="{{{o.fixed_username}}}"></input>
{[ } ]}
<input
class="form-control" name="{{{o.name}}}" type="{{{o.type}}}" id="{{{o.id}}}"
{[ if (o.autocomplete) { ]} autocomplete="{{{o.autocomplete}}}" {[ } ]}
{[ if (o.placeholder) { ]} placeholder="{{{o.placeholder}}}" {[ } ]}
{[ if (o.value) { ]} value="{{{o.value}}}" {[ } ]}
{[ if (o.required) { ]} required="required" {[ } ]} />
</div>

View File

@ -0,0 +1,22 @@
import { html } from "lit-html";
export default (o) => html`
<div class="form-group">
${ o.type !== 'hidden' ? html`<label for="${o.id}">${o.label}</label>` : '' }
<!-- This is a hack to prevent Chrome from auto-filling the username in
any of the other input fields in the MUC configuration form. -->
${ (o.type === 'password' && o.fixed_username) ? html`
<input class="hidden-username" type="text" autocomplete="username" value="${o.fixed_username}"></input>
` : '' }
<input
autocomplete="${o.autocomplete || ''}"
class="form-control"
id="${o.id}"
name="${o.name}"
placeholder="${o.placeholder || ''}"
type="${o.type}"
value="${o.value || ''}"
?required=${o.required} />
</div>`;

View File

@ -1,4 +0,0 @@
<div class="form-group">
<label for="{{{o.id}}}">{{{o.label}}}</label>
<select class="form-control" id="{{{o.id}}}" name="{{{o.name}}}" {[ if (o.multiple) { ]} multiple="multiple" {[ } ]}>{{o.options}}</select>
</div>

View File

@ -0,0 +1,11 @@
import { html } from "lit-html";
const tpl_option = (o) => html`<option value="${o.value}" ?selected="${o.selected}">${o.label}</option>`;
export default (o) => html`
<div class="form-group">
<label for="${o.id}">${o.label}</label>
<select class="form-control" id="${o.id}" name="${o.name}" ?multiple="${o.multiple}">
${o.options?.map(o => tpl_option(o))}
</select>
</div>`;

View File

@ -1,2 +0,0 @@
<label class="label-ta">{{{o.label}}}</label>
<textarea name="{{{o.name}}}">{{{o.value}}}</textarea>

View File

@ -0,0 +1,6 @@
import { html } from "lit-html";
export default (o) => html`
<label class="label-ta">${o.label}</label>
<textarea name="${o.name}">${o.value}</textarea>
`;

View File

@ -1,4 +0,0 @@
<label>
{{{o.label}}}
<a class="form-url" target="_blank" rel="noopener" href="{{{o.value}}}">{{{o.value}}}</a>
</label>

View File

@ -0,0 +1,6 @@
import { html } from "lit-html";
export default (o) => html`
<label>${o.label}
<a class="form-url" target="_blank" rel="noopener" href="${o.value}">${o.value}</a>
</label>`;

View File

@ -1,15 +0,0 @@
<div class="form-group">
{[ if (o.label) { ]}
<label>
{{{o.label}}}
</label>
{[ } ]}
<div class="input-group">
<div class="input-group-prepend">
<input name="{{{o.name}}}" type="{{{o.type}}}"
{[ if (o.value) { ]} value="{{{o.value}}}" {[ } ]}
{[ if (o.required) { ]} required="required" {[ } ]} />
<div class="input-group-text col" title="{{{o.domain}}}">{{{o.domain}}}</div>
</div>
</div>
</div>

View File

@ -0,0 +1,15 @@
import { html } from "lit-html";
export default (o) => html`
<div class="form-group">
${ o.label ? html`<label>${o.label}</label>` : '' }
<div class="input-group">
<div class="input-group-prepend">
<input name="${o.name}"
type="${o.type}"
value="${o.value || ''}"
?required="${o.required}" />
<div class="input-group-text col" title="${o.domain}">${o.domain}</div>
</div>
</div>
</div>`;

View File

@ -1,6 +1,5 @@
import { html } from "lit-html";
import { __ } from '../i18n';
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
import { __ } from 'i18n';
export default (o) => {
const i18n_save = __('Save');
@ -10,8 +9,7 @@ export default (o) => {
<fieldset class="form-group">
<legend>${o.title}</legend>
${ (o.title !== o.instructions) ? html`<p class="form-help">${o.instructions}</p>` : '' }
<!-- Fields are generated internally, with xForm2webForm -->
${ o.fields.map(field => unsafeHTML(field)) }
${ o.fields }
</fieldset>
<fieldset>
<input type="submit" class="btn btn-primary" value="${i18n_save}">

View File

@ -1,15 +0,0 @@
<legend class="col-form-label">{{{o.__("Account Registration:")}}} {{{o.domain}}}</legend>
<p class="title">{{{o.title}}}</p>
<p class="form-help instructions">{{{o.instructions}}}</p>
<div class="form-errors hidden"></div>
<fieldset class="buttons">
<input type="submit" class="btn btn-primary" value="{{{o.__('Register')}}}"/>
{[ if (!o.registration_domain) { ]}
<input type="button" class="btn btn-secondary button-cancel" value="{{{o.__('Choose a different provider')}}}"/>
{[ } ]}
<div class="switch-form">
<p>{{{ o.__("Already have a chat account?") }}}</p>
<p><a class="login-here toggle-register-login" href="#converse/login">{{{o.__("Log in here")}}}</a></p>
</div>
</fieldset>

View File

@ -0,0 +1,28 @@
import { __ } from 'i18n';
import { api } from "@converse/headless/core";
import { html } from "lit-html";
export default (o) => {
const i18n_choose_provider = __('Choose a different provider');
const i18n_has_account = __("Already have a chat account?");
const i18n_legend = __("Account Registration:");
const i18n_login = __("Log in here");
const i18n_register = __('Register');
const registration_domain = api.settings.get('registration_domain')
return html`
<legend class="col-form-label">${i18n_legend} ${o.domain}</legend>
<p class="title">${o.title}</p>
<p class="form-help instructions">${o.instructions}</p>
<div class="form-errors hidden"></div>
${ o.form_fields }
<fieldset class="buttons form-group">
${ o.fields ? html`<input type="submit" class="btn btn-primary" value="${i18n_register}"/>` : '' }
${ registration_domain ? '' : html`<input type="button" class="btn btn-secondary button-cancel" value="${i18n_choose_provider}"/>` }
<div class="switch-form">
<p>${i18n_has_account}</p>
<p><a class="login-here toggle-register-login" href="#converse/login">${i18n_login}</a></p>
</div>
</fieldset>`;
}

View File

@ -1 +0,0 @@
<option value="{{{o.value}}}" {[ if (o.selected) { ]} selected="selected" {[ } ]} >{{{o.label}}}</option>

View File

@ -7,15 +7,15 @@ import URI from "urijs";
import log from '@converse/headless/log';
import tpl_audio from "../templates/audio.js";
import tpl_file from "../templates/file.js";
import tpl_form_captcha from "../templates/form_captcha.html";
import tpl_form_checkbox from "../templates/form_checkbox.html";
import tpl_form_input from "../templates/form_input.html";
import tpl_form_select from "../templates/form_select.html";
import tpl_form_textarea from "../templates/form_textarea.html";
import tpl_form_url from "../templates/form_url.html";
import tpl_form_username from "../templates/form_username.html";
import tpl_form_captcha from "../templates/form_captcha.js";
import tpl_form_checkbox from "../templates/form_checkbox.js";
import tpl_form_help from "../templates/form_help.js";
import tpl_form_input from "../templates/form_input.js";
import tpl_form_select from "../templates/form_select.js";
import tpl_form_textarea from "../templates/form_textarea.js";
import tpl_form_url from "../templates/form_url.js";
import tpl_form_username from "../templates/form_username.js";
import tpl_image from "../templates/image.js";
import tpl_select_option from "../templates/select_option.html";
import tpl_video from "../templates/video.js";
import u from "../headless/utils/core";
import { api, converse } from "@converse/headless/core";
@ -583,38 +583,38 @@ u.fadeIn = function (el, callback) {
/**
* Takes a field in XMPP XForm (XEP-004: Data Forms) format
* and turns it into an HTML field.
* Returns either text or a DOM element (which is not ideal, but fine for now).
* @private
* @method u#xForm2webForm
* Takes an XML field in XMPP XForm (XEP-004: Data Forms) format returns a
* [TemplateResult](https://lit-html.polymer-project.org/api/classes/_lit_html_.templateresult.html).
* @method u#xForm2TemplateResult
* @param { XMLElement } field - the field to convert
* @param { XMLElement } stanza - the containing stanza
* @param { Object } options
* @returns { TemplateResult }
*/
u.xForm2webForm = function (field, stanza, options) {
u.xForm2TemplateResult = function (field, stanza, options) {
if (field.getAttribute('type') === 'list-single' ||
field.getAttribute('type') === 'list-multi') {
const values = u.queryChildren(field, 'value').map(el => el?.textContent);
const options = u.queryChildren(field, 'option').map(option => {
const value = option.querySelector('value')?.textContent;
return tpl_select_option({
return {
'value': value,
'label': option.getAttribute('label'),
'selected': values.includes(value),
'required': !!field.querySelector('required')
});
};
});
return tpl_form_select({
options,
'id': u.getUniqueId(),
'name': field.getAttribute('var'),
'label': field.getAttribute('label'),
'options': options.join(''),
'multiple': (field.getAttribute('type') === 'list-multi'),
'name': field.getAttribute('var'),
'required': !!field.querySelector('required')
});
} else if (field.getAttribute('type') === 'fixed') {
const text = field.querySelector('value')?.textContent;
return '<p class="form-help">'+text+'</p>';
return tpl_form_help({text});
} else if (field.getAttribute('type') === 'jid-multi') {
return tpl_form_textarea({
'name': field.getAttribute('var'),
@ -670,4 +670,5 @@ u.xForm2webForm = function (field, stanza, options) {
});
}
}
export default u;