martini-contrib / render

Martini middleware/handler for easily rendering serialized JSON, XML, and HTML template responses.
MIT License
245 stars 57 forks source link

Multiple Extension Points in Layouts #35

Closed blachniet closed 10 years ago

blachniet commented 10 years ago

Maybe this is already possible, and I just don't know how to do it. I would like to be able to define different extensible areas in my layout. I'm thinking of something similar to Jade's named blocks.

<!-- templates/layout.tmpl -->
<body>
  <div class="container">
    {{ yield content }}
  </div>

  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
  {{ yield javascripts }}
</body>

So here I've defined 2 different sections that can be extended, content and javascripts. In my template I would want to be able to extend each of those areas.

{{ extends content }}
  <h1>Hello World</h1>
  <span id="blah"></span>
{{ end }}

{{ extends javascripts }}
  <script type="text/javascript">
    $("#id").text("Hello from Javascript");
  </script>
{{ end }}

The syntax here is obviously theoretical. I'm new to Go, so maybe this is somehow already possible, or maybe I'm thinking about the problem wrong. If so, feel free to set me straight.

mickelsonm commented 10 years ago

@blachniet: Anything is possible. I think what you're looking for is here: http://golang.org/pkg/text/template/ or {{template "path/to/my/template" .}}

On your layout you can do something like:

<body>
    {{template "shared/header" .}}
    {{yield}}
    {{template "shared/footer" .}}
</body>

Make sure you put your layout and all other associated templates in your templates directory and that you remember that yield acts more like a placeholder for whatever template you call. For example if I had:

routing on main.go

m.Get("/", func(r render.Render) {
    r.HTML(200, "index")
})

index.tmpl (content)

<h1>Widgets and Gizmos!</h1>

index.tmpl (rendered with my layout)

My first page would get rendered/displayed as:

<body>
    <header>My content from shared/header.tmpl</header>
    <h1>Widgets and Gizmos!</h1>
    <footer>My content from shared/footer.tmpl</footer>
</body>

The main thing to watch out for is that your file paths are pointing to the right spots, otherwise you'll get errors. Also, keep in mind that the {{template}} way of doing things is built into the Go templating system, so you can build things to act like web components and re-use them in different parts of your web application. Hope this helps!

mattkanwisher commented 10 years ago

Yeah problem is different pages need to inject different javascript into the HEAD section of the page. In rails they allow named yields, so the inner template can inject in multiple spots into the layout

mickelsonm commented 10 years ago

Sounds like a nifty feature in Rails. If I were trying to tackle this using Go templates/render package, I would probably set mine up something like this:

layout.tmpl

<html>
<head>
    {{range .JavaScriptFiles}}
    <script type="text/javascript" src="{{.}}"></script>
    {{end}}
</head>
<body>
    <header>
        <ul>
            <li><a href="/">Home</a></li>
            <li><a href="/about">About Us</a></li>
        </ul>
    </header>
    <section>
    {{yield}}
    </section>
    <footer>
        &copy; MyCompany Inc.
    </footer>
</body>
</html>

index.go

package main

import (
    "net/http"

    "github.com/go-martini/martini"
    "github.com/martini-contrib/render"
)

func main() {
    m := martini.Classic()

    m.Use(render.Renderer(render.Options{
        Directory:       "templates",
        Layout:          "layout",
        Extensions:      []string{".tmpl", ".html"},
        Delims:          render.Delims{"{{", "}}"},
        Charset:         "UTF-8",
        HTMLContentType: "text/html",
    }))

    m.Get("/", func(req *http.Request, r render.Render) {
        data := make(map[string]interface{})
        data["JavaScriptFiles"] = getJavaScriptFiles(req.URL.Path)
        r.HTML(200, "home", data)
    })

    m.Get("/about", func(req *http.Request, r render.Render) {
        data := make(map[string]interface{})
        data["JavaScriptFiles"] = getJavaScriptFiles(req.URL.Path)
        r.HTML(200, "about", data)
    })

    m.Run()
}

func getJavaScriptFiles(path string) []string {
    var scripts []string

    //common scripts that all pages have
    scripts = []string{
        "https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js",
    }

    //specific pages
    switch path {
    case "/about":
        scripts = append(scripts,
            "https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.1/jquery-ui.min.js",
        )
    }

    //application script
    scripts = append(scripts, "scripts/app.js")

    return scripts
}

Setting it up this way you have some control on what scripts go what page, in what order, etc. This works, but I would look into using something like RequireJS for javascript loading on specific pages, managing dependencies, modularizing the javascript, etc. It is also nice because your template would have a single javascript file. Lastly, it is good practice to put scripts at the bottom of the page in the body section of the page. Good luck and I hope this helps!

blachniet commented 10 years ago

The only thing that I don't like about this approach is that the controller-esque code needs some view-specific knowledge. However, as you pointed out, this could be avoided by using RequireJS.