Dynamic loading tutorial - Part II

This post starts where the previous ended and we will dig into the sample project to show how it works and how to leverage dynamic loading in your projects

Some basic concepts

Before begin digging the code let me introduce some important concepts.

Entry points

What is an entry point for polymerize ? An entry point is simply a dart file in the web folder that has a main top level function and has any top level declaration or directive annotated with @entryPoint.

The main function is necessary only because pub+ddc requires it in order to consider that file for compilation (if you don’t put it that file will not be compiled and neither its dependencies).

Note : the main function serves only as a marker for pub+ddc and apart from the main entry point (see below) it will NOT be executed. So you can put even a blank body.

If you want to execute something when your module is loaded just create a top level function and annotate it with @init.

Main entry point

A main entry point is simply an entry point for which there’s a <script> tag in the main html file, for example:

1
    <script src='my_main_entry_point.dart' type='application/dart'>

The main entry point will be loaded as the first module and its main function executed.

If there are top-level function annotated with @init in the main entry point that will be executed before the main function.

It is wrong to annotate the main function with @init because it would be executed twice.

Project structure

The sample project has three modules :

Lets see them in more detail.

The main entry point : the loader

This is the main entry point of the application, and it consist in the web/loader.dart file.

You can see that it is the main entry point because the index.html file contains the corresponding script tag:

1
    <script src="loader.dart" type="application/dart"></script>

This module only contains the main function:

1
2
3
4
5
    main(List<String> args) async {  
      // require load default module
      await require('web__module1');
    }
    

The require function will allow you to load a new module. You have to provide the corresponding requirejs name for the module. Until a better mapping is made available you can obtain the requirejs module name simply prepending web__ to the corresponding dart file name without the .dart extension.

In this case the module module1.dart correponds to web__module1 requirejs module name and that’s the module that get loaded.

The require function will return a Future that completes when the module is loaded. In the next part of this tutorial we will se how to access the loaded module internals (like creating object with classes defined in that module or executing functions). But at the moment there’s no need for that and we will skip that part.

Note : Loading a module is very simple : you just have to use the require polymerize function.

Module1

The first module that gets loaded is web/module1.dart. Lets see what it does:

1
2
3
4
5
6
7
8
    @entryPoint
    import 'package:loader_sample/module1.dart';
    import 'package:polymer_element/polymer_element.dart';
    
    
    main(List<String> args) {
      print("This will never get executed");
    }

Wait a moment… It does nothing ? Of course the answer is no. It imports package:loader_sample/module1.dart and that’s enough. Let’s see the content of that file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    import 'package:polymer_element/polymer_element.dart';
    // ignore: UNUSED_IMPORT
    import 'package:loader_sample/component1.dart';
    // ignore: UNUSED_IMPORT
    import 'package:html5/html.dart';
    
    @init
    void startModule1() {
      print('module 1 loaded');
      HTMLDivElement div = document.querySelector('[loader]');
      div.remove();
    
      ComponentOne one = document.createElement('component-one');
      document.querySelector('body').appendChild(one);
    }

First of all here we find a top level @init annotated function. That function will be executed as soon as the corresponding file is loaded (that is lib/module1.dart in this case).

Another thing it does is importing package:loader_sample/component1.dart where the first polymer component (<component-one>) is declared.

The startModule1 function will use the component defined in package:loader_sample/component1.dart and simply replace the “loader” stuff with an instance of that component.

The component <component-one> will display the initial page with an button. The click event of the button is bounded to an event handler that will trigger module2.dart loading:

1
2
3
4
5
6
7
8
9
10
    loadTwo(Event e) async {
        if (!module2Loaded) {
          loading = true;
          await require('web__module2');
          module2Loaded = true;
          loading = false;
        }
        currentPage = 'second-page';
    }
    

You can see that before starting loading the module a flag is used in order to make the paper-spinner start spinning and after the module is loaded the flag is reset.

NOTE : the module2Loaded flag is used to avoid reloading the module. Actually reloading a module doesn’t arms because the request simply gets ignored and the previous loaded module is returned. The real function of that flag is to make a dom-if template to be shown (see below). Infact it is important that an element gets rendered only when the component is defined otherwise you won’t get polymer binding and event handlers working:

1
2
3
4
5
    <dom-if if='[[module2Loaded]]'>
        <template>
          <component-two on-go-back='toFirst'></component-two>
        </template>
    </dom-if>

Here the on-go-back event handler will work because the tag is inside a dom-if and it will be rendered only when the corresponding component definition has been loaded.

Module2

Finally module2 is defined in lib/module2.dart, the only thing it does is importing the new component definition:

1
2
3
4
5
6
7
    @entryPoint
    import 'package:polymer_element/polymer_element.dart';
    // ignore: UNUSED_IMPORT
    import 'package:loader_sample/component2.dart';
    
    
    main(List<String> args) {}

Conclusions

Defining and loading entry-points is very simple with polymerize. Just follow some simple guide-lines and use the require function when needed.

In the next part of this tutorial series we will see some advanced usages of dynamic loading such as loading a invoking a top level function inside a loaded module.