通过 ajax 调用使用模式 "MMMM yyyy" 更新组件 primefaces datepicker 导致 "uncaught name at position [...]" 异常

updating component primefaces datepicker with pattern "MMMM yyyy" via ajax call leads to "uncaught name at position [...]" exception

我在模态对话框中有一个 primeface 日期选择器。使用模式“MMMM yyyy”(德语)设置初始日期效果很好。当我尝试通过 ajax 调用更新组件时,我收到 javascript 异常 "uncaught name at positon [...]".

我用的是Primefaces 7.0,自己写了一个年月转换器

PrimeFaces 语言环境:

PrimeFaces.locales['de'] = {
    closeText: 'Schließen',
    prevText: 'Zurück',
    nextText: 'Weiter',
    monthNames: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
    monthNamesShort: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
    dayNames: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
    dayNamesShort: ['Son', 'Mon', 'Die', 'Mit', 'Don', 'Fre', 'Sam'],
    dayNamesMin: ['S', 'M', 'D', 'M ', 'D', 'F ', 'S'],
    weekHeader: 'Woche',
    firstDay: 1,
    isRTL: false,
    showMonthAfterYear: false,
    yearSuffix: '',
    timeOnlyTitle: 'Nur Zeit',
    timeText: 'Zeit',
    hourText: 'Stunde',
    minuteText: 'Minute',
    secondText: 'Sekunde',
    currentText: 'Aktuelles Datum',
    ampm: false,
    month: 'Monat',
    week: 'Woche',
    day: 'Tag',
    allDayText: 'Ganzer Tag',
};

转换器:

/**
 * The Class YearMonthConverter.
 */
@Slf4j
@SuppressWarnings("common-java:DuplicatedBlocks")
@FacesConverter(value = "yearMonthConverter")
public class YearMonthConverter implements Converter {

    private static final String PATTERN = "MMMM yyyy";

    @Override
    public Object getAsObject(FacesContext facesContext, UIComponent uiComponent, String s) {
        String componentPattern = extractPattern(uiComponent);
        String pattern = componentPattern.equals("") ? PATTERN : componentPattern;
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern, Locale.GERMANY);

        try {
            return YearMonth.parse(s, formatter);
        } catch (DateTimeParseException e) {
            log.warn("conversion of date '{}' expected pattern '{}' failed with {}", s, pattern, e);
            return YearMonth.now();
        }
    }


    @Override
    public String getAsString(FacesContext facesContext, UIComponent uiComponent, Object o) {
        if (o instanceof YearMonth) {
            String componentPattern = extractPattern(uiComponent);
            String pattern = componentPattern.equals("") ? PATTERN : componentPattern;
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern, Locale.GERMANY);

            return formatter.format((YearMonth) o);
        } else {
            return null;
        }
    }

    private String extractPattern(UIComponent component) {
        // try to get the pattern from component
        if (component instanceof Calendar) {
            Calendar calendarComponent = (Calendar) component;
            return calendarComponent.getPattern();
        } else if (component instanceof DatePicker) {
            DatePicker datepickerComponent = (DatePicker) component;
            return datepickerComponent.getPattern();
        }

        return "";
    }
}

示例 xhtml 文件:

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:p="http://primefaces.org/ui"
      xmlns:composite="http://java.sun.com/jsf/composite"
      xmlns:h="http://xmlns.jcp.org/jsf/html">

<composite:interface>

</composite:interface>

<composite:implementation>

    <style type="text/css">
        .ui-panel .ui-panel-content {
            padding: 0;
        }
    </style>

        <!-- modaler someDialog -->
        <p:dialog id="someDialog"
                  header="some header"
                  widgetVar="someDlg"
                  modal="true"
                  width="500"
                  height="280"
                  resizable="false"
                  closeOnEscape="true">

            <p:panel id="pnlContent" styleClass="ui-noborder">
                <p:panelGrid id="inputArea" columns="2">
                    <p:outputLabel value="Label:" style="width: 12em; display: block;"/>
                    <p:outputLabel value="example"
                                   style="display: block;"/>

                    <p:outputLabel value="Monat:" style="width: 12em; display: block;"/>
                    <h:panelGroup>
                        <!-- Monat zurück -->
                        <p:commandButton
                                id="btnPrevMonth"
                                action="#{someDialogVC.previousMonth}"
                                style="padding: 2px"
                                icon="fa fa-angle-double-left"/>
                        <!-- Monatsanzeige -->
                        <p:datePicker id="dpMonat" view="month"
                                      value="#{someDialogVC.state.selektierterMonat}"
                                      converter="yearMonthConverter" pattern="MMMM yyyy" yearNavigator="true"
                                      yearRange="2000:2050" inputStyle="width: 17em;" readonlyInput="true">
                            <p:ajax event="dateSelect" listener="#{someDialogVC.monthChanged}"/>
                        </p:datePicker>
                        <!-- Monat vor -->
                        <p:commandButton
                                id="btnNextMonth"
                                action="#{someDialogVC.nextMonth}"
                                style="padding: 2px"
                                icon="fa fa-angle-double-right"/>
                    </h:panelGroup>
                </p:panelGrid>

                <p:separator style="border-color: #aaaaaa; margin-bottom: 10px;"/>

                <div style="display: flex; justify-content: flex-end">
                    <p:commandButton id="btnCreate"
                                     value="Anlegen"
                                     action="#{someDialogVC.createAndExit()}"
                                     disabled="#{someDialogVC.doGetAnlegenDisabled()}"/>
                </div>
            </p:panel>

            <p:blockUI block="pnlContent" trigger="btnCreate">
                <p:graphicImage name="images/loader.gif"/>
            </p:blockUI>

        </p:dialog>

