Skip to content

Latest commit

 

History

History
2779 lines (1946 loc) · 97.8 KB

TUTORIAL.md

File metadata and controls

2779 lines (1946 loc) · 97.8 KB

Golgi Tutorial

Index

First Steps

You'll need access to a web server and its file system. Any Web Server on any platform will do.

Create a directory that is accessible by the web server, eg /golgi.

Then create a sub-directory within it named components, ie /golgi/components.



Golgi Components



Create a Simple Demo

Inside the /golgi directory, create the following two files using the filenames and contents shown below:

index.html

  <!DOCTYPE html>
  <html lang="en">
    <head>
      <title>Golgi Demo</title>
    </head>
  
    <body>
      <script type="module" src="./demo.js"></script>
    </body>
  </html>

demo.js

  (async () => {
    const {golgi} = await import('https://cdn.jsdelivr.net/gh/robtweed/golgi/src/golgi.min.js');
  
    let context = {
      componentPaths: {
        demo: window.location.origin + '/golgi/components/'
      }
    };
  
    golgi.renderComponent('demo-div', 'body', context);

  })();

And then, in the */golgi/components directory create:

demo-div.js

  export function load() {
  
    let componentName = 'demo-div';
    let count = -1;
  
    customElements.define(componentName, class demo_div extends HTMLElement {
      constructor() {
        super();
        count++;
  
        const html = `
  <div>This is Golgi!</div>
        `;
  
        this.html = `${html}`;
        this.name = componentName + '-' + count;
      }
    });
  };


Run The Demo

Now, load the index file into a browser. Note that it must be a modern browser that supports WebComponents, eg:

  http://localhost:3000/golgi

or

  http://localhost:3000/golgi/index.html

If see the text below appearing in your browser:

  This is Golgi!

then Gogli is working for you and you're ready to explore it in more detail.



How Did It Work?

Clearly this is a really unremarkable demonstration of Golgi's abilities: all we've done is display a div tag in your browser! However, if we now analyse what happened and what we specified in those three files, you'll begin to get an initial idea of how the most basic of Golgi's building blocks - Golgi Components - are defined and used.



The index.html Page

Every Golgi application needs an HTML page from which it loads and starts. You'll rarely need anything other than the one you used in this simple example, so keep it as a template for future use. It's the bare minimum you need to kick everything off, even if your application is a massive and complex one that needs additional JavaScript and CSS Stylesheet files in order for its UI to behave correctly.

The idea of Golgi is that it allows you to build applications that are dynamically built out, using only the stuff you need at the time. This significantly reduces startup load times. It is ES6 Modules that really make this possible, and that's what Golgi harnesses for you.

So let's look at the bare-bones HTML page you used:

  <!DOCTYPE html>
  <html lang="en">
    <head>
      <title>Golgi Demo</title>
    </head>
  
    <body>
      <script type="module" src="./demo.js"></script>
    </body>
  </html>

The key piece is this line:

      <script type="module" src="./demo.js"></script>

This tells the browser to load your main root application module, in this case demo.js.



The Golgi Root Application Module

The demo.js Module is an extremely simple example of a Golgi main or root application module.

Your root application module is where you define the main shape and starting point of your UI and its application logic.

To make it work with all modern browsers, it should be wrapped as a self-loading async function. Safari, in particular, needs this due to the way it handles what's known as top-level await. So that's this piece:

  (async () => {

    ...etc

  })();

The contents of this self-loading function should adhere to the following pattern

  • load the Golgi Module
  • define the paths where your Golgi Components and Golgi Assemblies reside
  • start rendering the initial Golgi Components or Golgi Assemblies that will kick off your application.

Let's drill down into each of those steps:

Loading The Golgi Module

In our example, we dynamically loaded the Golgi Module directly from this Github repo:

  const {golgi} = await import('https://cdn.jsdelivr.net/gh/robtweed/golgi/src/golgi.min.js');

Loading it from Github in this way is a quick way to get started, but has two downsides:

  • download speed may not be ideal

  • All subsequent module imports performed internally by Golgi, eg to import your Golgi Components will, if not otherwise specified, use paths relative to from where the Golgi Module itself was loaded, in this case:

    https://cdn.jsdelivr.net/gh/robtweed/golgi/src/
    

Defining the Golgi Component and Assembly Paths

You specify the paths from which the Golgi Module will import your Golgi Components and Assemblies in a context object:

  let context = {
    componentPaths: {
      {{namespace}}: {{path}}
    }
  };

Golgi Components are WebComponents, and WebComponents create custom HTML tags that must have a hyphenated name, eg in our example:

  demo-div

In Golgi, the text before the first hyphen (in our case demo) is used as a namespace. The idea is you create directories for each of your libraries of Golgi Components. Within such a directory, all your Golgi Components will have the same prefix or namespace. These can be then be used as the basic building blocks for an application, or, indeed, for as many of your applications as you wish.

Because we loaded the Golgi Module from the Github repo, we can't use relative paths for our Golgi Components, so we need to specify the absolute path to your web server, which we can do by making use of the window.location.origin value. So, the path to our one and only Golgi Component with the namespace demo is specified as follows:

  let context = {
    componentPaths: {
      demo: window.location.origin + '/golgi/components/'
    }
  }; 

Things are a lot simpler if you put a local copy of the Golgi module onto your web server, eg:

  /golgi/golgi.min.js

You can now load it using this:

  const {golgi} = await import('./golgi.min.js');

... and because it was loaded locally from the same location as your Golgi Components, you can now use relative paths to point to their location on your web server:

  let context = {
    componentPaths: {
      demo: './components/'
    }
  };

Rendering the demo-div Golgi Component

We now have everything needed to start our simple demo application. In our case we just want to render the demo-div Golgi Component. We do that using Golgi's renderComponent() method:

  golgi.renderComponent('demo-div', 'body', context);

As you can see, this takes three arguments:

  • the name of the Golgi Component

  • the target element within your HTML page DOM to which it will be appended. We want to append it to the body tag, so instead of specifying it in full, ie:

    golgi.renderComponent('demo-div', document.getElementsByTagName('body')[0], context);
    

    ... and because the body tag is a very common one to use as an initial target, the renderComponent() method allows you to simply specify it as the string value 'body'.

  • the context object which then becomes available for use by Golgi and also for use in your Golgi Components if you need or want it.

The renderComponent() method dynamically imports the demo-div Module using a path constructed as follows:

  context.componentPaths[{{namespace}}] + {{component name}} + '.js'

eg in our "local" example:

  './components/' + 'demo-div' + '.js'

So this highlights an important pattern you must adhere to when using Golgi: the filename for each of your Golgi Components must match the name of the WebComponent it defines.

Once imported, the Golgi Component's load() method is invoked to register its WebComponent, and it is then appended to the specified target element of your HTML page.



The demo-div Golgi Component

So now we need to examine the last of our files (demo-div.js), which contains the definition of the demo-div Golgi Component.

The Golgi Component Pattern

Golgi Components must adhere to the following pattern:

  export function load() {
    // define the WebComponent
  };

In other words, they are simple ES6 Modules that wrap a single WebComponent within an exported load() method.

I recommend the following pattern for defining the WebComponent:

  • define the component's name and an instance counter (initialised at -1).
    The component's name must match that of the filename in which the Module resides, ie in our example

    let componentName = 'demo-div';
    let count = -1;
    

