I have an Angular component in a private npm package using ng-packagr and hosted on AWS (CodeArtifact).

The purpose of the component is to be a reactive form field that shows and hides errors based on user input, and has a couple of variations. I use PrimeNg for each variation to get a consistent look.

The general structure of the TS is:

@Component({
  selector: 'lib-reactive-form-field',
  ...
})
export class ReactiveFormField {
  public readonly control = input.required<FormControl>();
  public readonly inputType = input<'phone' | 'text' | ...>('text');

  // logic for controlling errors and other inputs
}

and the structure of the HTML is:

<div class="field-container">
  <label [for]="fieldId()">
    <!-- label for field  -->
  </label>

  @switch (inputType()) {
    @case ('phone') {
      <app-phone-wrapper 
        [control]="control()"
      />
    }
    <!-- bunch of other types  -->
    @default {
      <input
        [formControl]="control()"
        pInputText 
      />
    }
  }

  @if (errorMessage(); as error) {
    <span class="error-message">{{ error }}</span>
  }
</div>

The PhoneWrapper (app-phone-wrapper) uses the intl-tel-input package. A consumer of this package might not want to use the phone variation so I marked intl-tel-input as an optional peer dependency. The PhoneWrapper is not included in the npm packages public API; it is only useable through the ReactiveFormField.

I can install my private package just fine without intl-tel-input, but when running my app I see an error saying that intl-tel-input is missing.

I had a thought that I could fix this by using a @defer block inside the @case('phone') block, so the relevant HTML section looks like:

@switch (inputType()) {
  @case ('phone') {
    @defer (when true) {
      <app-phone-wrapper 
        [control]="control()"
      />
    }
  }
  <!-- bunch of other types  -->
  @default {
    <input
      [formControl]="control()"
      pInputText 
    />
  }
}

However I am not sure this is a correct use case for @defer as the package will already have been built and presumably intl-tel-input may have been included as it is used in the PhoneWrapper.

Can anyone shed some light on this?

3 Replies 3

Remove the optional dependency part, because the package should contain intl-tel-input since it is used in the package. From the package perspective it is mandatory.

Ensure you have app-phone-wrapper as standalone: true, this will guarantee the intl-tel-input chunk ( module ) is loaded only when the component is loaded.

Finally wrap your intl-tel-input inside the @defer so that it loads only when the module is actually used.

During runtime if the component is needed all the related dependencies of this component are also loaded.

@Naren Murali

I had hoped there was a way to use my package without having to install intl-tel-input unless you use the ReactiveFormField with the inputType set to 'phone'.

My package is used in numerous projects in my company, and only a subset of them need the intl-tel-input functionality.

If I understand you correctly, what I want is not possible and intl-tel-input will need to be installed in all projects which use my private package.

In regard to the @defer, are you suggesting wrapping the contents of the PhoneWrapper HTML instead of the contents of the @case('phone') block?

ReactiveFormField HTML:

@switch (inputType()) {
  @case ('phone') {
    <app-phone-wrapper 
      [control]="control()"
    />
  }
  <!-- bunch of other types  -->
}

PhoneWrapper HTML:

@defer (on viewport) {
  <intl-tel-input 
    [initOptions]="telOptions"
  />
}

I am on viewport is the event to use here, as I'd only want to load the intl-tel-input dependency when the PhoneWrapper is rendered.

Usually tree shaking will prevent then inclusion of unused code, so even if the package intel-tel-input is installed, it will only be included in your output if the consuming component app-phone-wrapper is used. But if you're component is being imported to a component and added to a template then the next, best thing you can do is be smart with your imports. No

This scripts documentation seems to point a scenario that should effectively accomplish what you want. If you look at Getting Started (Using a bundler), you'll see how they defer the import of a utilities script until the initialization function intlTelInput is called.

You could go one step further and either asynchronously import the module or if you're using TypeScript 5.9 you can get nuts and try using the import defer syntax. Here's an example on how an async import might work in your component:

async initTel(input: HTMLInputElement): Promise {
  const { default: intlTelInput } = await import('intel-tel-input');
  intlTelInput(input, { 
    loadUtils: () => import("intl-tel-input/utils"),
  });
}

This should prevent at of the intl-tel-input library's code from loading until the point it's needed.

Your Reply

By clicking “Post Your Reply”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.