Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.0] Support lit-html and LitElement #61

Closed
t2ym opened this issue May 9, 2018 · 1 comment
Closed

[3.0] Support lit-html and LitElement #61

t2ym opened this issue May 9, 2018 · 1 comment

Comments

@t2ym
Copy link
Owner

t2ym commented May 9, 2018

[3.0] Support lit-html and LitElement

Tasks

Notes

@t2ym
Copy link
Owner Author

t2ym commented Dec 29, 2018

I18N for lit-html

Highlights

  • Extendable and composable HTML template literals based on lit-html
import { html, i18n, bind } from 'i18n-element/i18n.js';
class MyI18nElement extends i18n(HTMLElement) {
  ... // a few boilerplate mandatory methods are omitted here
  render() {
    return html`${bind(this, 'my-i18n-element')}
      <span>localizable message with ${this.property}</span>`;
  }
  ...
}
class ExtendedElement extends MyI18nElement {
  render() {
    return html`${bind(this, 'extended-element')}
      <div>extended message with ${this.property}</div>
      ${super.render()}`;
  }
}
class CompositeElement extends i18n(HTMLElement) {
  render() {
    return html`${bind(this /* bound to 'composite-element' */)}
      <div>composite element with ${getMessage()}</div>
      <extended-element></extended-element>`;
  }
}
const binding = bind('get-message', import.meta); // bound to a pseudo-element name
const getMessage = () => html`${'get-message', binding}<span>get message</span>`;
  • Each HTML template literal is bound to its (pseudo-)element name
  • Fetch JSON for locale resources at locales/{element-name}.{locale}.json

Get Started

  • Branch https://github.com/t2ym/i18n-element/tree/lit-html
    • Now merged into master branch
  • Tag https://github.com/t2ym/i18n-element/tree/3.0.0-rc.1
  • Install
    • npm install i18n-element@3.0.0-rc.1 or npm install i18n-element
      • with lit-html@^1.0.0
  • Import
    • import { html, i18n, bind } from 'i18n-element/i18n.js'
  • Demo at URL path /demo/preprocess/
    • Src demo/clock/
    • Dev Server
      • polymer serve --npm --module-resolution=node
    • Static Server
      • polymer build
        • build/{esm-unbundled|esm-bundled|es6-bundled|es5-bundled}
    • I18N Automation
      • npm run demo
        • I18N processing
      • npm run demo -- locales --targets="de es fr ja zh-Hans"
        • Add locales (npm run demo must be run again after this command)
  • Under investigation: Corrupted package-lock.json issue
    • npm may unexpectedly OVERRIDE CUSTOM PACKAGES FROM GitHub with official ones in npm install on certain conditions. Custom packages in package-lock.json must be checked after each npm install operation.
    • Once package-lock.json is corrupted, npm install --save-dev github:t2ym/espree#es2018 github:t2ym/escodegen#es2018 should fix the corruption.
    • If package-lock.json is not corrupted, npm ci is strongly recommended for dependency installation

Example Project - PWA Starter Kit with I18N

Status

  • Using i18n-behavior/i18n-behavior.js 3.0.0-rc.2
  • Fetching JSON resources for locales from the server
    • Mixing in methods from BehaviorsStore._I18nBehavior
  • Not dependent on LitElement but compatible with LitElement
  • Each HTML template is bound to its unique Binding via bind() pseudo-directive
    • Bound to this: ${bind(this)}
      • The element class must be named in CamelCasing to generate the corresponding custom element name like camel-casing
    • Bound to a unique name: ${bind('unique-pseudo-element-name', import.meta)}
      • If the binding is stored in a separate variable like binding, the pseudo-directive has to be a sequence expression starting with a literal string for the name so that static preprocessor can easily detect the bound name for the template
    • Bound to this with a unique name: ${bind(this, 'this-element-name')}
      • The element class can be extended and render() can be used by the extended class as ${super.render()}
    • 'lang-updated' event can be captured via bind().element.addEventListener('lang-updated',...)
  • Automated I18N process
    • cd demo; ../node_modules/.bin/gulp
    • Extract JSON from HTML template literals in JavaScript code
    • Preprocessing HTML template literals in JavaScript at build time
      • Put under /demo/preprocess/
      • Prepend @license comments to preprocessed JavaScripts
    • Add locales by npm run demo -- locales --targets="de es fr ja zh-Hans"
    # populate empty locales/*.{de|es|fr|ja|zh-Hans}.json
    # existing JSON files are not touched
    npm run demo -- locales --targets="de es fr ja zh-Hans"
    # merge en resources into localized ones
    npm run demo
    # translate demo/xliff/bundle.{de|es|fr|ja|zh-Hans}.xlf
    # merge XLIFF translations into localized JSON files
    npm run demo
  • Optimization
    • Status: With cached templates, html process for a non-preprocessed raw template literal is as fast as its corresponding preprocessed one after the first call of html.
      • Actually, it is slightly faster than the preprocessed sources due to extra strings.shift(); parts.shift();
    • Caching preprocessed templates
      • preprocessedStrings and preprocessedParts are cached
    • Cache uncamel-cased element-names for UncamelCase()
    • Avoid redundant boundElement.lang updates
  • templateDefaultLang
    • Appreciate binding.element.templateDefaultLang, defaulting to <html lang>, then en
  • Implement fire()
  • No decent error handling
    • Just throwing errors
  • Adaptive polyfill of attributeChangedCallback
  • Tests
    • Tests on Travis CI and Sauce Labs are unstable on weekdays, while they are pretty stable on weekends when the cloud services are not loaded.
