Vanilla JavaScript
The MuraDecoupled GitHub project is an example of building a Mura Decoupled website using standard JavaScript and Mura JS. The project is a Docker repository, so you will need that installed on your computer. This will allow you to quickly spin up the example and begin exploring a vanilla Mura / Javascript integration.
Initial Setup
You will need a couple of items installed on your test machine. The latest version of Node, as well as Docker.
Next you will need the project itself. You should download MuraDecoupled to a working directory (i.e. a directory where the code can be deployed).
Next you need to start Mura up:
git clone https://github.com/murasoftware/mura-decoupled.git
cd mura-decoupled
git checkout master
docker-compose up
Then go to http://localhost:8888 to initialize Mura's install. You can login with the default (admin/admin) and edit the default site's settings:
- Domain= The domain of the remote site that the the content will be served on. (localhost)
- Is Remote = true
- Remote Context = The directory structure off of the remote site's web root that the site lives (Leave Empty)
- Remote Port = The port of the remote site (80)
- Resource Domain = The domain that Mura will use the access resource like css and js scripts that are dynamically loaded. (localhost)
You can now visit the site at http://localhost
And finally go to your Mura admin (http://localhost:8888/admin) and reload Mura one more to and it will see the mura.config.json from the ./app directory.
For the rest of the details on this project, you can view the ReadMe file on the MuraDecoupled project itself.
Key Project Files
There are several key project files you will want to examine and/or modify while exploring this example project.
| Directory or File | Editable | Description |
|---|---|---|
| /index.html | Yes | This is the landing page for the website, and contains only a) a reference to the local Mura JS instance, the application's /app/index.js file, and the most basic HTML required (HEAD/BODY tags). |
| /app/index.js | Yes | This is your core JavaScript project file, the one that does all the required work of fetching content via Mura JS and rendering it to the /index.html <BODY> tag. |
| /app/mura.config.json | Yes | This is the configuration file for the project. If you are familiar with Mura, you will recognize many of the standard configuration settings. This is where you register templates and configuration variables, register modules, and adjust site settings. |
| /app/configurators/examplejs.html | Yes | This is the base file for the ExampleJS module, a custom module built for this site. |
JavaScript
The /app/index.js in the vanilla (sans any external frameworks like jQuery) JS Decoupled example is the core JavaScript project file, containing all of the JavaScript-related to rendering the base decoupled website, where Mura JS acts as both the DOM selector and framework-agnostic ... framework.
The first thing to be aware of is where the content is rendered to.
let currentContent;
This is the variable where the current content is registered to. Note that this becomes a Mura JS "Content" object, containing not only the associated content but a range of helper functions to render the page (i.e. navigation).
Next is the Mura initializaiton function:
Mura.preInit(function(){
// your init code here
}
The "preInit" function is called as a precursor to the Mura "render" function, so that all actions taken here are pre-render. This can be important if your code informs what will take place on the page (such as the instantiation of Modules that the page will need to render, as per this example).
Within this block, we will create a number of custom Mura Modules, which are self-contained and often dynamic components that make up a decoupled website's page. For instance, the page's header, footer, any sidebar or main content area, can all be created as Modules. Custom Modules can also be created, and contained within any of these aforementioned layout Modules.
Custom Module
In this example, we first create a custom module for our project called "Examplejs".
Mura.Module.Examplejs = Mura.UI.extend({
renderClient: function() {
this.context.mytitle = this.context.mytitle || "The 'Title' param has not been assigned";
this.context.targetEl.innerHTML=`<h2>${Mura.escapeHTML(this.context.mytitle)}</h2>`;
return this;
}
});
Note that this Module, and indeed all Modules, are all extensions of the Mura.UI class. This class provides the important renderClient() and renderServer() methods that render your content.
You will notice that Modules are essentially an encapsulated, reusable component with their own render function. If you recall from the Creating Custom Modules section, this.context is the variable that holds all data associated to the module, including any settings registered via the configurator when it is added to the page.
Note that this.context contains one reserved variable name, this.context.targetEl. This is the target element to which your rendered content is applied, which is then rendered by the renderClient() function.
Layout Modules
We next find the actual components. First the Header Module, which will render our breadcrumbs and navigation:
Mura.Module.Header = Mura.UI.extend({
//...
});
We achieve this by referencing the current (referenced as currentContent) .get('crumbs') method. If you examine the JSON API endpoint of any content page, you will see a variety of endpoints available, including one called "crumbs". This request returns an array of shorthand "minimal" content beans called ContentNavBeans. We then render the data returned by this request:
const crumbCollection=await currentContent
.get('crumbs')
.then((crumbs)=>{
return crumbs;
});
As with all Mura JS asynchronous requests, the response is returned as a Promise and in this example uses the await operator to handle the request.
Next we have a custom Module called Examplecollectionlayout, which is included as an example of how you might render a Collection in a decoupled website.
Mura.Module.Examplecollectionlayout = Mura.UI.extend({
renderClient: function() {
// ... rendering code here
}
});
You will find several important references to this in the mura.config.Json, which should help you understand the context of how these are registered specifically as "collection"-rendering objects. This Module includes several key concepts related to collections, such as paging navigation, via helper methods that are included in the context of any Mura Collection.
Note that this Module contains a conditional scroll, where new content is automatically called (asynchronously and then appended to the bottom of the page as the user scrolls downward. This is of course not a necessary embellishment, but is a commonly used one and good example of the flexibility of these types of requests.
With these two Modules sorted, we have reached the point where we are ready to render the page. All of this will occur in the encapsulated Mura() namespace, which ensures that all pre-render actions have been completed.
Mura(function(){
});
The first thing we have here is our layout template. A decoupled site can contain any number of templates (as defined in your mura.config.json file), but for this example we have one, "default", defined as follows:
const templates={
default:`
<nav id="primary-nav">
<ul class="mura-primary-nav"></ul>
</nav>
<div class="mura-region-container" data-region="primarycontent"></div>
<footer>
<!-- Create a component named "Footer" and it will render here-->
<div class="mura-object" data-object="component" data-objectid="footer"></div>
</footer>
<div class="mura-html-queues"></div>
`
};
In this template we defined several regions: a place for our navigation (mura-primary-nav), a container for our primary content (mura-region-container), and a footer. There is also a mura-html-queues which we use as a container to load any queued assets. Let's look a closer look at the footer, as this is an example of how you can hard-code a Module into your templates.
Footer
<footer> <!-- Create a component named "Footer" and it will render here--> <div class="mura-object" data-object="component" data-objectid="footer"></div> </footer>
In this code you can see the div contains a class of mura-object and an attribute data-object of "component". When an object has a class of mura-object, this tells Mura JS that this is a Module, upon which the the element's data-object will be used to determine which Module is being referred to. In this case, we are referring to one of Mura's baked-in Modules, the Component Module. Finally, there is also a data-objectid of "footer", which identifies the specific Component we want loaded. You can apply this same methodology to bake in custom Modules, such as the ones we created above.
Rendering
Now that we have a complete template, we can begin the process of rendering the page. We create a dedicated render() function, as we want to refer to this function whenever the hash of the page changes (as when a user clicks on a navigation link).
First, lets look at the request that loads the current hash (url) and retrieves the content:
let hash= location.hash || '#';
let query=Mura.getQueryStringParams();
Mura.renderFilename(
hash.split('#')[1],
Mura.extend(Mura.getQueryStringParams(),{expand:"crumbs"})
).then(function(content){
currentContent=content;
});
In this example, we are retrieving the "hash" of the url and passing this to the renderFilename() function, which takes not only the hash but any query string params, and uses those to retrieve the requested content page from Mura.
Configuration
The /app/mura.config.json file contains the configuration required to assemble your decoupled Mura website. This is where you will enter all of your global and site-specific configuration, settings specific to the Layout Manager, identify modules and even deploy custom Mura ORM objects.
{
"global":{
"rendererProperties":{
"templateArray":["Default"],
"collectionLayoutArray":["Examplecollectionlayout"],
"hashurls":true,
"primaryContentTypes":"Page,Link,File",
"spacingoptions":[
{"name":"Tight","value":"tight"},
{"name":"Normal","value":"normal"},
{"name":"Loose","value":"loose"}
],
"modulethemeoptions":[
{"name":"Brand Default","value":"module-brand"} // ...
],
"coloroptions":[
{"name":"White","value":"#ffffff"} // ...
],
"ckeditor":{
"contentcss":"http://localhost/app/css/editor.css",
"templates":"",
"stylesset":"",
"customconfig":""
}
},
"modules":{
"Examplejs":{
"name":"Examplejs",
"contenttypes":"*",
"configurator":[
{"type":"text","name":"mytitle","label":"My Label"}
]
},
"Examplecollectionlayout":{
"name":"Example Collection Layout",
"contenttypes":""
},
"Header":{
"name":"Header",
"contenttypes":"*"
}
},
"entities":{
},
"sites":{
"default":{
"rendererProperties":{
},
"modules":{
}
}
}
}
}
| Configuration | Description |
|---|---|
| global | Contains all configuration information for the decoupled Mura instance. |
| renderProperties | Configuration related to the rendering of content and Layout Manager. |
| templateArray | An array of template names, as defined within the JavaScript code. |
| collectionLayoutArray | An array of Module names that are to be treated as Collections. |
| hashurls | If set to true, the renderClient() function will update when the url hash changes. |
| primaryContentTypes | The list of Content Types that will be displayed in the Front End Editing Toolbar. |
| spacingOptions | Spacing options defined for the Layout Manager |
| moduleThemeOptions | Classes available to Module themes in the Layout Manager |
| colorOptions | Named colors available to Module themes in the Layout Manager |
| ckeditor | Editor-specific configuration |
| entities | An array of Mura ORM objects described in JSON (see the Assembler for details) |
| modules | The custom modules identified in the project's JavaScript (see Decoupled JavaScript) |
| sites | Contain a list of all the Mura sites available in the Front End Editing Toolbar |
| default | An example "default" site identified in the site Array |
| renderProperties (et.al.) | All configuration options available to global can be set here as well |
You can of course include your own configuration information here, and access it via Mura.get('value'). For more insight into these settings, you may want to examine the Content Renderer documentation, as all settings there pair to the ones you see above.