Now define the WebComponent:

  customElements.define(componentName, class demo_div extends HTMLElement {

The class name must be unique. By convention, I simply replace the hyphens in the component name with underscores, ie:

  demo_div

What follows defines the Web Component in a fairly standard way. I recommend you adhere to the pattern and conventions shown here:

  customElements.define(componentName, class demo_div extends HTMLElement {

      // standard WebComponent boilerplate stuff:

      constructor() {
        super();

        // increment the instance counter

        count++;

        // Now define the HTML tag(s) that this WebComponent represents
        // Make sure you use backticks around it

        const html = `
      <div>This is Golgi!</div>
        `;
        
        // now add this as the WebComponent's html property (for later use by Golgi)

        this.html = `${html}`;

        // assign a default, unique name property to the WebComponent
        //  you may later decide to redefine the name property with a
        //  more memorable name

        this.name = componentName + '-' + count;
      }

  });

So, in summary, this will create a WebComponent named demo-div that represents a simple div tag with some pre-defined text.

Can you use Shadow DOM with Golgi?

In the simple tutorial example, we're not using the Shadow DOM capability that WebComponents provide. However, Golgi supports the use of Shadow DOM: simply attach the shadowDOM and change the line that maps the Component's HTML, which should now be to this.shadowRoot.innerHTML. See the lines highlighted by comments below:

  export function load() {
  
    let componentName = 'demo-div';
    let count = -1;
  
    customElements.define(componentName, class demo_div extends HTMLElement {
      constructor() {
        super();
        this.attachShadow({ mode: 'open' });      // <==== ****
        count++;
  
        const html = `
  <div>This is Golgi!</div>
        `;
  
        this.shadowRoot.innerHTML = `${html}`;    // <===== *****
        this.name = componentName + '-' + count;
      }
    });
  };

How Your Golgi Component Is Loaded and Rendered

In our simple example, we're loading and rendering our demo-div Golgi Component by using the renderComponent() method. What this method does is to:

  • import the demo-div.js Module file
  • invoke its load() method which instantiates an instance of the WebComponent it defines
  • append the WebComponent tag to the target HTML element that was specified in the renderComponent() method invocation.

There's actually other stuff that Golgi does, but none of that is relevant yet to our simple example and we'll discover all that other good stuff later in the tutorial.

The net result is that the div tag appears in the browser with its text displaying:

  This is Golgi!

By now you're probably thinking: "That's an awfully complicated way to just display a div tag!"

If that's all we wanted to do, then yes, of course, you'd be right. But this simple example has hopefully explained the basic mechanics of Golgi and the patterns you should adopt, without any other complexities getting in the way or confusing things.

We're now going to start building on top of this very simple demonstration example, and you'll gradually begin to see how Golgi can be used for UI development.



Check The Browser's Developer Tools

Before we move on, if you're using a browser such as Chrome, you can open the Developer Tools panel, and, if you select the Network tab, you'll be able to see the sequence of events that occur when you load/reload the index.html page. You should see in sequence:

  • index.html is loaded
  • demo.js is then loaded by index.html
  • golgi-min.js is then loaded, and once it's finished loading:
  • demo-div.js is loaded by the renderComponent() method in demo.js

If you select the Elements tab, you'll be able to see what your index.html page now looks like in the browser:

  <!DOCTYPE html>
  <html lang="en">
    <head>
      <title>Golgi Demo</title>
      <style>...</style>
    </head>
    <body>
      <script type="module" src="./demo-1a.js"></script>
      <demo-div>
        <div>This is Golgi!</div>
      </demo-div>
    </body>
  </html>

We can ignore the style tag that's been added by your browser to the head section.

The important thing to notice is the <demo-div> tag that has been added, inside of which is the div tag we defined in its WebComponent.

The Developer Tools panel is a very useful and powerful tool to use when developing with Golgi, allowing you to check that the correct things have loaded as you'd expected, and that the HTML document's DOM is being correctly updated by your Golgi Components and Assemblies.

I'd recommend that you refer to it throughout the rest of this tutorial when you load/reload each new version of the demonstration application.



Using Golgi's Log

A further means of seeing and checking what Golgi is doing is to enable its log (by default it is disabled). When enabled, Golgi reports various key steps to the browser's console. During development and debugging, it's a good idea to turn Golgi's logging on.

To turn logging on, add this line to your root application module:

  golgi.setLog(true);

To inspect its log, use the Console tab in the browser's Developer Tools panel.



Adding And Using A SetState() Method

One of the things you'll want to be able to do with your Golgi Components is to manipulate their state. The convention I use is to add a method named setState() to your Golgi Component.

To see this in operation, edit your /golgi/components/demo-div.js file, adding this method to the WebComponent definition:

  setState(state) {
    if (state.text) {
      this.rootElement.textContent = state.text;
    }
  }

Within a WebComponent, this refers to the Component itself. When processed by Golgi, the root HTML element in the HTML you specified is automatically referenced via a property named rootElement (even if you choose to use Shadow DOM). In our example, that's the div tag.

So this setState() method will replace the div tag's textContent with whatever you specify in the state object's text property.

The new version of your demo-div Golgi Component should now therefore look like this:

  export function load() {
    let componentName = 'demo-div';
    let count = -1;
    customElements.define(componentName, class demo_div extends HTMLElement {
      constructor() {
        super();
        count++;
        const html = `
  <div>This is Golgi!</div>
        `;
        this.html = `${html}`;
        this.name = componentName + '-' + count;
      }
  
      setState(state) {
        if (state.text) {
          this.rootElement.textContent = state.text;
        }
      }
    });
  };

Let's now edit your root application module (/golgi/demo.js)

First we'll change the invocation of the golgi.renderComponent() method to the following:

  let demoComponent = await golgi.renderComponent('demo-div', 'body', context);

So we await completion of the asynchronous renderComponent() method, and what it then returns to us is the actual instance of the demo-div WebComponent that was rendered.

We can now access any of that WebComponent's methods and properties.

So let's add this:

  demoComponent.setState({text: 'Hello World'});

In summary, the /golgi/demo.js file should now look like this:

  const {golgi} = await import('./golgi.min.js');
  let context = {
    componentPaths: {
      demo: './components/'
    }
  };

  golgi.setLog(true);
  
  let demoComponent = await golgi.renderComponent('demo-div', 'body', context);
  demoComponent.setState({text: 'Hello World'});

Save the file and reload the index.html page in the browser.

Instead of the text "This is Golgi!", you should now see the text:

  Hello World


Assigning WebComponent HTML Tags To Properties

Thus far, the WebComponent within our demonstration Golgi Component consists of a single div tag. However, the WebComponent can define as much and as complex a chunk of nested HTML tags as you wish. When you do this, you'll often want to be able to access specific tags within the WebComponent so that you can later manipulate them.

Whilst you could achieve this using the standard HTML DOM APIs, this would end up being very laborious during development, and not particularly easy to follow and maintain at a later date.

Golgi provides a very simple alternative way to make these assignments by allowing you to add a special reserved attribute name - golgi:prop - to any tag you define within a WebComponent. The value of this attribute is used as a property name, that property being automatically added to the WebComponent, and that property value provides the DOM pointer to that tag.

Let's try it out and it will become much clearer.

First, we're going to modify the demo-div Golgi Component. So edit the /golgi/components/demo-div.js file, and change the HTML assignment to the following:

  <div>
    <span golgi:prop="spanTag"></span>
  </div>

If you remember from earlier, the outer div tag is already automatically referenceable via Golgi as this.rootElement. By adding the golgi:prop attribute to its new child span tag, we'll now be able to reference and access that span tag as this.spanTag (note that this would be true even if we had chosen to use Shadow DOM).

Let's now modify the setState() method to use this, because we now want state.text to modify the textContent of the span tag rather than the div tag:

  setState(state) {
    if (state.text) {
      this.spanTag.textContent = state.text;
    }
  }

Try saving this edited version and reload the page in the browser. It should display the exact same thing as before - "Hello World" = but if you examine the Elements view in the browser's Developer Tools, you should now see that the text is inside the span tag:

  <!DOCTYPE html>
  <html lang="en">
    <head>
      <title>Golgi Demo</title>
      <style>...</style>
    </head>
    <body>
      <script type="module" src="./demo-1a.js"></script>
      <demo-div>
        <div>
          <span>This is Golgi!</span>
        </div>
      </demo-div>
    </body>
  </html>

As a further demonstration, try editing the /golgi/demo.js root Module as follows:

  let demoComponent = await golgi.renderComponent('demo-div-3', 'body', context);
  demoComponent.setState({text: demoComponent.spanTag.tagName});

You can see from this that the spanTag property created by the golgi:prop attribute gives us access directly to the span tag within our instance of the demo-div WebComponent.



Adding Handlers to Golgi Components

Something you'll often want to do is to add handlers to tags within a Golgi Component. Sometimes you'll want to do that when its WebComponent is being instantiated so that the handler(s) is/are always present.

Once again, Golgi makes this very simple. Let's add an onClick handler to our demo-div Component.

Edit the /golgi/components/demo-div.js file once more, and change the HTML assignment within the WebComponent:

  <div>
    <span golgi:prop="spanTag" golgi:on_click="report"></span>
  </div>

and within the body of the WebComponent, add a method named report:

  report() {
    this.setState({text: 'You clicked the div at ' + Date.now()});
  };

Save the file and reload the index.html file in the browser, and now whenever you click the text, you'll see it updating the span tag's text content.

Let's examine why this happened.

When running the renderComponent() method, one of the things Golgi looks for within the HTML that you define in each of your WebComponents is a special attribute: golgi:on{{eventName}}, where eventName is any applicable DOM event (eg click, mouseover, submit etc). In our example we want a click event, hence we specified the attribute name golgi:on_click. The value you specify is used by Golgi to add an event listener to the DOM element, invoking the specified WebComponent method. In our example, this is a method named report which we added to the WebComponent, and which, when invoked, updates the Component's text state property.

Note: Golgi ensures that the this context within the method is the WebComponent itself, as you'd expect.



Removing Golgi Components from the DOM

Sometimes you'll want to remove stuff from the DOM that represents your UI. You should do this by using the remove() method that is automatically added to WebComponents by Golgi.

It's not a good idea to use the simple, raw removeChild() DOM method to remove Golgi Components. That's because:

  • as you'll discover when we look at Golgi Assemblies, the Golgi Component you're removing may have other child Golgi Components nested within it, which, in turn, may have others nested within them, and so on ad nauseam. Each of these child Components should be cleanly and explicitly removed too in order to avoid memory leaks.

  • furthermore, the WebComponent(s) being removed may have been augmented with handler functions (as per our previous example), and these will be left hanging about in memory if you simply use removeChild() to delete the WebComponent from your DOM.

Let's deal with these in reverse sequence. When adding handlers via the golgi:on_ attribute, Golgi registers the handler in a Map that is maintained within the WebComponent.

Then, dealing with the first issue, by using Golgi's remove() method, it will recurse down through any lower-level nested Golgi Components, starting at the lowest-level leaf Components, first removing any registered handlers and then deleting the WebComponent before moving up to its parent and repeating the process. The remove() method, together with the addHandler() method, therefore ensure that memory leaks are avoided when deleting a Golgi Component.

So, let's put all these together into our example.

Edit your root Module (/golgi/demo.js), and add a setTimeout function that will remove the demo-div Component from the DOM after 5 seconds:

  (async () => {
    const {golgi} = await import('./golgi.min.js');
    let context = {
      componentPaths: {
        demo: './components/'
      }
    };
    golgi.setLog(true);
    let demoComponent = await golgi.renderComponent('demo-div', 'body', context);
    
    setTimeout(function() {
      demoComponent.remove();
    }, 5000);
  })();

Now reload the index.html page. After 5 seconds, the text will disappear, and if you examine the DOM using the browser's Developer Tools elements tab, you'll see confirmation that the <demo-div> tag no longer exists.

Try it again, and this time watch the Golgi log being reported to the browser's Console.

You'll see it reporting the removal of the demo-div element and its click handler.




Golgi Assemblies



What Are Golgi Assemblies?

So far we've just looked at how to define and use a single Golgi Component. Where things begin to get really interesting and powerful is when we start to look at Golgi Assemblies.

At their simplest, Golgi Assemblies are a set of nested Golgi Components.

However, Golgi allows you to do all sorts of things to customise the Golgi Components you use within an Assembly.

Golgi Assemblies always follow the simple pattern:

  export function load() {

    // Define your assembly as a nested set of XML tags, known as "gx"
    // with each XML tag repesenting a Golgi Component of the same name

    // optionally define one or more "hook" methods linked to the gx tags

    return {gx, hooks};  // or return {gx} if you didn't define any hooks
  }; 


Create A Simple Example



Create An Assemblies Directory

Let's create a simple Golgi Assembly using the demo-div Golgi Component we created earlier.

First, it's a good idea to create a separate directory for your Golgi Assemblies. So, on your web server, create a subdirectory beneath the /golgi directory you created earlier, and name it assemblies, ie you should now have a directory:

  /golgi/assemblies


Create Your Assembly File

Within this new directory, create a file named demo_assembly.js (ie /golgi/assemblies/demo_assembly.js), containing the following:

  export function load() {
    let gx=`
  <demo-div text="Welcome to Golgi Assemblies">
    <demo-div text="I'm inside the other div!" />
  </demo-div>
    `;
  
    return {gx};
  };


Edit The Root Application Module

Next, edit your root application module (ie /golgi/demo.js) to render this assembly rather than the single Golgi Component we've been using so far:

  (async () => {
    const {golgi} = await import('./golgi.min.js');
    let context = {
      componentPaths: {
        demo: './components/'
      },
      assemblies: './assemblies'
    };
    
    golgi.setLog(true);
   
    let rootComponent = await golgi.renderAssembly('demo_assembly', 'body', context);
  })();


Run The Example

That's it! Now see what happens when you reload the index.html page in your browser.

You should see two lines:

  Welcome to Golgi Assemblies
  I'm inside the other div!

Take a look at the browser's Developer Tools Elements view and you'll see that it has indeed rendered and nested two of our demo-div Components:

  <demo-div>
    <div>
      </span>Welcome to Golgi Assemblies</span>
      <demo-div>
        <div>
          </span>I'm inside the other div!</span>
        </div>
      </demo-div>
    </div>
  </demo-div>


How And Why Did The Example Work?

Let's analyse in detail what happened and why.



The Root Application Module

Let's start with the root application module.



The Context Object

The first thing to notice is that we extended the context object with the path for the directory we created for our assembly files:

  let context = {
    componentPaths: {
      demo: './components/'
    },
    assemblies: './assemblies'
  };

In an individual Golgi application, all your Golgi Assembly files must reside together in the same directory. The Golgi Components used by your Assemblies may come from more than one path: if you remember, Golgi Components are namespaced according to the prefix in their hyphenated name.



The renderAssembly() Method

A Golgi application will normally be started by rendering an initial "root" Golgi Assembly. That Assembly or its logic within it may render other Assemblies, but your main application module should render the root Assembly using Golgi's renderAssembly() method. Here that is in our example:

  let rootComponent = await golgi.renderAssembly('demo_assembly', 'body', context);

You'll notice that this is almost identical to the renderComponent() method we used previously.
It returns an object that represents the root component withing your Assembly. By default that will be the first/outermost Component in the Assembly.

The renderAssembly() method requires three arguments:

  • the name of the Golgi Assembly

  • the target element within your HTML page to which the root Component within the Assembly will be appended. We want to append it to the body tag, so instead of specifying it in full, ie:

    let rootComponent = await golgi.renderAssembly('demo_assembly', document.getElementsByTagName('body')[0], context);
    

    ... and because the body tag is a very common one to use as a target, the renderAssembly() method allows you to simply specify it as the string value 'body'.

  • the context object which then becomes available for use within your Golgi Assembly's exported function if you need or want it.

The renderAssembly() method dynamically imports the demo_assembly Module using a path constructed as follows:

  context.componentPaths.assemblyPath + {{assembly name}} + '.js'

eg in our example:

  './assemblies/' + 'demo_assembly' + '.js'

So this highlights an important pattern you must adhere to when using Golgi: the filename for each of your Golgi Assemblies must match the name by which your refer to it.



Our Golgi Assembly

So now let's look at our simple demonstration Golgi Assembly.



The Golgi Assembly Pattern

The first thing to notice is that it must be defined as an ES6 module that exports a function named *load(). This function has a single argument which is the context object that you passed into the renderAssembly() method as its third argument:

  export function load() {
    // assembly definition
  };

Inside this function, we define something refered to as gx which is a group of XML tags representing the Golgi Components and/or other Golgi Assemblies that constitute our Assembly.

Note that the gx must be specified within back-ticks:

    let gx=`
  <demo-div text="Welcome to Golgi Assemblies">
    <demo-div text="I'm inside the other div!" />
  </demo-div>
    `;

The block of gx must follow the basic XML rules, so that means no attributes without values, and no unclosed tags: this isn't lazy-formatted HTML!

The gx that you define must be returned using:

  return {gx};


How the renderAssembly() Method Works

The renderAssembly() method first dynamically imports the Golgi Assembly Module, and then invokes its load() method. This results in a cascade of activity:

-each Golgi Component referenced in your gx is dynamically imported and rendered in turn, first starting with the parent gx tag(s), then, once each has completed its load and render sequence, any child gx tags are similarly processed. This process is repeated, recursing down through all the nested gx tags that you have specified.

Note 1: Golgi processes each child gx tag in strict sequence, to ensure that they are correctly appended to their parent element's target(s) in the sequence, as defined by the sequencing of your gx tags. The importation of each child's associated Golgi Component modules must therefore await completion of its previous sibling before it can begin its own importation and processing. All of the previous sibling's descendent Components also have to complete rendering before the next sibling Component's rendering can complete.

Note 2: Each Golgi Component that you use is only physically imported once. Any subsequent references to/use of that same Golgi Component will use a cached version without any noticeable overhead. Also, of course, the browser itself will thereafter cache the modules imported by your application, so any network transport delays will be significantly reduced whenever the application is re-run in the browser.

Note 3: Even if a large UI application involves the importation of many Golgi Components, you should nevertheless find that both perceived and actual performance is very high because (a) the imported modules are typically tiny files; (b) the progressive, "only as needed" build-out of an Golgi application means that only those Component modules needed at any point within your application are actually physically imported; and (c) Your UI elements should appear in sequence as soon as they are loaded, so the user shouldn't typically see any significant delays whilst looking at a blank screen.



What Happened When Our Assembly Was Rendered?

Hopefully everything will become clearer if we step through our example Assembly and analyse how Golgi processed it.



The Parent gx Tag

It starts with the parent gx tag which, in our case is:

  <demo-div text="Welcome to Golgi Assemblies">...</demo-div>

This tells Golgi to render our demo-div Golgi Component. If it hasn't already been imported, Golgi imports it, and once ready, it renders the Component, attaching it to the parent DOM node defined by the renderAssembly() function's second argument.



The demo-div Golgi Component

Let's just remind ourselves what the demo-div Golgi Componentt looked like:

  export function load() { 
    let componentName = 'demo-div';
    let count = -1;
    customElements.define(componentName, class demo_div extends HTMLElement {
      constructor() {
        super();
        count++;
        const html = `
  <div>
    <span golgi:prop="spanTag" golgi:on_click="report"></span>
  </div>
        `;
        this.html = `${html}`;
        this.name = componentName + '-' + count;
      }

      setState(state) {
        if (state.text) {
          this.spanTag.textContent = state.text;
        }
      }

      report(e) {
        e.stopPropagation();
        this.setState({text: 'You clicked the div at ' + Date.now()});;
      }
    });
  };


The DOM After The Parent gx Tag is Rendered

At this point, your page's DOM will look like this:

  <body>
    <demo-div>
      <div>
        <span>Click Me!</span>
      </div>
    </demo-div>
  </body>


Setting State Using gx Tag Attributes

The next thing that happens is that Golgi looks through the gx tag's attributes. Unless prefixed with golgi:, each one is used to specify initial state for the Component.

The logic used by Golgi is as follows:

  • if a method with the same name as the attribute name exists within the WebComponent, it is invoked and the attribute's value is used as the argument for that method

  • if no matching method is found, Golgi will try to use the WebComponent's setState() method.

So, on finding this attribute:

  text="Welcome to Golgi Assemblies"

... then, if a method named test() exists within the WebComponent, it is executed, ie:

  component.text("Welcome to Golgi Assemblies")

...and if not, the Component's setState() method is used, ie:

  component.setState({text: "Welcome to Golgi Assemblies"});

So you can see in the demo-div WebComponent definition it will use the latter, which will result in:

  this.spanTag.textContent = state.text;

Remember that this.spanTag was defined by the golgi:prop attribute in the span tag here:

    <span golgi:prop="spanTag" golgi:on_click="report"></span>


The DOM After State Is Set

And so the text within the demo-div Component's span tag will change to Welcome to Golgi Assemblies, ie:

  <body>
    <demo-div>
      <div>
        <span>Welcome to Golgi Assemblies</span>
      </div>
    </demo-div>
  </body>

So that completes the processing of the parent gx tag.



The Child gx Tag

Golgi now moves on to process any of its child gx tags, and finds this one:

    <demo-div text="I'm inside the other div!" />

So it repeats the steps. Golgi notices that it's already imported and registered the demo-div WebComponent, so it can use it straight away. It renders the WebComponent's HTML and then it needs to decide where to append it.



The childrenTarget Property

Unless told otherwise, Golgi will assume that the rendered HTML should be appended to the parent Component's element designated by a childrenTarget property. By default, and unless otherwise instructed to do so, Golgi will automatically assign a WebComponent's HTML rootElement to be the childrenTarget.

We'll see later how you can change and control this, but in our example, Golgi has used its default logic, so the childrenTarget is the outer <div> tag of the demo-div Component, ie:

                   <body>
                     <demo-div>
  childrenTarget ==>   <div>
                         <span>Welcome to Golgi Assemblies</span>
                       </div>
                     </demo-div>
                   </body>

As a result, the second instance of the demo-div WebComponent is appended as a child of the first instance's div tag.



The DOM After the Child gx Tag is Rendered

So the DOM now looks like this:

  <body>
    <demo-div>
      <div>
        <span>Welcome to Golgi Assemblies</span>

        <demo-div>
          <div>
            <span>Click Me!</span>
          </div>
        </demo-div>

      </div>
    </demo-div>
  </body>

The text attribute of the inner demo-div gx tag is now processed:

 text="I'm inside the other div!"

which, once again, applies the setState() method, but this time applied to the inner WebComponent instance, resulting in the DOM changing to:

  <body>
    <demo-div>
      <div>
        <span>Welcome to Golgi Assemblies</span>

        <demo-div>
          <div>
            <span>I'm inside the other div!</span>
          </div>
        </demo-div>

      </div>
    </demo-div>
  </body>

and with that, Golgi find no more gx tags in our Assembly, so processing of our example completes!



Try Out the demo-div Component Click Handler

Now that the completed page is rendered in your browser, if you remember back to when we created the demo-div component, we added a click handler to its span tag which invoked a method named report:

      report(e) {
        e.stopPropagation();
        this.setState({text: 'You clicked the div at ' + Date.now()});
      }

Try clicking each of the lines of text. You should notice that they each respond to clicks and also behave independently, each updating the text within their own instance of the demo-div WebComponent.



Adding Hooks To Golgi Components Within Assemblies



What Are Hooks?

Golgi Assemblies have a very powerful trick up their sleeve: Hooks.

Hooks are methods that you can optionally define and assign to specific Components within your Assemblies.

A Hook Method is invoked immediately after its owner Component is rendered and attached to the DOM. Critically, the Hook Method is only invoked for the instance of the Component for which it is defined.

**Note: ** a Component's Hook Method is invoked immediately after the Component's state is updated in accordance with any attributes in the Component's gx tag,



An Example Use Case for Hooks

So far you've seen how you can define properties and methods within the WebComponent definition of a Golgi Component. Such methods and properties become available for every instance of that Golgi Component within your Assemblies.

You'll frequently have situations where you want to augment just one particular instance of a Golgi Component within your Assemblies, for example adding a particular handler to it, whilst leaving all other instances of the Component as standard. Hooks make this trivially simple.



Specifying A Hook



The golgi:hook gx Attribute

You add a hook to a Component by adding the special attribute golgi:hook to the Component's gx tag. You must provide a name for the method (this allows you to have multiple instances of a Component within your Assembly, each with a different hook method if you wish).

So change the gx of your example Assembly as follows, adding a hook to the inner instance of the demo-div tag:

  let gx=`
  <demo-div text="Welcome to Golgi Assemblies">
    <demo-div text="I'm inside the other div!" golgi:hook="addHandler" />
  </demo-div>
  `;


The hooks Object

We now need to define the actual addHandler hook method. We do that by adding an object named hooks within your Assembly's load() function. The hooks object should follow the pattern below:

  let hooks = {
    {{gx tag name}}: {

      {{hook name}}: function() {

        // define the hook logic here

      }
    }
  }

Note that provided you use the syntax above, specifically defining the function usng function() {....}, then the this context within the Hook method will be the instance of the Web Component within the specified Golgi Component.

You can define as many hook methods with an Assembly as you like, one for every gx tag if you want! Simply ensure that the object names in the hooks object match up with the tag names and *golgi:hook" values used in the gx tags.

Having added the hooks object, you must add it to the exports from the Assembly's load function. This is the most common mistake I make and then I wonder why my hooks aren't working!

  return {gx, hooks};


Add A Hook To Our Example Assembly

So, let's now add our addHandler() Hook to our Assembly:

  let hooks = {
    'demo-div': {
      addHandler: function() {

        // note that "this" refers to the specific instance of
        // the owner WebComponent when the Hook is invoked

        const fn = () => {
          this.spanTag.textContent = 'You moused over at ' + Date.now();
        };
        this.addHandler(fn, this.spanTag, 'mouseover');
      }
    }
  };

  return {gx, hooks};

You'll notice that we're using a special method - this.addHandler() - that Golgi has automatically added to the WebComponent for you during rendering. The addHandler() method adds the DOM EventListener, but also registers the handler within Golgi, and, in doing so, it ensures that if this Component is destroyed using this.remove(), then the handler will also be removed from memory.

You should always use this.addHandler() when dynamically adding event listeners to any Golgi Components.

this.addHandler() takes up to 3 arguments:

  • the function to be executed when the handler is triggered
  • the DOM element to which the listener should be attached
  • the event to be listened for. If not specified, click is assumed.


Try It Out!

Now all you need to do is try reloading your index.html page in your browser. Whenever you run your mouse pointer over the second line of text, you should find that it changes. However, the outer instance of the demo-div Component, which is showing the first line of text, won't be affected by a mouseover event. Both instances still respond to a click event, because this is defined in the underlying WebComponent used by each instance.



Customising How Golgi Components Are Appended to their Parent Component

As you've seen earlier, by default, child Components are appended to the rootElement of their parent Component.

This is because:

  • by default, a child Golgi Component is appended to the element denoted by its parent component's childrenTarget property

  • unless told otherwise, Golgi assigns a Component's root element to its childrenTarget property.

You have control over both of these factors.



Re-assigning the childrenTarget Property

You've seen previously how you can assign a property name to an element within a Golgi Component by adding the special golgi:prop attribute to it. For example, in our demo-div component we assigned its span tag to its spanTag property:

  const html = `
  <div>
    <span golgi:prop="spanTag">Click Me!</span>
  </div>
  `;

Let's amend the demo-div Component definition as follows:

 const html = `
  <div>
    <span golgi:prop="spanTag" golgi:on_click="report">Click Me!</span>
    <pre>+++++++++</pre>
    <div class="test" golgi:prop="childrenTarget"></div>
    <pre>---------</pre>
  </div>
  `;

and try reloading the index.html page in your browser. You'll see that it now looks a little different. Take a look at the DOM using the browser's Developer Tools Elements tab, and you'll see that it now looks like this:

  <body>
    <demo-div>
      <div>
        <span>Welcome To Golgi Assemblies</span>
        <pre>+++++++++</pre>
        <div class="test">

          <demo-div>
            <div>
              <span>I'm inside the other div!</span>
              <pre>+++++++++</pre>
              <div class="test"></div>
              <pre>---------</pre>
            </div>
          </demo-div>

        </div>
        <pre>---------</pre>
      </div>
    </demo-div>
  </body>

So, simply by specifying:

  golgi:prop="childrenTarget"

within one of the HTML elements in a Golgi Component reassigns it as the target element to which child Components will be automatically appended.



Multiple Parent Append Target Elements

Sometimes you might want to define more than one element in a parent Component as target elements for child Components to be appended to. For example, you might design a UI Component named my-card that defines within it an empty head, body and footer section. It would be nice to be able to do the following with such a Component:

  <my-card>
    <my-card-header-content />
    <my-card-body-content />
    <my-card-footer-content />      
  </my-card>

but how would Golgi know not to simply append all three child components to the my-card Component's childrenTarget element?

It's actually a very simple two-step process:

  • within the parent Component, my-card in the example above, specify property names to the three elements that need to act as the header, body and footer targets, eg:

    ...
    <div class="header" golgi:prop="headerTarget" />
    ....
    <div class="body" golgi:prop="bodyTarget" />
    ...
    <div class="footer" golgi:prop="footerTarget" />
    ...
    
  • in each of the child Component gx tags, add the special golgi:appendTo property, with its name specifying the relevant parent property name, eg:

    <my-card>
      <my-card-header-content golgi:appendTo="headerTarget" />
      <my-card-body-content golgi:appendTo="bodyTarget" />
      <my-card-footer-content golgi:appendTo="footerTarget" />      
    </my-card>
    

Let's try something similar with our demo-div example.

Edit its HTML definition to the following:

 const html = `
  <div>
    <span golgi:prop="spanTag" golgi:on_click="report">Click Me!</span>
    <pre>+++++++++</pre>
    <div class="test" golgi:prop="testTarget"></div>
    <pre>---------</pre>
  </div>
  `;

And now change the gx tags within the demo_assembly to this:

  let gx=`
  <demo-div text="Welcome to Golgi Assemblies" >
    <demo-div text="I'm attached to the parent's root element" golgi:hook="addHandler" />
    <demo-div text="I'm attached to the parent's testTarget" golgi:appendTo="testTarget" />
  </demo-div>
  `;

Then reload the index.html page in your browser and confirm that it worked by examining the DOM using the browser's Developer Tools Elements tab. Note how we used the default childrenTarget property in the parent for the first child demo-div gx tag.



Using Golgi Assemblies within gx

So far we've seen how the gx within a Golgi Assembly defines the set of Golgi Components that we want to put together.

You've probably been wondering, is it possible to refer to another Golgi Assembly within gx?

The answer is yes. You can specify an Assembly in gx by prefixing its name with assembly:, eg:

  <demo-div>
    <assembly:my_assembly />
  </demo-div>

This tells Golgi to load and render an Assembly module named my_assembly.js and append it to the childrenTarget element of its parent demo-div Component.

Essentially the usual gx features apply, eg:

  • you can specify the parent target element to which to append the Assembly by using the gx golgi:appendTo attribute. By default it will be appended to the parent Component's childrenTarget element.

  • you can specify a Hook method by using the gx golgi:hook attribute. The Hook method is invoked immediately after the Assembly has completed loading and being appended to its parent element.

    Note: unlike a Component, this within an Assembly Hook method refers to the Golgi object itself, not an individual WebComponent. Nevertheless, you still have access to the same methods as you would have within a Component.

Note 1: A gx assembly: tag cannot have child tags.

Note 2: One thing to be careful of - make sure you don't try to load an Assembly that also tries to load another instance of itself. You'll put Golgi into an infinite loop if you do!



Using ordinary HTML tags within gx

Normal HTML markup tags can be used within a Golgi Assembly's gx.

You can even use the golgi-appendTo and golgi-hook attributes, and the HTML tag can include gx Golgi Components or Assemblies as child gx tags!

Here's an example:

  let gx=`
  <demo-div text="Welcome to Golgi Assemblies" golgi:stateMap="message:text" >
    <demo-div text="I'm inside the parent root element" />

    <div class="testing" golgi:appendTo="testTarget" golgi:hook="divhook">
      <demo-div text="demo-div within html div!" />
    </div>

    <demo-div text="I'm inside the parent testTarget" golgi:appendTo="testTarget" />
  </demo-div>
  `;

  let hooks = {
    div: {
      divhook: function() {
       // do something when div tag has loaded and attached!
      }
    }
  }


Rendering Multiple Instances of an Assembly Within a Loop

Background

You're likely to encounter situations where you want to dynamically generate and render multiple instances of a Golgi Assembly, controlled by a programmatic loop, eg within another Assembly's Component Hook method. Golgi provides a method - renderAssembly - that you can use for this purpose.

For example, if you are using the golgi-sbadmin Component Library, you might need to programmatically generate a set of Carousel Items.

In such situations, you'll almost certainly want to populate the Components within the repeatedly-generated Assembly using data held in an array.

Golgi provides a straightforward and convenient way to do this by making use of the Golgi Context Object.

Every Golgi Assembly and Component has access to the Context Object: normally it's used internally within Golgi itself to locate and load your Components and Assemblies. However, you can augment the Context Object for your own purposes.

Your Context data can be accessed by any attribute within a Component in an Assembly's gx, eg:

    <img src="golgi:context=personData.imageUrl" />

Example

Let's look at a simple example: suppose we have retrieved from a back-end database some data for a set of people, and it's held in an array - peopleData, with each array element being an object that defines the data items for each person, eg:

    [
      {name: 'Rob', city: 'London'},
      {name: 'Mike', city: 'Edinburgh'},
      ...etc
    ]

We'll probably want to perform our dynamic logic as a Hook method for the sbadmin-carousel Component, eg:

     <sbadmin-carousel golgi:hook="populate" />

You need to ensure that the Hook method makes the Golgi Context object available by adding it as an argument. I usually name it ctx, eg:

     {{hook-method-name}}: function(ctx) {
       ...etc
     }

The sbadmin-carousel's Hook Method will then look something like this:

    let hooks = {
      'sbadmin-carousel': {
        populate: async function(ctx) {
          // fetch the data and return an array of objects - summarised in the call below:

          let peopleData = await fetchDataFromDatabase();

          // loop through the people array

          for (let person of peopleData) {

            // add/overwrite the person object to the Context Object

            ctx.personData = person;

            // render an instance of the carousel item for each person,
            //  attaching it to the sbadmin-carousel's children target element
           
            let itemComponent = await this.renderAssembly('myCarouselItem', this.childrenTarget, ctx);

          }

        }
      }
    };

Next, let's take a look at what the gx within the myCarouselItem Assembly might look like.

    let gx = `
      <sbadmin-carousel-item>
        <sbadmin-card bgColor="light" textColor="dark" width="75%" position="center">
          <sbadmin-card-header text="golgi:context=personData.name" />
          <sbadmin-card-body>
            <sbadmin-card-text text="golgi:context=personData.city" />
          </sbadmin-card-body>
        </sbadmin-card>
      </sbadmin-carousel-item>
    `;

You can see that we're telling Golgi to set the text attribute of the sbadmin-card-header Component to the value held in ctx.personData.name property.

Similarly we're telling Golgi to set the text attribute of the sbadmin-card-text Component to the value held in ctx.personData.city property.

The result will be a set of Carousel items, each one showing the name and city for each person fetched from the back-end database.

Note that the golgi:context functionality can be applied to any appropriate attribute within a Component specified in gx.

Note also that if you specify an invalid Context path, an error value will be returned and used as the attribute value.

Hoisting Classes to the Top-Level Component Element

When using a UI Library such as Bootstrap 5 with Golgi, you will sometimes need to specify a particular class or group of classes within the actual Component's outer tag. By default, when you define the HTML for a Golgi Component, you're actually defining the associated WebComponent's innerHTML which is appended to the WebComponent's element tag.

In many/most circumstances it won't matter if you apply the required class(es) to the rootElement of the WebComponent's innerHTML, but if the styling gets confused, you'll need to hoist the class(es) to the WebComponent's own element tag.

For example, suppose we needed to define a particular class in a Golgi Component (sbadmin-sidebar-menu) that defined a Bootstrap 5 sidebar menu as follows:

  const html = `
  <div class="sb-sidenav-menu">
    <div class="nav" golgi:prop="childrenTarget" />
  </div>
  `;

If we find that the styling inside this menu doesn't work properly, it's probably because that sb-sidenav-menu class needs to be on the <sbadmin-sidebar-menu> tag rather than the <div> tag of its innerHTML.

Now we could easily fix that programmatically within the Component's onBeforeState() lifecycle method, but you can instead do it declaratively within the HTML as follows:

  const html = `
  <div golgi:component-class="sb-sidenav-menu">
    <div class="nav" golgi:prop="childrenTarget" />
  </div>
  `;

The special golgi:component-class attribute allows you to tell Golgi to hoist the class value up to the <sbadmin-sidebar-menu> tag, resulting in this Component being added to the DOM as:

  <sbadmin-sidebar-menu class="sb-sidenav-menu">
    <div>
      <div class="nav"></div>
    </div>
  </sbadmin-sidebar-menu>

Now the styling issues should disappear!



Dynamically Loading JavaScript and CSS Resources

Although many JavaScript UI frameworks are beginning to provide their JavaScript files as ES6 modules, many are still only available to a browser by loading them using a <script> tag within the HTML page.

Similarly, when using Golgi with another UI framework, you'll need to load its CSS stylesheets by using a <link> tag within the HTML page's head section.

Although you could add these <script> and <link> tags to the index.html file that you use to start your Golgi application, Golgi makes it easy for you to load such resources dynamically from within a Golgi Assembly's gx.

"Why would you want to do this?", you might ask.

There are several reasons:

  • keeping the index.html pared down to the bare minimum, with no application dependency needed

  • keeping any resource dependencies defined within the associated Golgi Assemblies that use them. This is extremely useful from a maintenance perspective.

  • you might need a specialised UI library, eg a charting library such as Chart.js, but not until the user clicks a link to generate a chart. If that was a relatively rare occurrence within the application, pre-loading the Chart.js Javascript file would be a waste of time for most users.

Golgi therefore allows you to dynamically load any resources on a "just in time* basis by adding <script> and <css> tags as child gx tags of a parent Component gx tag.

For example:

  <chart-root>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.min.js" await="true" />

    <chart-area-plot golgi:hook="draw" golgi:stateMap="chart:update"/>
  </chart-root>

This tells Golgi to dynamically load the Chart.js JavaScript file from a CDN source immediately before importing and rendering the chart-root Golgi Component module.

The gx script tag attribute *await="true" forces the importing and rendering of the chart-root Component to await the completion of the loading of the JavaScript file.

Dynamic loading of such JavaScript files is actually performed by Golgi adding a script tag to the DOM.

Here's another example, dynamically loading resources needed for a Bootstrap 5 application:

  let gx=`
  <sbadmin-root golgi:hook="loadContent">

    <script src="/golgi/components/sbadmin/js/fontawesome-5.15.3.all.min.js" />
    <script src="/golgi/components/sbadmin/js/bootstrap.bundle.min.js" await="true" />
    <css src="/golgi/components/sbadmin/css/styles.css" />

    ... etc

  </sbadmin-root>

`;

In this example, two JavaScript files and one CSS file are loaded before the sbadmin-root Component is imported and loaded. Notice that Golgi will wait until the Bootstrap 5 JavaScript file is fully loaded and ready, but it doesn't need to wait for the fontawesome JavaScript.

Similarly, Golgi doesn't need to await the completion of the CSS file loading.

Whether or not your Components need to wait for the JavaScript and CSS resources to complete loading will vary depending on the UI library you use. The important thing is that Golgi provides you with the mechanism to await or not before proceeding with its Component rendering.

Note: Both the gx script and css tags allow you to also specify the attribute crossorigin="anonymous which, if added, adds this to the actual script tag that is added to the DOM.



Dynamically Adding Meta Tags to the DOM

Responsive, mobile-first UI libraries such as Bootstrap 5 require you to add a number of <meta> tags to your HTML page.

As discussed above for JavaScript and Stylesheet resource loading, you can, of course, add these to the index.html file that you use to launch your Golgi application.

However, to keep any application dependencies localised to your *Golgi Assemblies", Golgi also allows these <meta> tags to be added to the DOM dynamically from within an Assembly's gx.

Simply add the required <meta> tags as child gx tags of your application's top-level Assembly's gx. For example:

  <sbadmin-root>
    <script src="/golgi/components/sbadmin/js/fontawesome-5.15.3.all.min.js" />
    <script src="/golgi/components/sbadmin/js/bootstrap.bundle.min.js" await="true" />
    <css src="/golgi/components/sbadmin/css/styles.css" />

    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />

    ... etc

  </sbadmin-root>

The corresponding <meta> tags will be added dynamically to the DOM by Golgi before the parent Component is imported and rendered.




Golgi Component Lifecycle Methods

Golgi provides a number of lifecycle methods that you can specify within the WebComponent definition of a Golgi Component. In lifecycle sequence they are:

  • onBeforeState() {...}

    This is invoked immediately after the Golgi Component is loaded into the DOM, and before any state values (as specified as gx attributes) are applied.

    This is a useful lifecycle method to use for additional custom augmentation of the Component's WebComponent's methods and/or properties.

  • onBeforeHooks() {...}

    This is invoked after any state values (as specified as gx attributes) are applied to the WebComponent, but before the Component's Hook Method (if defined) is invoked

  • onAfterHooks() {...}

    This is invoked after the Component's Hook Method (if defined) is invoked. This is the last lifecycle method to fire before Golgi moves on to process the next gx tag.

  • onChildComponentReady(childComponent) {...}

    This is invoked if the Component, when rendered as part of an Assembly, has one or more child Components. If so, each Child Component will trigger a call to its parent Component's onChildComponentReady() method (if present). The instance of the Child Component is passed to this method as its argument.

    This method is useful in situations where the parent Component's methods need later access to its Child Component's properties or methods.

Note that any or all of these lifecycle methods can be specified as async if required.

Note: If you have augmented the WebComponent with anything else that Golgi is unaware of, and that should be explicitly destroyed if the Component is removed, you should use the standard WebComponent disconnectedCallback() lifecycle method.




Golgi Properties for Navigation between Golgi Components

During Component rendering, Golgi adds a number of useful properties to Components and Elements within Components. These allow you to quickly and efficiently navigate between your rendered Components:

  • this.rootComponent

    The value of this property, assigned to every Golgi Component, is your application's root Component, ie the first one you render within your application root module, and which you will have appended to the DOM's body tag.

  • this.parentComponent

    Every Component, except the root Component has this property whose value is the Component to which it has been appended

  • this.ownerComponent

    Every HTML Element within a Component is given this property whose value is the Component containing the Element



Customising Navigation between Golgi Components within Assemblies

You'll often want to be able to quickly and easily access the Components within an Assembly.

As noted earlier, the renderAssembly() method will return the Assembly's Root Component, eg:

    let itemComponent = await this.renderAssembly('myCarouselItem', this.childrenTarget, ctx);

In this earlier example, the myCarouselItem Assembly's gx was:

    let gx = `
      <sbadmin-carousel-item>
        <sbadmin-card bgColor="light" textColor="dark" width="75%" position="center">
          <sbadmin-card-header text="golgi:context=personData.name" />
          <sbadmin-card-body>
            <sbadmin-card-text text="golgi:context=personData.city" />
          </sbadmin-card-body>
        </sbadmin-card>
      </sbadmin-carousel-item>
    `;

The renderAssembly() method's return value - itemComponent - will be a Component object representing the outermost Component in the Assembly, ie sbadmin-carousel-item Component.

If you want to be able to access the Carousel Item's sbadmin-card-header Component at a later stage, for example so that you can dynamically change its header text, you need to be able to locate. You have several options:

  • use the getComponentsByName() method. This returns an array of all Components anywhere within the DOM that match the specified name, eg:

      let headerComponents = itemComponent.getComponentsByName('sbadmin-card-header');
    

    This is fine if there is only one instance of the specified Component, but gets more tricky to use if there are multiple instances.

    Note that this method works even if you use ShadowDOM.

  • a better approach is to add the special Golgi attribute to the Component you want to be able to reference: golgi:ref, eg:

      <sbadmin-card-header golgi:ref="header" text="golgi:context=personData.name" />
    

    When the Assembly is rendered, the returned Component will now have an additional property - in this case header - that represents the specified reference Component, eg after rendering the assembly:

      let itemComponent = await this.renderAssembly('myCarouselItem', this.childrenTarget, ctx);
    

    You'll now be able to access the sbadmin-card-header Component's properties and methods using:

      itemComponent.header
    

    eg to change the header text:

      itemComponent.header.text = 'New Header Text';
    

You can specify as many reference Components as you wish within an Assembly. Note that you must take care not to overwrite a reserved Component property name.



Golgi Methods For Use Within Golgi Components and Hook Methods

The WebComponents that underpin your Golgi Components are automatically augmented with a number of methods that allow you to manipulate and navigate about within your rendered Components in the DOM. They are all accessed as this.{{methodName}}.

  • this.addHandler(fn [, targetElement] [, eventName])

    This method should always be used to dynamically add event handlers to your Components, because the remove() method will automatically destroy the handler methods if the Component is removed (either directly or via a removed parent Component).

    The handler function should always be specified

    If the second argument (*targetElement) is not specified, the handler will be added to the Component's rootElement.

    If the third argument (eventName) is not specified, a 'click' event is assumed.

  • this.getComponentsByName(componentName)

    This method is used to find and return one or more Components matching the specified name anywhere within the DOM (even if you use ShadowDOM within some of your Components). It returns an array of matching Component objects.

  • this.getComponentByName(componentName [, name_property_value] [, parentElement])

    This method is used to find and return one or more Components matching the specified name. What it returns will depend on the arguments you specify:

    • if you just specify the first argument (*componentName), then a NodeList of all Components in the DOM with the specified tagName is returned.

    • if you specify a second argument (name_property_value), then it will return the first Component it finds whose name property matches the specified value. For this to work you must make sure that such Components include a setState() condition to allow the name property to be set.

    • if you specify a third argument (*parentElement), the search for matching Components is limited to descendant nodes of the specified parent Element. Note that this is of limited use if you use ShadowDOM within your Components, as it cannot cross the ShadowDOM boundary.

  • this.getParentComponent(componentName)

    This method recurses up through the DOM, starting from the current Component, until it finds an element whose tagName matches the specified Component Name. It then returns that Component Element. Note that this method also automatically caters for Components that use ShadowDOM.

  • this.remove()

    Removes the current Component and all of its child Components. This method also removes any handlers that were added to the removed Components, provided those handlers were added using the this.addHandler() method

  • this.renderAssembly(assembly_name, append_target_element, context)

    This method will import and load the specified Golgi Assembly and, when ready, will append it to the specified target Element (append_target_element). The Golgi Context object, available within the current Component as this.context, should also be specified as the third argument).

    It returns an Object representing the outermost Component within the Assembly.

  • this.renderComponent(component_name, append_target_element, context)

    This method will import and load the specified Golgi Component and, when ready, will append it to the specified target Element (append_target_element). The Golgi Context object, available within the current Component as this.context, should also be specified as the third argument).

    It returns an Object representing the instance of the specified Component.

  • this.addStateMap(state_property_name)

    This method is described in detail in the next section. It is used to add a data-binding state map to the component. Note that the state_property_name value should be unique to the specific instance of the Component.