browser raw preprocessed bundled coverage
Chrome 72 ✔︎ ✔︎ ✔︎ ✔︎
Firefox 65 ✔︎ ✔︎ ✔︎ ✔︎
Safari 12 ✔︎ ✔︎ ✔︎ ✔︎
Safari 9 ✔︎ ✔︎ ✔︎ ✔︎
Edge 17 ✔︎ ✔︎ ✔︎ ✔︎
IE 11 ✔︎ ✔︎ ✔︎ ✔︎
  • Primitive lit-html syntax handling
    • @event=${function handler(event) {...}}
    • .property=${property_value}
    • attribute=${attribute_value}
    • ?boolean-attribute=${true_or_false}
  • Patching Polymer template syntax for attributes
    • attr$="${attr}" -> attr="${attr}"
  • Inline SVG can be kept from preprocessing by quoting <svg>...</svg> with ${svg`...`}
  • polyserve --npm --module-resolution=node

Preprocessed HTML Templates

const binding = bind('get-message', import.meta);
html([
    '<!-- localizable -->',
    '<div>',
    '</div><div>',
    '</div>'
  ], ...bind(('get-message', binding), (_bind, text, model, effectiveLang) => [
    _bind,
    text['div'],
    getMutatingMessage()
  ], {
    'meta': {},
    'model': {},
    'div': 'message'
  }));
html([
      '<!-- localizable -->',
      '\n      <style>\n        :host {\n          display: block;\n          width: 100%;\n        }\n        world-clock {\n          display: flow;\n          max-width: 300px;\n        }\n      </style>\n      <div>',
      '</div>\n      ',
      '\n    '
    ], ...bind(this, (_bind, text, model, effectiveLang) => [
      _bind,
      text['div_1'],
      repeat(this.timezones, item => item, (item, index) => html`<world-clock .timezone=${ item }></world-clock>`)
    ], {
      'meta': {},
      'model': {},
      'div_1': 'World Clocks'
    }));
html([
      '<!-- localizable -->',
      '\n      <style>\n        :host {\n          display: block;\n          width: 100%;\n          max-width: 350px;\n          padding: 2px;\n        }\n      </style>\n      <div><i18n-format lang="',
      '"><span>',
      '</span><span slot="1">',
      '</span><button @click="',
      '" slot="2">',
      '</button><button @click="',
      '" slot="3">',
      '</button></i18n-format></div>\n      ',
      '\n    '
    ], ...bind(this, 'world-clock', (_bind, text, model, effectiveLang) => [
      _bind,
      effectiveLang,
      text['div_1']['0'],
      (this.timezone < 0 ? '' : '+') + this.timezone / 60,
      () => this.timezone -= 60,
      text['div_1']['2'],
      () => this.timezone += 60,
      text['div_1']['3'],
      super.render()
    ], {
      'meta': {},
      'model': {},
      'div_1': [
        ' Timezone: GMT{1}\n        {2}\n        {3} ',
        '{{parts.0}}',
        '-1h',
        '+1h'
      ]
    }));