</composite:implementation>

</html>

在 bean 中设置初始日期:

    @PostConstruct
    public void init() {
        data = new someDialogVO();
        state = new someDialogVS();

        state.setSelektierterMonat(YearMonth.now());
    }

代码片段bean:

    public void previousMonth() {
        state.setSelektierterMonat(state.getSelektierterMonat().minusMonths(1L));
        updateMonat();
    }

    public void nextMonth() {
        state.setSelektierterMonat(state.getSelektierterMonat().plusMonths(1L));
        updateMonat();
    }

    private void updateMonat() {
        PrimeFaces.current().ajax().update("contentForm:someDialog:dpMonat");
    }

在 ajax 更新调用之后,我可以看到,新的月份是在视觉上选择的,但几毫秒后我得到了描述的 javascript 异常。使用模式“MM.yyyy”时一切正常。

这似乎是 datePicker 组件的问题。我已经尝试通过简约测试重现此内容:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:p="http://primefaces.org/ui">

<h:head>
    <script>
    PrimeFaces.locales ['de'] = {
            closeText: 'Schließen',
            prevText: 'Zurück',
            nextText: 'Weiter',
            monthNames: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember' ],
            monthNamesShort: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez' ],
            dayNames: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
            dayNamesShort: ['Son', 'Mon', 'Die', 'Mit', 'Don', 'Fre', 'Sam'],
            dayNamesMin: ['S', 'M', 'D', 'M ', 'D', 'F ', 'S'],
            weekHeader: 'Woche',
            firstDay: 1,
            isRTL: false,
            showMonthAfterYear: false,
            yearSuffix:'',
            timeOnlyTitle: 'Nur Zeit',
            timeText: 'Zeit',
            hourText: 'Stunde',
            minuteText: 'Minute',
            secondText: 'Sekunde',
            currentText: 'Aktuelles Datum',
            ampm: false,
            month: 'Monat',
            week: 'Woche',
            day: 'Tag',
            allDayText: 'Ganzer Tag'
        };
    </script>
</h:head>
<h:body>

    <p:datePicker view="month" locale="de"
        pattern="MMMM yyyy" value="#{monthOverviewController.currentDate}">
    </p:datePicker>

</h:body>
</html>

果然,同样的 uncaught exception: Unknown name at position 0 发生了,这很可能源自 here. I have tested this with a couple of the translations provided on PrimeFaces' Wiki,所有结果都相同。

问题出在datePicker组件的_setInitValues函数中:

_setInitValues: function () {
    var parsedDefaultDate = this.parseValue(this.options.defaultDate);

    this.value = parsedDefaultDate;
    this.viewDate = this.options.viewDate ? 
                this.parseValue(this.options.viewDate) 
                :
                ((((this.isMultipleSelection() || this.isRangeSelection()) && parsedDefaultDate instanceof Array) ? parsedDefaultDate[0] : parsedDefaultDate) || this.parseValue(new Date()));
    this.options.minDate = this.parseOptionValue(this.options.minDate);
    this.options.maxDate = this.parseOptionValue(this.options.maxDate);
    this.ticksTo1970 = (((1970 - 1) * 365 + Math.floor(1970 / 4) - Math.floor(1970 / 100) + Math.floor(1970 / 400)) * 24 * 60 * 60 * 10000000);

    if (this.options.yearRange === null && this.options.yearNavigator) {
        var viewYear = this.viewDate.getFullYear();
        this.options.yearRange = (viewYear - 10) + ':' + (viewYear + 10);
    }

    if (this.options.userLocale && typeof this.options.userLocale === 'object') {
        $.extend(this.options.locale, this.options.userLocale);
    }

    if (this.options.disabledDates) {
        for (var i = 0; i < this.options.disabledDates.length; i++) {
            this.options.disabledDates[i] = this.parseOptionValue(this.options.disabledDates[i]);
        }
    }
},

defaultDate 是使用组件模式和语言环境格式化的 bean 属性 (currentDate) 的值,在我们的例子中产生了德语字符串。现在 _setInitialValues 中的第一条指令试图解析这个日期。但是,自定义翻译尚未加载(请参阅 $.extend(this.options.locale, this.options.userLocale);,因此无法解析月份名称(在大多数情况下)。我认为这是组件中的错误和 best/most 稳定解决方案应该等待适当的修复。一个中间解决方案(请记住,这充其量只是一个 hack)将复制 JavaScript 文件并将翻译的加载移动到第一个解析之上:

_setInitValues: function () {
    if (this.options.userLocale && typeof this.options.userLocale === 'object') {
        $.extend(this.options.locale, this.options.userLocale);
    }
    var parsedDefaultDate = this.parseValue(this.options.defaultDate);