State Management and Data Binding In Golgi



Defining A State Map in an Assembly

Golgi provides a powerful means of state management and data binding which is deceptively simple to use. The Golgi Object includes a property named golgi_state which is actually a Proxy Object that traps any changes you make to it.

Within a gx tag in an Assembly, you can define a State Map that maps a golgi_state object property name to a setState() property within the Component. You do this by using a special gx attribute named golgi:stateMap. Its value has two parts, separated by a colon:

  • the golgi_state object property name to trap. This can use dot syntax to specify lower sub-levels within the state object.
  • the target Component's method or setState property to which to map the state value

For example:

  <demo-div text="Welcome to Golgi Assemblies" golgi:stateMap="message:text">

Having specified this, and once this Golgi Component has been rendered and is ready for use, you can then do the following anywhere else in a component's methods, for example in a hook method:

  this.golgi_state.message = 'Hello World';

Note that this.golgi_state is how you access the state object from within a Golgi Component.

What will happen is that the instance of the Component to which we applied the stateMap will invoke:

  • a WebComponent method named text if it is defined:

    component.text('Hello World');
    
  • if not, the WebComponent's setState() method for the property named text as follows:

    component.setState({text: 'Hello World'});
    

In our example demo-div Component, this, of course, updates the Component's span text content.

      setState(state) {
        if (state.text) {
          this.spanTag.textContent = state.text;
        }
      }

