Get unlimited public & private packages + team-based management with npm Teams.Learn more »

trans-render

0.0.142 • Public • Published

trans-render

Published on webcomponents.org

Actions Status

Size of web component, with all optional processors included:

Yes, there is an actual web component in this package. However, it won't make sense unless the core functions described first are (at least partly) understood.

trans-render provides an alternative way of instantiating a template. It draws inspiration from the (least) popular features of XSLT. Like XSLT, trans-render performs transforms on elements by matching tests on elements. Whereas XSLT uses XPath for its tests, trans-render uses css path tests via the element.matches() and element.querySelector() methods.

XSLT can take pure XML with no formatting instructions as its input. Generally speaking, the XML that XSLT acts on isn't a bunch of semantically meaningless div tags, but rather a nice semantic document, whose intrinsic structure is enough to go on, in order to formulate a "transform" that doesn't feel like a hack.

Likewise, with the advent of custom elements, the template markup will tend to be much more semantic, like XML. trans-render tries to rely as much as possible on this intrinisic semantic nature of the template markup, to give enough clues on how to fill in the needed "potholes" like textContent and property setting. But trans-render is completely extensible, so it can certainly accommodate custom markup (like string interpolation, or common binding attributes) by using additional, optional helper libraries.

This leaves the template markup quite pristine, but it does mean that the separation between the template and the binding instructions will tend to require looking in two places, rather than one. And if the template document structure changes, separate adjustments may be needed to make the binding rules in sync. Much like how separate style rules often need adjusting when the document structure changes.

Advantages

By keeping the binding separate, the same template can thus be used to bind with different object structures.

Providing the binding transform in JS form inside the init function signature has the advantage that one can benefit from TypeScript typing of Custom and Native DOM elements with no additional IDE support.

Another advantage of separating the binding like this, is that one can insert comments, console.log's and/or breakpoints, in order to walk through the binding process.

For more musings on the question of what is this good for, please see the rambling section below.

NB It's come to my attention (via template discussions found here) that there are some existing libraries which have explored similar ideas:

  1. pure-js
  2. weld

Workflow

trans-render provides helper functions for cloning a template, and then walking through the DOM, applying rules in document order. Note that the document can grow, as processing takes place (due, for example, to cloning sub templates). It's critical, therefore, that the processing occur in a logical order, and that order is down the document tree. That way it is fine to append nodes before continuing processing.

Drilling down to children

For each matching element, after modifying the node, you can instruct the processor which node(s) to consider next.

Most of the time, especially during initial development, you won't need / want to be so precise about where to go next. Generally, the pattern, as we will see, is just to define transform rules that match the HTML Template document structure pretty closely.

So, in the example we will see below, this notation:

const Transform = {
    details: {
        summary: x => model.summaryText
    }
};

means "if a node has tag name "details", then continue processing the next siblings of details, but also, find the first descendent of the node that has tag name "summary", and set its textContent property to model.summaryText."

If most of the template is static, but there's a deeply nested element that needs modifying, it is possible to drill straight down to that element by specifying a "Select" string value, which invokes querySelector. But beware: there's no going back to previous elements once that's done. If your template is dense with dynamic pockets, you will more likely want to navigate to the first child by setting Select = '*'.

So the syntax shown above is equivalent to:

const Transform = {
    details: {
        Select: 'summary',
        Transform: {
            summary: x => model.summaryText
        }
    }
};

In this case, the details property is a "NextStep" JS Object.

Clearly, the first example is easier, but you need to adopt the second way if you want to fine tune the next processing steps.

Matching next siblings

We most likely will also want to check the next siblings down for matches. Previously, in order to do this, you had to make sure "matchNextSibling" was passed back for every match. But that proved cumbersome. The current implementation checks for matches on the next sibling(s) by default. You can halt going any further by specifying "SkipSibs" in the "NextStep" object, something to strongly consider when looking for optimization opportunities.

It is deeply unfortunate that the DOM Query Api doesn't provide a convenience function for finding the next sibling that matches a query, similar to querySelector. Just saying. But some support for "cutting to the chase" laterally is also provided, via the "NextMatch" property in the NextStep object.

At this point, only a synchronous workflow is provided.

Syntax Example:

<template id="sourceTemplate">
    <details>
        ...
        <summary></summary>
        ...
    </details>
</template>
<div id="target"></div>
<script type="module">
    import { init } from '../init.js';
    const model = {
        summaryText: 'hello'
    }
    const Transform = {
        details: {
            summary: x => model.summaryText
        }
    };
    init(sourceTemplate, { Transform }, target);
</script> 

Produces

<div id="target">
    <details>
        ...
        <summary>hello</summary>
        ...
    </details>
</div>

Or even simpler, your transform can hardcode some values:

<template id="sourceTemplate">
    <details>
        ...
        <summary></summary>
        ...
    </details>
</template>
<div id="target"></div>
<script type="module">
    import { init } from '../init.js';
    const Transform = {
        details: {
            summary: 'Hallå'
        }
    };
    init(sourceTemplate, { Transform }, target);
</script> 

produces:

<div id="target">
    <details>
        ...
        <summary>Hallå</summary>
        ...
    </details>
</div>

"target" is the HTML element we are populating. The transform matches can return a string, which will be used to set the textContent of the target. Or the transform can do its own manipulations on the target element, and then return a "NextStep" object specifying where to go next, or it can return a new Transform, which will get applied the first child by default.

Note the unusual property name casing, in the JavaScript arena for the NextStep object: Transform, Select, SkipSibs, etc. As we will see, this pattern is to allow the interpreter to distinguish between css matches for a nested Transform, vs a "NextStep" JS object.

Conditional Display

If a matching node returns a boolean value of false, the node is removed. For example:

...
"section[data-type='attributes']":({ target, ctx}) => {
    const attribs = tags[idx].attributes;
    if (attribs === undefined) return false;
    return {
        details: {
            dl: ({ target, ctx}) => {
                repeat(attributeItemTemplate, ctx, attribs.length, target, {
                    dt: ({ idx }) => attribs[Math.floor(idx / 2)].name,
                    dd: ({ idx }) => ({
                        'hypo-link[data-bind="description"]': attribs[Math.floor(idx / 2)].description,
                    }) 
                } as TransformRules);
            }
        }
    }
},
...

Here the tag "section" will be removed if attributes is undefined.

NB: Be careful when using this technique. Once a node is removed, there's no going back -- it will no longer match any css if you use trans-render updating. If your use of trans-render is mostly to display something once, and you recreate everything from scratch when your model changes, that's fine. However, if you want to apply incremental updates, and need to display content conditionally, it would be better to use a custom element for that purpose.

What does wdwsf stand for?

As you may have noticed, some abbreviations are used by this library:

  • init = initialize
  • ctx = (rendering) context
  • idx = (numeric) index of array
  • SkipSibs = Skip Siblings
  • attribs = attributes
  • props = properties
  • refs = references

Use Case 1: Applying the DRY principle to (post) punk rock lyrics

Example 1a

Demo

Demonstrates including sub templates.

If you are here, the demo appears below:

Note the transform rule:

Transform: {
    '*': {
        Select: '*'
    },

"*" is a match for all css elements. What this is saying is "for any element regardless of css-matching characteristics, continue processing its first child (Select => querySelector). This, combined with the default setting to match all the next siblings means that, for a "sparse" template with very few pockets of dynamic data, you will be doing a lot more processing than needed, as every single HTMLElement node will be checked for a match. But for initial, pre-optimization work, this transform rule can be a convenient way to get things done more quickly.

Example 1b

Demo

Demonstrates use of update, rudimentary interpolation, recursive select.

Reapplying (some) of the transform

Often, we want to reapply a transform, after something changes -- typically the source data.

The ability to do this is illustrated in the previous example. Critical syntax shown below:

<script type="module">
    import { init } from '../init.js';
    import { interpolate } from '../interpolate.js';
    import {update} from '../update.js';
    const ctx = init(Main, {
        model:{
            Day1: 'Monday', Day2: 'Tuesday', Day3: 'Wednesday', Day4: 'Thursday', Day5: 'Friday',
            Day6: 'Saturday', Day7: 'Sunday',
        },
        interpolate: interpolate,
        $: id => window[id],
    }, target);
    changeDays.addEventListener('click', e=>{
        ctx.model = {
            Day1: 'måndag', Day2: 'tisdag', Day3: 'onsdag', Day4: 'torsdag', Day5: 'fredag',
            Day6: 'lördag', Day7: 'söndag',
        }
        update(ctx, target);
    })
</script> 

Loop support (NB: Not yet optimized)

The next big use case for this library is using it in conjunction with a virtual scroller. As far as I can see, the performance of this library should work quite well in that scenario.

However, no self respecting rendering library would be complete without some internal support for repeating lists. This library is no exception. While the performance of the initial list is likely to be acceptable, no effort has yet been made to utilize state of the art tricks to make list updates keep the number of DOM changes at a minimum.

Anyway the syntax is shown below. What's notable is a sub template is cloned repeatedly, then populated using the simple init / update methods.

<div>
    <template id="itemTemplate">
    <li></li>
    </template>
    <template id="list">
    <ul id="container"></ul>
    <button id="addItems">Add items</button>
    <button id="removeItems">Remove items</button>
    </template>
    <div id="target"></div>
 
    <script type="module">
    import { init } from '../init.js';
    import { repeat} from '../repeat.js';
    import {update} from '../update.js';
    const options = {matchNext: true};
    const itemTransform = {
        li: ({ idx }) => 'Hello ' + idx,
    };
    const ctx = init(list, {
        Transform: {
            ul: ({ target, ctx }) =>  repeat(itemTemplate, ctx, 10, target, itemTransform)
        }
    }, target, options);
    ctx.update = update;
    addItems.addEventListener('click', e => {
        repeat(itemTemplate, ctx, 15, container, itemTransform);
    });
    removeItems.addEventListener('click', e =>{
        repeat(itemTemplate, ctx, 5, container);
    })
    </script> 
</div>

Simple Template Insertion

A template can be inserted directly inside the target element as follows:

<template id="summaryTemplate">
My summary Text
</template>
<template id="sourceTemplate">
    <details>
        ...
        <summary></summary>
        ...
    </details>
</template>
<div id="target"></div>
<script type="module">
    import { init } from '../init.js';
    const model = {
    const Transform = {
        details: {
            summary: summaryTemplate
        }
    };
    init(sourceTemplate, { Transform }, target);
</script>

Multiple matching with "Ditto" notation

Sometimes, one rule will cause the target to get (new) children. We then want to apply another rule to process the target element, now that the children are there.

But uniqueueness of the keys of the JSON-like structure we are using prevents us from listing the same match expression twice.

We can specify multiple matches as follows:

<script type="module">
    import { init } from '../init.js';
    const model = {
    const Transform = {
        details: {
            summary: summaryTemplate,
            '"': ({target}) => ...,
            '""': ...,
            '"3': ...
        }
    };
    init(sourceTemplate, { Transform }, target);
</script>

I.e. any selector that starts with a double quote (") will use the last selector that didn't.

Ramblings From the Department of Faulty Analogies

When defining an HTML based user interface, the question arises whether styles should be inlined in the markup or kept separate in style tags and/or CSS files.

The ability to keep the styles separate from the HTML does not invalidate support for inline styles. The browser supports both, and probably always will.

Likewise, arguing for the benefits of this library is not in any way meant to disparage the usefulness of the current prevailing orthodoxy of including the binding / formatting instructions in the markup. I would be delighted to see the template instantiation proposal, with support for inline binding, added to the arsenal of tools developers could use. Should that proposal come to fruition, this library, hovering under 1KB, would be in mind-share competition (my mind anyway) with one that is 0KB, with the full backing / optimization work of Chrome, Safari, Firefox. Why would anyone use this library then?

And in fact, the library described here is quite open ended. Until template instantiation becomes built into the browser, this library could be used as a tiny stand-in. Once template instantiation is built into the browser, this library could continue to supplement the native support (or the other way around, depending.)

For example, in the second example above, the core "init" function described here has nothing special to offer in terms of string interpolation, since CSS matching provides no help:

<div>Hello {{Name}}</div>

We provide a small helper function "interpolate" for this purpose, but as this is a fundamental use case for template instantiation, and as this library doesn't add much "value-add" for that use case, native template instantiation could be used as a first round of processing. And where it makes sense to tightly couple the binding to the template, use it there as well, followed by a binding step using this library. Just as use of inline styles, supplemented by css style tags/files (or the other way around) is something seen quite often.

A question in my mind, is how does this rendering approach fit in with web components (I'm going to take a leap here and assume that HTML Modules / Imports in some form makes it into browsers, even though I think the discussion still has some relevance without that).

I think this alternative approach can provide value, by providing a process for "Pipeline Rendering": Rendering starts with an HTML template element, which produces transformed markup using init or native template instantiation. Then consuming / extending web components could insert additional bindings via the CSS-matching transformations this library provides.

To aid with this process, the init and update functions provide a rendering options parameter, which contains an optional "initializedCallback" and "updatedCallback" option. This allows a pipeline processing sequence to be set up, similar in concept to Apache Cocoon.

NB In re-reading the template instantiation proposal with a fresh set of eyes, I see now that there has in fact been some careful thought given to the idea of providing a kind of pipeline of binding. And as mentioned above, this library provides little help when it comes to string interpolation, so the fact that the proposal provides some hooks for callbacks is really nice to see.

I may not yet fully grasp the proposal, but it still does appear to me that the template instantiation proposal is only useful if one defines regions ahead of time in the markup where dynamic content may go.

This library, on the other hand, considers the entire template document open for amendment. This may be alarming, if as me, you find yourself comparing this effort to the ::part ::theme initiative, where authors need to specify which elements can be themed.

However, the use case is quite different. In the case of stylesheets, we are talking about global theming, affecting large numbers of elements at the same time. The use case I'm really considering is one web component extending another. I don't just mean direct class inheritance, but compositional extensions as well. It doesn't seem that unreasonable to provide maximum flexibility in that circumstance. Yes, I suppose the ability to mark some tags as "undeletable / non negotiable" might be nice, but I see no way to enforce that.

Client-side JS faster than SSR?

Another interesting case to consider is this Periodic Table Codepen example. Being what it is, it is no suprise that there's a lot of repetitive HTML markup needed to define the table.

An intriguing question, is this: Could this be the first known scenario in the history of the planet, where rendering time (including first paint) would be improved rather than degraded with the help of client-side JavaScript?

The proper, natural instinct of a good modern developer, including the author of the codepen, is to generate the HTML from a concise data format using a server-side language (pug).

But using this library, and cloning some repetitive templates on the client side, reduces download size from 16kb to 14kb, and may improve other performance metrics as well. These are the performance results my copy of chrome captures, after opening in an incognito window, and throttling cpu to 6x and slow 3g network.

Trans-Rendering:

Trans Rendered

Original:

Original

You can compare the two here: This link uses client-side trans-rendering. This link uses all static html

Results are a bit unpredictable, and usually the differences are less dramatic.

Lighthouse scrores also provide evidence that trans-rendering improves performance.

Trans-Rendering:

Trans Rendered Lighthouse

Original:

Original Lighthouse

Once in a while the scores match, but most of the time the scores above are what is seen.

So the difference isn't dramatic, but it is statistically significant, in my opinion.

See this side-by-side comparison for more evidence of the benefits.

Miscellaneous Helper Functions

insertAdjacentTemplate(template: HTMLTemplateElement, target: Element, position: InsertPosition)

This function is modeled after insertAdjacentElement / insertAdjacentHTML. Only here we are able to insert a template. By using the preferred "afterEnd" as the insert position, the trans-rendering will be able to process those nodes like any other nodes.

Declative-ish property setting

Object.assign and its modern abbreviated variations, provides a quite declarative feeling when populating an object with values. Unfortunately, Object.assign can't be used to recursively set read-only properties like style and dataset (are there others?). An alternative to object.assign are convenience functions like JQuery.extends, JQuery.attr and "h", which domMerge draws inspiration from.

The function domMerge provides similar help.

The (tentative) signature is

export function domMerge(target: HTMLElement, vals: Vals): void

where

export interface Vals {
  attribs?: { [key: string]: string | boolean | number };
  propVals?: object;
}

Behavior enhancement

Vue (with common roots from Polymer 1) provides an elegant way of turning an existing DOM element into a kind of anonymous custom element. The alternative to this is the "is" built-in custom element api, which, while implemented in two of the three major browsers, remains strongly opposed by the third, and the reasons seem, to my non-expert ears, to have some merit.

Even if the built-ins do become a standard, I still think the "decorate" function, described below, would come in handy for less formal occasions.

Tentative Signature:

export function decorate(
  target: HTMLElement,
  source: DecorateArgs
)

where

export interface DecorateArgs extends Vals{
    propDefs?: object,
    methods?: {[key: string] : Function},
    on?: {[key: string] : (e: Event) => void},
}

For example:

    <div id="decorateTest">
        <button>Test</button>
    </div>
    <script type="module">
        import {decorate} from '../decorate.js';
        import {init, attribs} from '../init.js';
        init(decorateTest, {
            Transform: {
                div: {
                    button: ({target}) => decorate(target, {
                        propVals:{
                            textContent: 'Hello',
                        },
                        attribs: {
                            title: 'Hello, world'
                        },
                        propDefs:{
                            count: 0
                        },
                        on:{
                            click: function(e){
                                this.count++;
                            }
                        },
                        methods:{
                            onPropsChange(){
                                alert(this.count)
                            }
                        }
                    })
                }
            }            
        })
    </script> 
 

decorate can also attach behaviors to custom elements, not just native elements, in a decorative way.

Avoiding namespace collisions

Reflections on the Revolutionary Extensible Web Manifesto NB: All names, characters, and incidents portrayed in the following discussion are fictitious. No identification with actual persons (living or deceased), places, buildings, and products is intended or should be inferred. No person or entity associated with this discussion received payment or anything of value, or entered into any agreement, in connection with the depiction of tobacco products. No animals were harmed in formulating the points discussed below.
In a web-loving land, there was a kingdom that held sway over a large portion of the greatest minds, who in turn guided career choices of the common folk. The kingdom's main income derived from a most admirable goal -- keeping friends and family in touch. The kingdom was ruled by conservatives. "Edmund Burke" conservatives, who didn't see the appeal of allowing heretics to join freely in their kingdom. They were tolerant, mind you. If you were not a tax-paying subject born to a family of the kingdom, i.e. a heretic, and you wanted to visit their kingdom, you could do so. You only had to be heavily surrounded by guards, who would translate what you had to say, and vice versa, into Essex, the de-facto language of the web, according to the kingdom's elites.

The heretics called these conservatives unflattering words like "reactionaries."

"Why can't we speak directly to your subjects? What are you afraid of?" the counter-cultural heretics would plead.

The ruling elites countered with fancy words like "heuristics" and "smoosh." "We've put our greatest minds to the problem, and, quite frankly, they're stumped. We don't see how we can let you speak freely without corrupting the language of the web. The web rules over all of us, and what if the web wants to introduce an attribute that is already in heavy use? What are we to do then? Don't you see? We are the true lovers of the web. We are protecting the web, so it can continue to evolve and flourish."

Which all sounded like a good faith argument. But why, at least one heretic thought, has the main web site used to bind family and friends together introduced the following global constants, which surely could cause problems if the web wanted to evolve:

A subset of global constants.
facebook
meta_referrer
pageTitle
u_0_11
u_0_11
u_0_11
u_0_11
u_0_11
u_0_12
u_0_13
u_0_14
u_0_15
u_0_16
u_0_17
pagelet_bluebar
blueBarDOMInspector
login_form
email
pass
loginbutton
u_0_2
u_0_3
u_0_4
lgnjs
locale
prefill_contact_point
prefill_source
prefill_type
globalContainer
content
reg_box
reg_error
reg_error_inner
reg
reg_form_box
fullname_field
u_0_b
u_0_c
u_0_d
u_0_e
fullname_error_msg
u_0_f
u_0_g
u_0_h
u_0_i
u_0_j
u_0_k
u_0_l
u_0_m
password_field
u_0_n
u_0_o
u_0_p
u_0_q
month
day
year
birthday-help
u_0_r
u_0_s
u_0_9
u_0_a
u_0_t
terms-link
privacy-link
cookie-use-link
u_0_u
u_0_v
referrer
asked_to_login
terms
ns
ri
action_dialog_shown
reg_instance
contactpoint_label
ignore
locale
reg_captcha
security_check_header
outer_captcha_box
captcha_box
captcha_response_error
captcha
captcha_persist_data
captcha_response
captca-recaptcha
captcha_whats_this
captcha_buttons
u_0_w
u_0_x
u_0_y
reg_pages_msg
u_0_z
u_0_10
pageFooter
contentCurve
js_0
u_0_18
u_0_19

And why does the kingdom not want to empower its subjects to choose for themselves if this is a valid concern?

Now I do think this is a concern to consider. Focusing on the decorate functionality described above, the intention here is not to provide a formal extension mechanism, as the built-in custom element "is" extension proposal provides (and which Apple tirelessly objects to), but rather a one-time duct tape type solution. Whether adding a property to a native element, or to an existing custom element, to err on the side of caution, the code doesn't pass the property or method call on to the element it is decorating.

NB If:

  1. You are slapping properties onto an existing native HTML element, and:
  2. The existing native HTML element might, in the future, adopt properties / methods with the same name.

Then it's a good idea to consider making use of Symbols:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="decorateTest">
        <button>Test</button>
    </div>
    <script type="module">
        import {decorate, attribs} from '../decorate.js';
        import {init} from '../init.js';
        const count = Symbol('count');
        const myMethod = Symbol('myMethod');
        init(decorateTest, {
            Transform: {
                button: ({target}) => decorate(target, {
                    propVals:{
                        textContent: 'Hello',
                    },
                    attribs:{
                        title: "Hello, world"
                    }, 
                    propDefs:{
                        [count]: 0
                    },
                    on:{
                        click: function(e){
                            this[count]++;
                        }
                    },
                    methods:{
                        onPropsChange(){
                            this[myMethod]();
                        },
                        [myMethod](){
                            alert(this[count]);
                        }
                    }
                })
            }
            
            
        })
    </script> 
</body>
</html>

The syntax isn't that much more complicated, but it is probably harder to troubleshoot if using symbols, so use your best judgment. Perhaps start properties and methods with an underscore if you wish to preserve the easy debugging capabilities. You can also use Symbol.for('count'), which kind of meets halfway between the two approaches.

Even more indirection

The render context which the init function works with provides a "symbols" property for storing symbols. The transform does look a little scary at first, but hopefully it's manageable:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="decorateTest">
        <button>Test</button>
    </div>
    <script type="module">
        import {decorate} from '../decorate.js';
        import {init} from '../init.js';
        init(decorateTest, {
            symbols: {
                count: Symbol('count'),
                myMethod: Symbol('myMethod')
            },
            Transform: {
                button: ({target, ctx}) => decorate(target, {
                    propVals: {
                        textContent: 'Hello',
                    },
                    attribs:{
                            title: "Hello, world"
                    },
                    propDefs:{
                        [ctx.symbols['count']]: 0
                    },
                    on:{
                        click: function(e){
                            this[ctx.symbols['count']]++;
                        }
                    },
                    methods:{
                        onPropsChange(){
                            this[ctx.symbols['myMethod']]();
                        },
                        [ctx.symbols['myMethod']](){
                            alert(this[ctx.symbols['count']]);
                        }
                    }
                })
            }
        })
    </script> 
</body>
</html>

appendTag(container: HTMLElement, name: string, config: DecorateArgs) : HTMLElement

Just saves a tiny bit of boiler plate (document.createElement, container.appendChild)

split(target: HTMLElement, textContent: string, search: string | null | undefined)

Splits text based on search into stylable spans

replaceElementWithTemplate(target: HTMLElement, template: HTMLTemplateElement, ctx: RenderContext)

During pipeline processing, replace a tag with a template. The original tag goes into ctx.replacedElement

replaceTargetWithTag(target: TargetType, tag: string, ctx: RenderContext, preSwapCallback?: (el: ReplacingTagType) => void)

During pipeline processing, replace a tag with another tag. The original tag goes into ctx.replacedElement

pierce(el: TargetType, ctx: RenderContext, targetTransform: TransformRules)

Pierce into shadow root, and (asynchronously) apply transform rules within the shadow root.

trans-render the web component

A web component wrapper around the functions described here is available.

Example syntax

Demo

If you are here what appears next should work:

Install

npm i trans-render

DownloadsWeekly Downloads

523

Version

0.0.142

License

MIT

Unpacked Size

1.31 MB

Total Files

130

Last publish

Collaborators

  • avatar