html([
      '<!-- localizable -->',
      '\n      <style>\n        :host {\n          display: block;\n        }\n        .square {\n          position: relative;\n          width: 100%;\n          height: 0;\n          padding-bottom: 100%;\n        }\n        \n        svg {\n          position: absolute;\n          width: 100%;\n          height: 100%;\n        }\n        \n        .clock-face {\n          stroke: #333;\n          fill: white;\n        }\n        \n        .minor {\n          stroke: #999;\n          stroke-width: 0.5;\n        }\n        \n        .major {\n          stroke: #333;\n          stroke-width: 1;\n        }\n        \n        .hour {\n          stroke: #333;\n        }\n        \n        .minute {\n          stroke: #666;\n        }\n        \n        .second, .second-counterweight {\n          stroke: rgb(180,0,0);\n        }\n        \n        .second-counterweight {\n          stroke-width: 3;\n        }\n      </style>\n      <div id="target" @click="',
      '" .property="',
      '" attr="',
      '" ?enabled-boolean-attr="',
      '" ?disabled-boolean-attr="',
      '" i18n-target-attr="',
      '"><i18n-format lang="',
      '"><span>',
      '</span><span slot="1">',
      '</span><span slot="2">',
      '</span></i18n-format></div>\n      <div>',
      '</div>\n      <div class="square"> <!-- so the SVG keeps its aspect ratio -->\n        \n        <svg viewBox="0 0 100 100">\n          \n          <!-- first create a group and move it to 50,50 so\n              all co-ords are relative to the center -->\n          <g transform="translate(50,50)">\n            <circle class="clock-face" r="48"></circle>\n            <g>',
      '</g><!-- g tag to avoid i18n-format conversion -->\n            <g>',
      '</g><!-- g tag to avoid i18n-format conversion -->\n\n            <!-- hour hand -->\n            <line class="hour" y1="2" y2="-20" transform="rotate(',
      ')"></line>\n    \n            <!-- minute hand -->\n            <line class="minute" y1="4" y2="-30" transform="rotate(',
      ')"></line>\n    \n            <!-- second hand -->\n            <g transform="rotate(',
      ')">\n              <line class="second" y1="10" y2="-38"></line>\n              <line class="second-counterweight" y1="10" y2="2"></line>\n            </g>\n          </g>\n        </svg>\n      </div>\n    '
    ], ...bind(this, 'lit-clock', (_bind, text, model, effectiveLang) => [
      _bind,
      event => {
        let div = event.composedPath().filter(el => el.id === 'target')[0];
        alert('div.outerHTML = ' + div.outerHTML + ' div.property = ' + div.property + ' div.getAttribute("attr") = ' + div.getAttribute('attr') + ' div.getAttribute("i18n-target-attr") = ' + div.getAttribute('i18n-target-attr'));
      },
      'property value',
      'attr value',
      true,
      false,
      model['target']['i18n-target-attr'],
      effectiveLang,
      text['target']['0'],
      this.date.getHours(),
      this.date.getMinutes(),
      getMessage(),
      minuteTicks,
      hourTicks,
      30 * this.date.getHours() + this.date.getMinutes() / 2,
      6 * this.date.getMinutes() + this.date.getSeconds() / 10,
      6 * this.date.getSeconds()
    ], {
      'meta': {},
      'model': { 'target': { 'i18n-target-attr': 'I18N target attribute value' } },
      'target': [
        'Time: {1}:{2}',
        '{{parts.5}}',
        '{{parts.6}}'
      ]
    }));