Of course, when defining a Golgi Component's setState() method logic, we could define all sorts of different state properties with all manner of actions, so you can assign a stateMap to as complex logic as you wish. For example, mapping an array of data points to an update state property could cause a graph to be plotted, simply by setting the data points array in the Golgi state object.

Let's try this out in your example. First change the outer gx tag in your Assembly to:

  <demo-div text="Welcome to Golgi Assemblies" golgi:stateMap="message:text">

and now modify the hook method we defined for the inner gx tag:

  let hooks = {
    'demo-div': {
      addHandler: function() {
        const fn = () => {
          this.spanTag.textContent = 'You moused over at ' + Date.now();

          // add this line:

          this.golgi_state.message = 'I also noticed that at ' + Date.now();

        };
        this.addHandler(fn, this.spanTag, 'mouseover');
      }
    }
  };

Reload the index.html page in the browser, and now try running your mouse pointer over the second line of text. As if by magic, the top line will also now change! This will happen every time you mouse over the second line of text.



Defining Data Binding Within A Component

Rather than having to define lots of methods within a Component, each of which controls the state of DOM elements within your WebComponent, Golgi provides a further, declarative approach which allows you to define the specific data binding used in the Component's state map.

First, you can specify the state map that the Component should use with the addStateMap() method. You'd usually define this in the Component's onBeforeState() lifecycle method, eg:

  onBeforeState() {
    this.addStateMap('article');
  }

