modulerizr
Version:
Integrate a powerful, component based architecture to your legacy project in just a few steps
948 lines (844 loc) • 26.6 kB
Markdown
# Modulerizr
> Integrate a powerful, component based architecture to your legacy project in just a few steps
[](https://www.npmjs.com/package/modulerizr)
[](https://www.npmjs.com/package/modulerizr)
[](https://www.npmjs.com/package/modulerizr)
Quick Links
- [Configuration](#modulerizrconfig)
- [Features](#features)
- [Plugins](#modulerizr-plugins)
- [ModulerizrWebpackPlugin](https://www.npmjs.com/package/modulerizr-webpack-plugin)
## Install
``` shell
npm install modulerizr --save-dev
```
<!-- - [Quicklink to the API](#api-description) -->
## The Problem
When designing larger websites you will end up in many problems you have to challenge, like
- very large files imports
- overwriting css-rules,
- overwriting/global scoped javascript-variables,
- serverside syntax like php,jsp,... mixed with html-content
- ...
Let's consider the html below:
``` html
<html>
<head>
<title>This could be a legacy page in your project</title>
<!--
This Stylesheet is 100kb large because it includes all the styles of your project.
And you just need ****** 5% oft thes styles on this page
-->
<link rel="stylesheet" type="text/css" href="path/to/your/css/allStyles.css">
<!--
This script all libraries you need in your project - again 95% that are not used.
-->
<script src="path/to/global-scripts.js"></script>
<style>
.aPseudoLocalClass{
color: green
}
</style>
</head>
<body>
<!--
Many legacy projects have serverside syntax in the templates PHP, jsp,... most of the time.
Here shown with the brackets.
-->
{+include-head}
<!-- **** There is a selector with !important to class aPseudoLocalClass in the globalStyles. Now it's pink... -->
<div class="aPseudoLocalClass">
My color is pink, not green :D
</div>
<script>
...
//oh, there was a global scoped variable. Now I have overwritten it an a onclick-script does does something wrong... *angry-smiley*
{!testmode}
var color = '{{serversideColor}}';
{/!testmode}
{testmode}
var color = '{{anyOtherColor}}';
{/testmode}
...
</script>
{+include-footer}
</body>
</html>
```
Many solutions exist to reduce these problems in web projects, one of the most important ones is __modularisation__.
There are many good frameworks that use this pattern and do a veeeery, very good job with it
- Angular
- Vue
- React
- ... many other ones
BUT: These Frameworks DON'T WORK GOOD WITHIN LEGACYPROJECTS. Have you ever seen Serverside-Syntax in Vue or Angular-Templates. No. If you have a legacy project and want to change it to a modular system, you will have big problems. That means you can not easily switch to modularisation when all your architecture is currently designed with php.
Here's is a solutionen - where you can have serverside syntax AND modularisation without big effort.
## The solution
__Modulerizr__.
While angular, react,... give you many great features from the scratch without having to think about it, Modulerizr does it vice versa. It gives you a simple infrastructure for modularisation and you can add the features you want.
Because of this, you can append it to almost every legacy project you can imagine.
## Usage
Imagine the following html-page "startpage.hml":
``` html
<html>
<head>
<title>Startpage</title>
...
</head>
<body>
{+include-head}
<!-- **** There is a selector with !important to class aPseudoLocalClass in the globalStyles. Now it's pink... -->
<custom-component-1>Some Text from component1</custom-component1>
<custom-component-2>{{serversideText}}</custom-component2>
{+include-footer}
</body>
</html>
```
>The Brackets { } show serverside syntax. It could also be php-syntax,...
The component "custom-component-1.component.hml":
``` html
<template name="custom-component1">
<style m-scoped>
.color{color:green}
</style>
<script m-scoped>
var x = "a scoped variable";
</script>
<div class="color"><slot></slot></div>
</template>
```
And the component "custom-component-2.component.hml":
``` html
<template name="custom-component2">
<style m-scoped>
.color{color:red}
</style>
<script m-scoped>
var x = "another scoped variable; was not defined before";
</script>
<div class="color"><slot></slot></div>
{{Some serverside Syntax here}}
</template>
```
Then run modulerizr:
``` javascript
const { modulerizr } = require('modulerizr');
modulerizr.run({
"src": ["startpage.html"],
"components": ["*.component.html"],
"dest": "./dest/",
});
```
> More ways to run modulerizr you see in the next section ["How to run modulerizr"](#how-to-run-modulerizr)
Voilà, your're done. This will be rendered to:
``` html
<html>
<head>
<title>Startpage</title>
...
</head>
<body>
{+include-head}
<div id="1e34329b" data-v-1e34329b data-component="custom-component1">
<style>
.color[data-v-1e34329b]{color:red}
</style>
<script>
(function(){
var x = "a scoped variable";
})();
</script>
<div class="color" data-v-1e34329b>Some Text from component1</div>
</div>
<div id="93d13c56" data-v-93d13c56 data-component="custom-component2">
<style>
.color[data-v-93d13c56]{color:red}
</style>
<script>
(function(){
var x = "another scoped variable; was not defined before";
})();
</script>
<div class="color" data-v-93d13c56>{{serversideText}}</div>
{{Some serverside Syntax here}}
</div>
{+include-footer}
</body>
</html>
```
If you need some more specials features, just [add a plugin](#plugins) that does what you need.
## How to run modulerizr
There are multiple ways to run it. With Terminal or with node.
### Run in Terminal
Before running it, add a modulerizr.config.js in the root-folder.
``` javascript
module.exports = {
"src": ["**/*.src.html"],
"components": ["*.component.html"],
"dest": "./dest/",
}
```
##### Variant 1 - globally installed:
Install the package
``` shell
npm install modulerizr --global
```
And run it
``` shell
modulerizr run
```
##### Variant 2 - locally installed:
Install the package
``` shell
npm install modulerizr --save-dev
```
Add a script to the package.json
``` javascript
{
...
"scripts": {
"modulerizr": "modulerizr run"
}
...
}
```
and run it
``` shell
npm run modulerizr
```
##### Commandline-Parameters
``` shell
#both overwrites debug-Attribute in config
#debug mode: shows logs;
modulerizr --debug
#production mode: hides logs; default;
modulerizr --production
```
#### Node
Here's how
``` javascript
const { modulerizr } = require('modulerizr');
const config = {...} // some config
modulerizr.run(config)
```
### Modulerizr.config
You can run one config or an array of configs
``` javascript
const { modulerizr } = require('modulerizr');
const config1 = {...} // some config
const config2 = {...} // some other config
//executes one config
modulerizr.run(config1);
//executes multiple configs
modulerizr.run([config1,config2]);
```
#### Config attributes
##### src
All src-files that will be prerendered. They will be copied into the destination-folder. [Glob-Syntax](https://www.npmjs.com/package/glob).
Type String or Array. Required.
``` javascript
{
...
// it can be a string
"src": "**/*.allsrcfiles.html",
//or an array of strings
"src": ["srcfile1.html","srcfile2.html","srcfile3.html"]
...
}
```
##### components
All component-files. [Glob-Syntax](https://www.npmjs.com/package/glob).
Type: String or Array. Optional. (But there will be a warning if you don't define it)
``` javascript
{
...
// it can be a string
"components": "**/*.component.html",
//or an array of strings
"components": ["comp1.component.html","comp2.component.html","comp3.component.html"]
...
}
```
##### dest
The folder where the files will be rendered to. MUST BE AN ABSOLUTE PATH.
Type: String. Optional. Defaults to "dest"
``` javascript
{
...
"dest": "./dest",
...
}
```
##### debug
Debugmode. Shows logs if debug == true.
Will be overwritten by the [--debug](#commandline-parameters) or [--production]((#commandline-parameters)) Parameter in command line.
Type: Boolean. Default: false.
``` javascript
{
...
"debug": true,
...
}
```
##### plugins
If you need a custom feature, you can add it via plugin.
Type: Array.
``` javascript
const { modulerizr } = require("modulerizr");
modulerizr.run({
...
//Add your plugins here
"plugins": [DebugPlugin()],
...
})
```
##### defaultComponentWrapper
By Default, components are wrapped by a div-tag. To change this, a component needs a "wrapper"-attribute or you can you can use a default-wrapper-tag for each component.
This will be overwritten by the tag assigned in the component.
``` javascript
const { modulerizr } = require("modulerizr");
modulerizr.run({
...
//Now all you components will be wrapped by a span
"defaultComponentWrapper": "span",
...
})
```
##### maxRecursionLevel
What happens if you add component X in component X and the content does not change? We have an infinte-loop.
```html
<component-a>
<component-a>
<component-a>
<component-a>
<component-a>
<component-a>
...
</component-a>
</component-a>
</component-a>
</component-a>
</component-a>
</component-a>
```
We assume this is not expected - that's why there is a maximum recursion level. This example above has a recurison level of 6 (until the three dots "...") because there are 6 levels of components.
By default ther is a maximumRecursionLevel of 100 - if you have more, there will be an error because we expect, that there is sth wrong.
Maybe there is a usecase where you need more levels. You can increase this level in the config with the maxRecursionLevel-attribute.
``` javascript
const { modulerizr } = require("modulerizr");
modulerizr.run({
...
//Now you can have 500 component-levels. Yippeeee
"maxRecursionLevel": "500",
...
})
```
## Features
To understand the the next features, it is good to know the differences between components and src-files:
- Src-Files:
- Any html how you already use it
- They are the Root-Files that will be prerendered.
- The transpilation of these files will be added in the dest-folder
- Many features like scoped variables,... don't work in src-files (if you don't change this via config)
- Components
- It is wrapped by a template-tag with some attributes
- Currently each component must have its own file - this will be changed in future
- A component can include other components
- All features like scoping,... work in components
- A component by itself won't be rendered - it (or a parent component) must be included into a src-file
### Basics
Without one of the next features, a component is just outsourced html from the original file - like a php-include. So we make sure, that the rendering process does not affect legacy files.
Example:
src-file.html
``` html
<html>
<head>...</head>
<body>
some text
<component-1></component-1>
some text
</body>
</html>
```
component1.component.html
```html
<template name="component-1">
This is component1
</template>
```
dest-file:
``` html
<html>
<head>...</head>
<body>
some text
This is component1
some text
</body>
</html>
```
To be honest: This feature by itself is not better then a php-include, it wouldn't make sense writing this package to add a feature that can be added without effort.
Let's add some more "magic":
### Components
Sorry, before adding "magic", we need the basics of components.
A component is always wrapped by a template-tag and has a uniqe name.
``` html
<template name="xyz">
here comes the content
</template>
```
If the name is missing or a component with this name already exists, there will be an error.
> Until now just one component per file is possible. Also inline components in a src-file are not possible. This will change in future.
#### Slots
The first "magic", well known from vue, web-components,...
Sometimes you want to do sth with the innerHTML in the component declaration.
##### Default Slot:
Anywhere in the src-file:
```html
...
<make-bold>
<div>
This content will be bold, even though no class or style can be seen in the src-file;
</div>
</make-bold>
...
```
In the component file:
```html
<template name="make-bold">
<div style="font-weight:bold;">
<slot></slot>
</div>
</template>
```
Will be rendered to:
```html
...
<div style="font-weight:bold;">
<div>
This content will be bold, even though no class or style can be seen in the src-file;
</div>
</div>
...
```
##### Named slots
Sometimes you need more slots per component. In this case, you can add named slots.
Anywhere in the src-file:
```html
...
<before-and-after>
<div slot="before">
This text is written before a static text.
</div>
<div slot="after">
This text is written after a static text.
</div>
</before-and-after>
...
```
In the component file:
```html
<template name="before-and-after">
<div><slot name="before"></slot></div>
<div>This Text is in the middle</div>
<div><slot name="after"></slot></div>
</template>
```
Will be rendered to:
```html
...
<div>This text is written before a static text.</div>
<div>This Text is in the middle</div>
<div>This text is written after a static text.</div>
...
```
#### Wrapper
Right now the default wrapper-element is a div. But for some components you may want another tag then div.
Add the "wrapper"-attribute to a component assignment to change the wrapper attribute.
```html
...
<make-bold m-wrapper="h1">This is a bold header</make-bold>
...
```
will be rendered to
```html
...
<h1 style="font-weight:bold;">
<div>
This is a bold header
</div>
</h1>
<!--
Instead of
<div style="font-weight:bold;">
<div>
This is a bold header
</div>
</div>
-->
...
```
If you want to change the wrapper-element for all elements, [set the "defaultComponentWrapper"-Attribute](#defaultComponentWrapper) in the plugin-configuration.
If both the config attribute ("defaultComponentWrapper") is set and the component attribute ("m-wrapper"), the component attribute will be used.
#### Scoped Styles
What happens if you have 2 components with the same style declaration, but different value? The style will be overwritten. :(
##### Problem
Component A
```html
<template name="red-text">
<style>
.textColor{color: red;}
</style>
<div class="textColor">This Text is red.</div>
</template>
```
Component B
```html
<template name="green-text">
<style>
.textColor{color: green;}
</style>
<div class="textColor">This Text is green.</div>
</template>
```
Src-file:
```html
...
<red-text></red-text>
<green-text></green-text>
...
```
This will be rendered to
```html
...
<style>
.textColor{color: red;}
</style>
<!-- Oh no, ****. This text is green, because the text color has been overwritten in another component.-->
<div class="textColor">This Text is red.</div>
<style>
.textColor{color: green;}
</style>
<div class="textColor">This Text is green.</div>
...
```
##### Solution
If you want scoped styles, just add a "scoped" attribute to the "style"-tag.
Component A
```html
<template name="red-text">
<style m-scoped>
.textColor{color: red;}
</style>
<div class="textColor">This Text is red.</div>
</template>
```
Component B
```html
<template name="green-text">
<style m-scoped>
.textColor{color: green;}
</style>
<div class="textColor">This Text is green.</div>
</template>
```
This will be rendered to
```html
...
<style>
.textColor [data-v-12345]{color: red;}
</style>
<!-- Yaaay, this is red now - as expected:) -->
<div data-v-12345 class="textColor">This Text is red.</div>
<style>
.textColor [data-v-67890]{color: green;}
</style>
<div class="textColor" data-v-67890>This Text is green.</div>
...
```
##### Efficency
What happens if you add the same component multiple times?
```html
..
<green-text></green-text>
<green-text></green-text>
<green-text></green-text>
...
```
Will the same styles exist multiple times?
```html
...
<!-- Will it be like this?-->
<style>
.textColor [data-v-67890]{color: green;}
</style>
<div class="textColor" data-v-67890>This Text is green.</div>
<style>
.textColor [data-v-67890]{color: green;}
</style>
<div class="textColor" data-v-67890>This Text is green.</div>
<style>
.textColor [data-v-67890]{color: green;}
</style>
<div class="textColor" data-v-67890>This Text is green.</div>
...
```
No. Same styleblocks with attribute "scoped" will just exist once. the example above would look like this:
```html
...
<style>
.textColor [data-v-67890]{color: green;}
</style>
<div class="textColor" data-v-67890>This Text is green.</div>
<div class="textColor" data-v-67890>This Text is green.</div>
<div class="textColor" data-v-67890>This Text is green.</div>
...
```
#### Scoped Scripts
If you add a raw script-tag in a component, it can have side effects to other components. Variables are global scoped and so they can overwrite other variables.
##### Example
Parent-Component
```html
<template name="parent-component">
<script>
var text = "Hello Parent";
</script>
<child-component></child-component>
<script>
console.log(text);
</script>
</template>
```
Child-Component
```html
<template name="child-component">
<script>
var text = "Hello child";
console.log(text);
</script>
</template>
```
This would be rendered like this:
```html
<script>
// Declaration in the parent component
var text = "Hello Parent";
</script>
<script>
// Declaration in the child component
var text = "Hello child";
console.log(text);
</script>
<script>
// Oh no, the text in the parent component has been overwritten. That's not expected;
console.log(text);
</script>
```
There are two Problems:
- the global Scope is polluted
- variables can be overwritten what is not expected
##### Solution
Scoped Scripts: Just add a "scoped"-Attribute to the script and this can not happen anymore.
I this case, the parent component stays the same. In the child component we add a "scoped"-Attriubte
Child-Component
```html
<template name="child-component">
<script m-scoped>
var text = "Hello child";
</script>
</template>
```
Would be rendered to
```html
<script>
// Declaration in the parent component
var text = "Hello Parent";
</script>
<div id="12345" data-component="12345" data-v-12345>
<script>
(function(window){
// This is added when you use a "scoped"-Attribute.
//It gives you important component information in javascript.
var _m = {
id: '12345',
name: 'child-component',
$el: document.getElementById('12345'),
slots: {
_default: "<script m-scoped>var text = "Hello child";</script>"
},
attributes: {
"attributeKey1": "attributeValue1"
...
}
};
var text = "Hello child";
console.log(text);
})(window);
</script>
</div>
<script>
// As expected, "Hello parent" will be logged here
console.log(text);
</script>
```
#### One Time Rendering
We work on a legacyproject and the codbease has no / not many components - and many scripts are assigned via script-tag.
Some scripts are only necessary for a specific features - for example jquery UI for a slider.
We can use modulerizr to only add external scripts into the src-files, when they're used in a component.
Let's imagine a slider component that needs the external jquery-ui-lib.
```html
<template name="custom-slider">
<script src="https://a-external-provider.com/jquery-ui.min.js"></script>
<link href="https://a-external-provider.com/jquery-ui.min.css">
<div>
Here we add the component elements.
...
</div>
</template>
```
Adding external scripts and styles in templates work as expected - but you have to take care of one scenario:
Using a component multiple times per page.
Imagine this:
```html
<custom-slider></custom-slider>
...
<custom-slider></custom-slider>
```
This would be rendered to
```html
<script src="https://a-external-provider.com/jquery-ui.min.js"></script>
<link href="https://a-external-provider.com/jquery-ui.min.css">
<div>
Here we add the component elements.
...
</div>
...
<!-- Oh no, the script is loaded twice -->
<script src="https://a-external-provider.com/jquery-ui.min.js"></script>
<!--
External Stylesheets are just loaded once per src-file.
<link href="https://a-external-provider.com/jquery-ui.min.css">
-->
<div>
Here we add the component elements.
...
</div>
```
By default external stylesheets are just loaded once per src-file (when assigend in a component). In case of scripts you have to assign them with a "once"-Attribute to assure that they are just loaded once.
```html
<script m-once src="https://a-external-provider.com/jquery-ui.min.js"></script>
<div>
Here we add the component elements.
...
</div>
```
Declared multiple times in a src-file it would be rendered like this:
```html
<script m-once src="https://a-external-provider.com/jquery-ui.min.js"></script>
<div>
Here we add the component elements.
...
</div>
...
<div>
Here we add the component elements.
...
</div>
```
#### Component Config
Your Component may need some specific configuration. You can add this in your component by the attribute "m-componentconfig".
```html
<template name="any-component">
<script m-componentconfig>
/*Add your component configuration here*/
</script>
</template>
```
This is useful if you for example want to preload data and use it with the modulerizr-jsrender-plugin. This script must be a node script and export an object.
```html
<template name="any-component">
<script m-componentconfig>
//This data could also be fetched from anywhere else asynchronous
const data = {
name: "dieter",
kids: [{name:'heinz'},{name:'gustav'}]
};
module.exports = {
data(){
return data;
}
}
</script>
</template>
```
This data will also be rendered in the scoped scripts:
```html
<script>
(function(window){
var _component = {
id: "c7350017",
name: "any-component",
$el: document.getElementById("c7350017"),
data: {"name":"dieter","kids":[{"name":"heinz"},{"name":"gustav","def":"ghi"}]},
attributes: {},
slots: {"_default":""},
};
})(window);
</script>
```
### Modulerizr-Plugins
You can use any other webpack-plugin to add more features you want, for example for bundling,...
But there are some specific modulerizr-plugins that exist.
#### Modulerizr-jsrender-plugin
The [modulerizr-jsrender-plugin](https://www.npmjs.com/package/modulerizr-jsrender-plugin) gives you the chance to add template-syntax in your templates. It is based on [JS-Render](https://www.npmjs.com/package/jsrender).
```html
Component:
<template name="jsrender-example">
Hello {{:name}}.
{{if age> 30}}
You are reeeeally old.
{{/if}}
{{if age <= 30}}
In Germany they would call you "Jungspund".
{{/if}}
</template>
Src:
<div>
<jsrender-example name="Peter" age="25"></jsrender-example>
</div>
Will be rendered to:
<div>
<div>
Hello Peter.
In Germany the would call you "Jungspund".
</div>
</div>
```
See more information about this plugin [here](https://www.npmjs.com/package/modulerizr-jsrender-plugin)
#### Use Webpack Plugins
A Modulerizr-Plugin is just a webpack-plugin. That means, you can add any webpack plugin to customize your files. [Here's a list of webpack-plugins](https://github.com/jantimon/html-webpack-plugin#plugins), that have nothing to do with modulerizr but can also be added in the plugins.
```javascript
// This works:
const { modulerizr } = require('modulerizr');
const {StyleExtHtmlWebpackPlugin } = require('style-ext-html-webpack-plugin');
modulerizr.run({
//...some other config...
plugins: [new StyleExtHtmlWebpackPlugin()]
});
```
#### Write your own Plugin
[Read here how to write your own Plugins](https://github.com/bassdman/modulerizr/blob/master/README.ownplugin.md).
### Features in future
- inline-templates in src-files, marked with a "inline-template"-Attribute.
- multiple components per file
- support attribute component declarations
- scoped link tags
## Contribution
- You have some ideas how to make modulerizr more simple?
- You have ideas for a new feature / new modulerizr-plugin?
- You found a bug?
- Or you have some general demands/questions?
Then [create an issue](https://github.com/bassdman/modulerizr/issues) or contact me.
Or if you have time and a good idea, then you can of course create a new plugin. This would be awesome :D
## Last but not least
Happy Coding. Let's get rid of your legacy code in your Projects :D