Is it possible to implement such HTML templating?
type HtmlElement = object
tag_with_attrs: string
text: string
children: seq[HtmlElement]
proc render: HtmlElement:
let todos = @["Buy milk"]
let editing = true
let class_modifier = if editing: ".editing" else: ""
h"ul.todo-list{class_modifier}":
for text in todos:
h"li.todo-item":
h"input type=checkbox"
if editing:
h"input.edit autofocus=true"
.text "Some text"
# .
else:
h".description"
.text "Some text"
echo render()
I know Karax does similar thing, but its macros looks quite complex, I wonder if its possible to do something similar but with short and simple implementation. The more real looking code going to be like this:
h"li.todo-list{class_modifier}":
h".view":
h"input.toggle type: checkbox"
.bind_to self.item.completed
h"label"
.text self.item.text
.on_dblclick (e: ClickEvent) => (self.editing = self.item.text.some)
h"button.destroy"
.on_click (e: ClickEvent) => self.on_delete(self.item.id)
if self.editing:
h"input.edit autofocus: true"
.bind_to self.editing
.on_keydown handle_edit
.on_blur (e: BlurEvent) => (self.editing = string.none)
The first test to do when you're faced with "is it possible to do X with a macro" is to check if the syntax parses. By using dumpTree from the macros module this is fairly easy and it shows that your proposed syntax would indeed be valid Nim:
import macros
dumpTree:
h"li.todo-list{class_modifier}":
h".view":
h"input.toggle type: checkbox"
.bind_to self.item.completed
h"label"
.text self.item.text
.on_dblclick (e: ClickEvent) => (self.editing = self.item.text.some)
h"button.destroy"
.on_click (e: ClickEvent) => self.on_delete(self.item.id)
if self.editing:
h"input.edit autofocus: true"
.bind_to self.editing
.on_keydown handle_edit
.on_blur (e: BlurEvent) => (self.editing = string.none)
So now that you know what the tree structure of the code looks like it's all a matter of making the conversion from that tree to the tree you'd like it to have. But it should definitely be technically possible.
A minor question, why this template fails?
template h(tag: string): tuple[tag: string] =
(tag: tag)
discard h"some"
Error: identifier expected, but found 'r"some"'
because all accurances of tag get replaced, so the expanded template would be:
"some": "some"
just give the parameter a different nameIt's almost possible with templates :)
type HtmlElement = ref object
tag: string
txt: string
children: seq[HtmlElement]
template h(html: string): HtmlElement =
let node = HtmlElement(tag: html)
when compiles(parent_h.children.add node):
parent_h.children.add node
node
template h(html: string, code): HtmlElement =
let node = HtmlElement(tag: html)
when compiles(parent_h.children.add node):
parent_h.children.add node
block:
let parent_h {.inject.} = node
code
node
proc text(node: HtmlElement, text: string): HtmlElement =
node.txt = text
node
proc render: HtmlElement =
let todos = @["Buy milk"]
let editing = true
let class_modifier = if editing: ".editing" else: ""
h("ul.todo-list" & class_modifier):
for text in todos:
discard h"li.todo-item":
discard h"input type=checkbox"
if editing:
discard h"input.edit autofocus=true"
.text "Some text"
else:
discard h".description"
.text "Some text"
echo render().repr
Actually it doesn't work, why code below fails to add the "c"?
type HtmlElement = ref object
tag: string
txt: string
children: seq[HtmlElement]
template `+`(node: HtmlElement): void =
it.children.add node
template `+`(node: HtmlElement, code): void =
it.children.add node
block:
let it {.inject.} = node
code
template h(html: string): HtmlElement =
HtmlElement(tag: html)
template h(html: string, code): HtmlElement =
let node = h(html)
block:
let it {.inject.} = node
code
node
proc simple_render: HtmlElement =
h"a":
+ h"b":
+ h"c"
echo simple_render().repr
It prints only two tags, a and b
HtmlElement(tag: "a", txt: "", children: @[HtmlElement(tag: "b", txt: "", children: @[])])
The problem, is this a bug? play
type Some = ref object
v: int
proc somefn(a: Some): void =
let b = a
b.v = 2
echo a.v, " ", b.v
template sometl(a: Some): void =
let b = a
b.v = 2
echo a.v, " ", b.v
somefn(Some(v: 1)) # => 2 2
sometl(Some(v: 1)) # => 1 2
Hmm, I don't understand, is there docs that would explain what's going on and why it works that way?
Here's another strange example
type Some = ref object
v: int
template somefn(a: Some): void =
a.v = 2
echo a.v
somefn(Some(v: 1)) # => 1
Templates are code substitution they place the generic parameters where you call them
import std/macros
expandMacros:
somefn(Some(v: 1))
As it's Some(v: 1) to be pasted, it becomes
Some(v: 1).v = 2
echo Some(v: 1)
Ha ha, this is funny, I see what it does :D
type Some = ref object
v: int
template somefn(a: Some): int =
a.v = 2
a.v
let some = Some(v: 1)
echo somefn(some) # => 2
echo somefn(Some(v: 1)) # => 1
Good luck debugging something like:
let some_rare_condition = rand(1000) == 0
if some_rare_condition:
somefn(some) # => 2
else:
somefn(Some(v: 1)) # => 1
Final example, TodoMVC, all compiles
import base, ../app, ../h
# Model --------------------------------------------------------------------------------------------
type TodoItemState = enum active, completed
type TodoItem = ref object
text: string
completed: bool
type Todos = seq[TodoItem]
proc id(self: TodoItem): string =
self.text
# TodoView -----------------------------------------------------------------------------------------
const enter_key = 13; const escape_key = 27
# Feature: stateful component, preserving its state between renders
type TodoView = ref object of Component
on_delete: proc(id: string): void
editing: Option[string] # value of `editing` field going to be maintained between requests
item: TodoItem
proc set_attrs(self: TodoView, item: TodoItem, on_delete: (proc(id: string): void)): void =
self.item = item; self.on_delete = on_delete
proc render(self: TodoView): HtmlElement =
proc handle_edit(e: KeydownEvent): void =
if e.key == enter_key:
self.item.text = self.editing.get
self.editing.clear
elif e.key == escape_key:
self.editing.clear
let class_modifier =
(if self.item.completed: ".completed" else: "") &
(if self.editing.is_some: ".editing" else: "")
# Feature: compact HTML template syntax
h"li.todo-list{class_modifier}":
+ h".view":
# Feature: two way binding with autocast
+ h"input.toggle type: checkbox"
.bind_to(self.item.completed)
+ h"label"
.text(self.item.text)
.on_dblclick(proc = self.editing = self.item.text.some)
+ h"button.destroy"
.on_click(proc = self.on_delete(self.item.id))
if self.editing.is_some:
+ h"input.edit autofocus: true"
.bind_to(self.editing)
.on_keydown(handle_edit)
.on_blur(proc = self.editing = string.none)
# TodosView -----------------------------------------------------------------------------------------
type Filter = enum all, active, completed
type TodosView = ref object of Component
items: Todos
filter: Filter
new_todo: string
toggle_all: bool
proc set_attrs(self: TodosView, items: Todos = @[], filter: Filter = all): void =
self.items = items; self.filter = filter
proc render(self: TodosView): HtmlElement =
let completed_count = self.items.count((item) => item.completed)
let active_count = self.items.len - completed_count
let all_completed = completed_count == self.items.len
let filtered =
case self.filter:
of all: self.items
of completed: self.items.filter((item) => item.completed)
of active: self.items.filter((item) => not item.completed)
proc create_new(e: KeydownEvent): void =
if e.key == enter_key and not self.new_todo.is_empty:
self.items.add(TodoItem(text: self.new_todo, completed: false))
self.new_todo = ""
proc toggle_all(e: ChangeEvent): void =
self.items.each((item: TodoItem) => (item.completed = self.toggle_all))
proc on_delete(id: string): void =
self.items.delete((item) => item.id == id)
proc set_filter(filter: Filter): auto =
proc = self.filter = filter
h"header.header":
+ h"h1".text("todos")
+ h"input.new-todo autofocus"
.attr("placeholder", "What needs to be done?")
.bind_to(self.new_todo)
.on_keydown(create_new)
if not self.items.is_empty:
+ h"section.main":
+ h"input.toggle-all type=checkbox".value(all_completed)
.on_change(toggle_all)
+ h"label for=toggle-all".text("Mark all as complete")
+ h"ul.todo-list":
for item in filtered:
# Feature: statefull componenets, attr names and values are typesafe
+ self.h(TodoView, item.id, (on_delete: on_delete, item: item))
+ h"footer.footer":
+ h"span.todo-count":
+ h"strong".text(active_count)
+ h"span".text((if active_count == 1: "item" else: "items") & " left")
+ h"ul.filters":
+ h"li":
+ h"a".attr("class", if self.filter == all: "selected" else: "").text("All")
.on_click(set_filter(all))
+ h"li":
+ h"a".attr("class", if self.filter == active: "selected" else: "").text("Active")
.on_click(set_filter(active))
+ h"li":
+ h"a".attr("class", if self.filter == completed: "selected" else: "").text("Completed")
.on_click(set_filter(completed))
if all_completed:
+ h"button.clear-completed".text("Delete completed")
.on_click(proc = self.items.delete((item) => item.completed))
Would be better to write this
proc toggle_all(e: ChangeEvent): void =
self.items.each((item: TodoItem) => (item.completed = self.toggle_all))
as this
proc toggle_all(e: ChangeEvent): void =
self.items.each((item: TodoItem) => (item.completed = self.toggle_all))
proc toggle_all(e: ChangeEvent): void =
self.items.each((item) => item.completed = self.toggle_all)
Or even as this (I know about it macros, it can't do that, as it should work for every single argument proc)
proc toggle_all(e: ChangeEvent): void =
self.items.each &.completed = self.toggle_all