If you did this, and then, at some later point, assigned an object to this state property, eg:

  this.golgi_state.article = {
    author: 'Rob',
    title: 'My Great Article',
    description: 'An amazing piece of work!',
    image: 'https://static.productionready.io/images/smiley-cyrus.jpg'
  };

... you can declaratively specify how and where to bind those three properties (author, title and description) within the Component's HTML assignment by using the special keyword golgi:bind={{property_name}}, eg:

  let html = `
  <div class="article-page">
    <div class="banner">
      <div class="container">
        <h1>golgi:bind=title</h1>
        <div>
          <h2>Author</h2>
          <img src="golgi:bind=image" />
          <span class="follow-author">golgi:bind=author</span>
        </div>
        <div>
          <h2>Description</h2>
          <span class="desc">golgi:bind=description</span>
        </div>
      </div>
    </div>
  </div>
  `;

You can see that you can apply the golgi-bind keyword as either an attribute value or as a tag's text content.

You can even apply it to an <input> or <textarea> field by specifying it as a value attribute, eg:

          <fieldset class="form-group">
            <input golgi:prop="image" value="golgi:bind=image" class="form-control" type="text" placeholder="URL of profile picture">
          </fieldset>

The result is that whenever the appropriate golgi_state property is set, if its value is an object, then the object's properties are automatically set to the corresponding DOM element attributes or text content, as defined with the golgi:bind keyword.

In the case of an <input> or <textarea> field, its value property is assigned the value, making it appear in the form.

You'll see examples of this use of golgi:bind within the examples provided in this repository.

  • Note: If you have multiple instances of a Component within your application at any time, and if you have used the golgi:bind keyword within it, then you need to ensure that each Component instance uses its own unique golgi_state property.

    Here's how you could achieve this:

    • within the Component's onBeforeState() lifecycle method, assign a unique name to the instance, and then use that as the state map name, eg:

        onBeforeState() {
          this.stateMapName = 'acticle-' + this.name;
          this.addStateMap(this.stateMapName);
        }
      
    • then when it comes to mapping/binding state values:

        this.golgi_state[specificComponent.stateMapName] = {
          author: 'Rob',
          title: 'My Great Article',
          description: 'An amazing piece of work!',
          image: 'https://static.productionready.io/images/smiley-cyrus.jpg'
        }
      

      where specificComponent is the instance of the Component you want to upate with these values.



Rendering Multiple Copies of a Component Mapped to a State Array

Suppose you want to render error messages returned from a back-end system, and the layout/presentation of each error message is defined in a Golgi Component.

Alternatively, imaging the back-end returns an array of results, each element in the array being an object whose key/value pairs represent columns in a table row that you want to display. The layout/presentation of each row might be defined in a Golgi Component.

