dvinciguerra/middleman-importmap

View on GitHub
README.md

Summary

Maintainability
Test Coverage

# middleman-importmap

[![Maintainability](https://api.codeclimate.com/v1/badges/aab4c3c09d920639f962/maintainability)](https://codeclimate.com/github/dvinciguerra/middleman-importmap/maintainability)

An Importmap extension for Middleman.


## Install

**Add gem to `Gemfile`**

`gem 'middleman-importmap'`


## Usage

#### Activate extension in `config.rb`**

```ruby
activate :importmap
```

#### Create the `importmap.yml` file at middleman root path

```shell
$ cd middleman_project && touch importmap.yml
```

#### Add importmaps to file (example)

```yaml
imports:
  "@hotwired/stimulus": https://unpkg.com/@hotwired/stimulus/dist/stimulus.js
```

The importmap.yml file keep the same structure of importmap in HTML


#### Replace default javascript tag by importmap

```ruby
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
    <meta name="viewport"
          content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- Use the title from a page's frontmatter if it has one -->
    <title><%= current_page.data.title || "Middleman" %></title>
    <%= stylesheet_link_tag "site" %>
    <%= javascript_importmap_tags %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>
```

#### Customize options if necessary

##### in `config.rb`:

```ruby
activate :importmap do |option|
  option.entrypoint = "site" # js entrypoint's filename without extension
  option.importmap = "importmap.yml" # importmap's filename with extension (yaml or json)
  option.use_shim = true # or false
  option.shim_src = "path/to/shim" # defaults to hardcoded js cdn
end
```

##### or by specifying arguments in template helpers:

This will override options in config.rb

```ruby
<%= javascript_importmap_tags("main", importmap: "importmap.json", shim: false) %>

# or customize one by one:
<%= javascript_importmap_shim_tag(shim_src: "another/path") %>
<%= javascript_inline_importmap_tag("importmap.json", shim: true) %>
<%= javascript_inline_module_tag("main", shim: true) %>

# See source code for methods implementation
```

## Examples
- [Creating an app using Stimulus JS](#creating-an-app-using-stimulus-js)
- [Creating an app using React and React Router](#creating-an-app-using-react-and-react-router)


### Creating an app using Stimulus JS

#### Add the following code to `/source/javascripts/site.js`

```javascript
import { Application } from "@hotwired/stimulus"

import HelloController from "./controllers/hello_controller.js"

window.Stimulus = Application.start()
Stimulus.register("hello", HelloController)
```

#### Create `controllers` directory

```shell
$ mkdir -p source/javascripts/controllers
```

#### Now add HelloController at `controllers/hello_controller.js`

```javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    console.log("Hello, Stimulus!", this.element)
  }

  greet() {
    console.log("Clicked Greet Button")
  }
}
```

#### One last and important thing is add element binding at `index.html.erb`

```ruby
---
title: Welcome to Middleman
---

<h1>
  Middleman is Running
</h1>

<div data-controller="hello">
  <input type="text">
  <button data-action="click->hello#greet">Greet</button>
</div>

<%= link_to(
  "Read Documentation",
  "https://middlemanapp.com/basics/templating_language/",
  target: "_blank"
) %>
```

If all things are OK, than start middleman server using command `bundle exec middleman server` and open your browser devtools to see the messages.

### Creating an app using React and React Router

This example is based on [DHH's Youtube video presenting rails-importmap gem using React and htm](https://www.youtube.com/watch?v=k73LKxim6tw).

#### Change `importmap.yml` file to be like this

```yaml
---
imports:
  "htm": "https://ga.jspm.io/npm:htm@3.1.1/dist/htm.module.js"
  "react": "https://ga.jspm.io/npm:react@18.2.0/index.js"
  "react-dom": "https://ga.jspm.io/npm:react-dom@18.2.0/index.js"
  "react-router-dom": "https://ga.jspm.io/npm:react-router-dom@6.21.1/dist/main.js"
  "htm_create_element": "/javascripts/htm_create_element.js"

scopes:
  "https://ga.jspm.io/":
    "@remix-run/router": "https://ga.jspm.io/npm:@remix-run/router@1.14.1/dist/router.js"
    "react-router": "https://ga.jspm.io/npm:react-router@6.21.1/dist/main.js"
    "scheduler": "https://ga.jspm.io/npm:scheduler@0.23.0/index.js"
```

#### Create `source/javascripts/htm_create_element.js` file

This file is necessary to use htm with React in an environment that doesn't have build process of JSX files.

```javascript
import { createElement } from 'react'
import htm from 'htm'

export const h = htm.bind(createElement)
```

#### Create `components` and `pages` directories

```shell
mkdir -p source/javascripts/components && mkdir -p source/javascripts/pages 
```

#### Create `components/Page.js` file

Creating this file to avoid code duplication of components and demonstrate how to use composition in this environment.

```javascript
import { h } from "htm_create_element"

const Footer = () => h`
  <footer class="footer mt-auto py-3 bg-body-tertiary">
    <div class="container">
      <span class="text-body-secondary">
        Build by <a href="https://github.com/dvinciguerra">dvinciguerra<//> using <a href="https://github.com/dvinciguerra/middleman-importmap">middleman-importmap<//>.
      </span>
    </div>
  </footer>
`

const Container = ({ children }) => h`
  <main class="flex-shrink-0">
    <div class="container">
      ${children}
    </div>
  </main>

  <${Footer} />
`

const Title = ({ children }) => h`
  <h1 class="mt-5">${children}</h1>
`

const Lead = ({ children }) => h`
  <p class="lead">${children}</p>
`

export default {
  Container,
  Title,
  Lead
}
```


#### Create `pages/Home.js` file

Now, let's create the Home page using the components created above and react-router-dom `Link` component.


```javascript
import { h } from "htm_create_element"
import { Link } from "react-router-dom"

import Page from "../components/Page.js"
 
export default () => h`
  <${Page.Container}>
    <${Page.Title}>Middleman Importmap React<//>
    <${Page.Lead}>
      This is a simple page created using Middleman-importmap and React to demonstrate how it is possible to build
      frontends in Middleman using importmap without any build.
    <//>

    <hr class="my-4" />
    
    <p>
      <${Link}
        to="/getting-started"
        class="btn btn-dark btn-lg"
        role="button"
      >
        Getting Started
      <//>
      <a
        href="https://github.com/dvinciguerra/middleman-importmap"
        class="btn btn-secondary btn-lg ms-1"
        role="button"
        target="_new"
      >
        GitHub
      <//>
    </p>
  <//>
`
```

#### Create `pages/About.js` file

Creating just another page to demonstrate how to use react-router-dom `Link` behaviour.

```javascript
import { h } from "htm_create_element"
import { Link } from "react-router-dom"

import Page from "../components/Page.js"
 
export default () => h`
  <${Page.Container}>
    <${Page.Title}>About<//>
    <${Page.Lead}>
      This is a simple About page
    <//>

    <hr class="my-4" />
    
    <p>
      <${Link}
        to="/"
        class="btn btn-dark btn-lg"
        role="button"
      >
        Back
      <//>
    </p>
  <//>
```

#### Create `components/App.js` file

Creating a component to wrap all pages and use react-router-dom `RouterProvider` component.

```javascript
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import { h } from 'htm_create_element'

import Home from "../pages/Home.js"
import About from "../pages/About.js"

const router = createBrowserRouter([
  { path: '/', element: h`<${Home} />`  },
  { path: '/about', element: h`<${About} />` }
])

export default () => h`<${RouterProvider} router=${router} />`
```

#### Add the following code to `site.js`

```javascript
import { render } from 'react-dom'
import { h } from 'htm_create_element'

import App from "./components/App.js"

const root = document.getElementById('root')
render(h`<${App} />`, root)
```

#### Add the following code to `source/index.html.erb`

```ruby
---
title: Welcome to Middleman
---

<div id="root"></div>

```

If all things are OK, than start middleman server using command `bundle exec middleman server`, open your browser and
access [http://127.0.0.1:4567/](http://127.0.0.1:4567/).


## See more

- [Importmap polyfill at guybedford/es-module-shims](https://github.com/guybedford/es-module-shims)
- [Can I Use about Import Maps browser support](https://caniuse.com/import-maps)
- [W3C Import Maps Spec](https://wicg.github.io/import-maps/)
- [The helper tags are inspired by rails/importmap-rails gem](https://github.com/rails/importmap-rails)

## License

See `./LICENSE` file for more details.

## Author

Daniel Vinciguerra