17

I'm looking to modularize my LWC without having to embed them in the template. Ideally, would this kind of pattern be feasible? I'm not sure if it's my lack of ES6 knowledge (modules) or that LWC blocks this but this is what I'm attempting:

UtilityLWC.js

import { LightningElement, api } from 'lwc';

export default class UtilityLWC extends LightningElement {

  @api
  foo() {
    console.log('foo from UtilityLWC');
  }

  @api
  bar() {
    console.log('bar from UtilityLWC');
  }

}

ConsumerLWC.js


import { LightningElement, api, track, wire } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
import UtilityLWC from 'c/UtilityLWC'; // doesn't work
// import * as util from c/UtilityLWC; // also doesn't work
// import { foo, bar } from c/UtilityLWC; // nope

export default class ConsumerLWC extends NavigationMixin(LightningElement) {

  connectedCallback() {
    // None of these work
    UtilityLWC.foo();
    UtilityLWC.bar();
    // util.foo();
    // util.bar();
    // foo();
    // bar();
  }

}

So the easy way out is to add <c:utilityLWC> inside ConsumerLWC.html and use standard querySelector to reach inside an call foo() / bar() but is that the only way to do this?

Edit: I am aware of the general JS code re-use patterns (example in my lwc-utils repo). This is more a question of, is it possible to have that kind of pattern but also, for example, have reusable wires?

1
  • As of Spring 19, this seems to infeasible. I will revisit this every now and then because I would prefer not to use inheritance (custom mixins or extensions) OR templating to supply reusable wires. It would be really nice to import pre-made wires from a service module. Some of my wires use some JS to post-process.
    – tsalb
    Mar 7, 2019 at 20:40

4 Answers 4

13

I am not ES6+ or JS expert, but there's a section for this on the documentation - Share JavaScript Code and also there are examples on lwc-recipe for this. In general, the export works as documented for ES6 module.

From LWC documentation:

A module can export a single default function or variable.

A module can also export named functions or variables.

So it doesn't seem that you can actually export the whole class. You will need to modify your shared JS based on the approach as mentioned in the documentation, i.e., you will need to export the functions as named functions. For further details, refer to the links mentioned above.


As a quick example, below is an approach for exporting and using a named function.

Let's say I have a shared JS as below:

sharedJS.js

const mySharedHelloWorld = () => {
     return 'Hello Shared World!'; 
}

export {mySharedHelloWorld};

And that I want to use this on another component, I will go doing as below in the component.

helloWorldJS.js

import { mySharedHelloWorld} from 'c/sharedJS';

export default class MyHelloWorld extends LightningElement {

    myHelloWorld = mySharedHelloWorld();
}

helloWorld.html

{myHelloWorld}
5
  • 1
    This pattern I'm aware of (and currently use). It's a bit abstracted in my example, but it would be the first step to leveraging re-usable wires (of our own design) that don't need to be exposed as an element on the template. I have some other things in design, such as an Aura Message Broker (LWC to aura without a wrapper) that would benefit from this pattern.
    – tsalb
    Mar 6, 2019 at 22:01
  • Well with the way it is as of today, it doesn't seem that there's a direct way of exporting a class other than named functions.
    – Jayant Das
    Mar 6, 2019 at 22:12
  • Yeah that seems to be the case - I'm trying to figure out if this is a limitation of ES6 or LWC. I'll keep this thread open for a bit to see if anyone can shed insight on that.
    – tsalb
    Mar 6, 2019 at 22:19
  • 1
    Well, it seems to be enforced by LWC primarily because if you look at the export documentation for ES6 modules, it does mention a class. But in LWC, from the docs the only available options mentioned are as quoted in the answer.
    – Jayant Das
    Mar 6, 2019 at 22:20
  • 2
    It does seem to be the case this behavior is LWC specific. How unfortunate!
    – tsalb
    Mar 7, 2019 at 20:38
5

Okay, so, I was thinking about this and I couldn't find a way to expose a whole Javascript class in another component, but I did come up with a clever little bit of "syntactic sugar" to make it work like we all want. Since the import only allows methods or variables, I simply created a const variable which was an object, whose properties are the class methods. Then, I only had to export/import that variable and voila! Now we can access them like they're classes.

someService.js

const foo = () => {
     return 'foo'; 
}
const bar = () => {
     return 'bar';
}
const SomeService = {
      foo: foo,
      bar: bar,
}

export {SomeService};

app.js

import { SomeService} from 'c/someService';

export default class App extends LightningElement {

    myFoo = SomeService.foo();
    myBar = SomeService.bar();
}

app.html

<template>
{myFoo} {myBar}
</template>

Now, it's not exactly the same thing as importing a whole Javascript Class, but, it works in similar ways. I tested it and methods in the Service class can call on each other, so "Private" methods are simply methods that aren't included in the "SomeService" object. Also, you can add other vars as you like, to that object, and now you can use it just like an External Service Class.

Please feel free to improve on my answer, here's the Playground Link: https://developer.salesforce.com/docs/component-library/tools/playground/bT9aPFeTv

1
  • Ah yes, this is a known pattern when you export regular functions. I'm specifically after exporting an entire LightningElement class, because I want to import @api, @wire etc.
    – tsalb
    Jul 4, 2019 at 23:01
3

You can use a mixin or inheritance design if you want to include inherited functionality.

As a mixin:

import { api } from 'lwc';
const Utils = (superclass) => class extends superclass {
  @api foo() { /* does something */ }
  @api bar() { /* does something else */ }
  @api baz; // a property
}
export default Utils;

As a class:

import { LightningElement, api } from 'lwc';
export default class Utils extends LightningElement {
  @api foo() { /* does something */ }
  @api bar() { /* does something else */ }
  @api baz; // just a property
}

To use these two, you first import them:

import Utils from 'c/utils';

Then you extend the appropriate class:

// Mixin syntax
export default class ConsumerLWC extends Utils(NavigationMixin(LightningElement)) {

// super class syntax
export default class ConsumerLWC extends NavigationMixin(Utils) {

You'll notice that the latter is perfectly acceptable, as LWC only cares that LightningElement is somewhere in the class chain.

You can now access the methods through this from ConsumerLWC:

connectedCallback() {
  this.foo();
  this.bar();
  console.log(this.baz);
}

And from ConsumerLWC's template:

{baz}

Or from the consumer's parent component:

const consumer = this.template.querySelector('c-consumer-l-w-c');
consumer.foo();
consumer.bar();
consumer.baz = 'hello world';

As well as assignment if they are properties (or getter/setter designs):

<c-consumer-l-w-c baz={someValue}>

All three decorators, track, api, and wire are supported in this manner. You can use this to not just expose common methods for use in your controller, but also expose common event handlers, wires for loading common data, and so on.

With mixins, it's even possible to mix and match functionality for further reusability. You could conceptually build entire components that consist of nothing but a long list of mixins and a custom base component.

Keep in mind that templates are also automatically inherited if you use the superclass notation. The template "closest" to the child class in the inheritance chain will use its template automatically. You could use this to make a common component, like a button, that can be specialized through inheritance.

If you need wire/track/api in the ConsumerLWC, you'll still have to import them, though, because of a compiler limitation. That aside, it's entirely possible to design entire components where all of the functionality that is used for these decorators are in the parent class, so you can then skip the need to import them again in the ConsumerLWC example.

1
  • This answer helped me solve a different problem. I was trying to invoke a parent api method from outside the child lwc. The parent method is not available unless I extend using NavigationMixin Thanks @sfdcfox. Sep 3, 2023 at 17:45
0

If you are extending the LightningElement, you can use the new class as a replacement.

import { NavigationMixin } from 'lightning/navigation';
import UtilityLWC from 'c/UtilityLWC';

export default class ConsumerLWC extends NavigationMixin(UtilityLWC) {
    connectedCallback() {
        this.foo();
        this.bar();
    }
}

The exception are any decorators (e.g. api, track, wire) you can export * from 'lwc' in your UtilityLWC and import them - though I believe this has more to do with eslint/lwr than technical reasons (I get a console error stating that api isn't being imported from 'lwc').

Though, this might not be the outcome you're expecting as all LightningElements are new instances - the foo and bar properties of component A are not the same as foo and bar of component B.

For me, a typical use case is supplying shared CSS for components without resorting to a synthetic dom:

import { LightningElement } from 'lwc';
import LoadCSS from 'utils/css';

export class LightningElementSlds extends LightningElement {

    constructor() {
        super();

        LoadCSS(
            '/lib/slds/styles/salesforce-lightning-design-system.min.css',
            this
        );
    }
}

All components that extend this LightingElementSlds will get the extra constructor as presented. As a point of caution, I wouldn't recommend connectedCallback, disconnectedCallback, renderedCallbacks in this new class. The documentation provided does not instruct one to call super in these children classes so any code shared would need proper documentation to call super.renderedCallback() at the top of each.

Best to handle it in the constructor OR as custom methods as you are doing in your example.

2
  • Symbols outside the class are private to that class. Everything inside the class is fair game for inheritance.
    – sfdcfox
    Jun 14, 2022 at 3:16
  • Also, I should have stated, the compiler itself enforces that track, wire, and api must be imported from lwc, and no other decorators are supported at this time. However, you can still use those decorators in the superclass and have them work correctly in the subclasses.
    – sfdcfox
    Jun 14, 2022 at 4:01

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .