Вызов шаблона с несколькими параметрами трубопровода
В шаблоне Go иногда способ передачи правильных данных в правильный шаблон мне кажется неудобным. Вызов шаблона с параметром конвейера выглядит как вызов функции только с одним параметром.
Скажем, у меня есть сайт для Gophers о Gophers. Он имеет основной шаблон главной страницы и шаблон утилиты для печати списка Gophers.
http://play.golang.org/p/Jivy_WPh16
Выход:
*The great GopherBook* (logged in as Dewey)
[Most popular]
>> Huey
>> Dewey
>> Louie
[Most active]
>> Huey
>> Louie
[Most recent]
>> Louie
Теперь я хочу добавить немного контекста в подтема: отформатируйте имя "Dewey" по-другому внутри списка, потому что это имя текущего пользователя. Но я не могу передать это имя напрямую, потому что есть только один возможный контур аргумента "точка"! Что я могу сделать?
- Очевидно, что я могу скопировать-вставить код подтемы в основной шаблон (я не хочу, потому что он полностью утрачивает интерес к тому, чтобы иметь подстроку).
- Или я могу манипулировать некоторыми глобальными переменными с помощью аксессуаров (я тоже не хочу).
- Или я могу создать новый конкретный тип структуры для каждого списка параметров шаблона (не очень).
Ответы
Ответ 1
Вы можете зарегистрировать функцию "dict" в ваших шаблонах, которые вы можете использовать для передачи нескольких значений вызову шаблона. Сам вызов будет выглядеть следующим образом:
{{template "userlist" dict "Users" .MostPopular "Current" .CurrentUser}}
Код для маленького помощника "dict", включая регистрацию его как функции шаблона, находится здесь:
var tmpl = template.Must(template.New("").Funcs(template.FuncMap{
"dict": func(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dict call")
}
dict := make(map[string]interface{}, len(values)/2)
for i := 0; i < len(values); i+=2 {
key, ok := values[i].(string)
if !ok {
return nil, errors.New("dict keys must be strings")
}
dict[key] = values[i+1]
}
return dict, nil
},
}).ParseGlob("templates/*.html")
Ответ 2
Вы можете определить функции в своем шаблоне, и эти функции будут закрываться, определенные в ваших данных, например:
template.FuncMap{"isUser": func(g Gopher) bool { return string(g) == string(data.User);},}
Затем вы можете просто вызвать эту функцию в своем шаблоне:
{{define "sub"}}
{{range $y := .}}>> {{if isUser $y}}!!{{$y}}!!{{else}}{{$y}}{{end}}
{{end}}
{{end}}
Эта обновленная версия на игровой площадке выглядит довольно !!
вокруг текущего пользователя:
*The great GopherBook* (logged in as Dewey)
[Most popular]
>> Huey
>> !!Dewey!!
>> Louie
[Most active]
>> Huey
>> Louie
[Most recent]
>> Louie
ИЗМЕНИТЬ
Поскольку вы можете переопределять функции при вызове Funcs
, вы можете предварительно заполнить функции шаблона при компиляции вашего шаблона и обновить их с помощью своего фактического закрытия следующим образом:
var defaultfuncs = map[string]interface{} {
"isUser": func(g Gopher) bool { return false;},
}
func init() {
// Default value returns `false` (only need the correct type)
t = template.New("home").Funcs(defaultfuncs)
t, _ = t.Parse(subtmpl)
t, _ = t.Parse(hometmpl)
}
func main() {
// When actually serving, we update the closure:
data := &HomeData{
User: "Dewey",
Popular: []Gopher{"Huey", "Dewey", "Louie"},
Active: []Gopher{"Huey", "Louie"},
Recent: []Gopher{"Louie"},
}
t.Funcs(template.FuncMap{"isUser": func(g Gopher) bool { return string(g) == string(data.User); },})
t.ExecuteTemplate(os.Stdout, "home", data)
}
Хотя я не уверен, как это происходит, когда несколько goroutines пытаются получить доступ к одному и тому же шаблону...
Рабочий пример
Ответ 3
на основе @tux21b
Я улучшил функцию, поэтому ее можно использовать даже без указания индексов (просто чтобы сохранить способ привязки переменных к шаблону)
Итак, теперь вы можете сделать это следующим образом:
{{template "userlist" dict "Users" .MostPopular "Current" .CurrentUser}}
или
{{template "userlist" dict .MostPopular .CurrentUser}}
или
{{template "userlist" dict .MostPopular "Current" .CurrentUser}}
но если параметр (.CurrentUser.name) не является массивом, вам определенно нужно поместить индекс, чтобы передать это значение в шаблон
{{template "userlist" dict .MostPopular "Name" .CurrentUser.name}}
см. мой код:
var tmpl = template.Must(template.New("").Funcs(template.FuncMap{
"dict": func(values ...interface{}) (map[string]interface{}, error) {
if len(values) == 0 {
return nil, errors.New("invalid dict call")
}
dict := make(map[string]interface{})
for i := 0; i < len(values); i ++ {
key, isset := values[i].(string)
if !isset {
if reflect.TypeOf(values[i]).Kind() == reflect.Map {
m := values[i].(map[string]interface{})
for i, v := range m {
dict[i] = v
}
}else{
return nil, errors.New("dict values must be maps")
}
}else{
i++
if i == len(values) {
return nil, errors.New("specify the key for non array values")
}
dict[key] = values[i]
}
}
return dict, nil
},
}).ParseGlob("templates/*.html")
Ответ 4
Самое лучшее, что я нашел до сих пор (и мне это не очень нравится) - это параметры мультиплексирования и демультиплексирования с помощью "родового" контейнера:
http://play.golang.org/p/ri3wMAubPX
type PipelineDecorator struct {
// The actual pipeline
Data interface{}
// Some helper data passed as "second pipeline"
Deco interface{}
}
func decorate(data interface{}, deco interface{}) *PipelineDecorator {
return &PipelineDecorator{
Data: data,
Deco: deco,
}
}
Я использую этот трюк для создания своего сайта, и мне интересно, есть ли еще какой-то идиоматический способ достичь того же.
Ответ 5
Ad "... выглядит как вызов функции только с одним параметром.":
В некотором смысле каждая функция принимает один параметр - многозначную запись вызова. С шаблонами это то же самое, что запись "invocation" может быть примитивным значением или многозначным {map, struct, array, slice}. Шаблон может выбрать, какой {ключ, поле, индекс} он будет использовать из "одного" параметра конвейера в любом месте.
IOW, в этом случае достаточно.
Ответ 6
Иногда карты - это быстрое и простое решение таких ситуаций, как это упомянуто в двух других ответах. Поскольку вы много используете Gophers (и поскольку, исходя из вашего другого вопроса, вам все равно, является ли текущий Gopher администратором), я думаю, что он заслуживает своей собственной структуры:
type Gopher struct {
Name string
IsCurrent bool
IsAdmin bool
}
Здесь обновляется код вашей игровой площадки: http://play.golang.org/p/NAyZMn9Pep
Очевидно, что он получает немного громоздкое ручное кодирование примеров structs с дополнительным уровнем глубины, но так как на практике они будут сгенерированы машиной, просто пометить IsCurrent
и IsAdmin
по мере необходимости.
Ответ 7
Как я подхожу к этому, нужно украсить общий конвейер:
type HomeData struct {
User Gopher
Popular []Gopher
Active []Gopher
Recent []Gopher
}
создав контекстно-зависимый конвейер:
type HomeDataContext struct {
*HomeData
I interface{}
}
Выделение контекстно-зависимого конвейера очень дешево. Вы получаете доступ к потенциально большому HomeData
, скопировав указатель на него:
t.ExecuteTemplate(os.Stdout, "home", &HomeDataContext{
HomeData: data,
})
Так как HomeData
встроен в HomeDataContext
, ваш шаблон будет обращаться к нему напрямую (например, вы все еще можете сделать .Popular
, а не .HomeData.Popular
). Кроме того, теперь у вас есть доступ к полю свободной формы (.I
).
Наконец, я делаю функцию Using
для HomeDataContext
так.
func (ctx *HomeDataContext) Using(data interface{}) *HomeDataContext {
c := *ctx // make a copy, so we don't actually alter the original
c.I = data
return &c
}
Это позволяет мне сохранить состояние (HomeData
), но передать подтаблицу произвольное значение.
См. http://play.golang.org/p/8tJz2qYHbZ.
Ответ 8
Я реализовал библиотеку для этой проблемы, которая поддерживает передаваемые по протоколу аргументы и проверку.
Демонстрационный
{{define "foo"}}
{{if $args := . | require "arg1" | require "arg2" "int" | args }}
{{with .Origin }} // Original dot
{{.Bar}}
{{$args.arg1}}
{{ end }}
{{ end }}
{{ end }}
{{ template "foo" . | arg "arg1" "Arg1" | arg "arg2" 42 }}
{{ template "foo" . | arg "arg1" "Arg1" | arg "arg2" "42" }} // will raise an error
Github repo