Issues

  • Separate polyfill.js module
  • Redundant attributeChangedCallback calls
    • Edge/IE/Safari9: Both custom element v1 polyfill and i18n class polyfill call it
    • Avoid unnecessary assignments of lang
    • Avoid 2 calls on attributedChangedCallback via setAttribute() for polyfilled custom elements v1
    • Avoid 2 calls on attributedChangedCallback via setAttributeNS() for polyfilled custom elements v1
  • templateDefaultLang is ineffective
  • _I18nBehavior.templateDefaultLang is unexpectedly set
    • Fix: Set a proper this object for _constructDefaultBundle() calls
  • fire() is not fully compatible with that in Polymer library.
    • Fix: Emulate fire() of Polymer library precisely
  • package-lock.json can be easily corrupted by some npm install commands
    • Custom github:t2ym/espree#es2018 and github:t2ym/escodegen#es2018 are NOT installed although they are clearly specified in package.json and corrupted package-lock.json is generated with other official versions of espree and escodegen
      • Fix: Manually install custom packages to update node_modules/ and package-lock.json
      • Fix: npm ci with the fixed package-lock.json
        • Under investigation: npm may unexpectedly OVERRIDE CUSTOM PACKAGES FROM GitHub with official ones in npm install in certain conditions. Custom packages in package-lock.json must be checked after each npm install operation.
  • Support compound formats for <i18n-format>
    • Fix regression in the changes
    • {{serialize(text.id.0)}} is converted to ${JSON.stringify(text['id']['0'])}
    • i18n-behavior@3.0.0-pre.18 or later is required
    • i18n-format@3.0.0-pre.12 or later is required
    • i18n-number@3.0.0-pre.9 or later is required
    • Example: In demo/clock/clock.js
      • Note: Changes in this.timezones are not propagated to UI for now.
      <i18n-format>
        <json-data>{
          "0": "No timezones",
          "1": "Only 1 timezone for {2} is shown.",
          "one": "{1} timezone other than {2} is shown.",
          "other": "{1} timezones other than {2} are shown."
        }</json-data>
        <i18n-number offset="1">${this.timezones.length}</i18n-number>
        <span>${'GMT' + (this.timezones[0] < 0 ? '' : '+') + (this.timezones[0] / 60)}</span>
      </i18n-format>
  • Support compound I18N target attributes
    • Example: <div i18n-target-attr="attribute with ${param1} and ${param2}">
      • i18n-behavior preprocesses as follows
        • <div i18n-target-attr$="{{i18nFormat(model.div_1.i18n-target.attr.0,parts.1,parts.2)}}">
      • Interpret as follows
        • <div i18n-target-attr="${_bind.element.i18nFormat(model['div_1']['i18n-target-attr']['0'],param1,param2)}">
  • lang attribute/property becomes empty ("") when fallback language is ""
    • Adjust lang and effectiveLang value on lang-updated events
  • Is the prepending <!-- localizable -->${bind(this)} really required for preprocessed templates? : Yes
    • What is the concern on just handing arguments to lit-html as they are like this?
      • element._processTasks() has to be called on rendering
const binding = bind('get-message', import.meta);
html([
    '<div>',
    '</div><div>',
    '</div>'
  ], ...bind(('get-message', binding), (text, model, effectiveLang) => [
    text['div'],
    getMutatingMessage()
  ], {
    'meta': {},
    'model': {},
    'div': 'message'
  }));
  • Support Async Iterator/Generator
    • Use npm install --save-dev github:t2ym/espree#es2018
    • Use npm install --save-dev github:t2ym/escodegen#es2018
  • effectiveLang is not set
    • Fix: Set efffectiveLang on lang-updated events
  • i18n-preference is not attached properly on load event
    • Fixed in i18n-behavior@3.0.0-pre.14
  • i18n-format is unexpectedly applied within inline SVG
    • Custom elements are not supported within inline SVG elements
    • Fix: i18n-format conversion must be skipped within inline SVG elements
    • Workaround 1: Surround parts in SVG by <g> tag
    • Workaround 2: Surround <svg>...</svg> tag with ${svg...}
  • Safari 9
    • Under investigation: Some tests are failing
      • Fix: Polyfill attributeChangedCallback() on Safari 9
    • Multiple instances but the first one do not fall back to the next fallback language at the first load
      • Fix: Call _processTasks() for multiple instances as well as the first one
  • Safari 12
    • Under investigation: Regression <world-clock> does not observe html.lang at the first load. If html.lang is manually updated, it mirrors the lang value.
      • Fix: If this.lang is en and previous value is empty, it follows the html.lang value
  • Edge 18
    • Edge 18 passes all tests if super.attributeChangedCallback is checked before typeof super.attributeChangedCallback === 'function' is checked
      • Root Cause: typeof super.attributeChangedCallback === 'function' even when super.attributeChangedCallback === undefined, which is controversial but actual with Edge 18
    • Under investigation: Some tests on attributes are failing
      • Edge 17 hangs up and reloads the page just after <simple-attribute-element>'s shadowRoot is accessed
        • Fix for Edge 18 also fixes the hang-up issue on Edge 17
      • Workaround: Treat Edge as ES5 browser to pass tests
        • gulp patch-browser-capabilities before testing
    • The values of transform attributes in SVG are unexpectedly set as none instead of variables
      • Fix: Temporarily substitute transform= for x-transform-x= to avoid the issue
    • attributeChangedCallback is not called
      • Fix: Polyfill attributeChangedCallback with MutationObserver
  • IE 11
    • observedAttributes is an empty []
      • Root Causes
        • Set in IE 11 does not support new Set(['set', 'of', 'initial', 'values'])
        • Set in IE 11 does not support iterators
      • Fix: Use new Set() and [].concat(super.observedAttributes || []) and set.add()
        • i18n.js
        • clock.js
        • test/{element classes}.js
      • Fix: Polyfill Set class constructor, iterator, keys, values
      • Fix: Polyfill Array.from(), which is required for Set object iteration in [...obj]
      • Fix: Polyfill Map class in sync with Set class
        • Polyfill constructor, iterator, keys, values, entries
    • MutationObserver.observe's attributeFilter: [] enables all attributes instead of no attributes
      • Fix: set observedAttributes properly
    • IE 11 does not support new.target
      • Fix: this.constructor is acceptable
    • IE 11 does not support [].includes(s)
      • Fix: [].indexOf(s) >= 0
    • IE 11 does not pass special characters in attribute names
      • @click .prop ?boolean are invalid attribute names
        • Fix: preprocess these attribute names
    • @webcomponents/webcomponentsjs/webcomponents-{loader|bundle}.js does not polyfill these missing properties in IE 11
      • Fix: Polyfill them
        • DocumentFragment.prorotype.children
        • SVGElement.prototype.children
    • @webcomponents/webcomponentsjs/webcomponents-{loader|bundle}.js does not polyfill attributeChangedCallback() properly in IE 11
      • attributeChangedCallback() is not called on this.lang mutation in IE 11
        • Fix for 'lang' : Explicitly call it on 'lang-updated'
        • More General Polyfill Fix for any attribute changes

t2ym added a commit that referenced this issue Jan 5, 2019
t2ym added a commit that referenced this issue Jan 6, 2019
t2ym added a commit that referenced this issue Jan 6, 2019
t2ym added a commit that referenced this issue Jan 6, 2019
…ct the variable name as "nameFromPath" for Polymer 3 elements
t2ym added a commit that referenced this issue Jan 6, 2019
…#61 Preprocess HTML templates in <template id> for "thin" syntax
t2ym added a commit that referenced this issue Jan 6, 2019
@t2ym t2ym changed the title [Polymer 3.0] Support LitElement [3.0] Support lit-html and LitElement Jan 6, 2019
t2ym added a commit that referenced this issue Jan 8, 2019
t2ym added a commit that referenced this issue Jan 10, 2019
t2ym added a commit that referenced this issue Jan 10, 2019
t2ym added a commit that referenced this issue Jan 10, 2019
… relocatable by require.resolve("i18n-behavior/i18n-attr-repo.js")
t2ym added a commit that referenced this issue Jan 10, 2019
t2ym added a commit that referenced this issue Jan 11, 2019
t2ym added a commit that referenced this issue Jan 11, 2019
t2ym added a commit that referenced this issue Jan 15, 2019
t2ym added a commit that referenced this issue Jan 16, 2019
t2ym added a commit that referenced this issue Jan 16, 2019
t2ym added a commit that referenced this issue Jan 16, 2019
…emo/gulpfile.js to handle hybrid apps properly
t2ym added a commit that referenced this issue Jan 16, 2019
t2ym added a commit that referenced this issue Jan 19, 2019
t2ym added a commit that referenced this issue Jan 20, 2019
t2ym added a commit that referenced this issue Jan 21, 2019
t2ym added a commit that referenced this issue Feb 17, 2019
t2ym added a commit that referenced this issue Feb 18, 2019
t2ym added a commit that referenced this issue Feb 18, 2019
…first rendering as well as the first one
t2ym added a commit that referenced this issue Feb 19, 2019
t2ym added a commit that referenced this issue Feb 19, 2019
…gedCallback() calls via setAttribute() on polyfilled custom elements v1
t2ym added a commit that referenced this issue Feb 19, 2019
t2ym added a commit that referenced this issue Feb 19, 2019
t2ym added a commit that referenced this issue Feb 20, 2019
t2ym added a commit that referenced this issue Feb 20, 2019
…y by truthiness to work around strange behaviors on Edge 17/18
t2ym added a commit that referenced this issue Feb 20, 2019
… truthiness to work around strange Edge issues
@t2ym t2ym closed this as completed in f4c4726 Feb 21, 2019
@t2ym t2ym unpinned this issue Feb 25, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant