How to create a webworkers driven multithreading App — Part 2

Tobias Uhlig
The Startup
Published in
16 min readJun 24, 2020

Content

  1. What are we going to build?
  2. Prerequisites
  3. Setting up the Infrastructure
  4. The result of this tutorial
  5. Creating the Footer
  6. Creating the Table View
  7. Loading the data for the Table View
  8. Adding renderers into the Table View
  9. Creating the Helix View
  10. Creating the Helix Controls Container
  11. Basic Routing
  12. Creating the 3d Gallery View
  13. Deploying your App for production
  14. Summary

1. What are we going to build?

Here is a demo video showing the different views of the App:

As you can see, this is already a pretty complex app. We will start from scratch and progressively enhance it, while looking into some of the basic concepts as well. To keep the scope reasonable, I will split the tutorial into multiple parts.

Welcome to Part 2!

Since the App is already finished, you can take a look at the full source code. This version goes beyond Part 2of the tutorial, so think twice if you already want to look into the final result at this point.

https://github.com/neomjs/covid-dashboard

You can find the neo.mjs repository here:

2. Prerequisites

To follow this tutorial, you need:

  1. a solid understanding of Javascript, CSS & HTML
  2. to be familiar with the ES6+ based class system
  3. to know the concepts of OOP
  4. Chrome v80+
  5. Since this is Part 2, you should have finished Part 1 before starting this one:

3. Setting up the Infrastructure

You can just use your own final version of Part 1 to start this tutorial. Make sure to upgrade neo.mjs to v1.2.16+.

In case you want to ensure that your starting point is exactly the same, you can as well fork the Part 1 repository:

https://github.com/neomjs/tutorial-covid-app-part-1

Just click on the button at the top right and you are ready to go:

Please don’t add pull requests on this repo.

4. The result of this tutorial

I recommend to not look at the repo unless you get stuck at some point. The commit log is very close to this tutorial.

5. Creating the Footer

As you hopefully remember, our final version of Part 1 looked like this:

Let us start with something really easy, the Footer. We do want to add links to the things we are using here.

Create the file: view/FooterContainer.mjs

Unless you love typing, I recommend to copy the content from here:

We start with importing the base class which we are going to extend: container.Base.

We are adding a fixed height of 25px (which will get mapped to the top level div node style automatically).

We are using the horizontal box (hbox) layout.

To access an instance of this class from a component.Controller scope, we are adding the reference: ‘footer’.

The interesting part here is the “itemDefaults” config. As the name suggests, we can add configs here, which will get added to every item config inside the items array. passing ntype: ‘component’ makes sense. We could argue about cls & style, since those are only needed for 3 of the 6 items. A good showcase for this demo, but a small DOM pollution.

Inside items, we added our 3 links. Looking close, you will notice that I just added them as a string inside the html config. In case you wanted to edit them dynamically later on, you would go for a real vdom config instead.

Compare:

The output is exactly the same, but with the second version, you could adjust the content more easily (e.g. change the src attribute).

I will create an in depth tutorial about working with JSON based virtual dom in the near future.

Looking at this example: by default, each vdom object uses a div tag. We can pass a different value for the tag property, like tag: ‘a’ or in case we don’t want to create a new tag, we can use vtype: ‘text’. This way we can add a text before (or after) the link, without the need to e.g. put it into a ‘span’ tag.

Since our 3 links are static, let’s just keep it simple and stick to the html: ‘x’ version.

Between each link is a flex:1 object => a spacer inside the flexbox layout.

At the end we are adding a Button, which already contains a handler. We need to add a cls config, since our itemDefaults value would otherwise override the button cls default value. You will notice that I did not import the component.Button base class, but creating it with the ntype config instead. We already imported Button into the HeaderContainer, so we do know that the class is available. Of course you could import it again here and Chrome and webpack are both smart enough to only fetch the file once.

Open: view/MainContainer.mjs

Import the new file at the top and just replace the third component inside the items array with the JS module.

Time to look at the result!

Using WebStorm, you can just right click on the index.html file:

Open in Browser → Chrome

You can as well use the webpack-dev-server:

npm run server-start

Reminder: this one does throw 2 errors which you can ignore, but will open a new Browser Tab automatically.

Open the console: the MainViewController will throw an error, since our Button handler is not yet defined. Obviously our Footer does not need its own Controller.

Open: view/MainContainerController.mjs:

Add the Button handler logic.

this.view is our MainContainer instance. We are calling remove() which is defined inside container.Base.

Open the docs app locally (docs/index.html) or use the online version:

https://neomjs.github.io/pages/node_modules/neo.mjs/docs/index.html

Open container.Base inside the API section.

Type “rem” into the class filter field and there is our remove method.

this.view.remove(this.getReference('footer'), true);

So we are passing our Footer instance as the item to remove and are using the second param → destroyItem: true.

The 2nd param is just there for convenience reasons. You could also write:

const footer = this.getReference('footer');this.view.remove(footer, false);
footer.destroy();

The nice thing is: you can remove an instance and not destroy it. This way you can re-use it and add it back later on or add it into a different spot of your App.

Reload your App inside your browser, click the “Remove Footer” Button and the Footer is gone.

6. Creating the Table View

We are going to focus on the Center area of our App next.

Let us replace the center items component with a TabPanel first and add 2 tabs with dummy content.

Open: view/MainContainer.mjs

The important part here is that we are using the “route” config for both tab.header.Buttons. By default this will replace the given key inside the window hash. In case you would like to replace the full hash value, you can use the config editRoute: false .

Open your App inside the Browser again. Look at the hash value. Click on the 2nd tab:

Before we create the Table View, we want to create the Model & Store first.

Create the file: model/Country.mjs

We are extending Neo.data.Model and just drop in the fields we care about from the Open Disease API (the new name of the NovelCOVID API as of July 1st, 2020):

Create the file: store/Countries.mjs

I personally stick to use singular for model file names and plural for stores (which contain multiple items). Not mandatory → up to you.

We are importing the Country Model which we just created and are extending Neo.data.Store.

We just need to to add the imported Model to the Store, using the model config.

We can add local filters & sorters to our Stores. In this case we want to ensure that the initial view is sorted by cases, descending.

[Side note] Neo.data.Store is not fully polished yet, this will be an (epic) item for one of the next minor releases.

Create the file: covid/view/country/Table.mjs

We are extending Neo.table.Container & are importing the Store we just created as well as our Utility class which we already used for coloring numbers inside our App Header (Part 1).

The most important part here is line 88: we are passing our store module into the store config of our Neo.table.Container extension. This way, we will get a new store instance for every instance of our Covid.view.country.Table class. In case you want to use the same store instance for multiple views, you can as well pass an existing store instance into the store config. You will most likely want to do this outside of the class definition → inside the config definition where you specify your table.Container instance.

Very similar to itemDefaults for Container items, we can use columnDefaults for the columns config. This prevents us from writing redundant boiler plate code.

Every column object should use a dataField config, which needs to match the name of a field inside our Model definition. The first 2 columns are docked to the left side → they will stick to their position in case you scroll horizontally.

By default, Neo.table.Container is using:

cls: ['neo-table-container'],

We are adding a second CSS class here to apply a custom styling. I added this one to the neo.mjs themes, so there is no need to apply the rules on your own. Take a look at:

https://github.com/neomjs/neo/blob/dev/resources/scss/src/apps/covid/country/_Table.scss

We are adding Util.format number as the renderer for every column except 2. For the country name, we want to use the default renderer. For our index column, we want to use a different renderer, which we have not defined yet.

Open Util.mjs and add the following method:

For our “RowNumberer” we want to start counting by 1 instead of 0.

Open: view/MainContainer.mjs

We only need to change 3 lines of code: import our new CountryTable in line 1 and replace the ntype & html configs of our first TabContainer item with the imported module (line 30). We also add a reference to it.

Time to reload our App inside the Browser:

Since we added a sorter to our Store, the “Cases” column will have the arrow down icon initially.

Scroll horizontally:

The 2 first columns which are docked to the left will stick to their initial positions.

7. Loading the data for the Table View

Open: view/MainContainerController.mjs

We already have the apiSummaryUrl config, time to add an apiUrl & data config as well. Inside onConstructed(), we are going to add an loadData() method which will trigger addStoreItems() afterwards.

The loadData() method is super similar to loadSummaryData() of Part 1.

Let us take a closer look at addStoreItems()

For now, we stick the activeTab to our table, since it is the first tab containing data anyway.

We need to manually iterate over the data to create the output for the casesPerOneMillion & infected fields.

We are saving the data inside the new data config, since we want to pass it into different stores later on. We could put it into a new Store as well, but since we don’t want to apply any top level sorting or filtering, let us keep it simple.

In line 57, we are adding the loaded data into the CountryStore data property manually. This one triggers a setter internally.

Time to reload our App inside the Browser:

You can now scroll horizontally & vertically.

8. Adding renderers into the Table View

We already have renderers for the numbers in place, but those can use some colors, to make it easier to distinguish them and stick to the color scheme of our header.

Open: Util.mjs

Add the formatInfected() method.

Open: view/country/Table.mjs

We are only adding the new renderers here.

Reload the App inside your Browser:

A lot better, isn’t it?

One last thing is missing compared to the online version of this App: adding country flags to the country name column.

Since we do want to use the flag parsing logic for other components as well, let us add it to our Util file.

Open: Util.mjs

We added the static flagRegEx, as well as the getCountryFlagUrl() method. We need this one, since country names inside the API do not always match the names of flaticon images we want to use.

Open: view/country/Table.mjs

Adjust the renderer for the country name column to the following:

Reload the App inside your Browser:

Now the Table View looks exactly the same as the one inside the Online Examples. Good job!

[side note] In case you have not done so already, this is the perfect time for a short break. Grab a cup of tea or coffee. Enjoy it!

9. Creating the Helix View

While creating a Component as complex as the Helix is a lot of work, using it is fairly easy. If you like to, you can explore the full source code of it here:

https://github.com/neomjs/neo/blob/dev/src/component/Helix.mjs

Create the file: view/country/Helix.mjs

We only need 172 lines of code. The important part here is the itemTpl config, which is basically a vdom skeleton for each helix item without data.

We are using the createItem() method to map the API data into each item. As mentioned before, we are using Util.formatNumber() once more.

Overriding getCloneTransform() is necessary, since we need to adjust the mode when expanding an item depending on the item size. In detail: you can click on an item, then hit “Enter” and it will move the the top-left position.

We are passing our CountryStore into the store config again, which will create a new instance of the Store for each Helix instance. We don’t want to keep the sorting in sync with the Table View, although we could do it.

Open: view/MainContainer.mjs

Import the CountryHelix and replace the 2nd Component inside the TabContainer with our module. We also add a reference.

Reload the App inside your Browser:

A black background color indicates that the Helix is in place, we just need to add data to it.

Open: view/MainContainerController.mjs

Inside the addStoreItems() method, add the following line at the very bottom:

me.getReference('helix').store.data = data;

Reload the App inside your Browser:

Beautiful, isn’t it?

Now we can do a lot of things:

  1. Scroll vertically to zoom in or out.
  2. Scroll horizontally to rotate the Helix.
  3. Click on an item to select or deselect it.
  4. Once an item is selected, use the Arrow keys to walk around.
  5. Hit the Space key to rotate the selected item into the front.
  6. Use the Enter key to expand an item. You will notice that the position does not really match the screen yet (we could adjust getCloneTransform()), but it will do so very soon.

10. Creating the Helix Controls Container

I will keep this section pretty short, since there is not a lot of new things to learn here. You are welcome to ask questions though :)

Create the file: view/HelixContainer.mjs

A lot of Buttons & RangeFields inside different layouts.

The constructor in line 243 is worth a look: We added the 2 configs: helix, helixConfig with a null value inside the class definition and now we are creating a Helix instance manually. We are saving a reference to the Helix into the helix config, to make it easier to access it later on.

To keep the class flexible, we are spreading the helixConfig into the helix config (what a wording :)). This way, you can easily change the helix configs when creating a new instance of the controls Container.

We can just add an existing Component instance into the items array of a Container (line 254). The framework will handle it.

Create the file: view/HelixContainerController.mjs

There is another internal config to access the Helix Component inside the Container. beforeGetHelix() is getting the Helix by reference. We could as well use this.view.helix.

A lot of handler methods for the Buttons & RangeFields. The important part is that the handlers will only assign new values to existing Helix configs, rather than calling methods. E.g.:

onFlipItemsButtonClick(data) {
this.helix.flipped = !this.helix.flipped;
}

This fits the config driven approach of neo.mjs very well.

Open: view/MainContainer.mjs

Replace the CountryHelix import with the new HelixContainer. Replace it inside the second TabContainer item as well & remove the reference there.

Reload the App inside your Browser:

Take a couple of minutes to play with the new controls.

E.g. zoom in, flip items, adjust the Min Opacity.

11. Basic Routing

You might have noticed that in case you reload your App with the mainview=helix hash value, it will still display the Table View as the active tab. About time to change this!

Open: view/MainContainerController.mjs

We are adding the activeMainTabIndex & mainTabs configs, getTabIndex() and onHashChange()

Storing the tab order inside mainTabs is just for convenience reasons → faster access to map a hash value to an existing tab.

onHashChange() will also trigger for the initial hash value. This will happen before your App is getting rendered. We are storing the new index inside the controller (optional) and we are adjusting the activeIndex config of our TabContainer.

Reload the App inside your Browser (and again with the Helix View as the active Tab):

It will show the Helix now as the new starting point. Looking into the console will reveal that we are still applying the DOM for the Helix and the Table View items at the same time. This is really bad from a performance point of view.

→ We do not want to add data to both stores right away.

Open: view/MainContainerController.mjs

We are only adjusting addStoreItems(). Using our 2 new configs, we can now easily identify the active tab and only add items to its matching store.

Reload the App inside your Browser.

If the route points to the Table View first, this one will show data. Reloading the App with the Helix route will show data there. When switching Tabs, the 2nd view will stay empty, so we need to enhance onHashChange() a bit more.

Open: view/MainContainer.mjs & view/MainContainerController.mjs

Inside the MainContainer, we need to adjust the reference from ‘table-container’ to ‘table’. This one is not in sync with the final App yet.

Inside the Controller, add getView() and adjust onHashChange().

We will now add data to a store, in case it is empty. We could further enhance this with adding an ID or timestamp to each store for the latest update.

For the Helix, I also now added an offsetValues reset. The Helix needs to know about the current Container size, to adjust the positioning in case you want to expand an item (Enter key).

Reload the App inside your Browser:

Perfect. One last thing.

Open: view/MainContainerController.mjs

In case we hit the reload data button, we want to refresh the current view as well and not only load the summary data.

12. Creating the 3d Gallery View

Since this one is super similar to creating the Helix View, let us add one last item.

Create the file: view/country/Gallery.mjs

This is almost exactly the same as for the Helix. itemTpl & createItem().

Create the file: view/GalleryContainer.mjs

Same as for the Helix, a lot of Buttons, RangeFields & layouts. Nothing new here.

Create the file: view/GalleryContainerController.mjs

Same story :)

Open: view/MainContainer.mjs

Import the GalleryContainer and add it as the third tab.

Open: view/MainContainerController.mjs

mainTabs: ['table', 'helix', 'gallery']

Just add the gallery into the mainTabs config.

Reload the App inside your Browser:

Take a couple of minutes to play with your gallery.

Hint: Avoid using the TranslateX & Y sliders, click on an item, use the Arrow keys. Definitely change the amount of rows, order by row and the sorting.

13. Deploying your App for production

Now you will most likely want to see your App in Firefox & Safari as well.

npm run build-all

http://localhost:8080/dist/development/apps/covid/#mainview=helix

Some minor styling glitches left in FF & Safari, but in general it should work.

14. Summary

Wow! In case you are reading this I am extremely proud of you!

This was the hell of a tutorial with a massive amount of content.

You just learned:

  1. how to build an already very complex App
  2. how to enhance your basic Part 1 App with complex components like the Table, Helix & Gallery
  3. how to work with Component Trees (the JSON markup)
  4. how to use Component Controllers & access View items
  5. how to deal with basic Routing

Some thoughts on my end:

This tutorial was very long. While there was no room left to talk about every Component or change in detail, the main goal is to show you the bigger picture: how to create a complex App using neo.mjs.

At this point you will hopefully be curious to take a dive into the framework & demo Apps source code and create an amazing App on your own.

I am really looking forward to see what smart minds like you can achieve!

Please do use the neo.mjs issues tracker:

https://github.com/neomjs/neo/issues

Providing feedback on existing tickets and creating new ones is key for open source projects.

In case you need more guides / tutorials, let me know about which area.

In case you have ideas for new features / components → ticket.

New Contributors are always welcome!

I hope you enjoyed this tutorial & learned something new.

Your feedback means a lot to me!

Best regards & happy coding,
Tobias

--

--