Essentially, therefore, in both example scenarios, for each member of the data array, you want to do two things:

  • render an instance of a Golgi Component that will display the array member

  • map the data in the array member's key/value pairs to fields defined in the Golgi Component using the golgi:bind keyword.

Golgi makes this very simple, and you can see examples of its use in the RealWorld Conduit UI example that is included in this repository.

Within any Component, you have access to the Golgi-provided method: this.renderComponentMap()

It has the following arguments:

  • componentName: the name of the Golgi Component to be rendered for each data array member

  • targetElement: the DOM element to which each instance of the Component is to be appended

  • context: the Golgi Content object (accessible as this.context within all Components)

  • dataArray: the array containing the data you want to map to each Component Instance

  • stateMapPropertyName: the name of the this.golgi_state property to be used for data binding.

  • callback: an optional callback function that is invoked after each Component is rendered and after databinding has occurred. The callback function has two arguments:

    • component: the instance of the Component that has been rendered and populated
    • dataArrayRow: the data array member that has been used to populate the instance of the Component

You'll see numerous examples of this method in use within this example Component.




Automatically Detecting DOM Changes in Golgi Components



Detecting Changes the Hard Way

Deep within your application you might have a Golgi Component that displays some text that you might want to change programmatically. Of course, to make the actual text change you can use the data binding provinded by Golgi's StateMap mechanism.

Suppose, however, that whenever the text changes, you want to invoke a method within the Component, causes other things to happen. Some HTML tags, for example input, allow you to add a change EventListener, so for these, within the Component's HTML definition you could do something like this:

    <input golgi:prop="myInput" golgi:on_change="doSomething" />

and you could then define a method within the Component:

    doSomething() {
      // do something because this.myInput.value has changed
    }

However, if the HTML tag was a simple div tag, this wouldn't work. You could, instead, define a MutationObserver for the div tag and trap any events associated with any Mutation childList types.

You don't need to do this, however, since Golgi already automates much of the MutationObserver logic for you within your Components. Golgi provides three ways of applying the MutationObserver:

  • you can observe and handle changes to attributes for any element within a Golgi Component;
  • you can observe and handle changes to the text content of any element within a Golgi Component;
  • for any other scenario, you can define your own mutationOberver handler in a more generic way.


Observing and Handling an Attribute Value Change

This can be done using the special attribute value:

    golgi:observer={handlerMethodName}

eg:

    golgi:observer=setVisibility

Note that Golgi permits an attribute to have multiple special Golgi directives as values. This is needed because you'll often want to observe and handle an attribute whose value is bound to a state-mapped object's property. Separate multiple Golgi directives with a semi-colon and optional space(s), eg:

    <div myAttr="golgi:bind=myProp; golgi:observer=handleMyAttr"></div>

The handler method is simply defined within the containing Component, eg:

    handleMyAttr(newValue, oldValue) {
      // do something 
    }

A Golgi Observer attribute-change handler method takes two arguments:

  • the new value of the attribute being observed
  • the old/previous value of the attribute being observed

Note that this within the handler method refers to the Component.

Note also that you can define as many attribute observers as you wish within a Component.



Observing and Handling an element's Text Content Change

This can be done using the special text value:

    golgi:observer={handlerMethodName}

eg:

    golgi:observer=setVisibility

Note that Golgi permits an element's text content to have multiple special Golgi directives as values. This is needed because you'll often want to observe and handle an element's text content whose value is bound to a state-mapped object's property. Separate multiple Golgi directives with a semi-colon and optional space(s), eg:

    <div>golgi:bind=myText; golgi:observer=handleMyText</div>

The handler method is simply defined within the containing Component, eg:

    handleMyText(value, targetTag, originalTag) {
      // do something 
    }

A Golgi Observer text-change handler method takes three arguments:

  • the new value of the element's text content that is being observed

  • the actual element whose text has changed. If you've used a golgi:bind directive to maintain the value, you should be aware that in order to unambiguously maintain text content, even if there are other nested sub-elements, Golgi actually adds a span tag to your original element, ie for the example above what Golgi actually renders is:

      <div>
        <span>the value of myText</span>
      </div>
    

    So in this case, the targetTag argument is the span tag

  • the original element whose text is being observed. This will always be the one specified in the Component's HTML. For the above example this will be the div tag.

Note that this within the handler method refers to the Component.

Note also that you can define as many textcontent observers as you wish within a Component.

Activating a Generic MutationObserver within a Golgi Component

If you need to observe and handle something other than a tag's attribute or text changes, you can make use of the more generic MutationObserver integration provided by Golgi. This is rather more involved that the attribute-specific and textcontent-specific functionality previously described. For that reason, it's best to use the attribute-specific and textcontent-specific MutationObserver functionality provided by Golgi.

To activate a generic MutationOBserver, invoke this.observerStart() in one of the Component's lifeCycle event handers such as onBeforeState, eg:

    onBeforeState() {
     this.observerStart();
    }

observerStart() will detect all the standard mutation types, ie what Golgi actually runs for you (for each HTML tag within your component) is:

  golgi.observer.observe(target, {
    attributes: true, 
    attributeOldValue: true, 
    characterData: true, 
    characterDataOldValue: true,
    childList: true, 
    subtree: true
  });

When this.observerStart() is invoked, any DOM changes within any of the Component's HTML tags will trigger the Component's observerCallback() method (if it is defined). This callback is triggered for each and every DOM change. What you do in this callback method is up to you, eg:

    observerCallback(mutation) {
      // do something as a result of this specific mutation object
    }

The mutation argument for the observerCallback() is a standard MutationObserver object representing a specific mutation, so it has all the standard properties you'd expect, eg:

    mutation.type: the type of mutation
    mutation.target: the DOM element that has mutated

Simple Example Using the Generic MutationObserver

To detect changes to the text within a div tag in your Component:

  • define the div tag within the Component's HTML definition, eg:

      <div golgi:prop="myText">golgi:bind=theText</div>
    
  • start the Mutation Observer and also define the state map for data binding within the component:

      onBeforeState() {
        this.observerStart();
        this.addStateMap('myExample');
      }
    
  • define the observerCallback() method:

      observerCallback(mutation) {
        // any text change will trigger a childList mutation...
    
        if (mutation.type === 'childList') {
    
          // When using Golgi's data binding, the mutation will have
          // actually occurred on a span tag that is added to the div
          // as a child tag
    
          // So we can determine that the mutation has occurred to
          // the myText div as follows:
    
          if (mutation.target.parentNode === this.myText) {
            this.doSomething();
          }
        }
      }
    
  • define the doSomething() method:

      doSomething() {
        console.log('The text was changed to ' + this.myText.textContent);
      }
    
  • now the div's text can be changed from anywhere within your application logic by simply setting the state map object:

      this.golgi_state.myExample = {
        theText: 'A new value for the div'
      }
    

    and as a result, the doSomething() method will also fire within the Component.