diff --git a/README.md b/README.md index fc6c997e..2eed3d00 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Muban boilerplate +# Muban [![muban-release-status]][muban-release] Muban is a backend-agnostic framework and development setup to enhance server-rendered HTML with [TypeScript](https://www.typescriptlang.org/) or [Babel](https://babeljs.io/) components and @@ -10,163 +10,55 @@ reloading, while using [Handlebars](http://handlebarsjs.com/) templates to rende The dist build will generates preview html pages and a js and css bundle that backend developers can use to integrate the pages in their server side templates or CMS of choice. -## Why Muban? - -Please read this [introduction](./docs/introduction.md) about why and how we created Muban. - -## Getting started - -Please read this [getting started guide](./docs/getting-started.md) if you're new to Muban. - -## Distribution implementation guide - -If you're a developer that needs to implement the dist build into an existing backend/cms, please -read the [implementation guide](./docs/dist-implementation-guide.md) that is also distributed with -the build output. - -## Setup - -After cloning this repos and removing the `.git` folder, run: - -```sh -yarn -``` - -This boilerplate comes with some sample pages, blocks and components. -If you don't need them in your project, you can remove them all with a simple command: - -```sh -yarn clean:boilerplate -``` - -You can also remove the storybook config, source and preset files if you don't need it: - -```sh -yarn clean:storybook -``` - -### Config - -The most basic settings can be found and changed in `build-tools/config/config.js`. - -### Development - -```sh -yarn dev -``` - -Open your browser at [http://localhost:9000](http://localhost:9000). - -**Using own server for html** - -When using server-generated html instead of the handlebars templates, you can use the following -command to just compile the `js` and `css` bundles (incl other assets). - -```sh -yarn dev:code -``` - -The files will be outputted/updated in the same folder as the normal build is done, but uses the -`development` environment, enables sourcemaps, and disables minification and other stuff. - -## Creating pages, blocks and components - -With seng-generator you're able to create pages, blocks and components with the CLI. The -seng-generator needs to be installed globally. - -```sh -yarn global add seng-generator -``` - -The easiest way to use it is by using the wizard - -```sh -sg wizard -``` - -Starts a wizard to create a component, page or block. - -For more information about the generating components, check the [docs](./docs/components.md). - -## Code Quality tools - -Muban uses multiple code quality tools like linters and formatters. Please read the -[extended documentation](docs/code-quality.md) for more information. - -## Build - -```sh -yarn build -``` - -The code is outputted in `/dist`. - -To preview the build in the browser, run: - -```sh -yarn preview -``` - -To analyze the created bundle, run: - -```sh -yarn analyze -``` - -Using the build script, you can also run some parts of the process separately: - -```sh -yarn build code # or yarn compile:code -yarn build partials # or yarn compile:partials -yarn build html # or yarn compile:html -``` - -### Diff - -If you want to generate a report on what has changed in the handlebars templates, -you can generate a diff report between two git commits (default to `master` and `HEAD`). - -```sh -yarn build:diff -``` - -It will generate a file in `dist/diff/templates.html` with a proper formatted diff. - -## Files and folders - -* `src/app/dev.js` Main dev file, you should never have to change anything here - -* `src/app/dist.js` Webpack entry file for production build, contains code that runs immediately. -* `src/app/partials.js` Webpack entry file for generating output html files. -* `src/app/bundle.js` Webpack entry that will include all js and css files referenced from all - template files. -* `src/app/polyfills.js` List of polyfills to include in the bundles. -* `src/app/component/layout/index/index.hbs` Template file to list all the pages. -* `src/app/component/layout/app/app.hbs` Template file that is used for all pages, contains basic - page layout (e.g. header, footer and wrapper). -* `src/app/component/` Contains all components, each folder is made up of: - * `component-name.hbs` The template file, can import a stylesheet using the html `link` tag, and a - script using the html `script` tag. - * `component-name.scss` The stylesheet, best to use a `component` prefix for your outer selector. - * `ComponentName.ts/js` An optional TS/JS file for the component, receives the DOM element, and - should have a static `block` property that corresponds with the `data-component` DOM attribute. -* `src/app/component/blocks/` Contains all _block_ components. They are dynamically rendered based - on the blocks entry in the json data file. -* `src/app/style` Folder containing global styles. All components will include their own stylesheet. -* `src/app/style/main.scss` Main stylesheet file, only for setting up global styles. -* `src/data` The yaml files for all preview pages. Each yaml file corresponds with a page. Using a - `.` in the filename will allow to group alternative variations for a single page. E.g. `home.yaml` - is the main page, and `home.alt.yaml` is an alternative version that can be visited via the - overview page. -* `.modernizrrc` config file for Modernizrrc used by `modernizr-loader`, config rules can be found - [here](https://github.com/Modernizr/Modernizr/blob/master/lib/config-all.json). -* `build-tools/generator-template/*` Template files for seng-generator, for creating pages, blocks - and components. - -## Storybook - -Storybook is a web-app that lets you preview and interact with the components in your project. You -can create presets that render your component with custom HTML, and pass different properties by -providing a json object. - -Please read the [extended documentation](docs/storybook.md) for more information. +## πŸŽ“ Documentation + +You can find the full documentation in the `/docs` folder. Here you will find the full +[table of contents](./docs/) covering all the subjects required to start on your own Muban project! + +- **Totally new:** If you are new to Muban we suggest to start by reading the + [preparations guide](./docs/02-setup-guide.md#preparations). This will guide you through the core + technologies and the required steps to setup your environment. +- **Ready to get started:** Once you've completed the preparations you can have a look at the + [getting started guide](./docs/02-setup-guide.md#getting-started). This guide will walk you + through all the steps to setup the a new Muban project +- **Give me some examples:** If you want to dive straight into examples have a look at the + [guides section](./docs/13-guides.md) of the documentation. This page contains a lot of example + situations that hopefully cover all your questions. + +## πŸš€ Quick start + +If you have all the [preparations](./docs/02-setup-guide.md#preparations) done and you don't want to +read the documentation you can follow these steps to get you started. + +1. Get the a _copy_ of the source code using one of the following methods + - Clone the repository and remove the `.git` folder. + - [πŸ“¦ Download](https://github.com/mediamonks/muban/archive/master.zip) the repository `zip` + file. +2. Install the project dependencies using `yarn`. +3. Startup the development server using `yarn dev`. + - Open your browser at [http://localhost:9000](http://localhost:9000). +4. Start editing! + +> **Note:** If you need more instructions we suggest you take a look at the full +> [getting started guide](./docs/02-setup-guide.md#getting-started)! + +## πŸ“š Ecosystem + +| Project | Status | Description | +| ---------------------------- | -------------------------------------------------------------------------- | ------------------------------- | +| [muban-core] | [![muban-core-status]][muban-core-package] | The core functionality of Muban | +| [muban-transition-component] | [![muban-transition-component-status]][muban-transition-component-package] | GSAP transitions for Muban | + +## πŸ“ License + +Muban is released under the [MIT](http://opensource.org/licenses/MIT) License. + +[muban-release]: https://github.com/mediamonks/muban/releases +[muban-release-status]: https://img.shields.io/github/release/mediamonks/muban.svg?colorB=41a6ff +[muban-core]: https://github.com/mediamonks/muban-core +[muban-transition-component]: https://github.com/riccoarntz/muban-transition-component +[muban-core-status]: https://img.shields.io/npm/v/muban-core.svg?colorB=41a6ff +[muban-transition-component-status]: + https://img.shields.io/npm/v/muban-transition-component.svg?colorB=41a6ff +[muban-core-package]: https://npmjs.com/package/muban-core +[muban-transition-component-package]: https://npmjs.com/package/muban-transition-component diff --git a/build-tools/config/webpack/webpack.partial.conf.plugins.js b/build-tools/config/webpack/webpack.partial.conf.plugins.js index 92b40f72..a0e63f13 100644 --- a/build-tools/config/webpack/webpack.partial.conf.plugins.js +++ b/build-tools/config/webpack/webpack.partial.conf.plugins.js @@ -105,7 +105,7 @@ module.exports = ({ config, isDevelopment, buildType, isPartials }) => webpackCo { // copy over readme context: path.resolve(config.projectRoot, 'docs'), - from: 'dist-implementation-guide.md', + from: '12-dist-implementation-guide.md', to: path.resolve(config.distPath), }, ].filter(_ => _)), diff --git a/docs/introduction.md b/docs/01-introduction.md similarity index 55% rename from docs/introduction.md rename to docs/01-introduction.md index 31b8af5d..9b8fe384 100644 --- a/docs/introduction.md +++ b/docs/01-introduction.md @@ -2,10 +2,11 @@ Here you can read about why and how we created Muban. -### Background +## Background -At MediaMonks we love frontend development. Our goal is always to make the development process as -smooth as possible, so developers can focus on what they love most, making beautiful websites. +At [MediaMonks](https://www.mediamonks.com/) we love frontend development. Our goal is always to +make the development process as smooth as possible, so developers can focus on what they love most, +making beautiful websites. While we know we can excel when building Single Page Applications, they are not always the best approach. Sometimes a server rendered website is the best or only option. @@ -14,80 +15,77 @@ Traditionally, those kind of websites had small pieces of interactivity, which w throwing in some jQuery plugins. But as things get bigger and more complicated, that way of working won't suffice. -Also, the last years we have been spoiled with amazing tools like webpack and babel, and a way of -working that isolates everything in small components, so why not make use of those when building -more traditional kind of websites? +Also, the last years we have been spoiled with amazing tools like [webpack](https://webpack.js.org/) +and [babel](https://babeljs.io/), and a way of working that isolates everything in small components, +so why not make use of those when building more traditional kind of websites? -### Challenges +## Challenges Before starting this project, we asked ourselves what didn't work quite well in the past, and what problems we'd like to solve. What we came up with was the following: -* Adding all kind script tags to the page doesn't make sense anymore, we'd like to make use of all +- Adding all kind script tags to the page doesn't make sense anymore, we'd like to make use of all the **node modules** out there, and a way to **bundle** them. - -* Development iteration should be fast and painless, so we'd like to have **hot reloading** in +- Development iteration should be fast and painless, so we'd like to have **hot reloading** in place. - -* We're going to be building frontends to all kind of websites, running on a diverge range of +- We're going to be building frontends to all kind of websites, running on a diverge range of backend languages, template systems and CMSs. We'd like our setup to be **backend-agnostic**. - -* Related to the above, we'd like to **develop, test and preview in isolation**. This means we can +- Related to the above, we'd like to **develop, test and preview in isolation**. This means we can start developing before or at the same time as the backend without being dependent on them. - -* We like a way to **preview components** we create, to test them and have a great overview of what +- We like a way to **preview components** we create, to test them and have a great overview of what can be used on the website. Like a style guide, but better! So whatever we choose to create, or update in the future, it will adhere to the above requirements. -### What we came up with +## What we came up with -#### webpack +### Webpack -We know we wanted to use webpack, for the bundling feature and the hot reloading. But also for the -great loader and plugin system, which will come up below. +We know we wanted to use [webpack](https://webpack.js.org/), for the bundling feature and the hot +reloading. But also for the great loader and plugin system, which will come up below. -#### template +### Template Because we want to be backend-agnostic, we needed a way to render templates on the client during development. Because the HTML should stay decoupled from JavaScript, we could not go for anything -like Vue, React or Knockout (well, technically we could, but it wouldn't make much sense). So we -started looking at a js/node template language that: +like [Vue](https://vuejs.org/), [React](https://reactjs.org/) or [Knockout](https://knockoutjs.com/) +(well, technically we could, but it wouldn't make much sense). So we started looking at a js/node +template language that: -* was easy enough to work with -* had enough features to get the basics done, and compatible enough with most backend template +- was easy enough to work with +- had enough features to get the basics done, and compatible enough with most backend template languages -* could be integrated in the frontend as well, ideally as a webpack loader. +- could be integrated in the frontend as well, ideally as a webpack loader. -After looking and playing around, Handlebars was our favorite, and had a great webpack loader. By -having this setup, even handlebar templates could be hot reloaded. +After looking and playing around, [Handlebars](https://handlebarsjs.com/) was our favorite, and had +a great webpack loader. By having this setup, even handlebar templates could be hot reloaded. -#### components +### Components When we knew we could use webpack and handlebars, we went ahead to set up a way to work with components. With a custom webpack loader, we could import our script and style files from the -handlebars component template file, similar to how Vue components work. +handlebars component template file, similar to how [Vue](https://vuejs.org/) components work. -By adding in a webpack context to find all our .hbs files, we could bundle all our components. +By adding in a webpack context to find all our `.hbs` files, we could bundle all our components. Using handlebars partials, we could include one component into another. Partial paths are resolved -by the handlebars-loader, that also added them (and the imported js/css files) to the webpack -dependency list. This will make sure all the required files are ending up in the build, without -including anything that isn't used. +by the [handlebars-loader](https://www.npmjs.com/package/handlebars-loader), that also added them +(and the imported js/css files) to the webpack dependency list. This will make sure all the required +files are ending up in the build, without including anything that isn't used. All component classes will register themselves to the application, so they can be constructed when the component html is present on the page. -#### application +### Application Now that the component setup worked, we needed a way to render a page. Because most CMSs have a concept of building a page by drag-and-dropping components in a grid from a list, we introduced the concept of 'blocks'. Blocks are sections that make up a page, and consist out of all kind of reusable components like headings, paragraphs, buttons, images, etc. -So to build up a page when developing, we set up to have a yaml file per page that contains an array -of block names, and the data that it would need to display content. The data is there to mock the -data that a backend system would need to render the actual page in the end. +So to build up a page when developing, we set up to have a [yaml](https://yaml.org/) file per page +that contains an array of block names, and the data that it would need to display content. The data +is there to mock the data that a backend system would need to render the actual page in the end. After that, we just needed an application template file that would loop over the yaml list, and include the block component partials, so all html for the page would be rendered. @@ -95,40 +93,43 @@ include the block component partials, so all html for the page would be rendered And when the DOM is constructed, we just select all the elements that have a `data-component` attribute, and construct the corresponding component class to make them interactive. -#### building +### Building Now that the development setup was done, we only needed to create two things. -* The js and css bundles, with a simple webpack config -* Preview html pages +- The JavaScript and CSS bundles, with a simple webpack config +- Preview HTML pages The preview pages are useful to upload and QA them; see if they match the design and don't contain any bugs. To generate those pages, we just use the same yaml page files, loop over the block components, and use the pre-compiled handlebar templates to generate the HTML. One file for each yaml. -#### preview components +### Preview components + +> ⚠️ Storybook is being moved to another repository and is therefore temporarily not available! While those preview pages are useful in their own way, sometimes you want to have an overview of individual components, with some documentation, and maybe the used data, and the source files that make up those components. For this we created muban-storybook, inspired by [React Storybook](https://storybook.js.org/), but with some additional features. -#### integration +### Integration While the above makes for an amazing frontend development experience, the job is not done. Getting everything integrated in the CMS is the last bit. Since we're being backend-agnostic, there is no easy way to do this. All templates are handlebars, so they cannot be copied over. -In the beginning we manually changed them to twig, django, or whatever the template language of that -project would be. With a similar syntax, it wasn't that complex, but it still took some time. The -most annoying part is missing out on small updates that had happened in the source templates, -resulting in out-of-sync templates between the frontend and backend. +In the beginning we manually changed them to [twig](https://twig.symfony.com/), +[django](https://www.djangoproject.com/), or whatever the template language of that project would +be. With a similar syntax, it wasn't that complex, but it still took some time. The most annoying +part is missing out on small updates that had happened in the source templates, resulting in +out-of-sync templates between the frontend and backend. -For this we're working on the muban-convert-hbs module, a transpiler that can convert 99% of the -templates to an increasing amount of template systems. To complement this, we're also creating a set -of handlebar helpers to implement useful features of other backend template languages, so they are -even more compatible. +For this we're working on the [muban-convert-hbs](https://www.npmjs.com/package/muban-convert-hbs) +module, a transpiler that can convert 99% of the templates to an increasing amount of template +systems. To complement this, we're also creating a set of handlebar helpers to implement useful +features of other backend template languages, so they are even more compatible. To streamline this process even more, it's important to keep the mock data in your yaml files similar to the backend data model. In that case, you won't even have to rename your template @@ -138,20 +139,20 @@ As a last option, there is a way to totally ignore all the handlebar templates, the scripts and styles. This could be done after initial conversion of the templates. However, that process has some drawbacks of its own: -* Future updates should be done in the backend templates. This means that during development, you'll +- Future updates should be done in the backend templates. This means that during development, you'll have to run the complete backend, and manually have to reload those pages (or implement browsersync). This will slow down development. -* You'll completely lose the storybook functionality, since your handlebar templates are removed - or out of sync. If you don't want this, you could choose to do your updates twice, in the backend - and in handlebars. +- You'll completely lose the storybook functionality, since your handlebar templates are removed or + out of sync. If you don't want this, you could choose to do your updates twice, in the backend and + in handlebars. ### Closing words We believe that, with the above, we've created a system that allows for a consistent and modern development experience, allowing you to create an amazing frontend for any website. -Other frameworks might pop up, but they are most likely linked to a specific backend, or will -add more logic in the HTML, which are things that don't match with our vision. +Other frameworks might pop up, but they are most likely linked to a specific backend, or will add +more logic in the HTML, which are things that don't match with our vision. Even though we created something that works, it doesn't mean that we're finished; every new project could introduce new challenges that we want to solve, and the frontend ecosystem keeps evolving, diff --git a/docs/02-setup-guide.md b/docs/02-setup-guide.md new file mode 100644 index 00000000..0269c87a --- /dev/null +++ b/docs/02-setup-guide.md @@ -0,0 +1,414 @@ +# Setup guide + +This section will allow to to get started with Muban. It will give you the most basic information +required to get started with a project. For more in depth instructions please see the tutorial +section for more specific tasks. + +## Preparations + +Before you can get started building your Muban site, you'll need to familiarize yourself with some +core web technologies and make sure that you have installed all required software tools. + +### Compatability note + +Muban does **not** support IE10 and below. However it supports all +[ECMAScript 5 compliant browsers](https://caniuse.com/#feat=es5). + +### Release notes + +Detailed release notes for each version are available on +[GitHub](https://github.com/mediamonks/muban/releases). + +### Get familiar with the core technologies + +As described in the introduction, you'll need to get familiar with a couple of technologies. Not all +of them are equally as important to start building websites but it's good to know which technologies +are being used. + +#### Handlebars + +[Handlebars](https://handlebarsjs.com/) is a templating engine that let's you dynamically generate +HTML pages. It's an extension of [Mustache](http://mustache.github.io/) with some extra features +(such as `if`, `with`, `unless`, `each` and more). + +#### SCSS + +[Sass](https://sass-lang.com/) is a stylesheet language that’s compiled to CSS. It allows you to use +[variables](https://sass-lang.com/documentation/variables), +[nested rules](https://sass-lang.com/documentation/style-rules#nesting), +[mixins](https://sass-lang.com/documentation/at-rules/mixin), +[functions](https://sass-lang.com/documentation/functions), and more, all with a fully +CSS-compatible syntax. Sass helps keep large stylesheets well-organized and makes it easy to share +design within and across projects. + +#### TypeScript + +[TypeScript](https://www.typescriptlang.org/) starts from the same syntax and semantics that +millions of JavaScript developers know today. Use existing JavaScript code, incorporate popular +JavaScript libraries, and call TypeScript code from JavaScript. + +> **Note:** The default language in Muban is TypeScript but if you want to you can still use +> JavaScript if you want to. + +#### Knockout + +[Knockout](https://knockoutjs.com/) is a JavaScript library that helps you to create rich, +responsive display and editor user interfaces with a clean underlying data model. Any time you have +sections of UI that update dynamically (e.g., changing depending on the user’s actions or when an +external data source changes), KO can help you implement it more simply and maintainably. + +> ⚠️ By default knockout is not loaded because for simple operations it is most likely not needed, +> so keep in mind that loading knockout will increase your bundle size! + +### Setup your environment + +Before we can get started with Muban you will need to prepare you development environment. Make sure +the following sections are covered before you get started! + +#### Install Node.js + +[Node.js](https://nodejs.org/en/) allows you to run JavaScript code on a server, Muban uses Node.js +so you'll need to make sure you have a recent version installed on your computer. + +##### Using Homebrew on MacOS, Linux or Windows 10. + +1. Make sure you have the latest version of [homebrew](https://brew.sh/) installed. + +2. Open up the terminal. + +3. Run the following command to install Node.js. + + ```bash + brew install node + ``` + +##### Alternative instructions + +Alternatively you can [download the LTS installer package](https://nodejs.org/en/download/) from the +Node.js website and follow the installation steps. + +#### Install Yarn + +Muban uses [Yarn](https://yarnpkg.com/) for dependency management. So to make sure all dependencies +are correct you will need to install it. + +##### Using Homebrew on MacOS, Linux or Windows 10. + +1. Open up the terminal. + +2. Run the following command to install Yarn + + ```bash + brew install yarn + ``` + +##### Alternative instructions + +You can find alternative install instructions on the +[Yarn website](https://yarnpkg.com/en/docs/install). + +#### Install the seng-generator CLI + +The [seng-generator](https://www.npmjs.com/package/seng-generator) is an optional step that is not +required to get started with Muban but it's highly recommended. The seng-generator is a command line +interface that creates code based on templates. It will make the development process go a whole lot +faster. + +##### Using Yarn + +```bash +yarn global add seng-generator +``` + +> **Note:** On all the other documentation if a CLI is referenced it will be the seng-generator! + +## Getting started + +Once all the preparations have been completed you can follow these steps to get started building +websites! + +> **Note:** In these examples assume you want the latest stable version of Muban. + +### Setup the project + +To get started with your new Muban project first start by creating a folder somewhere on your drive +that wil serve as the project root (for example: `/my-muban-project`). + +#### Download source code + +There are two ways of getting the Muban source code: + +1. Clone the [GitHub repository](https://github.com/mediamonks/muban) somewhere on your drive and + copy over the content (excluding the `.git` folder) to your newly created project root. + + **Clone with HTTPS** + + ```bash + git clone https://github.com/mediamonks/muban.git + ``` + + **Clone with SSH** + + ```bash + git clone git@github.com:mediamonks/muban.git + ``` + +2. Download the zip directly from GitHub and extract the contents in the newly create project root. + + [πŸ“¦ Download the zip file](https://github.com/mediamonks/muban/archive/master.zip) + +#### Install dependencies + +After downloading the source code we need to download all of Mubans dependencies. Simply install +them by running the following command in the project root (in the case of this example that would be +/`my-muban-project`). + +```bash +yarn +``` + +#### Running the development server + +After installing the dependencies you can start up the development server. Muban uses the +[webpack-dev-server](https://www.npmjs.com/package/webpack-dev-server) in combintation with +hot-reloading to serve you your project. + +You can start it up by running the following command in the root of your project. + +```bash +yarn dev +``` + +Once the server has started you can open your browser up at +[http://localhost:9000](http://localhost:9000/) to preview Muban with some boilerplate code. + +The index page in the root of the server will list you all the pages that are available, this way you can easily +navigate them and keep track of your progress. + +#### Running the Storybook server + +> ⚠️ Storybook will become an installable module, therefore it is temporarily unavailable! + +As described in the introduction Muban comes with a Storybook inspired by react-storybook. For a +more detailed section including examples please see the page on [Storybook](./10-storybook.md). + +You can start it up by running the following command in the root of your project. + +``` +yarn storybook +``` + +Once the server has started you can open your browser up at +[http://localhost:9002](http://localhost:9002/) to see Storybook in action. The components are +loaded in an iframe to be completely isolated, and you can click the `responsive` icon in the +top-left to play around with breakpoints (this works in every browser). + +#### Clean boilerplate code + +As described in the previous step Muban comes with some boilerplate code that you will most likely +want to remove when you start your project. + +To make life a little easier you can run the following command in the root of your project to remove +it. + +``` +yarn clean:boilerplate +``` + +#### Clean Storybook + +If for any reason you would want to totally remove Storybook from Muban you can easily do this by +running the following command in the root of your project. + +```bash +yarn clean:storybook +``` + +> **Note:** Once you remove Storybook all scripts related to storybook will no longer be available. + +### Create a distribution build + +Once you have finished your project you will most likely want to create a build that you can be used +in production. You can do this by running the following command in the root of your project. + +```bash +yarn build +``` + +Once this is done you will end up with a `dist` folder in the root of your project, this folder wil +contain the following folders and files: + +``` +- dist/ + - site/ + - data/ + - templates/ + - bundlesize-profile.json + - dist-implementation-guide.md +``` + +#### Preview your distribution build + +Inside of the dist folder is a folder called `site`, this folder contains the static html pages that +load the bundled JavaScript and CSS. This is very similar to the actual website where your frontend +code will be served by a backend. + +You can use the following command to startup a local server to preview that specific build. + +``` +yarn preview +``` + +Once the server has started you can open your browser up at +[http://localhost:9001](http://localhost:9001/) to preview the build. + +> **Note:** It is always good practice to build and preview your site before sending it over to +> anyone else, so you now for sure everything works properly. + +#### Analyze your distribution build + +When using a lot of different modules to manage small tasks for you your bundle might increase a lot +as well. To give a good overview of the size of your bundle you can run the following command in the +root of your project. + +```bash +yarn analayze +``` + +Once the server has started you can open your browser up at +[http://localhost:8888](http://localhost:8888/) to preview the bundle analyzer. + +#### Generate a difference report + +When you handover the code to the backend it can be quite difficult to see the difference between +generated HTML in the builds. To make this easier you can also create a difference report. This also creates a `/diff` +folder inside of the `/dist` folder containing the report. + +You can do this by running the following in the root of your project + +``` +yarn build:diff +``` + +> **Note:** To use the difference report you will need to make sure you are within a +> [Git repository](https://git-scm.com/). it can use either the `master branch`, a +> [tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) or `commit hash` to make the diff file. + +## Code quality + +### EditorConfig + +We use [EditorConfig](http://editorconfig.org/) define and maintain consistent coding styles between +different editors and IDEs. Please make sure to enable/install the EditorConfig plugin in your IDE +of choice. + +- indentation of `2 spaces` +- use `lf` line endings +- use `utf-8` charset +- trim trailing whitespaces +- add en empty newline at the end of each file + +### Prettier + +We use [Prettier](https://github.com/prettier/prettier) to format all our code. This is enabled for +`js`, `ts`, `scss` and `yaml` files. The corresponding linters are configured to adhere to the rules +from prettier (so they won't conflict), and linting errors should only occur for non-stylistic +errors. + +Prettier is configured for: + +- indentation of `2 spaces` +- the use of `semicolons` +- the use of `single quotes` +- a tab width of `100` + +Prettier is configured to run on the `pre-commit` using `husky` and `lint-staged` hook, and can also +be manually invoked by: + +``` +yarn prettify +``` + +Settings can be changed in `.prettierrc` and files can be ignored in `.prettierignore`. + +Please check the [editor integration](https://github.com/prettier/prettier#editor-integration) +section of the Prettier readme to enable running Prettier within your IDE of choice. + +> **Note:** Keep in mind, that if you choose to automatically run Prettier when saving your file, +> Webpack will run twice (on your manual save, and when prettier reformats your code), slowing down +> the developer experience. + +### Linting + +The below tools are used to lint our code. They can be all executed by opening up the terminal in +the root of your project and running the following command: + +``` +yarn lint +``` + +#### eslint + +We use [eslint](https://eslint.org/) lint our JavaScript code. It's configured for use with +Prettier, and set up to understand Webpack imports. It follows the +[AirBnB styleguide](https://github.com/airbnb/javascript) with some super small tweaks. + +To triger eslint you should open up the terminal in the root of your project and run the following +command: + +``` +yarn lint:js +``` + +> **Note:** Settings can be changed in `.eslintrc.js` and files can be ignored in `.eslintignore`. + +#### tslint + +We use [tslint](https://palantir.github.io/tslint/) lint our TypeScript code. It's configured for +use with Prettier. It follows the [AirBnB styleguide](https://github.com/airbnb/javascript) with +some super small tweaks. It's consistent with the eslint settings. + +To triger tslint you should open up the terminal in the root of your project and run the following +command: + +``` +yarn lint:ts +``` + +> **Note:** Settings can be changed in `.tslintrc.js`. + +#### stylelint + +We use [stylelint](https://github.com/stylelint/stylelint) lint our SCSS code. It's configured for +use with Prettier and uses +[stylelint-config-recommended-scss](https://github.com/kristerkari/stylelint-config-recommended-scss)without +any modifications. + +To triger stylelint you should open up the terminal in the root of your project and run the +following command: + +``` +yarn lint:css +``` + +> **Note:** Settings can be changed in `.stylelintrc` and files can be ignored in +> `.stylelintignore`. + +### Pre-commit hook + +To make sure all code checked in to git, we use [husky](https://github.com/typicode/husky) to +configure git commit hooks. The `pre-commit` hook is configured to run +[lint-staged](https://github.com/okonet/lint-staged) on the files that are about to be committed. + +It will run all linters on the appropriate files, and allows Prettier to reformat any code before +doing the actual commit. + +You can also run the command manually in the root of your project: + +``` +yarn precommit +``` + +> **Note:** Keep in mind that some lint errors might pop up in files that are not updated by +> changing other things (like imports that are not correct after renaming a file), so it's good +> practice to run `yarn lint` once in while to verify the complete codebase is valid. diff --git a/docs/03-component.md b/docs/03-component.md new file mode 100644 index 00000000..692e4b1b --- /dev/null +++ b/docs/03-component.md @@ -0,0 +1,249 @@ +# Component + +A component is a potentially re-usable set of logic, behaviours and interface elements that speeds +up the creation of an application. If you work on a Muban project its good too keep in mind that +everything is a component. + +> ⚠️ [See the guide](./13-guides.md#Create-a-component) on how to create your own component! + +## Types + +Muban basically has three types of components that extend each other. + +``` ++----------------------+ +| | +| Component | +| | ++----------+-----------+ + | ++----------v-----------+ +| | +| Smart-component | +| | ++----------+-----------+ + | ++----------v-----------+ +| | +| Block | +| | ++----------------------+ +``` + +### Component + +The most basic form of a component within Muban would be the regular component. The regular +component is used for basically all user interface elements that do not require any logic or +functionality. + +The basic component has the following structure. + +``` +my-component/ + - my-component.hbs + - my-component.scss + - preset.js +``` + +#### Handlebars [Component] + +The `.hbs` file is the core of any component within Muban. It contains the HTML that is required for +component + +The most basic example of a Muban component could be a file called `button.hbs` + +```handlebars +
Hi πŸ‘‹
+``` + +#### SCSS [Component] + +Since you will probably never render out HTML without styling there is also a `.scss` file +available. This file contains all the styling for your component. To make sure it's loaded you will +have to add it to the `.hbs` file. This way webpack will make sure it is bundled in your main css +file. + +```handlebars + + +
Hi! πŸ‘‹
+``` + +Since we are now trying to load a file called `my-button.scss` we will have to add it to the same +folder. + +```scss +.my-component { + color: red; +} +``` + +> **Note:** The `.scss` file is technically optional so if you don't need it you could remove it. + +#### Preset [Component] + +_Note: If you have removed storybook from your Muban project you can skip this part._ + +The final file for any component is the `preset.js` file, this file contains the information +required by Storybook to render out the stories. To read more about storybook and the preset files +please see the page on Storybook. + +### Smart-component + +A smart-component is the next step in components, it has the same base as the basic component except +it also has a TypeScript file that contains logic. + +The smart-component has the following structure. + +``` +my-smart-component/ + - my-smart-component.hbs + - my-smart-component.scss + - MySmartComponent.ts + - preset.js +``` + +#### Handlebars [Smart-component] + +In the handlebars the only difference is that the root element of your component will have an extra +data attribute that is used to initialise the component. + +```handlebars +
I'm smart! πŸ€“
+``` + +#### SCSS [Smart-component] + +The `.scss` file for the smart-component is exactly the same as the one for the basic component. + +#### Preset [Smart-component] + +The `preset.js` file for the smart-component is exactly the same as the one for the basic component. + +#### TypeScript [Smart-component] + +The smart part of the smart-component is the TypeScript file. This file adds all the logic to your +component. To enable it simply load it in your `.hbs` the same way you did as for the `.scss` file. + +```handlebars + + + +
I'm smart! πŸ€“
+``` + +Since we are now trying to load a file called `MyButton.ts` we will have to add it to the same +folder. + +```typescript +import AbstractComponent from '../AbstractComponent'; + +export default class MySmartComponent extends AbstractComponent { + static displayName: string = 'my-smart-component'; + + constructor(el: HTMLElement) { + super(el); + } + + public dispose() { + super.dispose(); + } +} +``` + +Here you can see that we create a class that extends the `AbstractComponent`, this makes sure that +we have all the base logic that is required for Muban to initialise the components and provide the +default functionality. + +It's important that the static `displayName` value matches the `data-component` attribute value +in the handlebars file because Muban uses these values to bind them together. + +> **Note:** If you don't want to use TypeScript you could also create a JavaScript file, just keep in +mind that your will lose all typings. + +### Block + +The final step in components would be a block, a block is the largest type of component in Muban. +Blocks are the building stones on which the pages within Muban are build. They are loaded on the +pages and will have data provided to them. + +A block has the following structure. + +``` +my-block/ + - my-block.hbs + - my-block.scss + - MyBlock.ts + - data.yaml + - preset.js +``` + +#### Handlebars [Block] + +The `.hbs` file for a block is exactly the same as the one for a smart-component. + +#### SCSS [Block] + +The `.scss` file for a block is exactly the same as the one for a smart-component. + +#### Preset [Block] + +The `preset.js` file for a block is exactly the same as the one for the smart-component. + +#### Data [Block] + +One of the main things that separates a block from a smart-component is the data that is attached to +it. The data files provide the content and structure of your block, they represent the eventual +backend data that will be used to render the templates on the server. + +Data files can be one of the following formats and can be used interchangeably. + +1. Yaml +2. [Json](./13-guides.md#Using-JSON-for-data-files) +3. [JavaScript](./13-guides.md#Using-JavaScript-for-data-files) + +Since `yaml` is less verbose, and can better handle multiline content, we've chosen that as the +default. If you want to use any of the other formats please see the Tutorial section. + +You could create as many data files as you want for each specific situation, for now just add a +single `data.yaml` file for your block. + +Inside of your data file we can add the content that will be rendered in the block. There are two +ways of defining data. + +1. Local data +2. Referenced data + +> **Note:** Make sure to match your data structure with the backend to avoid major differences while +> implementing + +##### Local data [Block] + +The local data is the data dat is entered directly in your data file. It is the most simple way of +adding data but in the long run this could cause a lot of duplication. + +```yaml +title: "Hi I'm a block! πŸ’ͺ" +content: "I'm the body copy for the block." +``` + +##### Imported data [Block] + +This imported data is fetched from another file using the +[json-import-loader](https://www.npmjs.com/package/json-import-loader). You can import other files +by adding the following prefix to your path. + +```javascript +import!path-to-file +``` + +Data could come from any other folder within your project, so if you would like to add +`otherContent` to your data file it would look like this. + +```yaml +title: "Hi I'm a block! πŸ’ͺ" +content: "I'm the body copy for the block." +otherContent: 'import!./some-other-content.yaml' +``` + +> **Note:** Keep in mind that import paths are relative! diff --git a/docs/04-page.md b/docs/04-page.md new file mode 100644 index 00000000..76c60769 --- /dev/null +++ b/docs/04-page.md @@ -0,0 +1,119 @@ +# Page + +As described in the page on components Muban exists of three types of components. The blocks are the +biggest components and they are used to build up the pages. A page in Muban is _a single file_ that +contains the structure and the data for that page. + +A page can be in one of the following formats and can be used interchangeably. + +1. Yaml +2. [Json](./13-guides.md#Using-JSON-for-page-files) +3. [JavaScript](./13-guides.md#Using-JavaScript-for-page-files) + +Since `yaml` is less verbose, and can better handle multiline content, we've chosen that as the +default. If you want to use any of the other formats please see the Tutorial section. + +A page file is build up in three different sections. + +1. Title +2. Meta +3. Blocks + +### Title + +The title is the most basic part and describes the page title. It is used for the following values: + +- The `` tag in the `<head>`. +- The name of the HTML page +- The path in the generated `index.html` overview. + +```yaml +title: 'Home' +``` + +### Meta + +The meta section contains the information that is used on the generated `index.html` overview page. + +```yaml +meta: + id: '01-home' # can be number or string, used for ordering + status: 'dev' # dev, qa, feedback, done + notes: 'Look at this awesome page.' # add some information about the page + category: 'pages' # to group pages in the overview +``` + +> **Note:** These values are not used in a production build and are only there for development +> purpose. + +### Blocks + +The blocks section is the list of blocks that should be rendered to display the page. + +A block exists of two values. + +1. `name` +2. `data` + +#### Name + +The name is used for the lookup of the block, the following path is generated: `component/block/{{name}}/{{name}}.hbs`. + +> **Note:** Keep in mind that the casing should be in `slug-case` + +```yaml +blocks: + - name: 'my-block' + data: 'import!../app/component/block/my-block/data.yaml' + - name: 'my-other-block' + data: 'import!../app/component/block/my-other-block/data.yaml' +``` + +#### Data + +The data provides the content and structure of your block, it represents the eventual backend data +that will be used to render the templates on the server. Usually all blocks contain data and it is +defined on the page that the block is rendered on. + +There can be two ways of providing data to your block. +Β  +##### Page data + +Page data is defined directly in the data key of your block. You will probably not use +this but if your block uses specific data that is only required on that page you could define it on +page level. + +```yaml +blocks: + - name: 'my-block' + data: + title: "Hi I'm a the same block but with different content! πŸ’ͺ" + content: "I'm the specific body copy for the block." +``` + +> **Note:** keep in mind that if you choose to use local data you will have to write all data for +> that block locally. + +##### Imported data + +Imported data is data that is imported from another location. If you look at the block section on +the page about components you can see that all blocks all have a local `data.yaml` file. This file +can be used to provide the same data to multiple instances of the same block without having to write +it multiple times. + +```yaml +blocks: + - name: 'my-block' + data: 'import!../app/component/block/my-block/data.yaml' +``` + +You can even take this a step further and create multiple variations of the data file. This way you could +easily render out different states of the same component and keep everything clean and more re-usable. + +```yaml +blocks: + - name: 'my-block' + data: 'import!../app/component/block/my-block/data-logged-in.yaml' + - name: 'my-block' + data: 'import!../app/component/block/my-block/data-logged-out.yaml' +``` diff --git a/docs/05-application.md b/docs/05-application.md new file mode 100644 index 00000000..d7005733 --- /dev/null +++ b/docs/05-application.md @@ -0,0 +1,96 @@ +# Application + +Everything in Muban is a component, this includes the application itself. This component is +called `App` and can be found in the `src/app/component/layout/app` folder. By default +the app does not do that much, it just renders out all the blocks that are provided by the +blocks list as described in the page about pages. + +The structure for the app component looks like this: + +``` +app/ + - app.hbs + - app.scss + - App.ts +``` + +If you would want to add some HTML that appears on all pages (for example a header and/or a footer) +you would add it here. Just make sure you leave the loop in there! + +```handlebars +<link rel="stylesheet" href="./app.scss"> +<script src="./App.ts"></script> + +<div data-component="app-root"> + <header>I'm the header</header> + {{#each blocks}} + {{> (lookup . 'name') data }} + {{/each}} + <footer>I'm the footer</footer> +</div> +``` + +> **Note:** If you want to more information about smart components please see smart-components +> section on the [page about components](./03-component.md) + +## Bootstrapping + +The bootstrap file is the starting point of your Muban project, it is the entry point for webpack +and can be found in the root of the `/src/app` folder. This file bootstraps Muban with the required +configuration. + +Since we have a development and a production version of the project we also have a development and a +production version of the bootstrap. + +- `bootstrap.dev.ts` +- `bootstrap.dist.ts` + +### Development bootstrapping + +The development version of the bootstrap file consists of two sections. The first one is the +initialisation of Muban with all the configuration, and the second one enables hot reloading for the +development server. For a full detailed description of the development bootstrap method +[see the API reference page](./09-api-reference.md)! + +### Production bootstrapping + +The production version of the bootstrap file also consists of two sections. The first one is the +creation of a require context for all the blocks. The second part is the actual initialisation of +Muban, compared to the development version this initialisation does not require any more +configuration since everything is already bundled. + +For a full detailed description of the production bootstrap method +[see the API reference page](./09-api-reference.md)! + +## Application lifecycle + +When a component file is loaded, it will register itself. When the app boots, all registered +components will be constructed: + +- Loop trough all registered component + +- Find DOM elements that match the components + + ``` + displayName + ``` + + - this is set statically on the component class + - this is set using the `data-component` attribute on the HTML tag + +- Sort found DOM elements based on their nesting depth + + - This will make sure child components are constructed first + +- Construct the component class and pass the DOM element to the constructor + +- Store a reference to the instance and the DOM element + +- The above allows any component constructor to select its child components DOM element and look up + its class instance to communicate with. This can be used to listen for events, read properties or + call functions. The `getComponentForElement(element:HTMLElement):AbstractComponent` function can + be used for that. + +- When running the dev server, and you change your component script file, it will be hot-reloaded by + webpack. Before constructing an instance from the updated file, `dispose()` will be called on the + old instance, so any references or event listeners to the DOM elements can be removed. diff --git a/docs/06-dynamic-data.md b/docs/06-dynamic-data.md new file mode 100644 index 00000000..89e1051f --- /dev/null +++ b/docs/06-dynamic-data.md @@ -0,0 +1,86 @@ +# Dynamic data + +Muban is designed to work with HTML that is fully generated by the server, where it only provides +the `JavaScript` and `CSS` to make the website look and work the way it should. The big downside is +that it's not possible to work with data-binding template engines that frameworks like +[Vue](https://vuejs.org/), [React](https://reactjs.org/) and [Angular](https://angular.io/) do, +because they have control over the HTML. + +This means we create (interactive) components by passing the HTML element, and the component should +use querySelectors and other DOM APIs to read from and write to the DOM. + +> **Note:** Usually this is enough but if you want to do more complex logic you are can load +> Knockout and use their full library to do data-bindings from within JavaScript. See the page about +> Knockout for more information.\* + +## Data provider + +There are multiple ways of providing the dynamic data to the component. Here we will discuss the +most common ones. + +In this case, dynamic data is everything that is not rendered as visible HTML. There are different +ways to pass down additional data to the browser so it can be used by JavaScript upon user +interaction. See the tutorial section for a detailed instruction on how to do this. + +- [data-attributes](./13-guides.md#Get-data-from-data-attributes) +- [embedded json](./13-guides.md#Get-data-from-embedded-json) +- [http-requests](./13-guides.md#Get-data-through-a-http-request) + +## Data templates + +Getting the dynamic data is just the first part, we also need to display the data on the screen. + +There are several ways to get this done, and choosing one depends on the complexity of the data and +the template itself. + +The following scenarios can occur: + +- moving around existing items in the DOM +- toggling visibility of different parts of the DOM +- updating an existing view / item +- adding new items based on an existing template +- adding new items without an existing template + +### Moving around existing items in the DOM + +This could happen when you render all items on the server, and having a client-side sort/filter. + +In this case you could simply pull all item DOM nodes from the container, extract the needed +information to apply a sort/filter, and add the resulting items back in the DOM. + +### Toggling visibility of different parts of the DOM + +This could happen when you have multiple views (e.g. a TabBar) that are all rendered on the server, +but in the client you only show the 'active' view, and hide the other ones. + +### Updating an existing view / item + +This could happen when you have a detail view, and you want to show a different variant without +reloading the page. + +### Adding new items based on an existing template + +This could happen when you have a 'Load More' button to do client-side pagination. + +An existing template could be one of a few things: + +- An existing DOM element of the item, we can then clone the element and update the content with + setting textContent or innerHTML. +- Rendering a `template` element (display:none, without any content), and use that the same way as + above. +- Reusing the `.hbs` template in the JS bundle, by calling the renderItem/renderItems methods from + muban-core. +- Creating a knockout template and render those. Normally the hbs templates are preferred, but if + you have additional logic to execute, this will be a nice solution. Or if you never render any + template on the server, and already include the knockout lib in your project, this is also fine. + +The main thing you want to minimize, is duplicate templates. So if you already rendering something +on the server, you want that to be the source of truth, without duplicating the template in the JS +bundle. That's why reusing .hbs is normally better than knockout, you only have to maintain a single +template (still keep them sync between server and client though). + +### Adding new items without an existing template + +This is similar to the case above, but now we're not sure we have an existing item in the DOM we +could clone. This could be conditionally (depending on which page you're on), or always (you never +render the template on the server at all). diff --git a/docs/07-handlebars.md b/docs/07-handlebars.md new file mode 100644 index 00000000..d332989b --- /dev/null +++ b/docs/07-handlebars.md @@ -0,0 +1,74 @@ +# Handlebars + +[Handlebars](https://handlebarsjs.com/) is a templating engine that let's you dynamically generate +HTML pages. It's an extension of [Mustache](http://mustache.github.io/) with some extra features +(such as `if`, `with`, `unless`, `each` and more). + +This page be the starting point for the core functionality of handlebars within Muban. For more +detailed documentation please check the official [Handlebars](https://handlebarsjs.com/) +documentation. + +> **Note:** You can read why handlebars was chosen in the +> [introduction section](./01-introduction.md#Template). + +## Partials + +Handlebars allows for template reuse through partials. Partials are normal Handlebars templates that +may be called directly by other templates. + +Usually you will need to register each partial individually if you want to be able to call it in +your `.hbs` file. Muban automatically resolves all the `.hbs` files in the `/src/app/component/` +directory using the [handlebars-loader](https://www.npmjs.com/package/handlebars-loader). + +So for example if you create a component in the general directory called `my-component` using the +wizard. You can then call this right away in any other `.hbs` file in the component directory. + +```handlebars +<div> + {{> general/my-component }} +</div> +``` + +> **Note:** Dynamically loading partials, like on the `app` compoennt is a bit more complex, see the +> guide on this for an example on how to do this. + +## Helpers + +Besides the default helpers that are shipped with Handlebars itself, Muban includes additional +helpers to make dynamic templates easier, and to be more compatible with backend template languages +that these templates will be converted to. + +The helpers are placed in the `/build-tools/handlebars-helpers` folder, and the filename should be +used as helper name. + +> When you are converting templates with the +> [muban-convert-hbs](https://github.com/mediamonks/muban-convert-hbs) library, make sure you try to +> stick with the helpers that are supported there, otherwise your custom helpers need to be manually +> updated after conversion. + +> When you use the Handlebars templates as-is in a backend system, make sure that implementation has +> the same helpers registered, otherwise your templates will fail rendering. + +### condition + +The `condition` helper can be used in any place where just truthy or falsy data values are not +enough, but you instead like to compare two data values, or compare something against a static +value. + +It supports all common operators, like `==`, `===`, `!=`, `!==`, `<`, `<=`, `>`, `>=`, `&&` and +`||`. + +**Usage:** + +```handlebars +{{#if (condition variable1 '!==' value) }} + foo +{{else if (condition variable2 '>=' 10) }} + bar +{{/if}} +``` + +#### Custom helpers + +If you want to create a custom helper please +[see the guide on how to do this](./13-guides.md#Create-a-custom-helper)! diff --git a/docs/08-knockout.md b/docs/08-knockout.md new file mode 100644 index 00000000..df80d62e --- /dev/null +++ b/docs/08-knockout.md @@ -0,0 +1,106 @@ +# Knockout + +Because Muban is built for server-rendered pages, there is no possibility for client-side +data-binding without bloating the html with template mumbo-jumbo. Even then, things like looping +over lists (or other thing where a template is used in the non-rendered state) are not really +possible. + +On the other hand, using just DOM APIs to read and update the DOM can become quite cumbersome and +error prone. + +Luckily, [Knockout](https://knockoutjs.com/) allows us to initiate data-bindings from javascript (as +opposed to in HTML), where they can be bound to observables and computes, just like your normally +would. + +The big advantage is that you can specify those in one place (e.g. in the constructor or in a +dedicated named method) so they are visible to everyone, and they will automatically update your +view when the data updates. + +## API reference + +There are two, almost identical, knockout functions we can use for data-binding; applyBindingsToNode +and applyBindingAccessorsToNode. + +The former is for simple use, and the latter expects each property to be a function, which allows us +write additional logic based on observables (basically creating an inline computed). + +### applyBindingsToNode + +Requires the DOM element to bind to, and an object with binding properties. Each key corresponds +with the normal data-binds you would normally write in your HTML (e.g. css, text, change). + +Within the data-bind values you can pass observables, but you have to do so without including the +(). If you do so, it will just return that value, and the changes won't be tracked. By supplying the +observable itself, changes can be tracked to update the binding. + +```typescript +ko.applyBindingsToNode(element, object); +``` + +```typescript +ko.applyBindingsToNode(this.element.querySelector('.search-results'), { + css: { opened: this.searchOpened }, +}); +``` + +### applyBindingAccessorsToNode + +Almost the same as `applyBindingsToNode`, but with a 3rd parameter that we don't really use, so just +pass an empty object here. + +The big difference lies in the values of the data-bind keys; they have to be functions. The return +value of that function is the value that the data-bind expects (e.g. a string or object). + +Within these functions you can use any observable to return a value, and all changes to those +observables will be tracked, just like in normal computeds. + +If one of the data-bind properties for an element needs to be a function, you have to switch to this +method, and all of the properties have to be a function. + +```typescript +ko.applyBindingsToNode(element, object, viewModel); +``` + +Below, the `style` property has to be a function because we are using to observables to return a +custom value. Because of this, the css property also has to be a function, but that one will just +reference the observable (calling it would also work here). + +```typescript +ko.applyBindingAccessorsToNode( + this.content, + { + style: () => ({ + maxWidth: model.deviceEmulateEnabled() ? model.viewportWidth() + 'px' : '100%', + }), + css: () => ({ resizing: model.isResizingViewport }), + }, + {}, +); +``` + +The following example applies a binding to a list of elements, where each element acts as a computed +by introducing some logic. For better performance, the reading of the attributes should be done only +once. + +```typescript +this.getElements('.bar').forEach(bar => { + ko.applyBindingAccessorsToNode( + bar, + { + css: () => { + let min: any = bar.getAttribute('data-size-min'); + let max: any = bar.getAttribute('data-size-max'); + min = min === '*' ? min : parseInt(min, 10); + max = max === '*' ? max : parseInt(max, 10); + + return { + active: + (model.viewportWidth() >= min || min === '*') && + (model.viewportWidth() <= max || max === '*'), + }; + }, + }, + {}, + ); +}); +``` diff --git a/docs/API.md b/docs/09-api-reference.md similarity index 62% rename from docs/API.md rename to docs/09-api-reference.md index fc956598..b2ff9268 100644 --- a/docs/API.md +++ b/docs/09-api-reference.md @@ -1,4 +1,5 @@ -# API reference +# API Reference +Muban provides a couple of util methods that are exposed. This file will describe all the methods that are exposed by the Muban-core. The utils are divided up into sub-sections to keep track on what part they apply to. ## Muban @@ -6,30 +7,25 @@ Init components for the passed container and all children in the DOM. -```ts +```typescript initComponents(rootElement: HTMLElement): void ``` -* **rootElement** - The container to make 'interactive'. This container and all elements in it will +- **rootElement** - The container to make 'interactive'. This container and all elements in it will be searched for Muban components (using the `data-component` attribute), and a new class instance will be created for every component. All HTML outside of this component will be ignored. - -Once the component tree for the passed rootElement is fully constructed, the `adopted()` lifecycle -method will be called on all new components that implement that method. -When the `adopted()` method is called, it means that the component is fully adopted by all its -parents and the application is fully mounted. ### cleanElement -Cleans all component classes previously created by calling initComponents on the same container. +Cleans all component classes previously created by calling initComponents on the same container. -```ts +```typescript cleanElement(rootElement: HTMLElement): void ``` -* **rootElement** - The container to 'clean'. This container and all elements in it will - be searched for Muban components (using the `data-component` attribute) that have an instance - registered. All HTML outside of this component will be ignored. +- **rootElement** - The container to 'clean'. This container and all elements in it will be searched + for Muban components (using the `data-component` attribute) that have an instance registered. All + HTML outside of this component will be ignored. You will only need this method when removing HTML from the DOM (or replacing it with something else), to make sure there won't be any active code around linked to elements that don't exist @@ -39,12 +35,12 @@ anymore. Updates an element with new HTML, makes use of `cleanElement` and `initComponents`. -```ts +```typescript updateElement(element: HTMLElement, html: string): void ``` -* **element** - The element to clean and insert the new HTML in. -* **html** - The new HTML to add to the element, it will be made interactive afterwards. +- **element** - The element to clean and insert the new HTML in. +- **html** - The new HTML to add to the element, it will be made interactive afterwards. First calls `cleanElement` to clean up the current HTML in the element, then adds the provided HTML in the DOM and calls `initComponents`. @@ -55,21 +51,93 @@ Mostly useful when receiving rendered HTML from an API call that should be inser Returns a class instance for DOM node. -```ts +```typescript getComponentForElement(element: HTMLElement): CoreComponent ``` -* **element** - A HTML element with a `data-component` attribute, where a previous call to +- **element** - A HTML element with a `data-component` attribute, where a previous call to `initComponents` created a class instance. -* returns **CoreComponent** - The created class instance for the provided element. +- returns **CoreComponent** - The created class instance for the provided element. This can be useful for inter-component communication. From `component A` you can do a `querySelector` to find the DOM node of another component, and then call `getComponentForElement` to retrieve the instance. From there, you can read properties, call methods or add events. Muban creates child components first, so in the `constructor` of any components, all children -classes already have been instantiated. If you want to have access to class instances of parent -DOM elements, you should call this method from the `adopted()` lifecycle method. +classes already have been instantiated. If you want to have access to class instances of parent DOM +elements, you should call this method from the `adopted()` lifecycle method. + +### bootstrap + +Starts up Muban and makes sure all components get initialised. + +```typescript +bootstrap( + appRoot: HTMLElement, + options: { + indexTemplate: (data: any) => string; + appTemplate: (data: any) => string; + dataContext: any; + partialsContext: any; + Handlebars: any; + onBeforeInit?: () => void; + onInit?: () => void; + onUpdate?: () => void; + onData: (data: object, pageName: string) => object; + registerPartialMap?: Array<(path: string) => string | null>; + pageName?: string; + } = {}): { + updateData: (changedContext) => void, + updatePartials: (changedContext) => void, + update: (updatedIndexTemplate, updatedAppTemplate) => void, + } +``` + +- **appRoot** - The container where the Muban application lives, most likely a div in the `<body>`. +- **indexTemplate** - The hbs template to render the Muban index page. +- **appTemplate** - The hbs template to render the application shell, includes logic to render all + the blocks. +- **dataContext** - A webpack context with all data (yaml/json) files. +- **partialsContext** - A webpack context with component hbs files. +- **Handlebars** - The Handlebars instance to register templates to. +- **onBeforeInit** - Optional callback that gets called right before the application becomes fully + interactive. +- **onInit** - Optional callback that gets called after the application is fully interactive. +- **onUpdate** - Optional callback that gets called after hot reloading did an update. +- **onData** - Optional callback that gets called before rendering the page, and gives you the + opportunity to modify the data before rendering. +- **registerPartialMap** - A map with functions to define if and how partials should be registered. +- **pageName** - Override a pageName to render, when you don't want to make use of the default url + parsing logic of Muban. +- returns **App** - An object with 3 functions that can be called when hot reloading triggers. The + app will do the appropriate re-rendering and trigger the `onUpdate` callback afterwards. + +Both `indexTemplate` and `appTemplate` are passed from the outside, so you have full control in the +project about where they are located. Together with the `dataContext` and `partialsContext` they are +kept outside so we can apply hot reloading logic. When any of those 4 things change, we update the +returned `app` object. + +##### registerPartialsMap + +The `registerPartialsMap` has a default value: + +```typescript +[path => (path.includes('/block/') ? /\/([^/]+)\.hbs/gi.exec(path)[1] : null)]; +``` + +The above will first check if there is `/block/` in the path, and if so it will return the basename. +So a path of `component/block/paragraph/paragraph.hbs` will return `paragraph`. This means that the +partial is registered as `paragraph`, and is used in the `data.yaml` with that block name. + +You can register multiple functions, but the first one that returns a non-null value will +short-circuit the map. + +When providing a custom map, make sure to also include the default value above if you want to keep +it, since it will overwrite the complete map. + +This option is most useful if you want to render non-block components in a dynamic way in +Handlebars. By default, the handlebars-loader will auto-require all static partial includes, but +dynamic includes (e.g. using the `lookup` helper) will need to be registered manually upfront. ## Handlebars @@ -77,7 +145,7 @@ DOM elements, you should call this method from the `adopted()` lifecycle method. Renders an item in the DOM using a handlebars template and some data. -```ts +```typescript renderItem( container: HTMLElement, template: (data?: any) => string, @@ -86,23 +154,24 @@ renderItem( ): void ``` -* **container** - The container to render the item in. -* **template** - A precompiled Handlebars template, where you can pass in data and get the rendered +- **container** - The container to render the item in. +- **template** - A precompiled Handlebars template, where you can pass in data and get the rendered HTML as a result. -* **data** - The data object being passed to the Handlebars template. -* **append** - When false (default), it cleans the container, so only the rendered HTML will end up +- **data** - The data object being passed to the Handlebars template. +- **append** - When false (default), it cleans the container, so only the rendered HTML will end up in the container. When true, it will append the rendered item at the end of the container. The rendered HTML will be made interactive by calling `initComponents` on the newly added components. This is useful when you don't have an HTML node available in the DOM to clone, or if the template -contains a lot of rendering logic that you don't want to repeat in JavaScript. The downside of -using this is that eventually the HTML will live in two places; in the imported .hbs template in the -JS bundle, and in the rendered HTML from the server. +contains a lot of rendering logic that you don't want to repeat in JavaScript. The downside of using +this is that eventually the HTML will live in two places; in the imported .hbs template in the JS +bundle, and in the rendered HTML from the server. An example: -```js + +```typescript import { renderItem } from 'muban-core/lib/utils/dataUtils'; import buttonTemplate from '../../general/button/button.hbs?include'; @@ -113,7 +182,7 @@ renderItem(container, buttonTemplate, { text: 'button text' }); Renders a list of items in the DOM using a handlebars template and an Array of data. -```ts +```typescript renderItems( container: HTMLElement, template: (data?: any) => string, @@ -122,11 +191,11 @@ renderItems( ): void ``` -* **container** - The container to render the item in. -* **template** - A precompiled Handlebars template, where you can pass in data and get the rendered +- **container** - The container to render the item in. +- **template** - A precompiled Handlebars template, where you can pass in data and get the rendered HTML as a result. The template will be called for each item in the data Array. -* **data** - An Array where each item is being passed to the Handlebars template. -* **append** - When false (default), it cleans the container, so only the rendered HTML will end up +- **data** - An Array where each item is being passed to the Handlebars template. +- **append** - When false (default), it cleans the container, so only the rendered HTML will end up in the container. When true, it will append the rendered items at the end of the container. This function is similar to `renderItem`, excepts it renders a list. See the documentation on @@ -145,17 +214,19 @@ initTextBinding( ): KnockoutObservable<string> ``` -* **element** - The element to apply the binding to, and where to extract the content from. -* **html** - When true, extracts the content as HTML, and sets the HTML binding. When false, extract +- **element** - The element to apply the binding to, and where to extract the content from. +- **html** - When true, extracts the content as HTML, and sets the HTML binding. When false, extract the text contents, and sets the text binding. -* returns **KnockoutObservable\<string>** - The observable that is linked to the element. When +- returns **KnockoutObservable\<string>** - The observable that is linked to the element. When updating this observable, the content in the element will update. -Please have a look at the knockout documentation for more information about the `text` and `html` -bindings. +Please have a look at the knockout documentation for more information about the +[`text`](https://knockoutjs.com/documentation/text-binding.html) and +[`html`](https://knockoutjs.com/documentation/html-binding.html) bindings. An example: -```js + +```typescript // init the binding const label = initTextBinding(this.getElement('.label')); @@ -170,7 +241,7 @@ label('new label text'); Sets up a foreach template binding to a container, and can optionally extract the old data. -```ts +```typescript initListBinding<T>( container: HTMLElement, templateName: string, @@ -179,48 +250,48 @@ initListBinding<T>( ): KnockoutObservable<Array<T>> ``` -* **container** - The container to apply the binding to, and where to extract the content from. -* **templateName** - The Knockout template to render for each -* **configOrData** - When passing an Array, it will be used as initial data to render. When passing +- **container** - The container to apply the binding to, and where to extract the content from. +- **templateName** - The Knockout template to render for each +- **configOrData** - When passing an Array, it will be used as initial data to render. When passing an Object, it will be used as configuration for `html-extract-data`. The extracted data from the HTML will be used to render the same content again using the Knockout template. -* **additionalData** - Useful when extracting data from HTML, it is passed as third parameter to +- **additionalData** - Useful when extracting data from HTML, it is passed as third parameter to `html-extract-data`, and merged with each item extracted. This could be used to setup bindings that don't deal with data (e.g. click bindings). -* returns **KnockoutObservable<Array<T>>** - The observable Array that is linked to the rendered +- returns **KnockoutObservable<Array<T>>** - The observable Array that is linked to the rendered items, filled with either the passed data Array or the extracted data from the DOM. When updating this observable, the list in the DOM will also update. _Note, the fields of individual items are not observable, only the list itself._ This function can be used in two ways: -* setting up a normal Knockout `foreach` binding by passing data to render, or -* by extracting the rendered items from the DOM as a starting point using the `html-extract-data` + +- setting up a normal Knockout `foreach` binding by passing data to render, or +- by extracting the rendered items from the DOM as a starting point using the `html-extract-data` module. This option is mostly useful when you already have rendered items in the DOM, and you want to append some more at a later time. Or when you want to make the current items interactive with additional Knockout bindings. - + Please have a look at the knockout documentation how to define templates, and how foreach bindings and observableArrays work. -And when extracting data, check the documentation from [html-extract-data](https://github.com/thanarie/html-extract-data). +And when extracting data, check the documentation from +[html-extract-data](https://github.com/thanarie/html-extract-data). A data example: -```js -const items = initListBinding( - this.getElement('.items'), - 'list-template', - [ - { name: 'Bob', age: 21 }, - { name: 'Alice', age: 19 }, - ] -); + +```typescript +const items = initListBinding(this.getElement('.items'), 'list-template', [ + { name: 'Bob', age: 21 }, + { name: 'Alice', age: 19 }, +]); // add a new item, will be added to the DOM items.push({ name: 'Joe', age: 24 }); ``` An extract example: -```js + +```typescript const items = initListBinding( this.getElement('.items'), 'list-template', @@ -228,104 +299,16 @@ const items = initListBinding( query: '.grid-item', data: { name: '.name', - age: { query: '.age', convert: parseInt } - } + age: { query: '.age', convert: parseInt }, + }, }, { onClick(vm) { console.log('clicked', vm); - } - } + }, + }, ); // add a new item, will be added to the DOM items.push({ name: 'Joe', age: 24 }); -``` - -## bootstrap - -### dist - -Bootstraps the Muban application during a production build. - -```ts -bootstrap( - appRoot: HTMLElement, - options: { - onInit?: () => void; - } = {}) -``` - -* **appRoot** - The container where the Muban application lives, most likely a div in the `<body>`. -* **onInit** - Optional callback that gets called after the application is fully interactive. - -### dev - -Bootstraps the Muban application during a development build. - - -```ts -bootstrap( - appRoot: HTMLElement, - options: { - indexTemplate: (data: any) => string; - appTemplate: (data: any) => string; - dataContext: any; - partialsContext: any; - Handlebars: any; - onBeforeInit?: () => void; - onInit?: () => void; - onUpdate?: () => void; - onData: (data: object, pageName: string) => object; - registerPartialMap?: Array<(path: string) => string | null>; - pageName?: string; - } = {}): { - updateData: (changedContext) => void, - updatePartials: (changedContext) => void, - update: (updatedIndexTemplate, updatedAppTemplate) => void, - } -``` - -* **appRoot** - The container where the Muban application lives, most likely a div in the `<body>`. -* **indexTemplate** - The hbs template to render the Muban index page. -* **appTemplate** - The hbs template to render the application shell, includes logic to render - all the blocks. -* **dataContext** - A webpack context with all data (yaml/json) files. -* **partialsContext** - A webpack context with component hbs files. -* **Handlebars** - The Handlebars instance to register templates to. -* **onBeforeInit** - Optional callback that gets called right before the application becomes fully interactive. -* **onInit** - Optional callback that gets called after the application is fully interactive. -* **onUpdate** - Optional callback that gets called after hot reloading did an update. -* **onData** - Optional callback that gets called before rendering the page, and gives you the opportunity to modify the data before rendering. -* **registerPartialMap** - A map with functions to define if and how partials should be registered. -* **pageName** - Override a pageName to render, when you don't want to make use of the default url - parsing logic of Muban. -* returns **App** - An object with 3 functions that can be called when hot reloading triggers. The - app will do the appropriate re-rendering and trigger the `onUpdate` callback afterwards. - -Both `indexTemplate` and `appTemplate` are passed from the outside, so you have full control in the -project about where they are located. Together with the `dataContext` and `partialsContext` they -are kept outside so we can apply hot reloading logic. When any of those 4 things change, we update -the returned `app` object. - -**registerPartialsMap** - -The `registerPartialsMap` has a default value: -```ts -[ - path => (path.includes('/block/') ? /\/([^/]+)\.hbs/gi.exec(path)[1] : null), -] -``` -The above will first check if there is `/block/` in the path, and if so it will return the basename. -So a path of `component/block/paragraph/paragraph.hbs` will return `paragraph`. This means that the -partial is registered as `paragraph`, and is used in the `data.yaml` with that block name. - -You can register multiple functions, but the first one that returns a non-null value will -short-circuit the map. - -When providing a custom map, make sure to also include the default value above if you want to keep -it, since it will overwrite the complete map. - -This option is most useful if you want to render non-block components in a dynamic way in -Handlebars. By default, the handlebars-loader will auto-require all static partial includes, but -dynamic includes (e.g. using the `lookup` helper) will need to be registered manually upfront. +``` \ No newline at end of file diff --git a/docs/storybook.md b/docs/10-storybook.md similarity index 86% rename from docs/storybook.md rename to docs/10-storybook.md index 1b739eeb..ee587ffe 100644 --- a/docs/storybook.md +++ b/docs/10-storybook.md @@ -1,5 +1,7 @@ # Storybook +> ⚠️ Storybook will become an installable module, therefore it is temporarily unavailable! + Storybook is a web-app that lets you preview and interact with the components in your project. You can create presets that render your component with custom HTML, and pass different properties by providing a yaml/json object. @@ -9,15 +11,15 @@ single preset isolated. Besides the component, it will show: -* the name -* the file path -* the description -* the preset html -* the preset data -* the component .hbs source -* the component .ts source -* the component .scss source -* the rendered html +- the name +- the file path +- the description +- the preset html +- the preset data +- the component .hbs source +- the component .ts source +- the component .scss source +- the rendered html The viewer also includes a media query viewer that read the media queries from your projects, just like it's done in Chrome DevTools. @@ -32,7 +34,7 @@ yarn storybook:preview # preview the built storybook on port 9003 Just create a `preset.js` file in your component folder and add a story like this: -``` +```typescript import { storiesOf } from 'storybook/utils/utils'; storiesOf('Paragraph', require('./paragraph')) @@ -54,7 +56,7 @@ storiesOf('Paragraph', require('./paragraph')) You can add multiple presets of the same component by chaining the `add()`: -``` +```typescript storiesOf('Paragraph', require('./paragraph')) .add('preset 1', ...) .add('preset 2', ...) @@ -69,16 +71,15 @@ the object you pass as the last argument. You can also store the data objects in a yaml file in the same folder and just require it in place: -``` -storiesOf('Paragraph', require('./paragraph')) - .add( - 'default', - 'A Paragraph block with a "read more" section you can show by clicking a button.', - `<hbs> +```typescript +storiesOf('Paragraph', require('./paragraph')).add( + 'default', + 'A Paragraph block with a "read more" section you can show by clicking a button.', + `<hbs> {{> paragraph @root }} </hbs>`, - require('./data'), - ) + require('./data'), +); ``` ## Customize @@ -102,7 +103,7 @@ The only configuration available at the moment is loading the stories. When the function is called, you have to require all the preset files, which can be done by using a webpack context: -``` +```typescript import { configure } from 'storybook/utils/utils'; const context = require.context('app/component/', true, /preset\.js$/); diff --git a/docs/11-transitions-and-animations.md b/docs/11-transitions-and-animations.md new file mode 100644 index 00000000..c5bd2978 --- /dev/null +++ b/docs/11-transitions-and-animations.md @@ -0,0 +1,12 @@ +# Transitions and animations + +Adding animations to your project could make your project feel more alive and sometimes CSS +animations are just not enough. + +This is where the +[muban-transition-component](https://www.npmjs.com/package/muban-transition-component) comes into +place. It uses [GreenSock](https://greensock.com/) to create timelines that can be nested to create +super complex animations that are triggered when the components enter the viewport. + +For a full implementation guide see the +[setup instructions](https://github.com/riccoarntz/muban-transition-component/wiki). diff --git a/docs/dist-implementation-guide.md b/docs/12-dist-implementation-guide.md similarity index 57% rename from docs/dist-implementation-guide.md rename to docs/12-dist-implementation-guide.md index 5e0f1483..d9611042 100644 --- a/docs/dist-implementation-guide.md +++ b/docs/12-dist-implementation-guide.md @@ -1,24 +1,24 @@ # Distribution implementation guide -_Note: this file is copied from docs to the dist package._ +> **Note:** this file is copied from docs to the dist package. -This package contains all the information that is needed for implementing the frontend in -your website. It contains assets that can be copied over as is, preview pages to view the -(static) end result, and the developer templates that can be referenced as example when -implementing the html in your template language of choice. +This package contains all the information that is needed for implementing the frontend in your +website. It contains assets that can be copied over as is, preview pages to view the (static) end +result, and the developer templates that can be referenced as example when implementing the html in +your template language of choice. The package content: -* **site/** - _the preview site_ - * **asset/** - contains all assets that should be copied over and included in the page HTML - * **font/** - custom fonts, will be loaded by the css - * **image/** - images required for the design/markup, will be loaded by css or js - * **\*.html** - statically rendered preview pages (with index.html as overview) - * The other folders contain static assets that are referenced from the preview html pages, and +- **site/** - _the preview site_ + - **asset/** - contains all assets that should be copied over and included in the page HTML + - **font/** - custom fonts, will be loaded by the css + - **image/** - images required for the design/markup, will be loaded by css or js + - **\*.html** - statically rendered preview pages (with index.html as overview) + - The other folders contain static assets that are referenced from the preview html pages, and should normally be served dynamically (so should not be copied over, it's just sample content). -* **template/** - the developer templates, can be used as reference -* **data/** - the developer mock data, can be used as reference -* **storybook/** - optionally, a component viewer 'site', where all components can be previewed, +- **template/** - the developer templates, can be used as reference +- **data/** - the developer mock data, can be used as reference +- **storybook/** - optionally, a component viewer 'site', where all components can be previewed, together with all the source code and documentation #### Preview @@ -47,83 +47,82 @@ Read the sections below for more information about certain files or folders. The content in the `site/asset/` folder is the output of our webpack build. Webpack takes all development source files, which are nicely structured per component and written using modern web -standards. It then transpiles, minifies, optimizes and bundles all the assets it encounters, -and places them in the output folder. +standards. It then transpiles, minifies, optimizes and bundles all the assets it encounters, and +places them in the output folder. -The JS and CSS that should be included in the HTML have a default filename, which stays the same -for each build. All other assets (that are loaded from JS and CSS) will get a unique filename with -the content-hash in the filename (see below for caching). +The JS and CSS that should be included in the HTML have a default filename, which stays the same for +each build. All other assets (that are loaded from JS and CSS) will get a unique filename with the +content-hash in the filename (see below for caching). The JS and CSS files are split between `common` and `bundle`. The common files contain all -library/vendor code that doesn't change (often), and the bundle files contain all application -code that could change more often. If desired, they can be outputted into a single file. +library/vendor code that doesn't change (often), and the bundle files contain all application code +that could change more often. If desired, they can be outputted into a single file. -By default, all those files are placed into the `asset` folder. This path is also included in the -JS and CSS files that load other assets (like fonts and images). This means that the folder should -be accessible at `https://www.example.com/asset/*`. When a different folder (structure) is desired, -we have two options: +By default, all those files are placed into the `asset` folder. This path is also included in the JS +and CSS files that load other assets (like fonts and images). This means that the folder should be +accessible at `https://www.example.com/asset/*`. When a different folder (structure) is desired, we +have two options: 1. We can build the files using a different (or nested) folder, or 2. The path (called the `publicPath`) can be configured at runtime. The default is set to `/`, but - can be changed to any path or domain. _Please note that this is just a prefix, and you still - need the `asset` folder after that._ + can be changed to any path or domain. _Please note that this is just a prefix, and you still need + the `asset` folder after that._ You can do this by setting the `webpackPublicPath` variable in a `<script>` tag on the window before any script file is loaded: ```js - window.webpackPublicPath = '/nested/folder/'; - // results in '/nested/folder/assets/...' + window.webpackPublicPath = '/nested/folder/'; + // results in '/nested/folder/assets/...' - // or - - window.webpackPublicPath = 'https://cdn-domain.site.com/nested/folder/'; - // results in 'https://cdn-domain.site.com/nested/folder/assets/...' + // or + + window.webpackPublicPath = 'https://cdn-domain.site.com/nested/folder/'; + // results in 'https://cdn-domain.site.com/nested/folder/assets/...' ``` ## Templates The content in the `site/templates/` folder contains the development templates written in -handlebars. Handlebars is powerful enough to create 'dynamic' html for the given mock data, but -also simple enough that all used backend/cms template languages (like twig, django, aem) have -support for the used 'template features'. +handlebars. Handlebars is powerful enough to create 'dynamic' html for the given mock data, but also +simple enough that all used backend/cms template languages (like twig, django, aem) have support for +the used 'template features'. -It also works nicely with this Webpack build system, and can be easily extended to implement -missing features that a 3rd party template engine might want to use. +It also works nicely with this Webpack build system, and can be easily extended to implement missing +features that a 3rd party template engine might want to use. The templates are set up in such a way that: -* All dynamic data (copy or content) that needs to be displayed on the pages, is passed from the +- All dynamic data (copy or content) that needs to be displayed on the pages, is passed from the mock yaml files. This means that everything else can be copied over as is. -* Iteration over a list of items, or conditionally rendering something, is already present. This +- Iteration over a list of items, or conditionally rendering something, is already present. This makes the intention of how templates should be used more clear than just looking at the preview pages. It also makes all the possible variations (e.g. conditionally adding a css-class) more clear. -* Everything that is 'used' multiple times (like buttons, form items, sliders) is its own component. - If the same structure is also implemented in the 'backend', future updates to components that - are used a lot will be as simple as changing a single file. - -* There is a division between **blocks** and other components. Blocks are 'special' components that - are 'top level' on a page, are 'stand-alone' and could be dynamically chosen from a CMS. - Normal components are just reusable parts that are referenced from blocks and other components. +- Everything that is 'used' multiple times (like buttons, form items, sliders) is its own component. + If the same structure is also implemented in the 'backend', future updates to components that are + used a lot will be as simple as changing a single file. + +- There is a division between **blocks** and other components. Blocks are 'special' components that + are 'top level' on a page, are 'stand-alone' and could be dynamically chosen from a CMS. Normal + components are just reusable parts that are referenced from blocks and other components. -* In the preview html pages, the templates are surrounded by html comments containing the template +- In the preview html pages, the templates are surrounded by html comments containing the template paths. This makes it easy to look at the preview page source, and see how it's built up. #### Data -As mentioned above, to fill the html content with data we make mock yaml files. These contain -the blocks we want to include in a page, and the data each block should display. Each `.yaml` file +As mentioned above, to fill the html content with data we make mock yaml files. These contain the +blocks we want to include in a page, and the data each block should display. Each `.yaml` file corresponds to a single page. Some pages have multiple yaml files, to show different variations, e.g. a carousel with 1 or 5 items, a search page with or without results. To make the integration easier, it is a good practice to keep the structure in the yaml files similar to the modals that are used on the server to render the actual dynamic pages. The more similar the models are, the less manual conversion has to be done. - #### Automatic template conversion @@ -134,17 +133,18 @@ manual conversion of templates needed. ## Storybook +> ⚠️ Storybook will become an installable module, therefore it is temporarily unavailable! + Storybook is inspired by the [React and Vue Storybook](https://storybook.js.org/). It is a UI development environment for your UI components. With it, you can visualize different states of your -UI components and develop them interactively. - +UI components and develop them interactively. Storybook runs outside of your app. So you can develop UI components in isolation without worrying about app specific dependencies and requirements. -Besides showing the component in isolation, it also shows the source files -(template, script, style), the mock data that is used, the rendered html, and some documentation -about how the component can be used. - +Besides showing the component in isolation, it also shows the source files (template, script, +style), the mock data that is used, the rendered html, and some documentation about how the +component can be used. + ## Caching All (static) assets should be cached by the server or a CDN. When files update you have two options, @@ -152,31 +152,32 @@ either invalidating the cache, or changing the filename. By default, the js and css files that should be included in the HTML have a normal filename that doesn't change when the content updates, so the responsibility of managing the cache is the -responsibility of the implementation party. When desired, the build output can also be configured -to include a hash in the filename that is derived from the file's content, so it only updates -when the file content is changed (e.g. `common.5486f02.js`). +responsibility of the implementation party. When desired, the build output can also be configured to +include a hash in the filename that is derived from the file's content, so it only updates when the +file content is changed (e.g. `common.5486f02.js`). Besides the js and css files that should be included in the HTML, there are also files in the assets folder that are loaded internally. Those files have the checksum content hash in the filename by -default, so the filename changes when the content of that file changes. This means that the cache -of those files can be configured to be very long, since they will never update, and will improve -load performance. +default, so the filename changes when the content of that file changes. This means that the cache of +those files can be configured to be very long, since they will never update, and will improve load +performance. ## Development The reason Muban exists is to create the best developer experience as possible, while still making it easy to integrate into any backend system. The benefits are: -* Let Frontend Developers use the tools they know and love in a setup they are comfortable with, +- Let Frontend Developers use the tools they know and love in a setup they are comfortable with, which include: - * Webpack for building everything - * Webpack for hot reloading, increasing the development speed - * Babel to use the newest javascript language features - * TypeScript for extra type safety - * SCSS to write stylesheets, and keep them next to your components - * Code quality tools such as editorconfig, eslint, stylelint and prettier - -* Handlebars as an easy to use template language which can be easily ported to other languages -* Having local mock data in yaml files, so development can start without having a backend ready -* Client and QA can checks and approve the static preview pages, without having a backend ready -* Having storybook as a nice overview of all the components, with documentation + + - Webpack for building everything + - Webpack for hot reloading, increasing the development speed + - Babel to use the newest javascript language features + - TypeScript for extra type safety + - SCSS to write stylesheets, and keep them next to your components + - Code quality tools such as editorconfig, eslint, stylelint and prettier + +- Handlebars as an easy to use template language which can be easily ported to other languages +- Having local mock data in yaml files, so development can start without having a backend ready +- Client and QA can checks and approve the static preview pages, without having a backend ready +- Having storybook as a nice overview of all the components, with documentation diff --git a/docs/13-guides.md b/docs/13-guides.md new file mode 100644 index 00000000..470d4ea1 --- /dev/null +++ b/docs/13-guides.md @@ -0,0 +1,1425 @@ +# Guides + +These guides will help you through certain tasks that you might need during your project! Have a look at the table of contents for a quick overview of the available guides. + +- [Guides](#Guides) + - [Muban](#Muban) + - [Create a component](#Create-a-component) + - [Using the wizard](#Using-the-wizard) + - [Using the shorthand](#Using-the-shorthand) + - [Create a smart-component](#Create-a-smart-component) + - [Create a block](#Create-a-block) + - [Create a page](#Create-a-page) + - [Using the wizard](#Using-the-wizard-1) + - [Using the shorthand](#Using-the-shorthand-1) + - [Do not use the default index template](#Do-not-use-the-default-index-template) + - [Using JSON for data files](#Using-JSON-for-data-files) + - [Using JavaScript for data files](#Using-JavaScript-for-data-files) + - [Object notation](#Object-notation) + - [Function notation](#Function-notation) + - [Using JSON for page files](#Using-JSON-for-page-files) + - [Using JavaScript for page files](#Using-JavaScript-for-page-files) + - [Object notation](#Object-notation-1) + - [Function notation](#Function-notation-1) + - [Use custom variables in your data](#Use-custom-variables-in-your-data) + - [Updating the HTML boilerplate](#Updating-the-HTML-boilerplate) + - [Excluding page files.](#Excluding-page-files) + - [Using assets](#Using-assets) + - [Static assets](#Static-assets) + - [Webpack assets](#Webpack-assets) + - [TypeScript](#TypeScript) + - [Ensure all components have been adopted](#Ensure-all-components-have-been-adopted) + - [Select child element/elements](#Select-child-elementelements) + - [Adding event listeners](#Adding-event-listeners) + - [Add a polyfill](#Add-a-polyfill) + - [Get data from data-attributes](#Get-data-from-data-attributes) + - [Get data from embedded json](#Get-data-from-embedded-json) + - [Get data through a http-request](#Get-data-through-a-http-request) + - [Getting HTML](#Getting-HTML) + - [Getting JSON](#Getting-JSON) + - [Post a form](#Post-a-form) + - [Post JSON](#Post-JSON) + - [File upload](#File-upload) + - [Update an entire section through a http-request](#Update-an-entire-section-through-a-http-request) + - [The API returns HTML](#The-API-returns-HTML) + - [The API returns JSON](#The-API-returns-JSON) + - [Sort or filter items already in the DOM](#Sort-or-filter-items-already-in-the-DOM) + - [Load more items to the page](#Load-more-items-to-the-page) + - [Clone and update element](#Clone-and-update-element) + - [Use a handlebars template](#Use-a-handlebars-template) + - [Data util methods](#Data-util-methods) + - [Use a knockout template](#Use-a-knockout-template) + - [Handlebars](#Handlebars) + - [Render a component](#Render-a-component) + - [Pass data to your component](#Pass-data-to-your-component) + - [Render data in your component](#Render-data-in-your-component) + - [Render data as HTML in your component](#Render-data-as-HTML-in-your-component) + - [Dynamically render components](#Dynamically-render-components) + - [Using icons](#Using-icons) + - [Create a custom helper](#Create-a-custom-helper) + - [A very basic example of a helper that reverses a word could look like this.](#A-very-basic-example-of-a-helper-that-reverses-a-word-could-look-like-this) + - [Knockout](#Knockout) + - [Apply bindings to a node.](#Apply-bindings-to-a-node) + - [Apply bindings to the entire component](#Apply-bindings-to-the-entire-component) + - [Seng-generator](#Seng-generator) + - [Create a custom template](#Create-a-custom-template) + +## Muban + +### Create a component + +Creating a component can be done manually by creating all the files as described in the +[page on the components](./03-component). This process takes up a lot of time and increases chance +of errors! To avoid this you can use the +[seng-generator](https://www.npmjs.com/package/seng-generator) to generate them for you! If you +followed the preparation instructions you will by now have this installed. + +### Using the wizard + +Start by opening the terminal in the root of your project and run the following command. + +``` +sg wizard +``` + +This will start up the wizard and it will prompt you with a couple of questions. Use the `up` and +`down` keys to select the template that you want to use and press the `enter` key to continue. + +``` +? Which template do you want to use? (Use arrow keys) + block +> component + page + smart-component +``` + +After that enter the desired name of your component and press `enter` again. + +``` +? Which template do you want to use? component +? What name do you want to use? () +``` + +> **Note:** The casing will automatically be changed to the required format. + +After you've provided the name you can choose the location where the component should be created. +The default directory is shown so if you don't want to change this just press `enter` to continue. + +> **Note:** If you want to provide a different location please provide the full relative path from +> the root of your project. + +``` +? Which template do you want to use? component +? What name do you want to use? my-component +? Where do you want to create the component? (./src/app/component) +``` + +After you pressed enter it will notify you that the component has been successfully created. + +``` +? Which template do you want to use? component +? What name do you want to use? my-component +? Where do you want to create the component? ./src/app/component +Generating files from 'component' template with name: my-component + +Done! +``` + +### Using the shorthand + +While using the wizard to generate your components is very easy and descriptive of what's happening +it requires quite a lot of interaction. If you do not want to go through this every time you can use +the shorthand to create the components. + +Open up the terminal in the root of your project and run the following command: + +```bash +sg component my-component +``` + +This have the same result as when the wizard is followed. + +### Create a smart-component + +> ⚠️ Creating a smart-component uses the same steps as described in the +> [creation of a basic component](#Create-a-component). + +### Create a block + +> ⚠️ Creating a block uses the same steps as described in the +> [creation of a basic component](#Create-a-component). + +### Create a page + +Creating a page can be done manually by creating the files as described in the +[page on the pages](./04-page.md). This process takes up a lot of time and increases chance of +errors! To avoid this you can use the [seng-generator](https://www.npmjs.com/package/seng-generator) +to generate them for you! If you followed the preparation instructions you will by now have this +installed. + +### Using the wizard + +Start by opening the terminal in the root of your project and run the following command. + +``` +sg wizard +``` + +This will start up the wizard and it will prompt you with a couple of questions. Use the `up` and +`down` keys to select the _page template_ and press `enter` to continue. + +``` +? Which template do you want to use? (Use arrow keys) + block + component +> page + smart-component +``` + +After that enter the desired name of your page and press `enter` again. + +``` +? Which template do you want to use? my-page +? What name do you want to use? () +``` + +> **Note:** The casing will automatically be changed to the required format. + +After you've provided the name you can choose the location where the page should be created. The +default directory is shown so if you don't want to change this just press `enter` to continue. + +``` +? Which template do you want to use? page +? What name do you want to use? my-page +? Where do you want to create the page? (./src/data) +``` + +> **Note:** If you want to provide a different location please provide the full relative path from +> the root of your project. + +After that you can provide an optional list of blocks that you want to render out on that page. If +you want to skip this step you can just press `enter`. Otherwise provide a `slug-cased` list of +components that you want to render. + +``` +? Which template do you want to use? page +? What name do you want to use? my-page +? Where do you want to create the page? ./src/data +? Add a list of comma separated blocks (optional) () +``` + +After you pressed enter it will notify you that the page has been successfully created. + +``` +? Which template do you want to use? page +? What name do you want to use? my-page +? Where do you want to create the page? ./src/data +? Add a list of comma separated blocks (optional) my-block +Generating files from 'page' template with name: my-page + +Done! +``` + +### Using the shorthand + +While using the wizard to generate your pages is very easy and descriptive of what's happening it +requires quite a lot of interaction. If you do not want to go through this every time you can use +the shorthand to create the pages. + +Open up the terminal in the root of your project and run the following command: + +``` +sg page my-page +``` + +> **Note:** This will generate a page file for you, but leave out the step to render in blocks. + +### Do not use the default index template + +The index overview template is always rendered in the development mode, if for any reason you would +not want this in the distrubution build you can simply create a page called `index`. + +### Using JSON for data files + +Using JSON as the source for your data files is not recommended but if for any reason you would want +to do this you can. + +Add a `data.json` file with the following structure: + +```json +{ + "title": "Hi I'm a block! πŸ’ͺ", + "content": "I'm the body copy for the block." +} +``` + +> If you are planning on using JSON for all data files, it is recommended to remove the template +> file `{name_sc}.yaml` from the page directory: `build-tools/generator-templates/block/` and add a +> JSON variant: `{name_sc}.json`. + +### Using JavaScript for data files + +If you want dynamic data, add loops or something from the `process.env` you can use JavaScript as +the source of your data. + +There are two ways of defining the data in JavaScript. + +#### Object notation + +This is a static object and will only be initialised once. + +```javascript +module.exports = { + title: "Hi I'm a block! πŸ’ͺ", + content: "I'm the body copy for the block.", +}; +``` + +#### Function notation + +This method is executed on run time so you could technically use this to renew data runtime. + +```javascript +module.exports = () => ({ + title: "Hi I'm a block! πŸ’ͺ", + content: "I'm the body copy for the block.", +}); +``` + +> If you are planning on using JavaScript for all data files, it is recommended to remove the +> template file `{name_sc}.yaml` from the page directory: `build-tools/generator-templates/block/` +> and add a JavaScript variant: `{name_sc}.js` + +### Using JSON for page files + +Using JSON as the source for your pages is not recommended but if for any reason you would want to +do this you can. + +Add a `my-page.json` file in the data `src/data/` folder, with the following structure: + +```json +{ + "title": "my-page", + "meta": { + "id": "", + "status": "", + "notes": "", + "category": "" + }, + "blocks": [ + { + "name": "my-block", + "data": "import!../app/component/block/my-block/data.json" + } + ] +} +``` + +> If you are planning on using JSON for all pages, it is recommended to remove the template file +> `{name_sc}.yaml` from the page directory: `build-tools/generator-templates/page/` and add a JSON +> variant: `{name_sc}.json` + +### Using JavaScript for page files + +If you want dynamic data, add loops or something from the `process.env` you can use JavaScript as +the source of your data. Just add a `my-page.js` file in the data `src/data/` folder. + +There are two ways of defining the data in JavaScript. + +#### Object notation + +This is a static object and will only be initialised once. + +```javascript +module.exports = { + title: 'my-page', + meta: { + id: '', + status: '', + notes: '', + category: '', + }, + blocks: [ + { + name: 'my-block', + data: 'import!../app/component/block/my-block/data.js', + }, + ], +}; +``` + +#### Function notation + +This method is executed on run time so you could technically use this to renew data runtime. + +```javascript +module.exports = () => ({ + title: 'my-page', + meta: { + id: '', + status: '', + notes: '', + category: '', + }, + blocks: [ + { + name: 'my-block', + data: 'import!../app/component/block/my-block/data.js', + }, + ], +}); +``` + +> If you are planning on using JavaScript for all pages, it is recommended to remove the template +> file `{name_sc}.yaml` from the page directory: `build-tools/generator-templates/page/` and add a +> JavaScript variant: `{name_sc}.js` + +### Use custom variables in your data + +Using variables in your data can be really usefull if you have to modify certain parts multiple +times. Imagine a situation where your all data files contain absolute paths to images and you would +have to change this for any reason. + +**Starting point:** `http://www.some-domain.com/some/path/to/image.jpg` + +**Expected result:** `http://www.some-other-domain.com/some/path/to/image.jpg` + +You could do this by running find and replace all the files, but apply a find and replace on +everything is always risky. Another way of doing this would be defining commonly used variables in +the `src/data/_variables.yaml` file. These variables will be replaced in the data before the +component is rendered. + +**Example \_variables.yaml file** + +```yaml +assetBase: http://www.some-other-domain.com +``` + +After defining the variable you can use it in your data by wrapping it in the `${}` notation. + +**Example data.yaml file** + +```yaml +image: ${assetBase}/some/path/to/image.jpg +``` + +> ⚠️ Make sure the current `_src/data/_variables.yaml` file is not empty. + +### Updating the HTML boilerplate + +The HTML templates of your project can be found in the `build-tools/templates` folder. This folder +contains three files that are used for the different layouts. + +| File | Description | +| -------------------------------- | --------------------------------------------------- | +| `build-html-template.hbs` | This template is used with all bundled assets. | +| `build-html-template-standalone` | This template is used with page specific assets. | +| `devserver-index.html` | This template is used to run the development server | `` | + +### Excluding page files. + +If for any reason you would like to keep the page files but have them excluded from the Muban +project you can do this by prefixing them with an underscore. + +Example: `_my-page.yaml` + +### Using assets + +In Muban there are two types of assets the way you use them is a bit different. + +1. Static assets +2. Webpack assets. + +### Static assets + +Static assets are assets that will not be processed by webpack and they will be copied over to the +root of the `dist` folder after you do a production build. The way to access them is to use the +absolute path to access the asset. + +> ⚠️ Assets used in `CSS` will always be bundled, if you don't want this use inline styling. + +```handlebars +<img src="/image/path/to/my-image.jpg" alt="Some alt text" /> +``` + +```typescript +const image = new Image(); +image.src = '/image/path/to/my-image.jpg'; +``` + +> **Note:** It is recommended to create folders for the type of asset. This way you can keep your +> assets organised. + +#### Webpack assets + +As the name states Webpack assets are assets that are loaded through webpack, this means they will +automatically be bundled and versioned once you do a production build. This is usefull for assets +that are static and are not provided by the backend. Based on the type of assets they should be kept +in the correct directory in the `src/app` directory. So for example images are kept within an +`image` folder in the `app` folder. + +```scss +.some-selector { + background: url('../../../image/some-image.jpg'); +} + +// The same but using the seng-css image mixin. +.some-other-selector { + background: image('some-image.jpg'); +} +``` + +```typescript +const image = new Image(); +image.src = require('../../../image/some-image.jpg'); +``` + +## TypeScript + +### Ensure all components have been adopted + +When the application is initialised it runs through a +[lifecycle](./05-application.md#Application-lifecycle). This basically means that all your +components will be initialised from the deepest child up. This could mean that your child component +is initialised before the parent component. This could (in some cases) cause problems if you rely on +parent components. + +If you want to ensure that your component is fully adopted by the application you can add the +`adopted` method to the components TypeScript file and it will be called once the application is +fully mounted. + +```typescript +import AbstractComponent from '../AbstractComponent'; + +export default class MySmartComponent extends AbstractComponent { + static displayName: string = 'my-smart-component'; + + constructor(el: HTMLElement) { + super(el); + + // I'm ready but might not be adopted by the application! 😒 + } + + public adopted(): void { + // I'm finally adopted by the application! πŸŽ‰ + } + + public dispose() { + super.dispose(); + } +} +``` + +### Select child element/elements + +Selecting elements is usually done with the querySelector or the querySelectorAll methods, when +using the querySelector the result will be typed as a Node and if you use the querySelectorAll it +will be typed as a NodeList. In a lot of situations this is not the desired output since you will +most likely want to loop over the Nodes in a forEach loop or use HTMLElement specific properties or +eventListeners. + +This would mean casting the result or modifying the NodeList every time you use these selectors. To +avoid typing a lot of the same code all AbstractComponents have two public methods available for +selecting elements. + +```typescript +const element = this.getElement('.some-selector'); +const elements = this.getElements('.some-selector'); +``` + +By default the selector is based on the components root element, if you would like to use a +different element you can provide a second parameter that should be used as a containing element. + +```typescript +const element = this.getElements('.some-selector', document.body); +const elements = this.getElements('.some-selector', document.body); +``` + +The methods both return an array of `HTMLElements`, if you want to modify the return type you can +still provide a generic to overwrite the default. + +```typescript +const element = this.getElements<HTMLVideoElement>('.some-selector'); +const elements = this.getElements<HTMLVideoElement>('.some-selector'); +``` + +### Adding event listeners + +Attaching an event handler to a specific element is a very common thing in JavaScript. In Muban this +works the same as it would on any other plain JavaScript setup: + +```typescript +import AbstractComponent from '../AbstractComponent'; + +export default class MySmartComponent extends AbstractComponent { + static displayName: string = 'my-component'; + + private button: HTMLButtonElement; + + constructor(el: HTMLElement) { + super(el); + + // 1. Select the button in the DOM + this.button = this.getElement<HTMLButtonElement>('.my-button'); + // 2. Attach the event listener. + this.button.addEventListener('click', this.onButtonClick); + } + + private onButtonClick = () => { + // 3. Handler for the event. + }; + + public dispose() { + // 4. Remove the listener once the component is disposed. + this.button.removeEventListener('click', this.onButtonClick); + super.dispose(); + } +} +``` + +This covers basically all DOM interactions, but sometimes you would want to dispatch custom events +from your component. For example if your carousel component opens the next slide and you want to +notify a parent component about this. + +The easiest way to do this is to use the [seng-event](https://www.npmjs.com/package/seng-event) +module. Please read the extensive documentation to learn more about this! + +### Add a polyfill + +Sometimes you want to use functionality that not supported by all browsers, to do this you can add a +polyfill for that functionality. You can do this by installing the polyfill and adding it to the +`src/app/polyfills.js` file + +For example if you want to install a polyfill for the fetch you first install the dependency. + +```bash +yarn add whatwg-fetch +``` + +After installing the polyfill you add the import to the `polyfills.js` file + +```javascript +... + +// Add the polyfill for fetch at the bottom of the file +import 'whatwg-fetch'; +``` + +### Get data from data-attributes + +Providing data to your TypeScript file through data attributes is very easy and can be done by +adding it to the root element in your `.hbs` file. + +```handlebars +<div class="my-component" data-component="my-component" data-colors="#CC9933,#22AA88,#FF8822"> + Hi, I'm a component! πŸ‘‹ +</div> +``` + +After that you can access it by using the `dataset` object on the element. + +```typescript +import AbstractComponent from '../AbstractComponent'; + +export default class MySmartComponent extends AbstractComponent { + static displayName: string = 'my-component'; + + constructor(el: HTMLElement) { + super(el); + + // Get the data from the colors data attribute. + let { colors } = this.element.dataset; + + // Make sure the colors are available. + if(colors) { + // Split the values. + const colorValues = colors.split(','); + // Do something with the values. + console.log(colorValues); + } + } + + public dispose() { + super.dispose(); + } +} +``` + +### Get data from embedded json + +When needing quite a big payload on your page, you can embed it in a non-JS script tag, and parse it +with JS afterwards. + +```handlebars +<div class="my-component" data-component="my-component"> + <script type="text/json"> + { + "users": [ + { + "id": 0, + "name": "Adam Carter", + "email": "adam.carter@unilogic.com", + "dob": "1978", + "address": "83 Warner Street", + "city": "Boston" + }, + { + "id": 1, + "name": "Leanne Brier", + "email": "leanne.brier@connic.org", + "dob": "13/05/1987", + "address": "9 Coleman Avenue", + "city": "Toronto" + } + ], + "images": [ + "img0.png", + "img1.png", + "img2.png" + ], + "coordinates": { + "x": 35.12, + "y": -21.49 + }, + "price": "$59,395" + } + </script> +</div> +``` + +After that you can access it by using the [`getElement`](#Select-child-elementelements) method. + +```typescript +import AbstractComponent from '../AbstractComponent'; + +export default class MySmartComponent extends AbstractComponent { + static displayName: string = 'my-component'; + + constructor(el: HTMLElement) { + super(el); + + // 1. Get the script element from the DOM. + const scriptElement = this.getElement('script[type="text/json"]'); + // 2. Parse the contents as JSON. + const data = JSON.parse(scriptElement.innerHTML); + } + + public dispose() { + super.dispose(); + } +} +``` + +### Get data through a http-request + +If the data is too big, or too dynamic, and the backend has an API in place, we can also get more +data that way. + +For basic XHR calls, you should use the Fetch API. To support older browsers (IE), you should +include the fetch polyfill (whatwg-fetch). See the section on +[installing polyfills](#Add-a-polyfill) on how to do this. + +> πŸ”§ If you need more features, you could use [Axios](https://www.npmjs.com/package/axios). It's a +> wrapper around `fetch`, but with more configuration options. + +#### Getting HTML + +```typescript +fetch('/users.html') + .then(response => response.text()) + .then(body => { + document.body.innerHTML = body; + }); +``` + +#### Getting JSON + +```typescript +fetch('/users.json') + .then(response => response.json()) + .then(json => { + console.log('parsed json', json); + }) + .catch(ex => { + console.error('parsing failed', ex); + }); +``` + +#### Post a form + +```typescript +fetch('/users', { + method: 'POST', + body: new FormData(this.getElement('form')), +}); +``` + +#### Post JSON + +```typescript +fetch('/users', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: 'Hubot', + login: 'hubot', + }), +}); +``` + +#### File upload + +```typescript +const input = this.getElement('input[type="file"]'); + +const data = new FormData(); +data.append('file', input.files[0]); // 1. Add the file that you want to upload. +data.append('user', 'hubot'); // 2. Add any other data that is required. + +fetch('/avatars', { + method: 'POST', + body: data, +}); +``` + +### Update an entire section through a http-request + +#### The API returns HTML + +Sometimes, a section rendered by the backend has multiple options, and when switching options you +want new data for that section. If the backend cannot return JSON, they might return a HTML snippet +for that section. In that case we should: + +1. Fetch the new section. +2. Clean up the old HTML element (remove attached classes, for memory leaks). +3. Replace the HTML on the page. +4. Initialize new component instances for that section and nested components. + +```typescript +import { cleanElement, initComponents } from 'muban-core'; +import AbstractComponent from '../AbstractComponent'; + +export default class MySmartComponent extends AbstractComponent { + static displayName: string = 'my-component'; + + constructor(el: HTMLElement) { + super(el); + } + + private update() { + fetch('/api/section/some-slug') + .then(response => response.text()) + .then(body => { + // 1. dispose all created component instances. + cleanElement(this.element); + + // 2. insert the new HTML into a temp container to construct the DOM. + const temp = document.createElement('div'); + temp.innerHTML = body; + const newElement = temp.firstChild; + + // 3. replace the HTML on the page. + this.element.parentNode.replaceChild(newElement, this.element); + + // 4. initialize new components for the new element. + initComponents(<HTMLElement>newElement); + }); + } + + public dispose() { + super.dispose(); + } +} +``` + +Since this is a lot of typoing there is a utility to do the exact same thing. + +```typescript +import { updateElement } from 'muban-core'; +import AbstractComponent from '../AbstractComponent'; + +export default class MySmartComponent extends AbstractComponent { + static displayName: string = 'my-component'; + + constructor(el: HTMLElement) { + super(el); + } + + private update() { + fetch('/api/section/some-slug') + .then(response => response.text()) + .then(body => { + updateElement(this.element, body); + }); + } + + public dispose() { + super.dispose(); + } +} +``` + +> ⚠️ While this seams like a good option, keep in mind that the whole section will be reset into its +> default state, which could (depending on the contents of the section) be a bad experience, +> especially when dealing with animation/transitions. + +#### The API returns JSON + +This one might be a bit more work compared to just replacing HTML, but gives you way more control +over what happens on the page. The big benefit is that the state doesn't reset, allowing you to make +nice transitions while the new data is updated on the page. + +```typescript +import { updateElement } from 'muban-core'; +import AbstractComponent from '../AbstractComponent'; + +export default class MySmartComponent extends AbstractComponent { + static displayName: string = 'my-component'; + + constructor(el: HTMLElement) { + super(el); + } + + private update() { + fetch('/api/section/some-slug') + .then(response => response.json()) + .then(json => { + // 1. Update the text in the DOM. + this.getElement('.js-content).innerHTML = json.content. + }); + } + + public dispose() { + super.dispose(); + } +} +``` + +### Sort or filter items already in the DOM + +Sometimes the server renders a list of items on the page, but you have to sort or filter them +client-side, based on specific data in those items. Since we already have all the items and data on +the page, it's not that difficult. + +We can just query all the items, and retrieve the information we need to execute our logic, and add +them back to the page. + +```typescript +import { updateElement } from 'muban-core'; +import AbstractComponent from '../AbstractComponent'; + +interface ItemData { + element: HTMLElement; + title: string; + tags: Array<string>; +} + +export default class MySmartComponent extends AbstractComponent { + static displayName: string = 'my-component'; + + private itemData: Array<ItemData>; + + constructor(el: HTMLElement) { + super(el); + + this.initItems(); + this.updateItems(); + } + + private initItems(): void { + // 1. Get all DOM nodes. + const items = this.getItems('.item'); + + // 2. Convert to list of useful data to filter/sort on. + this.itemData = items.map(item => ({ + element: item, + title: item.querySelector('.title').textContent, + tags: Array.from(item.querySelectorAll('.tag')).map(tag => tag.textContent.toLowerCase()), + })); + } + + private updateItems(): void { + // 1. Empty the container. + const container = this.element.querySelector('.items'); + + while (container.firstChild) { + container.removeChild(container.firstChild); + } + + // 2. filter on any tags that contains an 's'. + let newItems = this.filterOnTags(this.itemData, 's'); + + // 3. Sort descending. + newItems = this.sortOnTitle(newItems, false); + + // 4. append new items to the container. + const fragment = document.createDocumentFragment(); + newItems.forEach(item => fragment.appendChild(item.element)); + container.appendChild(fragment); + } + + private sortOnTitle(itemData, ascending: boolean = false): Array<ItemData> { + // Sort items base on the title attribute. + return [...itemData].sort((a, b) => a.title.localeCompare(b.title) * (ascending ? 1 : -1)); + } + + private filterOnTags(itemData, filter: string): Array<ItemData> { + // Filter items based on the tags array. + return itemData.filter(item => item.tags.some(tag => tag.includes(filter.toLowerCase()))); + } + + public dispose() { + super.dispose(); + } +} +``` + +### Load more items to the page + +Sometimes the server renders the first page of items, but they want to have the second page to be +loaded and displayed from the client. If the server returns HTML, we can just re-use some of the +logic in our HTML example above. + +However, if the server returns JSON, we sort of want to re-use the markup of the existing items on +the page. We could build up the HTML ourselves from JavaScript, but that would mean the HTML lives +in two places, on the server and in JavaScript, and it will be hard to keep them in sync. + +There are two options we can choose from. + +#### Clone and update element + +For smaller items, we could just clone the first element of the list, and create a function that +updates all the data in that item, so we can append it to the DOM. + +```typescript +import AbstractComponent from '../AbstractComponent'; + +export default class MySmartComponent extends AbstractComponent { + static displayName: string = 'my-component'; + + private template:HTMLElement; + private fragment:DocumentFragment; + + constructor(el: HTMLElement) { + super(el); + + this.template = this.getElement('.item'); + this.fragment = document.createDocumentFragment(); + + this.addNewItems([{title: 'foo'}, {title: 'bar'}]) + } + + private addNewItems(items:Array<{title:string;>):void { + // 1. Clone template, update data, and add to fragment + items.forEach(item => { + const clone = template.cloneNode(true); + clone.querySelector('.title').textContent = item.title; + clone.querySelector('.description').textContent = item.description; + fragment.appendChild(clone); + }); + + // 2. Add fragment to the list. + this.element.querySelector('.list').appendChild(fragment); + } + + + public dispose() { + super.dispose(); + } +} +``` + +#### Use a handlebars template + +If we already have a `.hbs` template, we can use this in JavaScript as well. If we import the .hbs +file, it will be pre-compiled by webpack to a JavaScript function. This function accepts 1 +parameter, the data, and returns the HTML string. + +```typescript +import { initComponents } from 'muban-core'; +import itemTemplate from '../../general/item/item.hbs?include'; +import AbstractComponent from '../AbstractComponent'; + +export default class MySmartComponent extends AbstractComponent { + static displayName: string = 'my-component'; + + private container:HTMLElement; + + constructor(el: HTMLElement) { + super(el); + + this.container = this.getElement('.list'); + + this.addNewItems([{title: 'foo'}, {title: 'bar'}]) + } + + private addNewItems(items:Array<{title:string;>):void { + items.forEach(item => { + // 1. Create the element based on the handlebars template. + const content = itemTemplate(item) + // 2. Append to the container. + this.container.appendChild(content); + }); + + // 3. If the new item has any logicy you can optionally call the + // init components method to make them interactive. + initComponents(this.container); + } + + + public dispose() { + super.dispose(); + } +} +``` + +##### Data util methods + +Even though the previous example is quite simple, it still requires a lot of typing to get it done. +To do this more efficient there are two render helpers available in Muban. + +1. [renderItem](./09-api-reference.md#renderItem) +2. [renderItems](./09-api-reference.md#renderItems) + +```typescript +import { renderItem, renderItems } from 'muban-core/lib/utils/dataUtils'; +import itemTemplate from '../../general/item/item.hbs?include'; +import AbstractComponent from '../AbstractComponent'; + +export default class MySmartComponent extends AbstractComponent { + static displayName: string = 'my-component'; + + private container: HTMLElement; + + constructor(el: HTMLElement) { + super(el); + + this.container = this.getElement('.list'); + + // 1. This will replace the current item in the container and initialise it. + renderItem(this.container, itemTemplate, { title: 'foo' }); + // 2. This will append a new item to the container and only initialise that one. + renderItem(this.container, itemTemplate, { title: 'foo' }, true); + // 3. This will replace an entire list of items and initialise all of them. + renderItems(this.container, itemTemplate, [{ title: 'foo' }, { title: 'bar' }]); + // 4. This will append a new list to the container and ony initialise the new ones. + renderItems(this.container, itemTemplate, [{ title: 'foo' }, { title: 'bar' }, true]); + } + + public dispose() { + super.dispose(); + } +} +``` + +#### Use a knockout template + +This option works best when only used on the client, but when having server-rendered items in the +DOM you would first need to convert them to data to properly render them. + +```handlebars +<!-- +List item template, keep in HTML since it will be used by javascript. +The HTML in the script-template is similar to the html in the handlebars list below. +The handlebars template will be rendered on the server, and the script-template will +be used by knockout to render the list client-side (when new data comes in). +--> +<script type="text/html" id="item-template"> + <h3 class="title" data-bind="text: title"></h3> + <p class="description" data-bind="html: description"></p> + <div class="tags"> + <!-- ko foreach: tags --> + <span class="tag" data-bind="text: $data"></span> + <!-- /ko --> + </div> +</script> + +<section class="items"> + {{#each items}} + <article class="item"> + <h3 class="title">{{title}}</h3> + <p class="description">{{description}}</p> + <div class="tags"> + {{#each tags}} + <span class="tag">{{this}}</span> + {{/each}} + </div> + </article> + {{/each}} +</section> +``` + +```typescript +import ko from 'knockout'; +import AbstractComponent from '../AbstractComponent'; + +export default class MySmartComponent extends AbstractComponent { + static displayName: string = 'my-component'; + + constructor(el: HTMLElement) { + super(el); + + // 1. transform old items to data get all DOM nodes + const items = this.getElements('.item'); + + // Convert to list of useful data to filter/sort on + const oldData = items.map(item => ({ + title: item.querySelector('.title').textContent, + description: item.querySelector('.description').innerHTML, + tags: Array.from(item.querySelectorAll('.tag')).map(tag => tag.textContent), + })); + + // 2. create observable and set old data + const itemData = ko.observableArray(oldData); + + // 3. apply bindings to list, this will re-render the items + ko.applyBindingsToNode(this.element.querySelector('.items'), { + template: { name: 'item-template', foreach: itemData }, + }); + + // 4. add new data to the observable or do any other funky stuff to the array, like sorting/filtering + itemData.push(...newData); + } + + public dispose() { + super.dispose(); + } +} +``` + +> ⚠️ Keep in mind that when you include knockout into your project the distribution bundle size will +> increase a lot. + +Even though the previous example is quite simple, it still requires a lot of typing to get it done. +To do this more efficient there is a [util available](./09-api-reference.md#initListBinding) in +Muban to do this for you. + +```typescript +import AbstractComponent from '../AbstractComponent'; + +export default class MySmartComponent extends AbstractComponent { + static displayName: string = 'my-component'; + + constructor(el: HTMLElement) { + super(el); + + const itemData = initListBinding(this.getElements('.items'), 'item-template', { + query: '.item', + data: { + title: '.title', + description: { query: '.description', htm: true }, + tags: { query: '.tag', list: true }, + }, + }); + // 4. add new data to the observable or do any other funky stuff to the array, like sorting/filtering + itemData.push(...newData); + } + + public dispose() { + super.dispose(); + } +} +``` + +## Handlebars + +### Render a component + +Rendering a component can be done by using the +[handlebars partial call syntax](https://handlebarsjs.com/partials.html). + +```handlebars +{{> general/my-component }} +``` + +> ⚠️ Just make sure the path is relative to the `src/app/component` directory + +### Pass data to your component + +Providing data to a components can be done by adding parameters. + +```handlebars +{{> general/my-component parameter="value" another-parameter="another-value"}} +``` + +### Render data in your component + +To render out provided data you can use handlebars expressions, the most basic version can be seen +in the following example. + +```handlebars +<div class="my-component"> + <h1>{{parameter}}</h2> +</div> +``` + +> **Note:** If you want more detailed instructions and examples on data rendering please have a look +> at the [handlebars documentation](https://handlebarsjs.com/expressions.html). + +### Render data as HTML in your component + +By default handlebars escapes all inlined HTML tags, if you want to disable this logic you can use +the `triple-stash` notation. + +```handlebars +<div class="my-component"> + <h1>{{{parameter}}}</h2> +</div> +``` + +### Dynamically render components + +If you want to dynamically render out child components within a component you can use the +[lookup helper](https://handlebarsjs.com/partials.html) from handlebars. + +For example if you have block data that dynamically renders out more blocks. + +```yaml +title: My awesome block with child components +childComponents: + - name: 'my-child-component' + data: "πŸ₯‡ I'm the first data." + - name: 'my-child-component' + data: "πŸ₯ˆI'm the second data." + - name: 'my-child-component' + data: "πŸ₯‰ I'm the third data." +``` + +```handlebars +<div> + <h1>{{title}}</h1> + {{#each childComponents}} + {{> (lookup . 'name') data }} + {{/each}} +</div> +``` + +> ⚠️ This only works for components in the `src/app/component/block` folder. + +### Using icons + +SVG icons are a big part of websites nowadays, Muban has a default component that can be used to +render them. To add an SVG icon to your project simply add the `.svg` file in the `src/app/svg` +folder and use the name without the extension of the file to reference it. + +```handlebars +{{> general/icon name="name-of-svg-file" }} +``` + +### Create a custom helper + +Handlebars comes with a set of built-in helpers, +[documentation](https://handlebarsjs.com/builtin_helpers.html) on these can be found on their +website. By default Muban already adds one helper that can be used in combintation with the `if` +helper to do more conditional rendering. If you want to add more custom helpers you can add them in +the `build-tools/handlebars-helpers` folder. + +#### A very basic example of a helper that reverses a word could look like this. + +```javascript +// file: reverse.js +module.exports = function(value) { + return value + .split('') + .reverse() + .join(); +}; +``` + +> **Note 1:** You do not need to register them using the `registerHelper` method this is all handled +> by webpack. + +> **Note 2:** The helper will take the name of the file that it's in! + +```handlebars +<p>We need to reverse the word "palindrome": {{reverse "palindrome"}}.</p> +``` + +## Knockout + +> ⚠️ Keep in mind that when you include knockout into your project the distribution bundle size will +> increase a lot. + +### Apply bindings to a node. + +This example will show you how to bind a knockout observable to an element in the DOM. If you want a +more detailed explanation on knockout in Muban please have a look at the +[page bout knockout](./08-knockout.md). + +```handlebars +<div class="my-component" data-component="my-component"> + <button>I'm the initial text</p> +</div> +``` + +```typescript +import ko from 'knockout'; +import AbstractComponent from '../AbstractComponent'; + +export default class MySmartComponent extends AbstractComponent { + static displayName: string = 'my-component'; + + private buttonActive = ko.observable(false); + + constructor(el: HTMLElement) { + super(el); + + // 1. Bind the value to the element + ko.applyBindingsToNode(this.getELement('p'), { + css: { isActive: this.buttonActive }, + }); + + // 2. Change the value and see the class change + this.searchOpened(true); + } + + public dispose() { + super.dispose(); + } +} +``` + +### Apply bindings to the entire component + +```handlebars +<div class="my-component" data-component="my-component"> + <button data-bind="text: buttonText"></p> +</div> +``` + +```typescript +import ko from 'knockout'; +import AbstractComponent from '../AbstractComponent'; + +export default class MySmartComponent extends AbstractComponent { + static displayName: string = 'my-component'; + + private buttonText = ko.observable("I'm the initial text"); + + constructor(el: HTMLElement) { + super(el); + + // 1. Apply the bindings to the component + ko.applyBindings(this, this.element); + + // 2. Update the button text + this.buttonText("I'm the modified text"); + } + + public dispose() { + super.dispose(); + } +} +``` + +## Seng-generator + +### Create a custom template + +The seng-generator CLI uses templates to generate the components and pages that we need to create a +website. These templates are stored in the `build-tools/generator-template` folder. If you add a new +folder there you the CLI will automatically pick this up an let's you use it when you run the +wizard. You can read more about the templates in the +[seng-generator documentation](https://www.npmjs.com/package/seng-generator). diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..64f64a48 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,154 @@ +# Table of contents + +1. [Introduction](./01-introduction.md) + 1. [Background](./01-introduction.md#Background) + 2. [Challenges](./01-introduction.md#Challenges) + 3. [What we came up with](./01-introduction.md#What-we-came-up-with) + 1. [Webpack](./01-introduction.md#Webpack) + 2. [Template](./01-introduction.md#Template) + 3. [Application](./01-introduction.md#Application) + 4. [Building](./01-introduction.md#Building) + 5. [Preview components](./01-introduction.md#Preview-components) + 6. [Integration](./01-introduction.md#Integration) + 4. [Closing words](./01-introduction.md#Closing-words) +2. [Setup guide](./02-setup-guide.md) + 1. [Preparations](./02-setup-guide.md#Preparations) + 1. [Compatability note](./02-setup-guide.md#Compatability-note) + 2. [Release notes](./02-setup-guide.md#Release-notes) + 3. [Get familiar with the technologies](./02-setup-guide.md#Get-familiar-with-the-core-technologies) + 1. [Handlebars](./02-setup-guide.md#Handlebars) + 2. [SCSS](./02-setup-guide.md#SCSS) + 3. [TypeScript](./02-setup-guide.md#TypeScript) + 4. [Knockout](./02-setup-guide.md#Knockout) + 4. [Setup your environment](./02-setup-guide.md#Setup-your-environment) + 1. [Install Node.js](./02-setup-guide.md#Install-Nodejs) + 2. [Install Yarn](./02-setup-guide.md#Install-Yarn) + 3. [Install the seng-generator CLI](./02-setup-guide.md#Install-the-seng-generator-CLI) + 2. [Getting started](./02-setup-guide.md#Getting-started) + 1. [Setup the project](./02-setup-guide.md#Setup-the-project) + 1. [Download source code](./02-setup-guide.md#Download-source-code) + 2. [Install dependencies](./02-setup-guide.md#Install-depenedencies) + 3. [Running the development server](./02-setup-guide.md#Running-the-development-server) + 4. [Running the Storybook server](./02-setup-guide.md#Running-the-Storybook-server) + 5. [Clean boilerplate code](./02-setup-guide.md#Clean-boilerplate-code) + 6. [Clean Storybook](./02-setup-guide.md#Clean-Storybook) + 2. [Create a distribution build](./02-setup-guide.md#Create-a-distribution-build) + 1. [Preview your distribution build](./02-setup-guide.md#Preview-your-distribution-build) + 2. [Analyze your distribution build](./02-setup-guide.md#Analyze-your-distribution-build) + 3. [Code quaility](./02-setup-guide.md#Code-quality) + 1. [EditorConfig](./02-setup-guide.md#EditorConfig) + 2. [Prettier](./02-setup-guide.md#Prettier) + 3. [Linting](./02-setup-guide.md#Linting) + 1. [esLint](./02-setup-guide.md#esLint) + 2. [tsLint](./02-setup-guide.md#tsLint) + 3. [StyleLint](./02-setup-guide.md#StyleLint) + 4. [Pre-commit hook](./02-setup-guide.md#Pre-commit-hook) +3. [Component](./03-component.md) + 1. [Types](./03-component.md#Types) + 1. [Component](./03-component.md#Component) + 1. [Handlebars](./03-component.md#Handlebars-Component) + 2. [SCSS](./03-component.md#SCSS-Component) + 3. [Preset](./03-component.md#Preset-Component) + 2. [Smart-component](./03-component.md#Smart-component) + 1. [Handlebars](./03-component.md#Smart-component-Smart-component) + 2. [SCSS](./03-component.md#SCSS-Smart-component) + 3. [TypeScript](./03-component.md#TypeScript-Smart-component) + 4. [Preset](./03-component.md#Preset-Smart-component) + 3. [Block](./03-component.md#Block) + 1. [Handlebars](./03-component.md#Handlebars-Block) + 2. [SCSS](./03-component.md#SCSS-[Block]) + 3. [TypeScript](./03-component.md#TypeScript-Block) + 4. [Data](./03-component.md#Data-[Block]) + 1. [Local data](./03-component.md#Local-data-Block) + 2. [Referenced data](./03-component.md#Imported-data-Block) + 5. [Preset](./03-component.md#Preset-Block) +4. [Page](./04-page.md) + 1. [Title](./04-page.md#Title) + 2. [Meta](./04-page.md#Meta) + 3. [Blocks](./04-page.md#Blocks) + 1. [Name](./04-page.md#Name) + 2. [Data](./04-page.md#Data) + 1. [Page data](./04-page.md#Page-data) + 2. [Imported data](./04-page.md#Imported-data) +5. [Application](./05-application.md) + 1. [Bootstrapping](./05-application.md#Bootstrapping) + 2. [Development bootstrapping](./05-application.md#Development-bootstrapping) + 3. [Production bootstrapping](./05-application.md#Production-bootstrapping) + 4. [Application Lifecycle](./05-application.md#Application-Lifecycle) +6. [Dynamic data](./06-dynamic-data.md) + 1. [Data provider](./06-dynamic-data.md#Data-provider) + 2. [Data templates](./06-dynamic-data.md#Data-templates) +7. [Handlebars](./07-handlebars.md) + 1. [Partials](./07-handlebars.md#Partials) + 2. [Helpers](./07-handlebars.md#Helpers) + 1. [Condition](./07-handlebars.md#Condition) + 2. [Custom helpers](./07-handlebars.md#Custom-helpers) +8. [Knockout](./08-knockout.md) + 1. [API Reference](./08-knockout.md#API-reference) + 1. [applyBindingsToNode](./08-knockout.md#applyBindingsToNode) + 2. [applyBindingAccessorsToNode](./08-knockout.md#applyBindingAccessorsToNode) +9. [API Reference](./09-api-reference.md) + 1. [Muban](./09-api-reference.md#Muban) + 1. [initComponents](./09-api-reference.md#initComponents) + 2. [cleanElement](./09-api-reference.md#cleanElement) + 3. [updateElement](./09-api-reference.md#updateElement) + 4. [getComponentForElement](./09-api-reference.md#getComponentForElement) + 2. [Handlebars](./09-api-reference.md#Handlebars) + 1. [renderItem](./09-api-reference.md#renderItem) + 2. [renderItems](./09-api-reference.md#renderItems) + 3. [Knockout](./09-api-reference.md#Knockout) + 1. [initTextBinding](./09-api-reference.md#initTextBinding) + 2. [initListBinding](./09-api-reference.md#initListBinding) + 4. [Bootstrap](./09-api-reference.md#Bootstrap) + 1. [Development](./09-api-reference.md#Development) + 2. [Distribution](./09-api-reference.md#Distribution) +10. [Storybook](./10-storybook.md) + 1. [Example](./10-storybook.md#Example) + 2. [Customize](./10-storybook.md#Customize) + 3. [Configuration](./10-storybook.md#Configuration) +11. [Transitions and animations](./11-transitions-and-animations.md) +12. [Dist implementation guide](./12-dist-implementation-guide.md) +13. [Guides](./13-guides.md) + 1. [Muban](./13-guides.md#Muban) + 1. [Create a component](./13-guides.md#Create-a-component) + 2. [Create a smart-component](./13-guides.md#Create-a-smart-component) + 3. [Create a block](./13-guides.md#Create-a-block) + 4. [Create a page](./13-guides.md#Create-a-page) + 5. [Do not use the default index template](./13-guides.md#Do-not-use-the-default-index-template) + 6. [Using JSON for data files](./13-guides.md#Using-json-for-data-files) + 7. [Using JavaScript for data files](./13-guides.md#Using-javascript-for-data-files) + 8. [Using JSON for page files](./13-guides.md#Using-json-for-page-files) + 9. [Using JavaScript for page files](./13-guides.md#Using-javascript-for-page-files) + 10. [Use custom variables in your data](./13-guides.md#Use-custom-variables-in-your-data) + 11. [Updating the HTML boilerplate](./13-guides.md#Updating-the-HTML-boilerplate) + 12. [Using assets](./13-guides.md#Using-assets) + 2. [TypeScript](./13-guides.md#TypeScript) + 1. [Ensure all components have been initalised](./13-guides.md#Ensure-all-components-have-been-initialised) + 2. [Select child element/elements](./13-guides.md#Select-child-element-elements) + 3. [Adding event listeners](./13-guides.md#Adding-event-listeners) + 4. [Add a polyfill](./13-guides.md#Add-a-polyfill) + 5. [Get data from data-attributes](./13-guides.md#Get-data-from-data-attributes) + 6. [Get data from embedded json](./13-guides.md#Get-data-from-embedded-json) + 7. [Get data through a http-request](./13-guides.md#Get-data-through-a-http-request) + 8. [Update an entire section through a http-request](./13-guides.md#Update-an-entire-section-through-a-http-request) + 1. [The API returns HTML](./13-guides.md#The-API-returns-HTML) + 2. [The API returns JSON](./13-guides.md#The-API-returns-JSON) + 9. [Sort or filter items already in the DOM.](./13-guides.md#Sort-or-filter-items-already-in-the-DOM) + 10. [Load more items to the page](./13-guides.md#Load-more-items-on-the-page) + 1. [Use an existing DOM element](./13-guides.md#Use-an-existing-DOM-element) + 2. [Use a handlebars template](./13-guides.md#Use-a-handlebars-template) + 3. [Use a knockout template](./13-guides.md#Use-a-knockout-template) + 3. [Handlebars](./13-guides.md#Handlebars) + 1. [Render data in your component](./13-guides.md#Render-data-in-your-component) + 2. [Render data as HTML in your component](./13-guides.md#Render-data-as-HTML-in-your-comonent) + 3. [Render a component](./13-guides.md#Render-a-component) + 4. [Dynamically render components](./13-guides.md#Dynamically-render-components) + 5. [Using icons](./13-guides.md#Using-icons) + 6. [Create a custom helper](./13-guides.md#Create-a-custom-helper) + 4. [Knockout](./13-guides.md#Knockout) + 1. [Apply bindings to a node](./13-guides.md#Apply-bindings-to-a-node) + 2. [Apply bindings to the entire component](./13-guides.md#Apply-bindings-to-the-entire-element) + 5. [Webstorm](./13-guides.md#Webstorm) + 1. [Prettier shortcut](./13-guides.md#Prettier-shortcut) + 6. [Seng-generator](./13-guides.md#Seng-generator) + 1. [Create a custom template](./13-guides.md#Create-a-custom-template) diff --git a/docs/application.md b/docs/application.md deleted file mode 100644 index 6187a01e..00000000 --- a/docs/application.md +++ /dev/null @@ -1,9 +0,0 @@ -# Application - -TODO - -* Bootstrapping the app and rendering pages -* App component -* Difference between dev and dist mode -* global state / events -- `model.ts` + Knockout observables -* [data files](./data-files.md) diff --git a/docs/code-quality.md b/docs/code-quality.md deleted file mode 100644 index aee9ce80..00000000 --- a/docs/code-quality.md +++ /dev/null @@ -1,107 +0,0 @@ -# Code Quality Tools - -## EditorConfig - -We use [EditorConfig](http://editorconfig.org/) define and maintain consistent coding styles between -different editors and IDEs. Please make sure to enable/install the EditorConfig plugin in your IDE -of choice. - -* indentation of `2 spaces` -* use `lf` line endings -* use `utf-8` charset -* trim trailing whitespaces -* add en empty newline at the end of each file - -## Prettier - -We use [Prettier](https://github.com/prettier/prettier) to format all our code. This is enabled for -`js`, `ts`, `scss` and `yaml` files. The corresponding linters are configured to adhere to the rules -from prettier (so they won't conflict), and linting errors should only occur for non-stylistic -errors. - -Prettier is configured for: - -* indentation of `2 spaces` -* the use of `semicolons` -* the use of `single quotes` -* a tab width of `100` - -Prettier is configured to run on the `pre-commit` using `husky` and `lint-staged` hook, and can also -be manually invoked by: - -``` -yarn prettify -``` - -Settings can be changed in `.prettierrc` and files can be ignored in `.prettierignore`. - -Please check the [editor integration](https://github.com/prettier/prettier#editor-integration) -section of the Prettier readme to enable running Prettier within your IDE of choice. - -Keep in mind, that if you choose to automatically run Prettier when saving your file, Webpack will -run twice (on your manual save, and when prettier reformats your code), slowing down the developer -experience. - -## Linting - -The below tools are used to lint our code. They can be all executed with: - -``` -yarn lint -``` - -### eslint - -We use [eslint](https://eslint.org/) lint our JavaScript code. It's configured for use with -Prettier, and set up to understand Webpack imports. It follows the -[AirBnB styleguide](https://github.com/airbnb/javascript) with some super small tweaks. - -Settings can be changed in `.eslintrc.js` and files can be ignored in `.eslintignore`. - -``` -yarn lint:js -``` - -### tslint - -We use [tslint](https://palantir.github.io/tslint/) lint our TypeScript code. It's configured for -use with Prettier. It follows the [AirBnB styleguide](https://github.com/airbnb/javascript) with -some super small tweaks. It's consistent with the eslint settings. - -Settings can be changed in `.tslintrc.js`. - -``` -yarn lint:ts -``` - -### stylelint - -We use [stylelint](https://github.com/stylelint/stylelint) lint our SCSS code. It's configured for -use with Prettier and uses -[stylelint-config-recommended-scss](https://github.com/kristerkari/stylelint-config-recommended-scss) -without any modifications. - -Settings can be changed in `.stylelintrc` and files can be ignored in `.stylelintignore`. - -``` -yarn lint:css -``` - -## pre-commit hook - -To make sure all code checked in to git, we use [husky](https://github.com/typicode/husky) to -configure git commit hooks. The `pre-commit` hook is configured to run -[lint-staged](https://github.com/okonet/lint-staged) on the files that are about to be committed. - -It will run all linters on the appropriate files, and allows Prettier to reformat any code before -doing the actual commit. - -You can also run the command manually: - -``` -yarn precommit -``` - -Keep in mind that some lint errors might pop up in files that are not updated by changing other -things (like imports that are not correct after renaming a file), so it's good practice to run -`yarn lint` once in while to verify the complete codebase is valid. diff --git a/docs/components.md b/docs/components.md deleted file mode 100644 index 33039152..00000000 --- a/docs/components.md +++ /dev/null @@ -1,189 +0,0 @@ -# Components - -A muban project consists of components. Everything is a component. - -At minimal, a component is a handlebars template file. Often it also contains a stylesheet file. To -make things interactive it can also contain a script file. - -## Examples - -A simple component could look like this: - -``` -<button class="component-button">{{text}}</button> -``` - -To make the button look nice, and handle some logic, you could link to a style and script file: - -``` -<link rel="stylesheet" href="./button.scss"> -<script src="./Button.ts"></script> - -<button data-component="button">{{text}}</button> -``` - -``` -[data-component="button"] { - border: 1px solid #ddd; - background-color: #eee; - padding: 4px 8px; - color: #333; - cursor: pointer; - - &:hover { - color: #000; - border-color: #bbb; - } -} -``` - -``` -import AbstractComponent from "app/component/AbstractComponent"; - -export default class Button extends AbstractComponent { - // this should match the 'data-component' attribute value on the HTML element - static displayName:string = 'button'; - - private btn:HTMLElement; - - // the html element from the handlebars template is passed - constructor(el:HTMLElement) { - super(el); - - // when selecting DOM elements, always search from 'this.element' - this.btn = this.element.querySelector('button'); - this.btn.addEventListener('click', this.handleButtonClick); - } - - private handleButtonClick = () => { - console.log('click'); - }; - - // only called in development when hot reloading a component - public dispose() { - this.btn.removeEventListener('click', this.handleButtonClick); - this.btn = null; - - super.dispose(); - } -} -``` - -### Data attributes - -Component data attributes are available in a component and can be accessed using data object on the -component. The data attributes are stored as camelCased keys in the data object, without the data- -prefix. So attribute `data-slide-interval` can be referenced within a component using -`this.data.slideInterval`. - -The component data object values are stored as strings. Validating and parsing this data is up to -the component author. - -An example of using a data attribute - -``` -<script src="./Carousel.ts"></script> -<div data-component="carousel" data-slide-interval="2000">...</div> -``` - -``` -import AbstractComponent from "app/component/AbstractComponent"; - -export default class Carousel extends AbstractComponent { - static displayName:string = 'carousel'; - - private slideInterval:number; - - constructor(el:HTMLElement) { - super(el); - - const interval = (this.data.slideInterval && parseInt(this.data.slideInterval, 10)) || 5000; - - this.slideInterval = setInterval(() => { - // Some code to slide your carousel - }, interval); - } - - public dispose() { - clearInterval(this.slideInterval); - this.slideInterval = null; - - super.dispose(); - } -} -``` - -### Element selecting - -Selecting elements is usually done with the `querySelector` or the `querySelectorAll` methods, when -using the querySelector the result will be typed as a `Node` and if you use the querySelectorAll it -will be typed as a `NodeList`. In a lot of situations this is not the desired output since you will -most likely want to loop over the `Nodes` in a forEach loop or use `HTMLElement` specific properties -or eventListeners. - -This would mean casting the result or modifying the NodeList every time you use these selectors. To -avoid typing a lot of the same code all `AbstractComponents` have two public methods available for -selecting elements. - -```typescript -const element = this.getElement('.some-selector'); -const elements = this.getElements('.some-selector'); -``` - -By default the selector is based on the components root element, if you would like to use a -different element you can provide a second parameter that should be used as a containing element. - -```typescript -const element = this.getElements('.some-selector', document.body); -const elements = this.getElements('.some-selector', document.body); -``` - -## Scaffolding - -With seng-generator you're able to create pages, blocks and components with the CLI. The -seng-generator needs to be installed globally - -``` -npm i -g seng-generator -``` - -The easiest way to use it is by using the wizard - -``` -sg wizard -``` - -Starts a wizard to create a component, page or block. - -``` -sg block foo -``` - -Generates a block with the name of foo. This can be done for pages and components too. - -``` -sg page foo -v blocks=header,footer -``` - -Generates a page with the name of foo and adds the blocks header and footer. - -## Lifecycle - -When a component file is loaded, it will register itself. When the app boots, all registered -components will be constructed: - -* Loop trough all registered component -* Find DOM elements that match the components `displayName` - * this is set statically on the component class - * this is set using the `data-component` attribute on the HTML tag -* Sort found DOM elements based on their nesting depth - * This will make sure child components are constructed first -* Construct the component class and pass the DOM element to the constructor -* Store a reference to the instance and the DOM element -* The above allows any component constructor to select its child components DOM element and look up - its class instance to communicate with. This can be used to listen for events, read properties or - call functions. The `getComponentForElement(element:HTMLElement):AbstractComponent` function can - be used for that. -* When running the dev server, and you change your component script file, it will be hot-reloaded by - webpack. Before constructing an instance from the updated file, `dispose()` will be called on the - old instance, so any references or event listeners to the DOM elements can be removed. diff --git a/docs/data-files.md b/docs/data-files.md deleted file mode 100644 index 8023684a..00000000 --- a/docs/data-files.md +++ /dev/null @@ -1,162 +0,0 @@ -# Data - -The data files, located at `src/data/`, provide the content and the structure of your pages. -Every page has it's own data file, which can be either `yaml`, `json` or `js`. -Since `yaml` is less verbose, and can better handle multiline content, we've chosen -that as the default. `js` can be useful for more dynamic data, when creating loops or needing -something from `process.env`. - -To render a page, the `src/app/component/layout/app/app.hbs` is provided with the data for the page, -and will decide how it will be rendered. -By default, a page will render a set of blocks, that are different for each page. -Blocks can be reused across different pages. Each block can be made up from multiple -components. Your block and component `hbs` files use and pass down the data provided -in the data files. - -## Using data - -The data used in Muban is used as a static mock representation of the eventual backend data that -will be used to render the templates on the server. Because of that it's good to communicate -with backed to set up a data structure that makes sense. - -The data will only be used to render HTML, and cannot be used by JavaScript directly. -For JS to use any data, it has to be rendered in the HTML, either by a `data-` attribute, -or by rendering a `plain/text` script tag and fetching the (json) content via `innerHTML`. - -## import - -Because it's a lot of work to provide all data for the complete page directly in the page data files, -and because you'll most likely reuse blocks and components across multiple pages, -we've made it possible to import one data file into another. This way you can keep your -data nicely together with your component. Importing can look like this: - -**home.yaml** -```yaml -blocks: - - name: "paragraph" - data: "import!../app/component/block/paragraph/data.yaml" - - - name: "two-col" - data: "import!../app/component/block/two-col/data.json" -``` - -**paragraph/data.yaml** -```yaml -title: What is Lorem Ipsum? - -content: > - industry. Lorem Ipsum has been the industry's standard dummy text ever since - the 1500s, when an unknown printer took a galley of type and scrambled it to - make a type specimen book. - -ctaReadMore: 'read more...' -``` - -Imports can be done by adding `"import!path-to-file"` on the place where a 'value' -should be located. The object or array in the data file will be inlined at the -location of the import statement. - -Importing works for `yaml`, `json` and `js`, can be nested without recursion, and -the path is relative from the file the import statement is located in. - -For more information, check out the [ThaNarie/json-import-loader](https://github.com/ThaNarie/json-import-loader) -documentation that makes this possible. - -### Dynamic data - -You can also use JS files for your data from the `import` statement. Your JS file -can either export an object, or a function that returns an object. You can add dynamic -code to create mock data, a large collection with little code, or use environment variables -to specify certain values. - -Example: https://github.com/ThaNarie/json-import-loader/blob/master/test/_fixtures/e.js - -## dev & dist - -During **dev**, the data files are required by a webpack context, and passed to the included -handlebar templates (that are also required by webpack and precompiled), so that they -can render the resulting HTML. - -If either the template or the data file is changed, the complete page will be re-rendered -without reloading. - -During *build* the templates are precompiled to a `partials.js` file and used by the -build script to render a HTML page for each data file. These files, together with the -JS, CSS and other assets, can be uploaded to a preview server. - -## Global data - -If you need some specific data on each page, that is not a block or component, you can -create a data file in the data folder, and import it at the top level on each page. - -_**Tip**: You can change the seng-generator template to make this work for all new pages._ - -_**Note**: data files that should not end up as a page, should start with a `_`, so they are skipped._ - -```yaml -title: Home - -extra: "import!_extra.yaml" - -meta: - id: '' # can be number or string, used for ordering - status: '' # dev, qa, feedback, done - notes: '' # add some information about the page - category: '' # to group pages in the overview -``` - -In your templates, you can access any page-level data using the `@root` context: -```hbs -<div data-component="paragraph"> - <h2>{{title}} {{@root.extra.foo}}</h2> -</div> -``` - -**Note:** See below for a slightly better approach for some cases making use of `_variables.yaml`. -The variables in that file are always available in all your pages and hbs files, so they won't have -to be imported manually in each page file. - -## Modify data & variables - -If you want to have a bit more control over your data, you are able to change the data -right before it gets rendered. During dev this can be done using the `onData` callback -that is passed to the dev `bootstrap` as an option. During build you can change the data -in `build-tools/script/util/getPages.js` for each page before rendering it to HTML. - -By default, Muban already allows you to use variables in your data files, that will be -replaced by the contents of the `src/data/_variables.yaml`. This can be useful for -files you are linking to the `static` folder or are fetching from a `CDN`, where you want -to change the server location easily (or dynamically per build). - -Variables can be used like this: -```yaml -title: Foobar -image: "${cdnPath}/images.foo.png" -``` - -This is implemented by default: -```js -const replaceVariables = require('../data/_variables.yaml'); - -const app = bootstrap(appElement, { - ...otherStuff, - onData: (data) => ({ - // include the _variables content by default in all page files - ...replaceVariables, - ...JSON.parse( - Object.keys(replaceVariables).reduce( - (data, varName) => - // replace ${foo} occurrences in the data to be rendered. - data.replace(new RegExp(`\\$\{${varName}}`, 'g'), () => replaceVariables[varName]), - JSON.stringify(data), - ), - ), - }), -}); -``` - -**Note**: data files that should not end up as a page, should start with a `_`, so they are skipped. - -**Note**: You can change the extension of the variable file, and replace the require in -`src/app/bootstrap.dev.ts` and `build-tools/script/util/getPages.js`. - diff --git a/docs/dynamic-data-examples.md b/docs/dynamic-data-examples.md deleted file mode 100644 index d4903390..00000000 --- a/docs/dynamic-data-examples.md +++ /dev/null @@ -1,369 +0,0 @@ -# Real world examples - -_Note: sometimes the examples below first show the 'manual' way to execute certain tasks, and after -that the utils that are available in Muban. This is to understand what's done under the hood._ - -## 1. Backend returns HTML for an updated section - -Sometimes, a section rendered by the backend has multiple options, and when switching options you -want new data for that section. If the backend cannot return JSON, they might return a HTML snippet -for that section. In that case we should: - -1. fetch the new section -2. clean up the old HTML element (remove attached classes, for memory leaks) -3. replace the HTML on the page -4. initialize new component instances for that section and nested components - -``` -// code is located a component, where this.element points to HTML element for that section - -import { cleanElement, initComponents } from 'muban-core'; - -fetch(`/api/section/${id}`) - .then(response => response.text()) - .then(body => { - const currentElement = this.element; - - // 2. dispose all created component instances - cleanElement(currentElement); - - // insert the new HTML into a temp container to construct the DOM - const temp = document.createElement('div'); - temp.innerHTML = body; - const newElement = temp.firstChild; - - // 3. replace the HTML on the page - currentElement.parentNode.replaceChild(newElement, currentElement); - - // 4. initialize new components for the new element - initComponents(<HTMLElement>newElement); - }); -``` - -Luckily there is a utility function for this: - -``` -// code is located a component, where this.element points to HTML element for that section - -import { updateElement } from 'muban-core'; - -fetch(`/api/section/${id}`) - .then(response => response.text()) - .then(body => { - updateElement(this.element, body); - }); -``` - -While this seams like a good option, keep in mind that the whole section will be reset into its -default state, which could (depending on the contents of the section) be a bad experience, -especially when dealing with animation/transitions. - -## 2. Backend returns JSON for an updated section - -This one might be a bit more work compared to just replacing HTML, but gives you way more control -over what happens on the page. The big benefit is that the state doesn't reset, allowing you to make -nice transitions while the new data is updated on the page. - -``` -fetch(`/api/section/${id}`) - .then(response => response.json()) - .then(json => { - // this part really depends on what the data will be - - // if it's just text, you could: - this.element.querySelector('.js-content').innerHTML = json.content; - - // or pass new data to a child component - this.childComponent.setNewData(json.content); - }); -``` - -Or when using knockout to update your HTML: - -``` -import { initTextBinding } from '../../../muban/knockoutUtils'; -import ko from 'knockout'; - -// when using knockout to bind your data, first init the observable with the correct intial data -this.content = ko.observable(this.element.querySelector('.content').innerHTML); - -// then apply the observable to the HTML element -ko.applyBindingsToNode(this.element.querySelector('.content'), { - 'html': this.content, -}); - -// or a better way to do the above two steps: -this.content = initTextBinding(<HTMLElement>this.element.querySelector('.content'), true); - -fetch(`/api/section/${id}`) - .then(response => response.json()) - .then(json => { - this.content(json.content); // content is an knockout observable - }); -``` - -## 3. Sorting or filtering lists - -Sometimes the server renders a list of items on the page, but you have to sort or filter them -client-side, based on specific data in those items. Since we already have all the items and data on -the page, it's not that difficult. - -We can just query all the items, and retrieve the information we need to execute our logic, and add -them back to the page. - -``` -constructor() { - this.initItems(); - this.updateItems(); -} - -private initItems() { - // get all DOM nodes - const items = Array.from(this.element.querySelectorAll('.item')); - - // convert to list of useful data to filter/sort on - this.itemData = items.map(item => ({ - element: item, - title: item.querySelector('.title').textContent, - tags: Array.from(item.querySelectorAll('.tag')).map(tag => tag.textContent.toLowerCase()), - })); -} - -private updateItems() { - // empty the container - const container = this.element.querySelector('.items'); - while (container.firstChild) { - container.removeChild(container.firstChild); - } - - // filter on any tags that contains an 's' - let newItems = this.filterOnTags(this.itemData, 's'); - // sort descending - newItems = this.sortOnTitle(newItems, false); - - // append new items to the container - const fragment = document.createDocumentFragment(); - newItems.forEach(item => fragment.appendChild(item.element)); - container.appendChild(fragment); -} - - -// sort items base on the title attribute -private sortOnTitle(itemData, ascending:boolean = false) { - return [...itemData].sort((a, b) => a.title.localeCompare(b.title) * (ascending ? 1 : -1)); -} - -// filter items based on the tags array -private filterOnTags(itemData, filter:string) { - return itemData.filter(item => item.tags.some(tag => tag.includes(filter.toLowerCase()))); -} -``` - -## 4. Load more items to the page - -Sometimes the server renders the first page of items, but they want to have the second page to be -loaded and displayed from the client. If the server returns HTML, we can just re-use some of the -logic in our HTML example above. - -However, if the server returns JSON, we sort of want to re-use the markup of the existing items on -the page. We _could_ build up the HTML ourselves from JavaScript, but that would mean the HTML lives -in two places, on the server and in JavaScript, and it will be hard to keep them in sync. - -There are two options we can choose from. - -#### 4.1. Clone and update element - -For smaller items, we could just clone the first element of the list, and create a function that -updates all the data in that item, so we can append it to the DOM. - -``` -// get the template node to clone later -const template = <HTMLELement>this.element.querySelector('.item'); -// create a documentFragment for better performance when adding items -const fragment = document.createDocumentFragment(); - -// clone template, update data, and add to fragment -newItems.forEach(item => { - const clone = template.cloneNode(true); - clone.querySelector('.title').textContent = item.title; - clone.querySelector('.description').textContent = item.description; - fragment.appendChild(clone); -}); - -// add fragment to the list -this.element.querySelector('.list').appendChild(fragment); -``` - -#### 4.2. Using the handlebars template - -If we already have a `.hbs` template, we can use this in JavaScript as well. If we import the `.hbs` -file, it will be pre-compiled by webpack to a JS function. This function accepts 1 parameter, the -data, and returns the HTML string. - -```js -// import the hbs template -import { initComponents } from 'muban-core'; -import buttonTemplate from '../../general/button/button.hbs?include'; - -// render the template by passing data -const content = buttonTemplate({ text: 'button text' }); - -// append the HTML to the DOM (container already exists in the DOM somewhere) -container.innerHTML = content; - -// make the component interactive -initComponents(container); -``` - -The example above is quite simple, but there are others that require some more work. For that there -are some dataUtils in muban-core you can use. - -**Replacing a single item** - -This is similar to the example above - -```js -import { renderItem } from 'muban-core/lib/utils/dataUtils'; -import buttonTemplate from '../../general/button/button.hbs?include'; - -renderItem(container, buttonTemplate, { text: 'button text' }); -``` - -It will: -* clean the container (both html and dispose all interactive classes) -* append the rendered template -* init the component - -**Appending a single item** - -```js -import { renderItem } from 'muban-core/lib/utils/dataUtils'; -import buttonTemplate from '../../general/button/button.hbs?include'; - -renderItem(container, buttonTemplate, { text: 'button text' }, true); -``` - -It will: -* keep the container as is -* append the rendered template -* _only_ init the new component - -**Replacing a list of items** - -```js -import { renderItems } from 'muban-core/lib/utils/dataUtils'; -import buttonTemplate from '../../general/button/button.hbs?include'; - -renderItems(container, buttonTemplate, [ { text: 'button text' }, { text: '2nd btn' } ]); -``` - -It will: -* clean the container (both html and dispose all interactive classes) -* append the rendered templates using a documentFragment -* init all components in the container - - -**Adding a list of items** - -```js -import { renderItems } from 'muban-core/lib/utils/dataUtils'; -import buttonTemplate from '../../general/button/button.hbs?include'; - -renderItems(container, buttonTemplate, [ { text: 'button text' }, { text: '2nd btn' } ], true); -``` - -It will: -* keep the container as is -* append the rendered templates using a documentFragment -* _only_ init the new components - -#### 4.3. Use Knockout with a template - -This option works best when only used on the client, but when having server-rendered items in the -DOM you would first need to convert them to data to properly render them. - -Handlebars template: - -``` -<!-- -List item template, keep in HTML since it will be used by javascript. -The HTML in the script-template is similar to the html in the handlebars list below. -The handlebars template will be rendered on the server, and the script-template will -be used by knockout to render the list client-side (when new data comes in). ---> -<script type="text/html" id="item-template"> - <h3 class="title" data-bind="text: title"></h3> - <p class="description" data-bind="html: description"></p> - <div class="tags"> - <!-- ko foreach: tags --> - <span class="tag" data-bind="text: $data"></span> - <!-- /ko --> - </div> -</script> - -<section class="items"> - {{#each items}} - <article class="item"> - <h3 class="title">{{title}}</h3> - <p class="description">{{description}}</p> - <div class="tags"> - {{#each tags}} - <span class="tag">{{this}}</span> - {{/each}} - </div> - </article> - {{/each}} -</section> -``` - -Script: - -``` -// 1. transform old items to data -// get all DOM nodes -const items = Array.from(this.element.querySelectorAll('.item')); - -// convert to list of useful data to filter/sort on -const oldData = items.map(item => ({ - title: item.querySelector('.title').textContent, - description: item.querySelector('.description').innerHTML, - tags: Array.from(item.querySelectorAll('.tag')).map(tag => tag.textContent), -})); - -// 2. create observable and set old data -const itemData = ko.observableArray(oldData); - -// 3. apply bindings to list, this will re-render the items -ko.applyBindingsToNode(this.element.querySelector('.items'), { - 'template' : { 'name': 'item-template', 'foreach': itemData }, -}); - -// 4. add new data to the observable -// or do any other funky stuff to the array, like sorting/filtering -itemData.push(...newData); -``` - -The above can be simplified by using a util. The 3rd parameter can also be `oldData` extract above -instead of the passed config for more control. - -``` -import { initListBinding } from '../../../muban/knockoutUtils'; - -// 1+2+3. extract data, create observable and apply bindings -const itemData = initListBinding( - <HTMLElement>this.element.querySelector('.items'), - 'item-template', - { - query: '.item', - data: { - title: '.title', - description: { query: '.description', htm: true }, - tags: { query: '.tag', list: true }, - } - }, -); - -// 4. add new data to the observable -// or do any other funky stuff to the array, like sorting/filtering -itemData.push(...newData); -``` diff --git a/docs/dynamic-data.md b/docs/dynamic-data.md deleted file mode 100644 index 4f5c69be..00000000 --- a/docs/dynamic-data.md +++ /dev/null @@ -1,250 +0,0 @@ -# Dynamic Data - -Muban is designed to work with HTML that is fully generated by the server, where it only provides -the `js` and `css` to make the website look and work the way it should. The big downside is that -it's not possible to work with data-binding template engines that frameworks like Vue, React and -Angular do, because they have control over the HTML. - -This means we create (interactive) components by passing the HTML element, and the component should -use querySelectors and other DOM APIs to read from and write to the DOM. We have added Knockout to -Muban to allow you to set up data-bindings from within JavaScript, but that only gets you so far. - -When having to deal with dynamic data fetched from JavaScript, or rendered lists that need to be -sorted of filtered client-side, we need to think of something else. Below are some common scenarios -and how you can deal with them. - -Please have a look at the [examples](./dynamic-data-examples.md) - -## Different types of dynamic data - -In this case, dynamic data is everything that is not rendered as visible HTML. There are different -ways to pass down additional data to the browser so it can be used by JavaScript upon user -interaction. - -* data-attributes -* embedded json -* http requests (fetch) - -### data-attributes - -If you don't have much additional data, it can be rendered within the data-attributes on the -component DOM node. - -**Passing additional color presets:** - -```html -<div data-component="foo" data-colors="#CC9933,#22AA88,#FF8822"></div> -``` -```js -const colors = this.element.dataset.colors.split(','); -``` - -**Structured data:** - -```html -<div data-component="foo" data-json="{ "users": [ { "id": 0, "name": "Adam Carter", "work": "Unilogic", "email": "adam.carter@unilogic.com", "dob": "1978", "address": "83 Warner Street", "city": "Boston", "optedin": true }, { "id": 1, "name": "Leanne Brier", "work": "Connic", "email": "leanne.brier@connic.org", "dob": "13/05/1987", "address": "9 Coleman Avenue", "city": "Toronto", "optedin": false } ], "images": [ "img0.png", "img1.png", "img2.png" ], "coordinates": { "x": 35.12, "y": -21.49 }, "price": "$59,395" }"></div> -``` -```js -const data = JSON.parse(this.element.dataset.json); -``` - -Just be sure to properly encode the content, especially when outputting user generated content. - -When the data becomes too big to put in data-attributes, it might be better to embed them in the -document. - -### Embedded json - -When needing quite a big payload on your page, you can embed it in a non-JS script tag, and parse -it with JS afterwards. - -```html -<div data-component="foo" data-colors="#CC9933,#22AA88,#FF8822"> - <script type="text/json"> - { - "users": [ - { - "id": 0, - "name": "Adam Carter", - "email": "adam.carter@unilogic.com", - "dob": "1978", - "address": "83 Warner Street", - "city": "Boston" - }, - { - "id": 1, - "name": "Leanne Brier", - "email": "leanne.brier@connic.org", - "dob": "13/05/1987", - "address": "9 Coleman Avenue", - "city": "Toronto" - } - ], - "images": [ - "img0.png", - "img1.png", - "img2.png" - ], - "coordinates": { - "x": 35.12, - "y": -21.49 - }, - "price": "$59,395" - } - </script> - <div class="content"> - foobar - </div> -</div> -``` -```js -const data = JSON.parse(this.getElement<HTMLScriptElement>('script[type="text/json"]').innerHTML); -``` - -### fetch() - -If the data is too big, or too dynamic, and the backend has an API in place, we can also get more -data that way. - -For basic XHR calls, you should use the -[Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch). To support -older browsers (IE), you should include the [fetch polyfill](https://github.com/github/fetch). - -Install: - -``` -yarn add whatwg-fetch -``` - -Import in the file in `bootstrap.dev.js` and `bootstrap.dist.js`: - -``` -import 'whatwg-fetch'; -``` - -##### Getting HTML - -``` -fetch('/users.html') - .then(response => response.text()) - .then(body => { - document.body.innerHTML = body; - }); -``` - -##### Getting JSON - -``` -fetch('/users.json') - .then(response => response.json()) - .then(json => { - console.log('parsed json', json); - }).catch(ex => { - console.error('parsing failed', ex); - }); -``` - -##### Post form - -``` -var form = document.querySelector('form') - -fetch('/users', { - method: 'POST', - body: new FormData(form), -}); -``` - -##### Post JSON - -``` -fetch('/users', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - name: 'Hubot', - login: 'hubot', - }), -}); -``` - -##### File Upload - -``` -const input = document.querySelector('input[type="file"]') - -const data = new FormData() -data.append('file', input.files[0]); // file -data.append('user', 'hubot'); // other data - -fetch('/avatars', { - method: 'POST', - body: data -}) -``` - -### Axios - -If you need more features, you could use [Axios](https://github.com/axios/axios). It's a wrapper -around `fetch`, but with more configuration options. - -## Dynamic Templates - -Getting the dynamic data is just the first part, we also need to display the data on the screen. - -There are several ways to get this done, and choosing one depends on the complexity of the data and -the template itself. - -The following scenarios can occur: - -* moving around existing items in the DOM -* toggling visibility of different parts of the DOM -* updating an existing view / item -* adding new items based on an existing template -* adding new items without an existing template - -### Moving around existing items in the DOM - -This could happen when you render all items on the server, and having a client-side sort/filter. - -In this case you could simply pull all item DOM nodes from the container, extract the needed -information to apply a sort/filter, and add the resulting items back in the DOM. - -### Toggling visibility of different parts of the DOM - -This could happen when you have multiple views (e.g. a TabBar) that are all rendered on the server, -but in the client you only show the 'active' view, and hide the other ones. - -### Updating an existing view / item - -This could happen when you have a detail view, and you want to show a different variant without -reloading the page. - -### Adding new items based on an existing template - -This could happen when you have a 'Load More' button to do client-side pagination. - -An existing template could be one of a few things: - -* An existing DOM element of the item, we can then clone the element and update the content with - setting `textContent` or `innerHTML`. -* Rendering a 'template' element (display:none, without any content), and use that the same way - as above. -* Reusing the `.hbs` template in the JS bundle, by calling the `renderItem`/`renderItems` methods - from `muban-core`. -* Creating a `knockout` template and render those. Normally the hbs templates are preferred, but if - you have additional logic to execute, this will be a nice solution. Or if you never render any - template on the server, and already include the knockout lib in your project, this is also fine. - -The main thing you want to minimize, is duplicate templates. So if you already rendering something -on the server, you want that to be the source of truth, without duplicating the template in the JS -bundle. That's why reusing `.hbs` is normally better than knockout, you only have to maintain a -single template (still keep them sync between server and client though). - -### Adding new items without an existing template - -This is similar to the case above, but now we're not sure we have an existing item in the DOM we -could clone. This could be conditionally (depending on which page you're on), or always (you never -render the template on the server at all). diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index dcb26b26..00000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,268 +0,0 @@ -# Getting started - - -## Setting up the project - -Start by cloning or downloading [this repos](https://github.com/mediamonks/muban), and copy the -contents (without the `.git` folder) in your new project folder. - -You might want to setup/enable tools for linting and formatting in your editor. Make sure you are -running the same node version as specified in the `.nvmrc` file. - -Run `yarn` (or `npm i`) in the root folder to get all the node modules installed. - -## Running and developing - -After that, you should be able to run `yarn dev` to get the webpack dev server started. Open your -browser at [http://localhost:9000](http://localhost:9000) to preview an empty website. - -The 'index' will list all the pages you have created, so you can easily navigate to them. By default -it will show a home and about page. On those pages, it will show some default components. - -Any change that you make from now on will automatically update the page, without reloading the -browser. - -## Creating a new component - -While you can create all the component files yourself, it's easier to generate them via the CLI. - -For this you can install seng-generator by running `yarn global add seng-generator` (or `npm i -g -seng-generator`). - -You can start the generator wizard by running `sg wizard` and choosing `block`. Give it the name -'foo' and select the default location. Now navigate your project browser to -`src/app/component/block/foo` and notice all the files that are created. - -A block is a special type of component that can be added to a page directly. Normal components can -only be included by blocks or other components. - -To have this component show up on a page, we can update the `src/data/home.yaml`. Add your new -component in the list, and see how your page updates in the browser: - -```yaml -title: "Home" -blocks: - - name: "paragraph" - data: "import!../app/component/block/paragraph/data.yaml" - - - name: "two-col" - data: "import!../app/component/block/two-col/data.yaml" - - - name: "foo" - data: "import!../app/component/block/foo/data.yaml" -``` - -## Adding a new page - -It's more fun if your component is displayed alone on a page, so let's create a new one by running -`sg wizard` and select `page`. Give it the name `bar` and choose the default location. Let's also -add our just created block by choosing `yes` and specify `foo` when asked for a list of blocks. - -Now inspect your new page at `src/data/bar.yaml`, it looks similar to the `home.yaml` before. - -## Update your component - -Now let's update our component with some content. We start out with this: - -```html -<link rel="stylesheet" href="./foo.scss"> -<script src="./Foo.ts"></script> - -<div data-component="foo"> - <h1>foo</h1> -</div> -``` - -Here you can see it imports a stylesheet and a script file. All components have a `data-component` -attribute to identify itself. It's used for styling (your component styles are wrapped in a -`[data-component="foo"] { }` selector) and for creating a new component class that matches with -a static `displayName`: - -```ts -export default class Foo extends AbstractBlock { - static displayName:string = 'foo'; -} -``` - -#### data - -The last piece of the puzzle is the data we want to display in the HTML. We don't want to hardcoded -this, since it will later be filled by the backend, and it would also be nice to show different -variations of our component by just passing different data. - -Open the `data.yaml` in your component folder, it contains an empty object by default, so just -add some fields: - -```yaml -title: Hello world -description: | - <strong>Lorum</strong> ipsum dolor amet... -tags: - - muban - - handlebars - - webpack -``` - -#### template - -Now we have the data, let's update our HTML to display this information by editing `foo.hbs`: - -```html -<link rel="stylesheet" href="./foo.scss"> -<script src="./Foo.ts"></script> - -<div data-component="foo"> - <h1>{{title}}</h1> - <p>{{{description}}}</p> - <div> - {{#each tags}} - {{> general/button text=this }} - {{/each}} - </div> -</div> -``` - -Above you see four things: - -1. we add the `{{title}}` variable -2. we add the `{{{description}}}` variable as raw html -3. we loop over our tags array using the handlebars `each` helper -4. we include the button component partial, and pass the tag as the text property - -If you still have your browser open, you should see your updated component on the page. - -#### code - -As mentioned earlier, if a component template includes a script tag, it will create a new instance -of the component class, and passes the DOM element in the constructor. - -Let's update our component class by editing `Foo.ts` in the component folder: - -```typescript -import AbstractBlock from "../AbstractBlock"; - -export default class Foo extends AbstractBlock { - static displayName:string = 'foo'; - - constructor(el:HTMLElement) { - super(el); - - this.getElements('[data-component="button"]').forEach(btn => { - btn.addEventListener('click', this.handleButtonClick); - }); - } - - handleButtonClick = (event) => { - console.log(event.currentTarget); - } - - public dispose() { - this.getElements('[data-component="button"]').forEach(btn => { - btn.removeEventListener('click', this.handleButtonClick); - }); - - super.dispose(); - } -} -``` - -The code above does a few things: - -1. It uses the `getElements` helper function to get child elements, and loops over them -2. for each button, it adds a click event listener that logs the button element -3. in the dispose, it removes the listeners again - -The dispose function is only needed during development when hot reloading (or when manually adding -and removing components into the DOM), since normally all listeners will be removed when the page -is reloaded. - -You could also make everything look a bit nicer by updating `foo.scss`, but that's all up to you! - -## Storybook - -Previewing components on the page is one thing, but sometimes it's more useful to see them in -isolation. For this we have storybook. You can close your dev server, and start `yarn storybook`. - -When opening `http://localhost:9002/` in your browser, you should immediately see your new 'foo' -component. If you click the `foo - default` link, it will just show that component preset, and all -information moves to the bottom. - -The component is loaded in an iframe to be completely isolated, and you can click the 'responsive' -icon in the top-left to play around with breakpoints (this works in every browser). - -At the bottom you see all the available information about the component, it's name, location and -description (that you must enter yourself). It also shows the preset source, the passed data, the -rendered HTML, and the source code of the template, style and script files. This is not only useful -for you as the frontend develop, but also for a backend developer that needs to implement your -templates. - -### presets - -Now let's add a second preset so to see how this works. Open the `preset.js` in your foo component -folder, and add a second preset: - -```typescript -import { storiesOf } from 'storybook/utils/utils'; - -storiesOf('foo', require('./foo.hbs')) - .add( - 'default', - 'No description yet...', - `<hbs> - {{> block/foo @root}} - </hbs>`, - require('./data'), - ) - .add( - 'no tags', - 'A version without any tag buttons', - `<hbs> - {{> block/foo @root}} - </hbs>`, - require('./data-no-tags'), - ); -``` - -And add an additional preset file called `data-no-tags.yaml`: - -```yaml -title: Hello world -description: | - <strong>Lorum</strong> ipsum dolor amet... -``` - -You can also directly add the data in the preset, but keeping it in yaml files allows you to -include it in any page yaml you want, giving you more flexibility. - -Now, if you click the file path in the info panel in your browser (`component/block/foo/foo.hbs`), -it will load the page to show all variants o the same component, and you'll see the component with -or without the tag buttons. - -This will help you out to test styling whe certain elements are different or missing. It could also -help you spot script errors if you expect certain elements to exist, but will give an error if you -add an event listener to non-existing elements. - -## Building - -Now that your site is 'done', it's time to make a distribution build by running `yarn build`. After -completion the `dist/site` folder will contain your preview site. By running `yarn preview` you can -start a local node server to preview those pages in the browser at `http://127.0.0.1:9001/`. This -time they are static html pages, that load a bundled js and css file, and is very similar to the -actual website where your frontend will be integrated. It's always a good practice to build and -preview your site before sending it over to anyone else, so you know for sure everything works -properly. - -Additionally you can run `yarn build:diff` to generate a diff report for all changed handlebars -templates. Command will create a `dist/diff` folder and put a report inside. Script can compare changes -with master branch or specific commit or git tag. - -## Further reading - -This is just the basics of working with Muban, to dive a bit deeper, check out the following: - -* The `docs/` folder that contains a lot more in-depth information about certain topics (like more - util functions, how to work with dynamic data, how to preview your build and storybook online, - and much much more). -* The [webpack documentation](https://webpack.js.org/) and the webpack config located in - `build-tools/config` (where `indedx.js` is a config file with paths and other settings). -* The [Handlebars documentation](http://handlebarsjs.com/) for writing more advanced templates diff --git a/docs/handlebars.md b/docs/handlebars.md deleted file mode 100644 index 5cc89f6c..00000000 --- a/docs/handlebars.md +++ /dev/null @@ -1,46 +0,0 @@ -# Handlebars - -## Helpers - -Besides the default helpers that are shipped with Handlebars itself, Muban includes additional -helpers to make dynamic templates easier, and to be more compatible with backend template languages -that these templates will be converted to. - -The helpers are placed in the `/build-tools/handlebars-helpers` folder, and the filename should be -used as helper name. - -> When you are converting templates with the -[muban-convert-hbs](https://github.com/mediamonks/muban-convert-hbs) library, make sure you try to -stick with the helpers that are supported there, otherwise your custom helpers need to be manually -updated after conversion. - -> When you use the Handlebars templates as-is in a backend system, make sure that implementation has -the same helpers registered, otherwise your templates will fail rendering. - -### condition - -The `condition` helper can be used in any place where just truthy or falsy data values are not -enough, but you instead like to compare two data values, or compare something against a static -value. - -It supports all common operators, like `==`, `===`, `!=`, `!==`, `<`, `<=`, `>`, `>=`, `&&` and -`||`. - -**Usage:** - -```handlebars -{{#if (condition variable1 '!==' value) }} - foo -{{else if (condition variable2 '>=' 10) }} - bar -{{/if}} -``` - -# TODO - -* [http://handlebarsjs.com/](http://handlebarsjs.com/) -* [https://github.com/pcardune/handlebars-loader](https://github.com/pcardune/handlebars-loader) -* How to use helpers -* How partials are resolved (source root, `foo > foo/foo.hbs`) -* How script and style files are included -* Stuff that doesn't work diff --git a/docs/knockout.md b/docs/knockout.md deleted file mode 100644 index 9b870130..00000000 --- a/docs/knockout.md +++ /dev/null @@ -1,140 +0,0 @@ -# Data binding with Knockout - -Because muban is built for server-rendered pages, there is no possibility for client-side -data-binding without bloating the html with template mumbo-jumbo. Even then, things like looping -over lists (or other thing where a template is used in the non-rendered state) are not really -possible. - -On the other hand, using just DOM APIs to read and update the DOM can become quite cumbersome and -error prone. - -Luckily, Knockout allows us to initiate data-bindings from javascript (as opposed to in HTML), where -they can be bound to observables and computes, just like your normally would. - -The big advantage is that you can specify those in one place (e.g. in the constructor or in a -dedicated named method) so they are visible to everyone, and they will automatically update your -view when the data updates. - -## Basic Examples - -Some examples on how to use this: - -``` -// initiate the observable -private searchOpened:KnockoutObservable<boolean> = ko.observable(false); - -// set the 'opened' css class based on the 'searchOpened' observable -ko.applyBindingsToNode(this.element.querySelector('.search-results'), { - 'css' : { 'opened': this.searchOpened } -}); - -// do some code logic when the search is opened -this.searchOpened.subscribe((value) => { - // make things awesome -}) - -... -// open the search -this.searchOpened(true); -``` - -## Data binding APIs - -There are two, almost identical, knockout functions we can use for data-binding; -`applyBindingsToNode` and `applyBindingAccessorsToNode`. - -The former is for simple use, and the latter expects each property to be a function, which allows us -write additional logic based on observables (basically creating an inline computed). - -### applyBindingsToNode - -Requires the DOM element to bind to, and an object with binding properties. Each key corresponds -with the normal data-binds you would normally write in your HTML (e.g. css, text, change). - -Within the data-bind values you can pass observables, but you have to do so without including the -`()`. If you do so, it will just return that value, and the changes won't be tracked. By supplying -the observable itself, changes can be tracked to update the binding. - -``` -ko.applyBindingsToNode(element, object); -``` - -``` -ko.applyBindingsToNode(this.element.querySelector('.search-results'), { - 'css' : { 'opened': this.searchOpened } -}); -``` - -### applyBindingAccessorsToNode - -Almost the same as `applyBindingsToNode`, but with a 3rd parameter that we don't really use, so just -pass an empty object here. - -The big difference lies in the values of the data-bind keys; they have to be functions. The return -value of that function is the value that the data-bind expects (e.g. a string or object). - -Within these functions you can use any observable to return a value, and all changes to those -observables will be tracked, just like in normal computeds. - -If one of the data-bind properties for an element needs to be a function, you have to switch to this -method, and all of the properties have to be a function. - -``` -ko.applyBindingsToNode(element, object, viewModel); -``` - -Below, the `style` property has to be a function because we are using to observables to return a -custom value. Because of this, the `css` property also has to be a function, but that one will just -reference the observable (calling it would also work here). - -``` -ko.applyBindingAccessorsToNode(this.content, { - 'style' : () => ({ - maxWidth: model.deviceEmulateEnabled() ? model.viewportWidth() + 'px' : '100%', - }), - 'css': () => ({ 'resizing' : model.isResizingViewport }), -}, {}); -``` - -The following example applies a binding to a list of elements, where each element acts as a computed -by introducing some logic. For better performance, the reading of the attributes should be done only -once. - -``` -$('.bar').toArray().forEach((bar) => { - ko.applyBindingAccessorsToNode(bar, { - 'css' : () => { - let min:any = bar.getAttribute('data-size-min'); - let max:any = bar.getAttribute('data-size-max'); - min = min === '*' ? min : parseInt(min, 10); - max = max === '*' ? max : parseInt(max, 10); - - return { - active: ( - (model.viewportWidth() >= min || min === '*') && - (model.viewportWidth() <= max || max === '*') - ), - } - } - }, {}); -}); -``` - -## Using templates - -_TODO - implement / test / finish_ - -When having to create lists based on (dynamic) data, you probably want to use a template item and -render that multiple times with the correct data. - -Without Knockout, you would either: - -* have to retrieve an existing rendered element from the DOM, clone it, and replace it values before - appending it to the DOM again, or -* build up a HTML fragment (using strings, yikes, or DOM apis), and append it to the DOM. - -With knockout, a HTML template can be defined in the handlebars template (not sure?) or in -JavaScript within a template string (might be better?) or as separate `.template.html` files that -can be imported using a custom webpack-knockout-partial loader. - -These templates could include all normal data-bind attributes. diff --git a/docs/webpack.md b/docs/webpack.md deleted file mode 100644 index 9a773f26..00000000 --- a/docs/webpack.md +++ /dev/null @@ -1,33 +0,0 @@ -# Webpack - -TODO - -* dev -* dist - * partials - * js and css - * assets - * comments -* standalone - -## publicPath - -The publicPath is the location your assets are retrieved from. The default is set to `/`. When you -deploy your site into a nested folder, you want to change the publicPath to that folder. This can be -done by passing the `--publicPath` flag: - -``` -yarn build --publicPath=/nested/folder/ -yarn storybook:build --publicPath=/nested/folder/ -``` - -When you don't know the publicPath during build time, you can also set it at runtime by setting the -`webpackPublicPath` variable on the window before any script file is loaded: - -``` -<script> - window.webpackPublicPath = '/nested/folder/'; - // or - window.webpackPublicPath = 'https://cdn-domain.site.com/nested/folder/'; -</script> -```