Browse Source

Initial load

Raven 2 years ago
commit
3771d21226
86 changed files with 6581 additions and 0 deletions
  1. BIN
      .DS_Store
  2. 12 0
      Dockerfile
  3. 10 0
      build.sh
  4. BIN
      content/.DS_Store
  5. 46 0
      content/advanced-functions/a-taste-of-using-vuetify-in-go.go
  6. 55 0
      content/advanced-functions/detail-page-for-complex-object.go
  7. 94 0
      content/advanced-functions/event-handling.go
  8. 36 0
      content/advanced-functions/its-the-whole-house.go
  9. 48 0
      content/advanced-functions/layout-function-and-page-injector.go
  10. 18 0
      content/advanced-functions/lazy-portals-and-reload.go
  11. 42 0
      content/advanced-functions/manipulate-page-url-in-event-func.go
  12. 20 0
      content/advanced-functions/navigation-drawer.go
  13. 37 0
      content/advanced-functions/page-func-and-event-func.go
  14. 43 0
      content/advanced-functions/partial-refresh-with-portal.go
  15. 30 0
      content/advanced-functions/reload-page-with-a-flash.go
  16. 33 0
      content/advanced-functions/summary-of-event-response.go
  17. 56 0
      content/advanced-functions/switch-pages-with-push-state.go
  18. 35 0
      content/advanced-functions/the-go-html-builder.go
  19. 50 0
      content/advanced-functions/web-scope.go
  20. BIN
      content/assets/logo.png
  21. 24 0
      content/basics/auto-complete.go
  22. 20 0
      content/basics/basic-inputs.go
  23. 62 0
      content/basics/editing-customizations.go
  24. 25 0
      content/basics/filter.go
  25. 30 0
      content/basics/form-handling.go
  26. 24 0
      content/basics/linkage-select.go
  27. 83 0
      content/basics/listing-customizations.go
  28. 101 0
      content/basics/listing.go
  29. 22 0
      content/basics/notification-center.go
  30. 22 0
      content/basics/permissions.go
  31. 24 0
      content/basics/shortcut.go
  32. 20 0
      content/basics/variant-sub-form.go
  33. 49 0
      content/digging-deeper/composite-new-component-with-go.go
  34. 113 0
      content/digging-deeper/integrate-a-heavy-vue-component.go
  35. 32 0
      content/getting-started/one-minute-quick-start.go
  36. 50 0
      content/getting-started/what-is-goplaid.go
  37. 34 0
      content/home.go
  38. BIN
      examples/.DS_Store
  39. 159 0
      examples/e00_basics/composite-components.go
  40. 268 0
      examples/e00_basics/event-handling.go
  41. 186 0
      examples/e00_basics/form-handling.go
  42. 34 0
      examples/e00_basics/hello-world-reload.go
  43. 18 0
      examples/e00_basics/hello-world.go
  44. 79 0
      examples/e00_basics/manipulate-page-url.go
  45. 98 0
      examples/e00_basics/page-transition.go
  46. 112 0
      examples/e00_basics/partial-reload.go
  47. 58 0
      examples/e00_basics/partial-update.go
  48. 48 0
      examples/e00_basics/reload-with-a-flash.go
  49. 48 0
      examples/e00_basics/shortcut.go
  50. 47 0
      examples/e00_basics/type-safe-builder-sample.go
  51. 65 0
      examples/e00_basics/use-tiptap-editor.go
  52. 162 0
      examples/e00_basics/web-scope.go
  53. 45 0
      examples/e01_hello_button/page.go
  54. 140 0
      examples/e05_hello_customized_component/page.go
  55. 201 0
      examples/e10_vuetify_autocomplete/page.go
  56. 132 0
      examples/e11_vuetify_basic_inputs/page.go
  57. 42 0
      examples/e12_hello_vuetify_grid/page.go
  58. 74 0
      examples/e13_vuetify_list/page.go
  59. 126 0
      examples/e14_vuetify_menu/page.go
  60. 98 0
      examples/e15_vuetify_navigation_drawer/page.go
  61. 60 0
      examples/e16_hello_vuetify_simple_components/page.go
  62. 160 0
      examples/e17_hello_lazy_portals_and_reload/page.go
  63. 51 0
      examples/e18_filter_component/page.go
  64. 121 0
      examples/e19_stripeui_key_info/page.go
  65. 66 0
      examples/e20_vuetify_expansion_panels/page.go
  66. 295 0
      examples/e21_presents/detailing.go
  67. 145 0
      examples/e21_presents/editing.go
  68. 45 0
      examples/e21_presents/filter.go
  69. 88 0
      examples/e21_presents/linkage_select_filter_item.go
  70. 334 0
      examples/e21_presents/listing.go
  71. 55 0
      examples/e21_presents/messages.go
  72. 78 0
      examples/e21_presents/model-builder-extensions.go
  73. 46 0
      examples/e21_presents/notification-center.go
  74. 85 0
      examples/e21_presents/permissions.go
  75. 117 0
      examples/e22_vuetify_variant_sub_form/page.go
  76. 132 0
      examples/e23_vuetify_components_kitchen/page.go
  77. 59 0
      examples/e24_vuetify_components_linkage_select/page.go
  78. 111 0
      examples/example_basics/listing.go
  79. 2 0
      examples/examples-generated.go
  80. 23 0
      examples/utils/db.go
  81. 3 0
      go.mod
  82. 23 0
      main.go
  83. 82 0
      menu.go
  84. 677 0
      mux.go
  85. 8 0
      plantbuild/build.jsonnet
  86. 75 0
      utils/utils.go

BIN
.DS_Store


+ 12 - 0
Dockerfile

@@ -0,0 +1,12 @@
+FROM golang:alpine as builder
+RUN apk update && apk add git gcc libc-dev sqlite sqlite-dev && rm -rf /var/cache/apk/*
+ARG GITHUB_TOKEN
+WORKDIR /go/src/github.com/qor5/docs
+COPY . .
+RUN set -x && go get -d -v ./docs/docsmain/...
+RUN GOOS=linux GOARCH=amd64 go build -o /app/entry ./docs/docsmain/
+
+FROM alpine
+RUN apk update && apk add sqlite sqlite-dev && rm -rf /var/cache/apk/*
+COPY --from=builder /app/entry  /bin/docsmain
+CMD /bin/docsmain

+ 10 - 0
build.sh

@@ -0,0 +1,10 @@
+CUR=$(pwd)/$(dirname $0)
+
+if test "$1" = 'clean'; then
+    echo "Removing node_modules"
+    rm -rf $CUR/docsjs/node_modules/
+fi
+
+rm -r $CUR/docsjs/dist
+echo "Building docsjs"
+cd $CUR/docsjs && yarn && yarn build

BIN
content/.DS_Store


+ 46 - 0
content/advanced-functions/a-taste-of-using-vuetify-in-go.go

@@ -0,0 +1,46 @@
+package advanced_functions
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e13_vuetify_list"
+	"github.com/qor5/docs/examples/e14_vuetify_menu"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+	. "github.com/theplant/htmlgo"
+)
+
+var ATasteOfUsingVuetifyInGo = Doc(
+	Markdown(`
+[Vuetify](https://vuetifyjs.com/en/) is a really mature Vue components library for
+[Material Design](https://material.io/design/). We have made the efforts to
+integrate most all of it as a go package. You can use it with ease just like any
+other go package.
+`),
+	utils.Anchor(H2(""), "Use container, toolbar, list, list item etc"),
+	Markdown(`
+This example is purely render, we didn't integrate any interaction (event func) to it.
+`),
+	ch.Code(examples.VuetifyListSample).Language("go"),
+	utils.Demo("Vuetify List", e13_vuetify_list.HelloVuetifyListPath, "e13_vuetify_list/page.go"),
+
+	utils.Anchor(H2(""), "Use menu, card, list, etc"),
+	Markdown(`
+This example uses the menu popup, card, list component. and some interactions of clicking
+buttons on the menu popup.
+`),
+	ch.Code(examples.VuetifyMenuSample).Language("go"),
+	Markdown(`
+~.Attr(web.InitContextVars, "{myMenuShow: false}")~ is a special vue directive that
+we created to initialize vue context component data variables. It will initialize
+~vars.myMenuShow~ to ~false~. So that you don't need to modify javascript code to do
+the initialization. It's often useful to control dialog, popups. At this example,
+We add it, So that the cancel button on the menu, could actually close the menu without
+requesting server backend.
+
+~toggleFavored~ event func did an partial update only to the favorite icon button. So that it won't close the
+menu popup, but updated the button to toggle the favorite icon.
+`),
+	utils.Demo("Vuetify Menu", e14_vuetify_menu.HelloVuetifyMenuPath, "e14_vuetify_menu/page.go"),
+).Title("A Taste of using Vuetify in Go").
+	Slug("vuetify-components/a-taste-of-using-vuetify-in-go")

+ 55 - 0
content/advanced-functions/detail-page-for-complex-object.go

@@ -0,0 +1,55 @@
+package advanced_functions
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e21_presents"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+	. "github.com/theplant/htmlgo"
+)
+
+var DetailPageForComplexObject = Doc(
+	Markdown(`
+By default, presets will only generate the listing page, editing page for a model,
+It's for simple objects. But for a complicated object with a lots of relationships and connections,
+and as the main data model of your system, It's better to have detail page for them. In there
+You can add all kinds of operations conveniently.
+`),
+	ch.Code(examples.PresetsDetailPageTopNotesSample).Language("go"),
+	utils.Demo("Presets Detail Page Top Notes", e21_presents.PresetsDetailPageTopNotesPath+"/customers", "e21_presents/detailing.go"),
+	Markdown(`
+- The name of detailing fields are just a place holder for decide ordering
+- ~CellComponentFunc~ customize how the cell display
+- ~stripeui~ package create basic components that similar to [Stripe Dashboard](https://dashboard.stripe.com)
+- ~stripeui.DataTable~ create a data table, Which the Listing page uses the same component
+- ~LoadMoreAt~ will only show for example 2 rows of data, and you can click load more to display all
+- ~stripeui.Card~ display a card with toolbar you can setup action buttons
+- We reference the new form drawer that ~b.Model(&Note{})~ creates, but hide notes in the menu
+`),
+	utils.Anchor(H2(""), "Details Info components and actions"),
+	Markdown(`
+A ~stripeui.DetailInfo~ component is used for display main detail field of the model.
+And you can add any actions to the detail page with ease:
+`),
+	ch.Code(examples.PresetsDetailPageDetailsSample).Language("go"),
+	utils.Demo("Presets Detail Page Details", e21_presents.PresetsDetailPageDetailsPath+"/customers", "e21_presents/detailing.go"),
+	Markdown(`
+- The ~stripui.Card~ Actions links to two event functions: Agree Terms, and Update Details
+- Agree Terms show a drawer popup that edit the ~term_agreed_at~ field
+- Update Details reuse the edit customer form
+`),
+
+	utils.Anchor(H2(""), "More Usage for Data Table"),
+	Markdown(`
+A ~stripeui.DataTable~ component is very featured rich, Here check out the row expandable example:
+`),
+	ch.Code(examples.PresetsDetailPageCardsSample).Language("go"),
+	utils.Demo("Presets Detail Page Credit Cards", e21_presents.PresetsDetailPageCardsPath+"/customers", "e21_presents/detailing.go"),
+	Markdown(`
+- ~RowExpandFunc~ config the content when data table row expand
+- ~cc.Editing~ setup the fields when edit
+- ~cc.Creating~ setup the fields when create
+`),
+).Title("Detail page for complex object").
+	Slug("presets-guide/detail-page-for-complex-object")

+ 94 - 0
content/advanced-functions/event-handling.go

@@ -0,0 +1,94 @@
+package advanced_functions
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e00_basics"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+	. "github.com/theplant/htmlgo"
+)
+
+var EventHandling = Doc(
+	Markdown(`
+We extend vue to support the following types of event handling, so you can simply use go code to implement some complex logic.
+
+Using the ~~~Plaid()~~~ method will create an event handler that defaults to using the current ~~~vars~~~ and ~~~plaidForm~~~.
+The default http request method is ~~~Post~~~, if you want to use the ~~~Get~~~ method, you can also use the ~~~Get()~~~ method directly to create an event handler
+	`),
+
+	utils.Anchor(H2(""), "URL"),
+	Markdown(`Request a page.`),
+	ch.Code(examples.EventHandlingURLSample).Language("go"),
+	utils.Demo("Event Handling", e00_basics.EventHandlingPagePath+"?api=url", "e00_basics/event-handling.go#L14-L22"),
+
+	utils.Anchor(H2(""), "PushState"),
+	Markdown(`Reqest a page and also changing the window location.`),
+	ch.Code(examples.EventHandlingPushStateSample).Language("go"),
+	utils.Demo("Event Handling", e00_basics.EventHandlingPagePath+"?api=pushstate", "e00_basics/event-handling.go#27-L35"),
+
+	utils.Anchor(H2(""), "Reload"),
+	Markdown(`Refresh page.`),
+	ch.Code(examples.EventHandlingReloadSample).Language("go"),
+	utils.Demo("Event Handling", e00_basics.EventHandlingPagePath+"?api=reload", "e00_basics/event-handling.go#40-L49"),
+
+	utils.Anchor(H2(""), "Query"),
+	Markdown(`Request a page with a query.`),
+	ch.Code(examples.EventHandlingQuerySample).Language("go"),
+	utils.Demo("Event Handling", e00_basics.EventHandlingPagePath+"?api=query", "e00_basics/event-handling.go#L54-L62"),
+
+	utils.Anchor(H2(""), "MergeQuery"),
+	Markdown(`Request a page with merging a query.`),
+	ch.Code(examples.EventHandlingMergeQuerySample).Language("go"),
+	utils.Demo("Event Handling", e00_basics.EventHandlingPagePath+"?api=merge_query", "e00_basics/event-handling.go#L67-L75"),
+
+	utils.Anchor(H2(""), "ClearMergeQuery"),
+	Markdown(`Request a page with clearing a query.`),
+	ch.Code(examples.EventHandlingClearMergeQuerySample).Language("go"),
+	utils.Demo("Event Handling", e00_basics.EventHandlingPagePath+"?api=clear_merge_query", "e00_basics/event-handling.go#L80-L88"),
+
+	utils.Anchor(H2(""), "StringQuery"),
+	Markdown(`Request a page with a query string.`),
+	ch.Code(examples.EventHandlingStringQuerySample).Language("go"),
+	utils.Demo("Event Handling", e00_basics.EventHandlingPagePath+"?api=string_query", "e00_basics/event-handling.go#L93-L101"),
+
+	utils.Anchor(H2(""), "Queries"),
+	Markdown(`Request a page with url.Values.`),
+	ch.Code(examples.EventHandlingQueriesSample).Language("go"),
+	utils.Demo("Event Handling", e00_basics.EventHandlingPagePath+"?api=queries", "e00_basics/event-handling.go#L106-L114"),
+
+	utils.Anchor(H2(""), "PushStateURL"),
+	Markdown(`Request a page with a url and also changing the window location.`),
+	ch.Code(examples.EventHandlingQueriesSample).Language("go"),
+	utils.Demo("Event Handling", e00_basics.EventHandlingPagePath+"?api=pushstateurl", "e00_basics/event-handling.go#L119-L127"),
+
+	utils.Anchor(H2(""), "Location"),
+	Markdown(`Open a page with more options.`),
+	ch.Code(examples.EventHandlingLocationSample).Language("go"),
+	utils.Demo("Event Handling", e00_basics.EventHandlingPagePath+"?api=location", "e00_basics/event-handling.go#L132-L140"),
+
+	utils.Anchor(H2(""), "FieldValue"),
+	Markdown(`Fill in a value on form.`),
+	ch.Code(examples.EventHandlingFieldValueSample).Language("go"),
+	utils.Demo("Event Handling", e00_basics.EventHandlingPagePath+"?api=fieldvalue", "e00_basics/event-handling.go#L145-L153"),
+
+	utils.Anchor(H2(""), "FormClear"),
+	Markdown(`Clear all form data.`),
+	ch.Code(examples.EventHandlingFieldValueSample).Language("go"),
+	utils.Demo("Event Handling", e00_basics.EventHandlingPagePath+"?api=formclear", "e00_basics/event-handling.go#L165-L178"),
+
+	utils.Anchor(H2(""), "EventFunc"),
+	Markdown(`Register an event func and call it when the event is triggered.`),
+	ch.Code(examples.EventHandlingEventFuncSample).Language("go"),
+	utils.Demo("Event Handling", e00_basics.EventHandlingPagePath+"?api=eventfunc", "e00_basics/event-handling.go#L183-L191"),
+
+	utils.Anchor(H2(""), "Script"),
+	Markdown(`Run a script code.`),
+	ch.Code(examples.EventHandlingBeforeScriptSample).Language("go"),
+	utils.Demo("Event Handling", e00_basics.EventHandlingPagePath+"?api=script", "e00_basics/event-handling.go#L196-L204"),
+
+	utils.Anchor(H2(""), "Raw"),
+	Markdown(`Directly call the js method`),
+	ch.Code(examples.EventHandlingRawSample).Language("go"),
+	utils.Demo("Event Handling", e00_basics.EventHandlingPagePath+"?api=raw", "e00_basics/event-handling.go#L209-L217"),
+).Title("Event Handling").Slug("basics/event-handling")

+ 36 - 0
content/advanced-functions/its-the-whole-house.go

@@ -0,0 +1,36 @@
+package advanced_functions
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e21_presents"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var ItsTheWholeHouse = Doc(
+	Markdown(`
+Presets let you config generalized data management UI interface for database.
+It's not a scaffolding to generate source code. But provide more abstract and
+flexible API to enrich features along the way.
+
+`),
+	ch.Code(examples.PresetHelloWorldSample).Language("go"),
+	Markdown(`
+And this ~*presets.Builder~ instance is actually also a ~http.Handler~, So that we can mount it
+to the http serve mux directly like this:
+`),
+	ch.Code(examples.MountPresetHelloWorldSample).Language("go"),
+	utils.Demo("Presets Hello World", e21_presents.PresetsHelloWorldPath+"/customers", "e21_presents/listing.go"),
+	Markdown(`
+With ~r.Model(&Customer{})~:
+
+- It setup the global layout with the left navigation menu
+- It setup the listing page with a data table
+- It add the new button to create a new record
+- It setup the editing and creating form as a right side drawer
+- It setup each row of data have a operation menu that you have edit and delete operations
+- It setup the global search box, can search the model's all string columns
+`),
+).Title("Not just scaffolding, it's the whole house").
+	Slug("presets-guide/its-the-whole-house")

+ 48 - 0
content/advanced-functions/layout-function-and-page-injector.go

@@ -0,0 +1,48 @@
+package advanced_functions
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+	. "github.com/theplant/htmlgo"
+)
+
+var LayoutFunctionAndPageInjector = Doc(
+	Markdown("Read this code first, Guess what it does."),
+	ch.Code(examples.DemoLayoutSample).Language("go"),
+	Markdown(`
+~ctx.Injector~ is for inject html into default layout's html head, and bottom of body.
+html head normally for page title, keywords etc all kinds meta data, and css styles,
+javascript libraries etc. You can see we put vue.js into head, but put main.js into the bottom of body.
+
+Next part describe about these asset references:
+`),
+	ch.Code(examples.ComponentsPackSample).Language("go"),
+
+	Markdown(`
+~web.JSComponentsPack~ is the production version of QOR5 core javascript code.
+Created by using [@vue/cli](https://cli.vuejs.org/guide/creating-a-project.html),
+It does the basic functions like render server side returned html as vue templates.
+Provide basic event functions that call to server, and manage push state
+(change browser address urls before or after do ajax requests). do page partial refresh etc.
+
+the javascript or css code are packed by using [embed](https://pkg.go.dev/embed).
+`),
+	ch.Code(examples.PackrSample).Language("go"),
+	Markdown(`
+And with ~web.PacksHandler~, You can merge multiple javascript or css assets together into one url.
+So that browser only need to request them one time. and cache them. The cache is set to the start
+time of the process. So next time the app restarts, it invalid the cache.
+`),
+	utils.Anchor(H2(""), "Summary"),
+	Markdown(`
+For a new project:
+
+- Use [@vue/cli](https://cli.vuejs.org/guide/creating-a-project.html) to create an asset project that manage your javascript and css. and compile them for production use
+- Use [embed](https://pkg.go.dev/embed) to pack them into Go code as ~ComponentPack~, which is a string
+- Use ~PacksHandler~ to mount them as available http urls
+- Write Layout function to reference them inside head, or bottom of body
+`),
+).Title("Layout Function and Page Injector").
+	Slug("basics/layout-function-and-page-injector")

+ 18 - 0
content/advanced-functions/lazy-portals-and-reload.go

@@ -0,0 +1,18 @@
+package advanced_functions
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e17_hello_lazy_portals_and_reload"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var LazyPortalsAndReload = Doc(
+	Markdown(`
+Use ~web.Portal().EventFunc("menuItems").Name("menuContent")~ to put a portal place holder inside a part of html, and it will load specified event func's response body inside the place holder after the main page is rendered in a separate AJAX request. Later in an event func, you could also use ~r.ReloadPortals = []string{"menuContent"}~ to reload the portal.
+`),
+	ch.Code(examples.LazyPortalsAndReloadSample).Language("go"),
+	utils.Demo("Lazy Portals", e17_hello_lazy_portals_and_reload.LazyPortalsAndReloadPath, "e17_hello_lazy_portals_and_reload/page.go"),
+).Title("Lazy Portals").
+	Slug("vuetify-components/lazy-portals")

+ 42 - 0
content/advanced-functions/manipulate-page-url-in-event-func.go

@@ -0,0 +1,42 @@
+package advanced_functions
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e00_basics"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var ManipulatePageURLInEventFunc = Doc(
+	Markdown(`
+Encode page state into query strings in url is useful. because user can paste the link to another person,
+That can open the page to the exact state of the page being sent, Not the initial state of the page.
+
+For example:
+`),
+	ch.Code(examples.MultiStatePageSample).Language("go"),
+	utils.Demo("Manipulate Page URL In Event Func", e00_basics.MultiStatePagePath, "e00_basics/manipulate-page-url.go"),
+	Markdown(`
+This page have several state that encoded in the url:
+
+- Page title have a default value, but if provided with a ~title~ query string, it will use that value
+- The edit panel can be open, or closed based on having the ~panel~ query string or not
+
+~web.Location(url.Values{"panel": []string{"1"}}).MergeQuery(true)~ means it will do a push state request to current page, with panel query string panel=1.
+~MergeQuery~ means that it will not touch other query strings like ~title=1~ we mentioned above.
+
+In ~update5~ event func, which is when you click the update button after open the panel, ~web.Location(url.Values{"panel": []string{""}}).MergeQuery(true)~ basically removes the query string panel=1, and won't touch any other query strings.
+
+Don't have to be in event func to use push state query, can use a simple ~web.Bind~ to directly change the query string like:
+
+~~~go
+A().Text("change page title").Href("javascript:;").
+	Attr("@click", web.POST().Queries(url.Values{"title": []string{"Hello"}}).Go()),
+~~~
+
+This don't have ~.MergeQuery(true)~, So it will replace the whole query string to only ~title=Hello~
+
+`),
+).Title("Manipulate Page URL in Event Func").
+	Slug("basics/manipulate-page-url-in-event-func")

+ 20 - 0
content/advanced-functions/navigation-drawer.go

@@ -0,0 +1,20 @@
+package advanced_functions
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e15_vuetify_navigation_drawer"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var NavigationDrawer = Doc(
+	Markdown(`
+Vuetify navigation drawer provide a popup layer that show on the side of the window.
+
+Here is one example:
+`),
+	ch.Code(examples.VuetifyNavigationDrawerSample).Language("go"),
+	utils.Demo("Vuetify Navigation Drawer", e15_vuetify_navigation_drawer.VuetifyNavigationDrawerPath, "e15_vuetify_navigation_drawer/page.go"),
+).Title("Navigation Drawer").
+	Slug("vuetify-components/navigation-drawer")

+ 37 - 0
content/advanced-functions/page-func-and-event-func.go

@@ -0,0 +1,37 @@
+package advanced_functions
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e00_basics"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var PageFuncAndEventFunc = Doc(
+	Markdown(`
+~PageFunc~ is used to build a web page, ~EventFunc~ is called when user interact with the page, For example button or link clicks.
+`),
+	ch.Code(examples.PageFuncAndEventFuncDefinition).Language("go"),
+	Markdown(`~web.Page(...)~ converts multiple ~EventFunc~s along with one ~PageFunc~ to a ~http.Handler~,
+event func needs a name to be used by ~web.POST().EventFunc(name).Go()~ to attach to an html element that post http request to call the ~EventFunc~ when vue event like ~@click~ happens`),
+	Markdown("Here is a hello world with more interactions. User click the button will reload the page with latest time"),
+	ch.Code(examples.HelloWorldReloadSample).Language("go"),
+	utils.Demo("Page Func and Event Func", e00_basics.HelloWorldReloadPath, "e00_basics/hello-world-reload.go"),
+	Markdown("Note that you have to mount the `web.Page(...)` instance to http.ServeMux with a path to be able to access the ~PageFunc~ in your browser, when mounting you can also wrap the ~PageFunc~ with middleware, which is ~func(in PageFunc) (out PageFunc)~ a func that take a page func and do some wrapping and return a new page func"),
+	ch.Code(examples.HelloWorldReloadMuxSample1).Language("go"),
+	Markdown("~wb.Page(...)~ convert any `PageFunc` into `http.Handler`, outside you can wrap any middleware that can use on Go standard `http.Handler`."),
+	Markdown(`In case you don't know what is a http.Handler middleware,
+It's a function that takes http.Handler as input, might also with other parameters,
+And also return a new http.Handler,
+[gziphandler](https://github.com/nytimes/gziphandler) is an example.`),
+
+	Markdown(`But What the heck is ~demoLayout~ there?
+Well it's a ~PageFunc~ middleware. That takes an ~PageFunc~ as input,
+wrap it's ~PageResponse~ with layout html and return a new ~PageFunc~.
+If you follow the code to write your own ~PageFunc~,
+The button click might not work without this.
+Since there is no layout to import needed javascript to make this work.
+continue to next page to checkout how to add necessary javascript, css etc to make the demo work.`),
+).Title("Page Func and Event Func").
+	Slug("basics/page-func-and-event-func")

+ 43 - 0
content/advanced-functions/partial-refresh-with-portal.go

@@ -0,0 +1,43 @@
+package advanced_functions
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e00_basics"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+	. "github.com/theplant/htmlgo"
+)
+
+var PartialRefreshWithPortal = Doc(
+	Markdown(`
+As said before, The results of an ~web.EventFunc~ could be:
+
+- Go to a new page
+- Reload the whole current page
+- Refresh part of the current page
+
+We have covered two. Now let's demonstrate refresh part of the current page:
+`),
+	ch.Code(examples.PartialUpdateSample).Language("go"),
+	utils.Demo("Partial Update", e00_basics.PartialUpdatePagePath, "e00_basics/partial-update.go"),
+	Markdown(`
+~web.Portal().Name("part1")~ Place a placeholder inside you page, and append ~web.PortalUpdate~ to ~er.UpdatePortals~ to update the portal with that name.
+Multiple portal can be updated at the same time.
+`),
+	utils.Anchor(H2(""), "Load Portal in separate AJAX request"),
+	Markdown(`
+With ~web.Portal~, We can also load the portal with a separate AJAX request after page load.
+It is useful for the type of the content is not that important to the page, But load them are
+quite heavy. Like related products of a product detail page of a ECommerce site.
+`),
+	ch.Code(examples.PartialReloadSample).Language("go"),
+	utils.Demo("Partial Reload", e00_basics.PartialReloadPagePath, "e00_basics/partial-reload.go"),
+	Markdown(`
+It is not only load the portal in separate AJAX request, Also you can reload it with ease ~er.ReloadPortals = []string{"related_products"}~ in an event func.
+
+Under the hood, We use Vue's [Dynamic & Async Components](https://vuejs.org/v2/guide/components-dynamic-async.html), to load Go generated html (vue runtime templates)
+from the server and mount those vue components into the page. It works the same way for reload the whole page, push state page switch, and refresh part of the current page.
+`),
+).Title("Partial Refresh with Portal").
+	Slug("basics/partial-refresh-with-portal")

+ 30 - 0
content/advanced-functions/reload-page-with-a-flash.go

@@ -0,0 +1,30 @@
+package advanced_functions
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e00_basics"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var ReloadPageWithAFlash = Doc(
+	Markdown(`
+The results of an ~web.EventFunc~ could be:
+
+- Go to a new page
+- Reload the whole current page
+- Refresh part of the current page
+
+Let's demonstrate reload the whole current page:
+`),
+	ch.Code(examples.ReloadWithFlashSample).Language("go"),
+	utils.Demo("Reload Page With a Flash", e00_basics.ReloadWithFlashPath, "e00_basics/reload-with-a-flash.go"),
+	Markdown(`
+~ctx.Flash~ Object is used to pass data between ~web.EventFunc~ to ~web.PageFunc~ just after the event func is executed. quite similar to [Rails's Flash](https://api.rubyonrails.org/classes/ActionDispatch/Flash.html).
+Different is here you can pass in any complicated struct. as long as the page func to use that flash properly.
+
+~er.Reload = true~ tells it will reload the whole page by running page func again, and with the result's body to replace the browser's html content. the event func and page func are executed in one AJAX request in the server.
+`),
+).Title("Reload Page with a Flash").
+	Slug("basics/reload-page-with-a-flash")

+ 33 - 0
content/advanced-functions/summary-of-event-response.go

@@ -0,0 +1,33 @@
+package advanced_functions
+
+import (
+	"github.com/qor5/docs/examples"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var SummaryOfEventResponse = Doc(
+	Markdown(`
+The behaviour of ~web.EventFunc~ is controlled by it's return type ~web.EventResponse~
+`),
+	ch.Code(examples.EventResponseDefinition).Language("go"),
+	Markdown(`
+- ~PageTitle~ set the html head title, It not only set when render html page directly which is
+  request the url directly from the browser. Also use javascript to set the page title when you do
+  push state AJAX request to load the page
+- ~Body~ is the set to ~web.PageResponse~'s body when ~Reload = true~ is set, Or set to the partial
+  html component when using ~ReloadPortals~ together with ~web.Portal().EventFunc("related")~
+- ~Reload~ is to reload the ~web.PageFunc~, before reload, you can set ~ctx.Flash~ object to let the
+  event func render the page differently (flash message, validation errors, etc)
+- ~Location~ is to change the browser url with push state, and AJAX load the page of that url
+- ~RedirectURL~ is to change the browser url without AJAX, reload the whole page html includes it's
+  head script, css assets
+- ~ReloadPortals~ is for reload the portal that uses ~web.Portal().EventFunc("related")~
+- ~UpdatePortals~ update the portal specified by the name ~web.Portal().Name("hello")~, ~pu.AfterLoaded~
+  set the javascript function that execute after the portal is updated, for example:
+  ~VarsScript: "setTimeout(function(){ comp.vars.drawer2 = true }, 100)"~
+- ~Data~ is for any AJAX call that want pure JSON, you can set ~er.Data = myobj~ to any object that
+  will marshals to JSON, and on the client side use javascript to utilize them
+`),
+).Title("Summary of Event Response").
+	Slug("basics/summary-of-event-response")

+ 56 - 0
content/advanced-functions/switch-pages-with-push-state.go

@@ -0,0 +1,56 @@
+package advanced_functions
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e00_basics"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+	. "github.com/theplant/htmlgo"
+)
+
+var SwitchPagesWithPushState = Doc(
+	Markdown(`Ways that page transition (between ~web.PageFunc~) in QOR5 web app:
+
+- Use a traditional link to a new page by url
+- Use a push state link to a new page that only change the current page body to new page body and browser url
+- Use a button etc to trigger post to an ~web.EventFunc~ that do some logic, then go to a new page
+
+Inside ~web.EventFunc~, two ways go to a new page:
+
+- Use [push state](https://developer.mozilla.org/en-US/docs/Web/API/History_API#Examples) to only reload the body of the new page, This won't reload javascript and css assets.
+- Use redirect url to reload the whole new page, This will reload target new page's javascript and css assets.
+
+This example demonstrated the above:
+`),
+	ch.Code(examples.PageTransitionSample).Language("go"),
+	utils.Demo("Switch Pages With Push State", e00_basics.Page1Path, "e00_basics/page-transition.go"),
+	Markdown(`
+When running the above demo, If you check Chrome Developer Tools about Network requests,
+You will see that the Location link and the Button is actually doing an AJAX request to the other page.
+
+Look like this:
+~~~
+POST /samples/page_2?__execute_event__=__reload__ HTTP/1.1
+~~~
+
+The result is an JSON object with page's html inside.
+~__reload__~ is another ~web.EventFunc~ that is the same as ~doAction2~,
+But it is default added to every ~web.PageFunc~. So that the web page can
+both respond to normal HTTP request from Browser, Search Engine, Or from
+other pages in the same web app that can do push state link.
+`),
+	utils.Anchor(H2(""), "Summary"),
+	Markdown(`
+- Write once with PageFunc, you get both normal html page render, and AJAX JSON page render
+- EventFunc is always called with AJAX request, and you can return to a different page, or rerender the current page,
+- EventFunc is not wrapped with layout function.
+- EventFunc is used to do data operations, triggered by page's html element. and it's result can be:
+	1. Go to a new page
+	2. Reload the whole current page
+	3. Update partial of the current page
+
+Next we will talk about how to reload the whole current page, and update partial of the current page.
+`),
+).Title("Switch Pages with Push State").
+	Slug("basics/switch-pages-with-push-state")

+ 35 - 0
content/advanced-functions/the-go-html-builder.go

@@ -0,0 +1,35 @@
+package advanced_functions
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e00_basics"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var TheGoHTMLBuilder = Doc(
+	Markdown(`
+Like at the beginning we said, That we don't use interpreted template language (eg go html/template)
+to generate html page. We think they are:
+
+- error prone without static type enforcing
+- hard to refactor
+- difficult to abstract out to component
+- yet another tedious syntax to learn
+- not flexible to use helper functions
+
+We like to use standard Go code. the library [htmlgo](https://github.com/theplant/htmlgo) is just for that.
+
+Although Go can't do flexible builder syntax like [Kotlin](https://kotlinlang.org/docs/reference/type-safe-builders.html) does,
+But it can also do quite well.
+
+Consider the following code:
+`),
+	ch.Code(examples.TypeSafeBuilderSample).Language("go"),
+	Markdown(`
+It's basically assembled what Kotlin can do, Also is legitimate Go code.
+`),
+	utils.Demo("The Go HTML Builder", e00_basics.TypeSafeBuilderSamplePath, "e00_basics/type-safe-builder-sample.go"),
+).Title("The Go HTML builder").
+	Slug("advanced-functions/the-go-html-builder")

+ 50 - 0
content/advanced-functions/web-scope.go

@@ -0,0 +1,50 @@
+package advanced_functions
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e00_basics"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var WebScope = Doc(
+	Markdown(`
+
+### Use Locals to init vue variables
+
+There is a concept of reactive object in vue. Reactive object can trigger view updates, and [Vue cannot detect normal property additions (e.g. this.myObject.newProperty = 'hi')](https://vuejs.org/v2/api/#Vue-set).
+We pre-set the "locals" object as a reactive object, and then we can initialize various types of values and slot it into "locals". And the valid scopes of these values are all inside web.Scope().
+
+For example:
+`),
+	ch.Code(examples.WebScopeUseLocalsSample1).Language("go"),
+	utils.Demo("Web Scope Use Locals", e00_basics.WebScopeUseLocalsPagePath, "e00_basics/web-scope.go"),
+	Markdown(`
+Use ~web.Scope()~ to determine the effective scope of the variable, then use ~.Init(...).VSlot("{ locals }")~ to initialize the variable and slot it into the ~locals~ object.
+
+In ~VBtn("")~, you can use the ~click~ event to change the variable value in ~locals~ to achieve the effect that the page changes with the click.
+
+In ~VBtn("Test Can Not Change Other Scope")~, values in ~locals~ will not change with the click, because the button is not in ~web.Scope()~.
+
+Video Tutorial (<https://www.youtube.com/watch?v=UPuBvVRhUr0>)
+`),
+
+	Markdown(`
+
+### Use PlaidForm
+
+The main use of PlaidForm is to submit one form which is inside another form, and the two forms are completely independent forms.
+
+In the following example, each color represents a completely separate form. The ~Material Form~ contains the ~Raw Material Form~. You can submit the ~Raw Material Form~ to the server first. After receiving it, server will save the ~Raw Material data~ and return the ~ID~.
+In this way, you can submit ~Raw Material ID~ directly in the ~Material Form~.
+
+For example:
+`),
+	ch.Code(examples.WebScopeUsePlaidFormSample1).Language("go"),
+	utils.Demo("Web Scope Use PlaidForm", e00_basics.WebScopeUsePlaidFormPagePath, "e00_basics/web-scope.go"),
+	Markdown(`
+Use ~web.Scope().VSlot("{ plaidForm }")~ to determine the scope of a form.
+`),
+).Title("Scope Component").
+	Slug("basics/scope-component")

BIN
content/assets/logo.png


+ 24 - 0
content/basics/auto-complete.go

@@ -0,0 +1,24 @@
+package basics
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e10_vuetify_autocomplete"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var AutoComplete = Doc(
+	Markdown(`
+AutoComplete is a more advanced component that vuetify provides, We extend it
+So that it can fetch remote options from an event func. here we show these examples:
+
+- An auto complete that you can select multiple with static data
+- An auto complete that you can select multiple with remote fetched dynamic data
+- A static normal select component
+
+`),
+	ch.Code(examples.VuetifyAutoCompleteSample).Language("go"),
+	utils.Demo("Vuetify AutoComplete", e10_vuetify_autocomplete.VuetifyAutoCompletePath, "e10_vuetify_autocomplete/page.go"),
+).Title("Auto Complete").
+	Slug("vuetify-components/auto-complete")

+ 20 - 0
content/basics/basic-inputs.go

@@ -0,0 +1,20 @@
+package basics
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e11_vuetify_basic_inputs"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var BasicInputs = Doc(
+	Markdown(`
+Vuetify provides many form basic inputs, and also with error messages display on fields.
+
+Here is one example:
+`),
+	ch.Code(examples.VuetifyBasicInputsSample).Language("go"),
+	utils.Demo("Vuetify Basic Inputs", e11_vuetify_basic_inputs.VuetifyBasicInputsPath, "e11_vuetify_basic_inputs/page.go"),
+).Title("Basic Inputs").
+	Slug("vuetify-components/basic-inputs")

+ 62 - 0
content/basics/editing-customizations.go

@@ -0,0 +1,62 @@
+package basics
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e21_presents"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+	. "github.com/theplant/htmlgo"
+)
+
+var EditingCustomizations = Doc(
+	Markdown(`
+Editing an object will be always in a drawer popup. select which fields can edit for each model
+by using the ~.Only~ func of ~EditingBuilder~, There are different ways to configure the type
+of component that is used to do the editing.
+
+`),
+	utils.Anchor(H2(""), "Configure field for a single model"),
+	Markdown(`
+Use a customized component is as simple as add the extra asset to the preset instance.
+And configure the component func on the field:
+`),
+	ch.Code(examples.PresetsEditingCustomizationDescriptionSample).Language("go"),
+	utils.Demo("Presets Editing Customization Description Field", e21_presents.PresetsEditingCustomizationDescriptionPath+"/customers", "e21_presents/editing.go"),
+	Markdown(`
+- Added the tiptap javascript and css component pack as an extra asset
+- Configure the description field to use the component func that returns the ~tiptap.TipTapEditor()~ component
+- Set the field name and value of the component
+`),
+	utils.Anchor(H2(""), "Configure field type for all models"),
+	Markdown(`
+Set a global field type to component func like the following:
+`),
+	ch.Code(examples.PresetsEditingCustomizationFileTypeSample).Language("go"),
+	utils.Demo("Presets Editing Customization File Type", e21_presents.PresetsEditingCustomizationFileTypePath+"/products", "e21_presents/editing.go"),
+	Markdown(`
+- We define ~MyFile~ to actually be a string
+- We set ~FieldDefaults~ for writing, which is the editing drawer popup to be a customized component
+- The component show an img tag with the string as src if it's not empty
+- The component add a file input for user to upload new file
+- The ~SetterFunc~ is called before save the object, it uploads the file to transfer.sh, and get the url back,
+  then set the value to ~MainImage~ field
+
+With ~FieldDefaults~ we can write libraries that add customized type for different models to reuse. It can take care
+of how to display the edit controls, and How to save the object.
+
+`),
+	utils.Anchor(H2(""), "Validation"),
+	Markdown(`
+Field level validation and display on field can be added by implement ~ValidateFunc~,
+and set the ~web.ValidationErrors~ result:
+`),
+	ch.Code(examples.PresetsEditingCustomizationValidationSample).Language("go"),
+	utils.Demo("Presets Editing Customization Validation", e21_presents.PresetsEditingCustomizationValidationPath+"/customers", "e21_presents/editing.go"),
+	Markdown(`
+- We validate the ~Name~ of the customer must be longer than 10
+- If the error happens, If will show below the field
+
+`),
+).Title("Editing Customizations").
+	Slug("presets-guide/editing-customizations")

+ 25 - 0
content/basics/filter.go

@@ -0,0 +1,25 @@
+package basics
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e21_presents"
+	"github.com/qor5/docs/utils"
+	"github.com/theplant/docgo/ch"
+
+	. "github.com/theplant/docgo"
+)
+
+var Filter = Doc(
+	Markdown(`
+
+To add a basic filter to the list page
+
+For example:
+`),
+	ch.Code(examples.FilterSample).Language("go"),
+	utils.Demo("Basic filter", e21_presents.PresetsBasicFilterPath+"/customers", "e21_presents/filter.go"),
+	Markdown(`
+	Call ~FilterDataFunc~ on a ~ListingBuilder~
+`),
+).Title("Filters").
+	Slug("basics/filter")

+ 30 - 0
content/basics/form-handling.go

@@ -0,0 +1,30 @@
+package basics
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e00_basics"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var FormHandling = Doc(
+	Markdown(`
+Form handling is an important part of web development. to make handling form easy,
+we have a global form that always be submitted with any event func. What you need to do
+is just to give an input a name.
+
+For example:
+`),
+	ch.Code(examples.FormHandlingSample).Language("go"),
+	utils.Demo("Form Handling", e00_basics.FormHandlingPagePath, "e00_basics/form-handling.go"),
+	Markdown(`
+Use ~.Attr(web.VFieldName("Abc")...)~ to set the field name, make the name matches your data struct field name.
+So that you can ~ctx.UnmarshalForm(&fv)~ to set the values to data object. value of input must be set manually to set the initial value of form field.
+
+The fields which are bind with ~.Attr(web.VFieldName("Abc")...)~ are always submitted with every event func. A browser refresh, new page load will clear the form value.
+
+~web.Scope(...).VSlot("{ plaidForm }")~ to nest a new form inside outside form, EventFunc inside will only post form values inside the scope.
+`),
+).Title("Form Handling").
+	Slug("basics/form-handling")

+ 24 - 0
content/basics/linkage-select.go

@@ -0,0 +1,24 @@
+package basics
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e21_presents"
+	"github.com/qor5/docs/examples/e24_vuetify_components_linkage_select"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var LinkageSelect = Doc(
+	Markdown(`
+LinkageSelect is a component for multi-level linkage select.
+    `),
+	ch.Code(examples.VuetifyComponentsLinkageSelect).Language("go"),
+	utils.Demo("Vuetify LinkageSelect", e24_vuetify_components_linkage_select.VuetifyComponentsLinkageSelectPath, "e24_vuetify_components_linkage_select/page.go"),
+	Markdown(`
+### Filter intergation
+    `),
+	ch.Code(examples.LinkageSelectFilterItem).Language("go"),
+	utils.Demo("LinkageSelect Filter Item", e21_presents.PresetsLinkageSelectFilterItemPath+"/addresses", "e21_presents/linkage_select_filter_item.go"),
+).Title("Linkage Select").
+	Slug("vuetify-components/linkage-select")

+ 83 - 0
content/basics/listing-customizations.go

@@ -0,0 +1,83 @@
+package basics
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e21_presents"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+	. "github.com/theplant/htmlgo"
+)
+
+var ListingCustomizations = Doc(
+	Markdown(`
+We get a default listing page with default columns, But default columns from database
+columns rarely fit the needs for any real application.
+
+`),
+	utils.Anchor(H2(""), "Change List Columns and Component of Field"),
+	Markdown(`
+Here is how do we change the columns of the list and how to we change the content display of a columns.
+`),
+	ch.Code(examples.PresetsListingCustomizationFieldsSample).Language("go"),
+	utils.Demo("Presets Listing Customization Fields", e21_presents.PresetsListingCustomizationFieldsPath+"/customers", "e21_presents/listing.go"),
+	Markdown(`
+What we did with above code:
+
+- Added a new field to listing table that not exists on the struct ~Customer~
+- Define the listing display for the listing table by using the ~Td()~ and fetch the company data from a different table with associated column value
+- Link the company name in the listing to link the edit drawer of company
+- Limit the edit drawer field to only have ~Name~ and ~CompanyID~
+- Made the ~CompanyID~ field a vuetify ~VSelect~ component
+- Add companies as a new navigation item, that you can manage companies data
+- ~.SearchColumns("name", "email")~ configure the top navigation search box searches which columns with sql like operation
+`),
+
+	utils.Anchor(H2(""), "Filters Panel"),
+	Markdown(`
+Here we continue to add filters for the list
+`),
+	ch.Code(examples.PresetsListingCustomizationFiltersSample).Language("go"),
+	utils.Demo("Presets Listing Filters", e21_presents.PresetsListingCustomizationFiltersPath+"/customers", "e21_presents/listing.go"),
+	Markdown(`
+~FilterDataFunc~ of ~presets.ListingBuilder~ setup to have the filter menu or not.
+And how it will combine the sql conditions when doing query. the filter menu will
+change the url query strings with the filter values, and for date type in url query
+string it uses unix epoch int value. So the sql condition has to convert the database
+column data to unix epoch in order to compare with the value in url query string.
+
+Current we support these types
+
+- ~ItemTypeDate~: set it as a date filter item, which have many switches to support date and date range
+- ~ItemTypeNumber~: set it to a number filter item, which have switches to support number and number range
+- ~ItemTypeString~: set it to a string filter item, which have contains, and match exactly
+- ~ItemTypeSelect~: set it to a select filter item, which have a options of values for selection
+`),
+
+	utils.Anchor(H2(""), "Filter Tabs"),
+	Markdown(`
+Filter tabs is based on Filters configuration. But display as tabs above the list,
+You can think it as a short cut that used very frequently to filter something instead of
+use the pop up panel of filter.
+`),
+	ch.Code(examples.PresetsListingCustomizationTabsSample).Language("go"),
+	utils.Demo("Presets Listing Filter Tabs", e21_presents.PresetsListingCustomizationTabsPath+"/customers", "e21_presents/listing.go"),
+	Markdown(`
+~Query~ string name must be from the Filter's item configuration key field.
+`),
+
+	utils.Anchor(H2(""), "Bulk Actions"),
+	Markdown(`
+Bulk actions makes the list row show checkboxes, and you can select one or many rows,
+Later do an bulk update data for all of them.
+
+Here is how to use it:
+`),
+	ch.Code(examples.PresetsListingCustomizationBulkActionsSample).Language("go"),
+	utils.Demo("Presets Listing Bulk Actions", e21_presents.PresetsListingCustomizationBulkActionsPath+"/customers", "e21_presents/listing.go"),
+	Markdown(`
+- ~ComponentFunc~ of the bulk action configure the component that will show to user to input after user clicked the bulk action button
+- ~UpdateFunc~ configure the logic that the bulk action execute
+`),
+).Title("Listing Customizations").
+	Slug("basics/listing-customizations")

+ 101 - 0
content/basics/listing.go

@@ -0,0 +1,101 @@
+package basics
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/example_basics"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var Listing = Doc(
+	Markdown(`
+By the [1 Minute Quick Start](/getting-started/one-minute-quick-start.html), We get a default listing page with default columns, But default columns from database columns rarely fit the needs for any real application. Here we will introduce common customizations on the list page.
+
+- Configure fields that displayed on the page
+- Modify the display value
+- Display a virtual field
+- Default scope
+- Extend the dot menu
+
+There would be a runable example at the last.
+
+## Configure fields that displayed on the page
+Suppose we added a new model called ~Category~, the ~Post~ belongs to ~Category~. Then we want to display ~CategoryID~ on the list page.
+`),
+
+	ch.Code(`
+type Post struct {
+	ID    uint
+	Title string
+	Body  string
+
+	CategoryID uint
+
+	UpdatedAt time.Time
+	CreatedAt time.Time
+}
+
+type Category struct {
+	ID   uint
+	Name string
+
+	UpdatedAt time.Time
+	CreatedAt time.Time
+}
+
+postModelBuilder.Listing("ID", "Title", "Body", "CategoryID")
+`),
+
+	Markdown(`
+## Modify the display value
+To display the category name rather than category id in the post listing page. The ~ComponentFunc~ would do the work.
+The ~obj~ is the ~Post~ record, and ~field~ is the ~CategoryID~ field of this ~Post~ record. You can get the value by ~field.Value(obj)~ function.
+`),
+
+	ch.Code(`postModelBuilder.Listing().Field("CategoryID").Label("Category").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
+	c := models.Category{}
+	cid, _ := field.Value(obj).(uint)
+	if err := db.Where("id = ?", cid).Find(&c).Error; err != nil {
+		// ignore err in the example
+	}
+	return h.Td(h.Text(c.Name))
+})
+`).Language("go"),
+
+	Markdown(`
+## Display virtual fields
+`),
+	ch.Code(`postModelBuilder.Listing("ID", "Title", "Body", "CategoryID", "VirtualValue")
+postModelBuilder.Listing().Field("VirtualField").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
+	return h.Td(h.Text("virtual field"))
+})
+`),
+
+	Markdown(`
+## DefaultScope
+If we want to display ~Post~ with ~category_id~ only. Use the ~Listing().Searcher~ to apply SQL conditions.
+`),
+	ch.Code(`postModelBuilder.Listing().Searcher = func(model interface{}, params *presets.SearchParams, ctx *web.EventContext) (r interface{}, totalCount int, err error){
+	qdb := db.Where("category_id != 0")
+	return gorm2op.DataOperator(qdb).Search(model, params, ctx)
+}
+`),
+
+	Markdown(`
+## Extend the dot menu
+You can extend the dot menu by calling the ~RowMenuItem~ function. If you want to overwrite the default ~Edit~ and ~Delete~ link, you can pass the items you wanted to ~Listing().RowMenu()~
+`),
+	ch.Code(`rmn := postModelBuilder.Listing().RowMenu()
+rmn.RowMenuItem("Show").ComponentFunc(func(obj interface{}, id string, ctx *web.EventContext) h.HTMLComponent {
+	return h.Text("Fake Show")
+})
+`),
+
+	Markdown(`
+## Full Example
+`),
+	ch.Code(examples.PresetsListingSample).Language("go"),
+	utils.Demo("Presets Listing Customization Fields", example_basics.ListingSamplePath+"/posts", "example_basics/listing.go"),
+).Title("Listing Page").
+	Slug("basics/listing")

+ 22 - 0
content/basics/notification-center.go

@@ -0,0 +1,22 @@
+package basics
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e21_presents"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var NotificationCenter = Doc(
+	Markdown(`
+To enable notification center: Call ~NotificationFunc~ on ~presets.Builder~ With 2 function parameters
+like this ~builder.NotificationFunc(NotifierComponent(), NotifierCount())~
+
+The first function is for rendering the content of the popup after user clicked the "bell icon".
+The second function is for rendering the number at the top right corner of the "bell icon".
+`),
+
+	ch.Code(examples.NotificationCenterSample).Language("go"),
+	utils.Demo("Notification Center", e21_presents.NotificationCenterSamplePath+"/pages", "e00_basics/notification-center.go"),
+).Slug("basics/notification-center").Title("Notification Center")

+ 22 - 0
content/basics/permissions.go

@@ -0,0 +1,22 @@
+package basics
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e21_presents"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var Permissions = Doc(
+	Markdown(`## To list all the permissions in your project`),
+	ch.Code(`perm.Verbose = true`).Language("go"),
+	Markdown(`Then reboot your app, you can see all the permissions in the console`),
+
+	Markdown(`
+## Permissions sample:
+`),
+	ch.Code(examples.PresetsPermissionsSample).Language("go"),
+	utils.Demo("Permissions Demo", e21_presents.PresetsPermissionsPath+"/customers", "e21_presents/permissions.go"),
+).Title("Permissions").
+	Slug("presets-guide/permissions")

+ 24 - 0
content/basics/shortcut.go

@@ -0,0 +1,24 @@
+package basics
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e00_basics"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var ShortCut = Doc(
+	Markdown(`
+To add keyboard shortcut to a button:
+
+Trigger the event by [GlobalEvents](https://www.npmjs.com/package/vue-global-events).
+You can configure your own keyboard event like ~@keyup.ctrl.enter~ to trigger the event.
+
+Also you can setup the ~filter~ function to limit when this event can be triggered by shortcut.
+In the example, the event would only be triggered when ~locals.shortCutEnabled~ is opened.
+`),
+
+	ch.Code(examples.ShortCutSample).Language("go"),
+	utils.Demo("Shortcut", e00_basics.ShortCutSamplePath, "e00_basics/shortcut.go"),
+).Slug("basics/shortcut").Title("Keyboard Shortcut")

+ 20 - 0
content/basics/variant-sub-form.go

@@ -0,0 +1,20 @@
+package basics
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e22_vuetify_variant_sub_form"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var VariantSubForm = Doc(
+	Markdown(`
+VSelect changes, the form below it will change to a new form accordingly.
+
+By use of ~web.Portal()~ and ~VSelect~'s ~OnInput~
+`),
+	ch.Code(examples.VuetifyVariantSubForm).Language("go"),
+	utils.Demo("Vuetify Variant Sub Form", e22_vuetify_variant_sub_form.VuetifyVariantSubFormPath, "e22_vuetify_variant_sub_form/page.go"),
+).Title("Variant Sub Form").
+	Slug("vuetify-components/variant-sub-form")

+ 49 - 0
content/digging-deeper/composite-new-component-with-go.go

@@ -0,0 +1,49 @@
+package digging_deeper
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e00_basics"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var CompositeNewComponentWithGo = Doc(
+	Markdown(`
+Any Go function that returns an ~htmlgo.HTMLComponent~ is a component,
+Any Go struct that implements ~MarshalHTML(ctx context.Context) ([]byte, error)~ function is an component.
+They can be composite into a new component very easy.
+
+This example is ported from [Bootstrap4 Navbar](https://getbootstrap.com/docs/4.3/components/navbar/):
+`),
+	ch.Code(examples.CompositeComponentSample1).Language("go"),
+	utils.Demo("Composite New Component With Go", e00_basics.CompositeComponentSample1PagePath, "e00_basics/composite-components.go"),
+	Markdown(`
+You can see from the example, We have created ~Navbar~ and ~Carousel~ components by
+simply create Go func that returns ~htmlgo.HTMLComponent~.
+It is easy to pass in components as parameter, and wrap components.
+By utilizing the power of Go language, Any component can be abstracted and reused with enough parameters.
+
+The ~Navbar~ is a responsive navigation header, Resizing your window, the nav bar will react to device window size and change to nav bar popup and hide search form.
+
+For this ~Navbar~ component to work, I have to import Bootstrap assets in this new layout function:
+`),
+	ch.Code(examples.DemoBootstrapLayoutSample).Language("go"),
+
+	Markdown(`
+You can utilize the command line tool [html2go](https://github.com/sunfmin/html2go) to convert existing html code to htmlgo code.
+By writing html in Go you get:
+
+- The static type checking
+- Abstract out easily to different functions
+- Easier refactor with IDE like GoLand
+- Loop and variable replacing is just like in Go
+- Invoke helper functions is just like in Go
+- Almost as readable as normal HTML
+- Not possible to have html tag not closed, Or not matched.
+
+Once you have these, Why generate html in any interpreted template language!
+
+`),
+).Title("Composite new Component With Go").
+	Slug("components-guide/composite-new-component-with-go")

+ 113 - 0
content/digging-deeper/integrate-a-heavy-vue-component.go

@@ -0,0 +1,113 @@
+package digging_deeper
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e00_basics"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+)
+
+var IntegrateAHeavyVueComponent = Doc(
+	Markdown(`
+We can abstract any complicated of server side render component with [htmlgo](https://github.com/theplant/htmlgo).
+But a lots of components in the modern web have done many things on the client side. means there are many logic
+happens before the it interact with server side.
+
+Here is an example, a rich text editor. you have a toolbar of buttons that you can interact, most of them won't
+need to communicate with server. We are going to integrate the fantastic rich text editor [tiptap](https://tiptap.scrumpy.io/)
+to be used as any ~htmlgo.HTMLComponent~.
+
+**Step 1**: [Create a @vue/cli project](https://cli.vuejs.org/guide/creating-a-project.html):
+
+~~~
+$ vue create tiptapjs
+~~~
+
+Modify or add a separate ~vue.config.js~ config file,
+
+`),
+	ch.Code(examples.TipTapVueConfig).Language("javascript"),
+
+	Markdown(`
+- Enable ~runtimeCompiler~ so that vue can parse template html generate from server.
+- Made ~Vue~ as externals so that it won't be packed to the dist production js file,
+  Since we will be sharing one Vue.js for in one page with other libraries.
+- Config svg module to inline the svg icons used by tiptap
+
+**Step 2**: Create a vue component that use tiptap
+
+Install ~tiptap~ and ~tiptap-extensions~ first
+~~~
+$ yarn add tiptap tiptap-extensions
+~~~
+
+And write the ~editor.vue~ something like this, We omitted the template at here.
+
+`),
+	ch.Code(examples.TipTapEditorVueComponent).Language("javascript"),
+	Markdown(`
+We injected the ~this.$plaid()~. that is from ~web/corejs~, Which you will need to use
+For every Go Plaid web applications. Here we uses one function ~fieldValue~ from it.
+It set the form value when the rich text editor changes. So that later when you call
+~EventFunc~ it the value will be posted to the server side. Here we will post the html value.
+Also allow component user to set ~fieldName~, which is important when posting the value to the
+server.
+
+**Step 3**: At ~main.js~, Use a special hook to register the component to ~web/corejs~
+
+`),
+	ch.Code(examples.QOR5RegisterVueComponentSample).Language("go"),
+	Markdown(`
+**Step 4**: Test the component in a simple html
+
+We edited the ~index.html~ inside public to be the following:
+
+`),
+	ch.Code(examples.TipTapDemoHTML).Language("html"),
+	Markdown(`
+- For ~http://localhost:3500/app.js~ to be able to serve. you have to run ~yarn serve~ in
+tiptapjs directory.
+- ~http://localhost:3100/app.js~ is QOR5 web corejs vue project.
+  So go to that directory and run ~yarn serve~ to start it. and then in
+- Run a web server inside tiptapjs directory like ~python -m SimpleHTTPServer~ and point your
+  Browser to the index.html file, and see if your vue component can render and behave correctly.
+
+**Step 5**: Use [packr](https://github.com/gobuffalo/packr) to pack the dist folder
+
+We write a packr box inside ~tiptapjs.go~ along side the tiptapjs folder.
+`),
+	ch.Code(examples.TipTapPackrSample).Language("go"),
+	Markdown(`
+And write a ~build.sh~ to build the javascript to production version, and run packr to pack
+them into ~a_tiptap-packr.go~ file.
+`),
+	ch.Code(examples.TiptapBuilderSH).Language("bash"),
+
+	Markdown(`
+**Step 6**: Write a Go wrapper to wrap it to be a ~HTMLComponent~
+`),
+	ch.Code(examples.TipTapEditorHTMLComponent).Language("go"),
+
+	Markdown(`
+**Step 7**: Use it in your web app
+
+To use it, first we have to mount the assets into our app
+`),
+	ch.Code(examples.TipTapComponentsPackSample).Language("go"),
+	Markdown(`
+And reference them in our layout function.
+`),
+	ch.Code(examples.TipTapLayoutSample).Language("go"),
+
+	Markdown(`
+And we write a page func to use it like any other component:
+`),
+	ch.Code(examples.HelloWorldTipTapSample).Language("go"),
+
+	Markdown(`
+And now let's check out our fruits:
+`),
+	utils.Demo("Integrate a Heavy Vue Component", e00_basics.HelloWorldTipTapPath, "e00_basics/use-tiptap-editor.go"),
+).Title("Integrate a heavy Vue Component").
+	Slug("components-guide/integrate-a-heavy-vue-component")

+ 32 - 0
content/getting-started/one-minute-quick-start.go

@@ -0,0 +1,32 @@
+package getting_started
+
+import (
+	. "github.com/theplant/docgo"
+)
+
+var OneMinuteQuickStart = Doc(
+	Markdown(`
+This article try to let you use the shortest time to get a taste of how powerful QOR5 is.
+
+One of the QOR5 module called ~presets~ that can quickly create admin interface like [these](/samples/presets-detail-page-cards/customers):
+
+Install the command line tool with:
+
+~~~
+$ go install github.com/qor5/admin/generator@master
+~~~
+
+And run:
+
+~~~
+$ qor5 init
+~~~
+
+It will promote you to input a Go package, and create the admin app in current directory.
+
+Change to the created package directory, and use ~docker-compose up~ to start the database, and then
+Use a new terminal to run ~source dev_env && go run main.go~ to start the admin app
+
+`),
+).Title("1 Minute Quick Start").
+	Slug("getting-started/one-minute-quick-start")

+ 50 - 0
content/getting-started/what-is-goplaid.go

@@ -0,0 +1,50 @@
+package getting_started
+
+import (
+	"github.com/qor5/docs/examples"
+	"github.com/qor5/docs/examples/e00_basics"
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	"github.com/theplant/docgo/ch"
+	. "github.com/theplant/htmlgo"
+)
+
+var WhatIsQOR5 = Doc(
+	Markdown(`
+QOR5 is yet another Go library to build web applications.
+different from other MVC frameworks. the concepts in QOR5 is **Page**, **Event**, **Component**.
+and doesn't include Model.
+
+A Page composite different kinds of Components, and Components trigger Events.
+A Page contains many event handlers, and renders one view, and event handlers reload the whole page,
+Or update certain part of the page, Or go to a different Page.
+
+QOR5 is opinionated in several ways:
+
+- It prefers writing HTML in static typing Go language, rather than a certain type of template language, Not even go template.
+- It try to minify the needs to write any JavaScript/Typescript for building interactive web applications
+- It maximize the reusability of Components. since it uses Go to write components, You can abstract component very easy, and use component from a third party Go package is also like using normal Go packages.
+- It prefers chain methods to set optional parameters of Component
+- It uses [Vue](https://vuejs.org/) js under the hood. and only Vue Component can be integrated
+
+`),
+	utils.Anchor(H2(""), "Hello World"),
+	Markdown(`
+Here is the most sample hello world, that show the header with Hello World.
+`),
+	ch.Code(examples.HelloWorldSample).Language("go"),
+	Markdown(`
+~H1("Hello World")~ is actually a simple component. it renders h1 html tag. and been set to page body.
+
+The above is the code you mostly writing. the following is the boilerplate code that needs to write one time.
+`),
+	ch.Code(examples.HelloWorldMuxSample1).Language("go"),
+	ch.Code(examples.HelloWorldMuxSample2).Language("go"),
+	ch.Code(examples.HelloWorldMainSample).Language("go"),
+	utils.Demo("Hello World", e00_basics.HelloWorldPath, "e00_basics/hello-world.go"),
+
+	Markdown(`
+If you wondering why ~H1("Hello World")~ and how this worked, Please go ahead and checkout next page
+`),
+).Title("What is QOR5?").
+	Slug("getting-started/what-is-qor5")

+ 34 - 0
content/home.go

@@ -0,0 +1,34 @@
+package content
+
+import (
+	"embed"
+
+	"github.com/qor5/docs/utils"
+	. "github.com/theplant/docgo"
+	. "github.com/theplant/htmlgo"
+)
+
+var Home = Doc(
+	Markdown(`
+QOR5 is yet another Go library to build web applications. We aim to accelerate the development speed and make the website highly customizable.
+
+- It prefers writing HTML in [static typing Go language](/advanced-functions/the-go-html-builder.html), rather than a certain type of template language, Not even go template.
+- It try to minify the needs to write any JavaScript/Typescript for building interactive web applications
+- It maximize the reusability of Components. since it uses Go to write components, You can abstract component very easy, and use component from a third party Go package is also like using normal Go packages.
+	`),
+
+	utils.Anchor(H2(""), "How is this document organized"),
+	Markdown(`
+Most of latter examples are based on the initial sample project. In another word, we will demonstrate how to build a rich functioned website by this document.
+
+- First, we will start with a quick sample project that would give you a rough but visual idea of what QOR5 can do.
+- Second, we will introduce the basic functions, The sequence is from listing page to editing page. You can find all commonly used Admin website features in this section.
+- Third, we will introduce the essentials of QOR5 and advanced functions, You would understand how QOR5 render a page and advanced features like "how to partially refresh a page".
+- At last, the digging deeper part, you would learn how to create new component for QOR5
+
+**Join the Discord community**: https://discord.gg/76YPsVBE4E
+`)).Title("Introduction").
+	Slug("/")
+
+//go:embed assets/**.*
+var Assets embed.FS

BIN
examples/.DS_Store


+ 159 - 0
examples/e00_basics/composite-components.go

@@ -0,0 +1,159 @@
+package e00_basics
+
+// @snippet_begin(CompositeComponentSample1)
+import (
+	"fmt"
+
+	"github.com/qor5/web"
+	. "github.com/theplant/htmlgo"
+)
+
+func Navbar(title string, activeIndex int, items ...HTMLComponent) HTMLComponent {
+	ul := Ul().Class("navbar-nav mr-auto")
+
+	for i, item := range items {
+		ul.AppendChildren(
+			Li(
+				item,
+			).Class("nav-item").ClassIf("active", activeIndex == i),
+		)
+	}
+
+	return Nav(
+		A(Text(title)).Class("navbar-brand").
+			Href("#"),
+
+		Button("").Class("navbar-toggler").
+			Type("button").
+			Attr("data-toggle", "collapse").
+			Attr("data-target", "#navbarNav").
+			Attr("aria-controls", "navbarNav").
+			Attr("aria-expanded", "false").
+			Attr("aria-label", "Toggle navigation").
+			Children(
+				Span("").Class("navbar-toggler-icon"),
+			),
+
+		Div(
+			ul,
+			Form(
+				Input("").Class("form-control mr-sm-2").
+					Type("search").
+					Placeholder("Search").
+					Attr("aria-label", "Search"),
+				Button("Search").Class("btn btn-outline-light my-2 my-sm-0").
+					Type("submit"),
+			).Class("form-inline my-2 my-lg-0"),
+		).Class("collapse navbar-collapse").
+			Id("navbarNav"),
+	).Class("navbar navbar-expand-lg navbar-dark bg-primary")
+}
+
+type CarouselItem struct {
+	ImageSrc string
+	ImageAlt string
+}
+
+func Carousel(carouselId string, activeIndex int, items []*CarouselItem) HTMLComponent {
+	var indicators = Ol().Class("carousel-indicators")
+	var carouselInners = Div().Class("carousel-inner")
+
+	for i, item := range items {
+		indicators.AppendChildren(
+			Li().Attr("data-target", "#"+carouselId).
+				Attr("data-slide-to", fmt.Sprint(i)).
+				ClassIf("active", activeIndex == i),
+		)
+
+		carouselInners.AppendChildren(
+			Div(
+				fakeImage(item.ImageAlt),
+			).Class("carousel-item").ClassIf("active", activeIndex == i).Style("font-size: 3.5rem;"),
+		)
+	}
+
+	return Div(
+		indicators,
+		carouselInners,
+		A(
+			Span("").Class("carousel-control-prev-icon").
+				Attr("aria-hidden", "true"),
+			Span("Previous").Class("sr-only"),
+		).Class("carousel-control-prev").
+			Href("#"+carouselId).
+			Role("button").
+			Attr("data-slide", "prev"),
+		A(
+			Span("").Class("carousel-control-next-icon").
+				Attr("aria-hidden", "true"),
+			Span("Next").Class("sr-only"),
+		).Class("carousel-control-next").
+			Href("#"+carouselId).
+			Role("button").
+			Attr("data-slide", "next"),
+	).Id(carouselId).
+		Class("carousel slide").
+		Attr("data-ride", "carousel")
+}
+
+func CompositeComponentSample1Page(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = Div(
+		Navbar(
+			"Hello",
+			1,
+
+			A(
+				Text("Home"),
+			).Class("nav-link").
+				Href("#"),
+
+			A(
+				Text("Features"),
+			).Class("nav-link").
+				Href("#"),
+
+			A(
+				Text("Pricing"),
+			).Class("nav-link").
+				Href("#"),
+
+			A(
+				Text("Disabled"),
+			).Class("nav-link disabled").
+				Href("#").
+				TabIndex(-1).
+				Attr("aria-disabled", "true"),
+		),
+
+		Div(
+			Div(
+				Div(
+					Carousel("hello1", 1, []*CarouselItem{
+						{
+							ImageAlt: "First slide",
+						},
+						{
+							ImageAlt: "Second slide",
+						},
+						{
+							ImageAlt: "Third slide",
+						},
+					}),
+				).Class("col-12 py-md-3 pl-md-3"),
+			).Class("row"),
+		).Class("container-fluid"),
+	)
+	return
+}
+
+var CompositeComponentSample1PagePB = web.Page(CompositeComponentSample1Page)
+
+const CompositeComponentSample1PagePath = "/samples/composite-component-sample1"
+
+// @snippet_end
+
+func fakeImage(title string) HTMLComponent {
+	return RawHTML(fmt.Sprintf(`
+<svg class="bd-placeholder-img bd-placeholder-img-lg d-block w-100" width="800" height="400" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice" focusable="false" role="img" aria-label="Placeholder: %s"><title>Placeholder</title><rect width="100%%" height="100%%" fill="#666"></rect><text x="40%%" y="50%%" fill="#444" dy=".3em">%s</text></svg>
+`, title, title))
+}

+ 268 - 0
examples/e00_basics/event-handling.go

@@ -0,0 +1,268 @@
+package e00_basics
+
+import (
+	"fmt"
+	"net/url"
+	"time"
+
+	. "github.com/qor5/ui/vuetify"
+	"github.com/qor5/web"
+	. "github.com/theplant/htmlgo"
+)
+
+// @snippet_begin(EventHandlingURLSample)
+func EventHandlingURL(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = Div(
+		VCard(
+			VCardTitle(Text("URL")),
+			VCardActions(VBtn("Go").Attr("@click", web.GET().URL(EventExamplePagePath).Go())),
+		),
+	)
+	return
+}
+
+// @snippet_end
+
+// @snippet_begin(EventHandlingPushStateSample)
+func EventHandlingPushState(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = Div(
+		VCard(
+			VCardTitle(Text("PushState")),
+			VCardActions(VBtn("Go").Attr("@click", web.GET().URL(EventExamplePagePath).PushState(true).Go())),
+		),
+	)
+	return
+}
+
+// @snippet_end
+
+// @snippet_begin(EventHandlingReloadSample)
+func EventHandlingReload(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = Div(
+		VCard(
+			VCardTitle(Text("Reload")),
+			Text(fmt.Sprintf("Now: %s", time.Now().Format(time.RFC3339Nano))),
+			VCardActions(VBtn("Reload").Attr("@click", web.POST().Reload().Go())),
+		),
+	)
+	return
+}
+
+// @snippet_end
+
+// @snippet_begin(EventHandlingQuerySample)
+func EventHandlingQuery(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = Div(
+		VCard(
+			VCardTitle(Text("Query")),
+			VCardActions(VBtn("Go").Attr("@click", web.GET().URL(EventExamplePagePath).PushState(true).Query("address", "tokyo").Go())),
+		),
+	)
+	return
+}
+
+// @snippet_end
+
+// @snippet_begin(EventHandlingMergeQuerySample)
+func EventHandlingMergeQuery(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = Div(
+		VCard(
+			VCardTitle(Text("MergeQuery")),
+			VCardActions(VBtn("Go").Attr("@click", web.GET().URL(EventExamplePagePath+"?address=beijing&name=qor5&email=qor5@theplant.jp").PushState(true).Query("address", "tokyo").MergeQuery(true).Go())),
+		),
+	)
+	return
+}
+
+// @snippet_end
+
+// @snippet_begin(EventHandlingClearMergeQuerySample)
+func EventHandlingClearMergeQueryQuery(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = Div(
+		VCard(
+			VCardTitle(Text("ClearMergeQuery")),
+			VCardActions(VBtn("Go").Attr("@click", web.GET().URL(EventExamplePagePath+"?address=beijing&name=qor5&email=qor5@theplant.jp").PushState(true).Query("address", "tokyo").ClearMergeQuery([]string{"name"}).Go())),
+		),
+	)
+	return
+}
+
+// @snippet_end
+
+// @snippet_begin(EventHandlingStringQuerySample)
+func EventHandlingStringQuery(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = Div(
+		VCard(
+			VCardTitle(Text("StringQuery")),
+			VCardActions(VBtn("Go").Attr("@click", web.GET().URL(EventExamplePagePath).PushState(true).StringQuery("address=tokyo").Go())),
+		),
+	)
+	return
+}
+
+// @snippet_end
+
+// @snippet_begin(EventHandlingQueriesSample)
+func EventHandlingQueries(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = Div(
+		VCard(
+			VCardTitle(Text("Queries")),
+			VCardActions(VBtn("Go").Attr("@click", web.GET().URL(EventExamplePagePath).PushState(true).Queries(url.Values{"address": []string{"tokyo"}}).Go())),
+		),
+	)
+	return
+}
+
+// @snippet_end
+
+// @snippet_begin(EventHandlingPushStateURLSample)
+func EventHandlingPushStateURL(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = Div(
+		VCard(
+			VCardTitle(Text("PushStateURL")),
+			VCardActions(VBtn("Go").Attr("@click", web.GET().PushStateURL(EventExamplePagePath).Go())),
+		),
+	)
+	return
+}
+
+// @snippet_end
+
+// @snippet_begin(EventHandlingLocationSample)
+func EventHandlingLocation(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = Div(
+		VCard(
+			VCardTitle(Text("Location")),
+			VCardActions(VBtn("Go").Attr("@click", web.POST().PushState(true).Location(&web.LocationBuilder{MyURL: EventExamplePagePath, MyStringQuery: "address=test"}).Go())),
+		),
+	)
+	return
+}
+
+// @snippet_end
+
+// @snippet_begin(EventHandlingFieldValueSample)
+func EventHandlingFileValue(ctx *web.EventContext) (pr web.PageResponse, err error) {
+
+	pr.Body = Div(
+		VCard(
+			VCardTitle(Text("FieldValue")),
+			VCardActions(VBtn("Go").Attr("@click", web.POST().EventFunc("form").FieldValue("name", "qor5").Go())),
+		),
+	)
+	return
+}
+
+// @snippet_end
+
+// @snippet_begin(EventHandlingFormClearSample)
+func EventHandlingFormClear(ctx *web.EventContext) (pr web.PageResponse, err error) {
+
+	pr.Body = Div(
+		VCard(
+			VCardTitle(Text("FormClear")),
+			VCardActions(VBtn("Go").Attr("@click", web.POST().EventFunc("form").FieldValue("name", "qor5").FormClear().Go())),
+		),
+	)
+	return
+}
+
+// @snippet_end
+
+// @snippet_begin(EventHandlingEventFuncSample)
+func EventHandlingEventFunc(ctx *web.EventContext) (pr web.PageResponse, err error) {
+
+	pr.Body = Div(
+		VBtn("Go").Attr("@click", web.POST().EventFunc("hello").Go()),
+	)
+	return
+}
+
+// @snippet_end
+
+// @snippet_begin(EventHandlingBeforeScriptSample)
+func EventHandlingScript(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = Div(
+		VCard(
+			VCardTitle(Text("Script")),
+			VCardActions(VBtn("Go").Attr("@click", web.POST().ThenScript(`alert("this is then script")`).AfterScript(`alert("this is after script")`).BeforeScript(`alert("this is before script")`).Go())),
+		),
+	)
+	return
+}
+
+// @snippet_end
+
+// @snippet_begin(EventHandlingRawSample)
+func EventHandlingRaw(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = Div(
+		VCard(
+			VCardTitle(Text("Raw")),
+			VCardActions(VBtn("Go").Attr("@click", web.POST().Raw(`pushStateURL("/samples/event_handling/example")`).Go())),
+		),
+	)
+	return
+}
+
+// @snippet_end
+
+func EventHandlingPage(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	api := ctx.R.URL.Query().Get("api")
+	switch api {
+	case "url":
+		return EventHandlingURL(ctx)
+	case "pushstate":
+		return EventHandlingPushState(ctx)
+	case "eventfunc":
+		return EventHandlingEventFunc(ctx)
+	case "reload":
+		return EventHandlingReload(ctx)
+	case "query":
+		return EventHandlingQuery(ctx)
+	case "merge_query":
+		return EventHandlingMergeQuery(ctx)
+	case "clear_merge_query":
+		return EventHandlingClearMergeQueryQuery(ctx)
+	case "string_query":
+		return EventHandlingStringQuery(ctx)
+	case "queries":
+		return EventHandlingQueries(ctx)
+	case "pushstateurl":
+		return EventHandlingPushStateURL(ctx)
+	case "fieldvalue":
+		return EventHandlingFileValue(ctx)
+	case "formclear":
+		return EventHandlingFormClear(ctx)
+	case "script":
+		return EventHandlingScript(ctx)
+	case "location":
+		return EventHandlingLocation(ctx)
+	case "raw":
+		return EventHandlingRaw(ctx)
+	default:
+		pr.Body = Div()
+		return
+	}
+}
+
+func ExamplePage(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = Div(
+		H1("ExamplePage"),
+	)
+	return
+}
+
+var ExamplePagePB = web.Page(ExamplePage).
+	EventFunc("form", func(ctx *web.EventContext) (r web.EventResponse, err error) {
+		r.VarsScript = fmt.Sprintf(`alert("form data is %s")`, ctx.R.FormValue("name"))
+		return
+	}).
+	EventFunc("hello", func(ctx *web.EventContext) (r web.EventResponse, err error) {
+		r.VarsScript = `alert("Hello World")`
+		return
+	})
+
+var EventHandlingPagePB = web.Page(EventHandlingPage)
+
+const EventHandlingPagePath = "/samples/event_handling"
+const EventExamplePagePath = "/samples/event_handling/example"

+ 186 - 0
examples/e00_basics/form-handling.go

@@ -0,0 +1,186 @@
+package e00_basics
+
+// @snippet_begin(FormHandlingSample)
+import (
+	"fmt"
+	"io"
+	"mime/multipart"
+
+	"github.com/qor5/docs/utils"
+	"github.com/qor5/web"
+	. "github.com/theplant/htmlgo"
+)
+
+type MyData struct {
+	Text1          string
+	Checkbox1      string
+	Color1         string
+	Email1         string
+	Radio1         string
+	Range1         int
+	Url1           string
+	Tel1           string
+	Month1         string
+	Time1          string
+	Week1          string
+	DatetimeLocal1 string
+	File1          []*multipart.FileHeader
+	HiddenValue1   string
+}
+
+func FormHandlingPage(ctx *web.EventContext) (pr web.PageResponse, err error) {
+
+	var fv MyData
+	err = ctx.UnmarshalForm(&fv)
+	if fv.Text1 == "" {
+		fv.Text1 = `Hello '1
+World`
+	}
+
+	if err != nil {
+		panic(err)
+	}
+
+	pr.Body = Div(
+		H1("Form Handling"),
+		H3("Form Content"),
+		utils.PrettyFormAsJSON(ctx),
+		H3("File1 Content"),
+		Pre(fv.File1Bytes()).Style("width: 400px; white-space: pre-wrap;"),
+		Div(
+			Label("Text1"),
+			Input("").Type("text").Value(fv.Text1).Attr(web.VFieldName("Text1")...),
+		),
+		Div(
+			Label("Checkbox1"),
+			Input("").Type("checkbox").Value("1").Checked(fv.Checkbox1 == "1").Attr(web.VFieldName("Checkbox1")...),
+		),
+
+		web.Scope(
+			Fieldset(
+				Legend("Nested Form"),
+
+				Div(
+					Label("Color1"),
+					Input("").Type("color").
+						Value(fv.Color1).
+						Attr(web.VFieldName("Color1")...),
+				),
+				Div(
+					Label("Email1"),
+					Input("").Type("email").Value(fv.Email1).Attr(web.VFieldName("Email1")...),
+				),
+
+				Input("").Type("checkbox").
+					Attr("v-model", "locals.checked").
+					Attr(web.VFieldName("Checked123")...),
+
+				Button("Uncheck it").Attr("@click", "locals.checked = false"),
+				Hr(),
+				Button("Send").Attr("@click", web.POST().
+					EventFunc("checkvalue").
+					Query("id", 123).
+					FieldValue("name", "azuma").
+					Go()),
+			),
+		).VSlot("{ plaidForm, locals }").Init("{checked: true}"),
+		web.Scope(
+			Fieldset(
+				Legend("Nested Form 2"),
+
+				Div(
+					Label("Email1"),
+					Input("").Type("email").Value(fv.Email1).Attr(web.VFieldName("Email1")...),
+				),
+
+				Button("Send").Attr("@click", web.POST().
+					EventFunc("checkvalue").
+					Go()),
+			),
+		).VSlot("{ plaidForm, locals }").Init("{checked: true}"),
+		Div(
+			Fieldset(
+				Legend("Radio"),
+				Label("Radio Value 1"),
+				Input("Radio1").Type("radio").
+					Value("1").Checked(fv.Radio1 == "1").Attr(web.VFieldName("Radio1")...),
+				Label("Radio Value 2"),
+				Input("Radio1").Type("radio").
+					Value("2").Checked(fv.Radio1 == "2").Attr(web.VFieldName("Radio1")...),
+			),
+		),
+		Div(
+			Label("Range1"),
+			Input("").Type("range").Value(fmt.Sprint(fv.Range1)).Attr(web.VFieldName("Range1")...),
+		),
+
+		web.Scope(
+			Div(
+				Label("Url1"),
+				Input("").Type("url").Value(fv.Url1).Attr(web.VFieldName("Url1")...),
+			),
+			Div(
+				Label("Tel1"),
+				Input("").Type("tel").Value(fv.Tel1).Attr(web.VFieldName("Tel1")...),
+			),
+			Div(
+				Label("Month1"),
+				Input("").Type("month").Value(fv.Month1).Attr(web.VFieldName("Month1")...),
+			),
+		).VSlot("{ locals }"),
+
+		Div(
+			Label("Time1"),
+			Input("").Type("time").Value(fv.Time1).Attr(web.VFieldName("Time1")...),
+		),
+		Div(
+			Label("Week1"),
+			Input("").Type("week").Value(fv.Week1).Attr(web.VFieldName("Week1")...),
+		),
+		Div(
+			Label("DatetimeLocal1"),
+			Input("").Type("datetime-local").Value(fv.DatetimeLocal1).Attr(web.VFieldName("DatetimeLocal1")...),
+		),
+		Div(
+			Label("File1"),
+			Input("").Type("file").Value("").Attr(web.VFieldName("File1")...),
+		),
+		Div(
+			Label("Hidden values with default"),
+			Input("").Type("hidden").Value(`hidden value
+'123`).Attr(web.VFieldName("HiddenValue1")...),
+		),
+		Div(
+			Button("Submit").Attr("@click", web.POST().EventFunc("checkvalue").Go()),
+		),
+	)
+	return
+}
+
+func checkvalue(ctx *web.EventContext) (er web.EventResponse, err error) {
+	er.Reload = true
+	return
+}
+
+func (m *MyData) File1Bytes() string {
+	if m.File1 == nil || len(m.File1) == 0 {
+		return ""
+	}
+	f, err := m.File1[0].Open()
+	if err != nil {
+		panic(err)
+	}
+	var b = make([]byte, 200)
+	_, err = io.ReadFull(f, b)
+	if err != nil {
+		panic(err)
+	}
+	return fmt.Sprintf("%+v ...", b)
+}
+
+var FormHandlingPagePB = web.Page(FormHandlingPage).
+	EventFunc("checkvalue", checkvalue)
+
+const FormHandlingPagePath = "/samples/form_handling"
+
+// @snippet_end

+ 34 - 0
examples/e00_basics/hello-world-reload.go

@@ -0,0 +1,34 @@
+package e00_basics
+
+// @snippet_begin(HelloWorldReloadSample)
+import (
+	"time"
+
+	"github.com/qor5/web"
+	. "github.com/theplant/htmlgo"
+)
+
+func HelloWorldReload(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = Div(
+		H1("Hello World"),
+		Text(time.Now().Format(time.RFC3339Nano)),
+		Button("Reload Page").Attr("@click", web.GET().
+			EventFunc(reloadEvent).
+			Go()),
+	)
+	return
+}
+
+func update(ctx *web.EventContext) (er web.EventResponse, err error) {
+	er.Reload = true
+	return
+}
+
+const reloadEvent = "reload"
+
+var HelloWorldReloadPB = web.Page(HelloWorldReload).
+	EventFunc(reloadEvent, update)
+
+const HelloWorldReloadPath = "/samples/hello_world_reload"
+
+// @snippet_end

+ 18 - 0
examples/e00_basics/hello-world.go

@@ -0,0 +1,18 @@
+package e00_basics
+
+// @snippet_begin(HelloWorldSample)
+import (
+	"github.com/qor5/web"
+	. "github.com/theplant/htmlgo"
+)
+
+func HelloWorld(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = H1("Hello World")
+	return
+}
+
+var HelloWorldPB = web.Page(HelloWorld) // this is already a http.Handler
+
+const HelloWorldPath = "/samples/hello_world"
+
+// @snippet_end

+ 79 - 0
examples/e00_basics/manipulate-page-url.go

@@ -0,0 +1,79 @@
+package e00_basics
+
+// @snippet_begin(MultiStatePageSample)
+import (
+	"net/url"
+
+	"github.com/qor5/web"
+	. "github.com/theplant/htmlgo"
+)
+
+func MultiStatePage(ctx *web.EventContext) (pr web.PageResponse, err error) {
+
+	title := "Multi State Page"
+	if len(ctx.R.URL.Query().Get("title")) > 0 {
+		title = ctx.R.URL.Query().Get("title")
+	}
+	var panel HTMLComponent
+	if len(ctx.R.URL.Query().Get("panel")) > 0 {
+		panel = Div(
+			Fieldset(
+				Div(
+					Label("Name"),
+					Input("").Type("text"),
+				),
+				Div(
+					Label("Date"),
+					Input("").Type("date"),
+				),
+			),
+			Button("Update").Attr("@click", web.POST().EventFunc("update5").Go()),
+		).Style("border: 5px solid orange; height: 200px;")
+	}
+
+	pr.Body = Div(
+		H1(title),
+		Ol(
+			Li(
+				A().Text("change page title").Href("javascript:;").
+					Attr("@click", web.POST().Queries(url.Values{"title": []string{"Hello"}}).Go()),
+			),
+			Li(
+				A().Text("show panel").Href("javascript:;").Attr("@click", web.POST().EventFunc("openPanel").Go()),
+			),
+		),
+		panel,
+
+		Table(
+			Thead(
+				Th("Name"),
+				Th("Date"),
+			),
+			Tbody(
+				Tr(
+					Td(Text("Felix")),
+					Td(Text("2019-01-02")),
+				),
+			),
+		),
+	)
+	return
+}
+
+func openPanel(ctx *web.EventContext) (er web.EventResponse, err error) {
+	er.PushState = web.Location(url.Values{"panel": []string{"1"}}).MergeQuery(true)
+	return
+}
+
+func update5(ctx *web.EventContext) (er web.EventResponse, err error) {
+	er.PushState = web.Location(url.Values{"panel": []string{""}}).MergeQuery(true)
+	return
+}
+
+var MultiStatePagePB = web.Page(MultiStatePage).
+	EventFunc("openPanel", openPanel).
+	EventFunc("update5", update5)
+
+const MultiStatePagePath = "/samples/multi_state_page"
+
+// @snippet_end

+ 98 - 0
examples/e00_basics/page-transition.go

@@ -0,0 +1,98 @@
+package e00_basics
+
+import (
+	"fmt"
+	"net/url"
+
+	"github.com/qor5/web"
+	. "github.com/theplant/htmlgo"
+)
+
+var page1Title = "Page 1"
+
+// @snippet_begin(PageTransitionSample)
+
+const Page1Path = "/samples/page_1"
+const Page2Path = "/samples/page_2"
+
+func Page1(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = Div(
+		H1(page1Title),
+		Ul(
+			Li(
+				A().Href(Page2Path).
+					Text("To Page 2 With Normal Link"),
+			),
+			Li(
+				A().Href("javascript:;").
+					Text("To Page 2 With Push State Link").
+					Attr("@click", web.POST().PushStateURL(Page2Path).Go()),
+			),
+		),
+		fromParam(ctx),
+	).Style("color: green; font-size: 24px;")
+	return
+}
+
+func Page2(ctx *web.EventContext) (pr web.PageResponse, err error) {
+
+	pr.Body = Div(
+		H1("Page 2"),
+		Ul(
+			Li(
+				A().Href("javascript:;").
+					Text("To Page 1 With Normal Link").
+					Attr("@click", web.POST().
+						PushStateURL(Page1Path).
+						Queries(url.Values{"from": []string{"page 2 link 1"}}).
+						Go()),
+			),
+			Li(
+				Button("Do an action then go to Page 1 with push state and parameters").
+					Attr("@click", web.POST().EventFunc("doAction2").Query("id", "42").Go()),
+			),
+			Li(
+				Button("Do an action then go to Page 1 with redirect url").
+					Attr("@click", web.POST().EventFunc("doAction1").Query("id", "41").Go()),
+			),
+		),
+	).Style("color: orange; font-size: 24px;")
+	return
+}
+
+func fromParam(ctx *web.EventContext) HTMLComponent {
+	var from HTMLComponent
+	val := ctx.R.FormValue("from")
+	if len(val) > 0 {
+		from = Components(
+			B("from:"),
+			Text(val),
+		)
+	}
+	return from
+}
+
+func doAction1(ctx *web.EventContext) (er web.EventResponse, err error) {
+	updateDatabase(ctx.QueryAsInt("id"))
+	er.RedirectURL = Page1Path + "?" + url.Values{"from": []string{"page2 with redirect"}}.Encode()
+	return
+}
+
+func doAction2(ctx *web.EventContext) (er web.EventResponse, err error) {
+	updateDatabase(ctx.QueryAsInt("id"))
+	er.PushState = web.Location(url.Values{"from": []string{"page2"}}).
+		URL(Page1Path)
+	return
+}
+
+var Page1PB = web.Page(Page1)
+
+var Page2PB = web.Page(Page2).
+	EventFunc("doAction1", doAction1).
+	EventFunc("doAction2", doAction2)
+
+// @snippet_end
+
+func updateDatabase(val int) {
+	page1Title = fmt.Sprintf("Page 1 (Updated by Page2 to %d)", val)
+}

+ 112 - 0
examples/e00_basics/partial-reload.go

@@ -0,0 +1,112 @@
+package e00_basics
+
+// @snippet_begin(PartialReloadSample)
+import (
+	"fmt"
+	"time"
+
+	"github.com/qor5/web"
+	. "github.com/theplant/htmlgo"
+)
+
+func PartialReloadPage(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	reloadCount = 0
+
+	ctx.Injector.HeadHTML(`
+<style>
+.rp {
+	float: left;
+	width: 200px;
+	height: 200px;
+	margin-right: 20px;
+	background-color: orange;
+}
+</style>
+`,
+	)
+	pr.Body = Div(
+		H1("Portal Reload Automatically"),
+
+		web.Scope(
+			web.Portal().Loader(web.POST().EventFunc("autoReload")).AutoReloadInterval("locals.interval"),
+			Button("stop").Attr("@click", "locals.interval = 0"),
+		).Init(`{interval: 2000}`).VSlot("{ locals }"),
+
+		H1("Load Data Only"),
+
+		web.Scope(
+			Ul(
+				Li(
+					Text("{{item}}"),
+				).Attr("v-for", "item in locals.items"),
+			),
+			Button("Fetch Data").Attr("@click", web.GET().EventFunc("loadData").ThenScript(`locals.items = r.data`).Go()),
+		).VSlot("{ locals }").Init("{ items: []}"),
+
+		H1("Partial Load and Reload"),
+		Div(
+			H2("Product 1"),
+		).Style("height: 200px; background-color: grey;"),
+		H2("Related Products"),
+		web.Portal().Name("related_products").Loader(web.POST().EventFunc("related").Query("productCode", "AH123")),
+		A().Href("javascript:;").Text("Reload Related Products").
+			Attr("@click", web.POST().EventFunc("reload3").Go()),
+	)
+	return
+}
+
+func related(ctx *web.EventContext) (er web.EventResponse, err error) {
+	code := ctx.R.FormValue("productCode")
+	er.Body = Div(
+
+		Div(
+			H3("Product A (related products of "+code+")"),
+			Div().Text(time.Now().Format(time.RFC3339Nano)),
+		).Class("rp"),
+		Div(
+			H3("Product B"),
+			Div().Text(time.Now().Format(time.RFC3339Nano)),
+		).Class("rp"),
+		Div(
+			H3("Product C"),
+			Div().Text(time.Now().Format(time.RFC3339Nano)),
+		).Class("rp"),
+	)
+	return
+}
+
+func reload3(ctx *web.EventContext) (er web.EventResponse, err error) {
+	er.ReloadPortals = []string{"related_products"}
+	return
+}
+
+var reloadCount = 1
+
+func autoReload(ctx *web.EventContext) (er web.EventResponse, err error) {
+	er.Body = Span(time.Now().String())
+	reloadCount++
+
+	if reloadCount > 5 {
+		er.VarsScript = `vars.interval = 0;`
+	}
+	return
+}
+
+func loadData(ctx *web.EventContext) (er web.EventResponse, err error) {
+	var r []string
+	for i := 0; i < 10; i++ {
+		r = append(r, fmt.Sprintf("%d-%d", i, time.Now().Nanosecond()))
+	}
+	er.Data = r
+	return
+}
+
+var PartialReloadPagePB = web.Page(PartialReloadPage).
+	EventFunc("related", related).
+	EventFunc("reload3", reload3).
+	EventFunc("autoReload", autoReload).
+	EventFunc("loadData", loadData)
+
+const PartialReloadPagePath = "/samples/partial_reload"
+
+// @snippet_end

+ 58 - 0
examples/e00_basics/partial-update.go

@@ -0,0 +1,58 @@
+package e00_basics
+
+// @snippet_begin(PartialUpdateSample)
+import (
+	"time"
+
+	"github.com/qor5/web"
+	. "github.com/theplant/htmlgo"
+)
+
+func PartialUpdatePage(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = Div(
+		H1("Partial Update"),
+		A().Text("Edit").Href("javascript:;").
+			Attr("@click", web.POST().EventFunc("edit1").Go()),
+		web.Portal(
+			Text("original portal content here"),
+		).Name("part1"),
+		Div().Text(time.Now().Format(time.RFC3339Nano)),
+	)
+	return
+}
+
+func edit1(ctx *web.EventContext) (er web.EventResponse, err error) {
+	er.UpdatePortals = append(er.UpdatePortals, &web.PortalUpdate{
+		Name: "part1",
+		Body: Div(
+			Fieldset(
+				Legend("Input value"),
+				Div(
+					Label("Title"),
+					Input("").Type("text"),
+				),
+
+				Div(
+					Label("Date"),
+					Input("").Type("date"),
+				),
+			),
+			Button("Update").
+				Attr("@click", web.POST().EventFunc("reload2").Go()),
+		),
+	})
+	return
+}
+
+func reload2(ctx *web.EventContext) (er web.EventResponse, err error) {
+	er.Reload = true
+	return
+}
+
+var PartialUpdatePagePB = web.Page(PartialUpdatePage).
+	EventFunc("edit1", edit1).
+	EventFunc("reload2", reload2)
+
+const PartialUpdatePagePath = "/samples/partial_update"
+
+// @snippet_end

+ 48 - 0
examples/e00_basics/reload-with-a-flash.go

@@ -0,0 +1,48 @@
+package e00_basics
+
+// @snippet_begin(ReloadWithFlashSample)
+import (
+	"fmt"
+	"time"
+
+	"github.com/qor5/web"
+	. "github.com/theplant/htmlgo"
+)
+
+var count int
+
+func ReloadWithFlash(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	var msg HTMLComponent
+
+	if d, ok := ctx.Flash.(*Data1); ok {
+		msg = Div().Text(d.Msg).Style("border: 5px solid orange;")
+	} else {
+		count = 0
+	}
+
+	pr.Body = Div(
+		H1("Whole Page Reload With a Flash"),
+		msg,
+		Div().Text(time.Now().Format(time.RFC3339Nano)),
+		Button("Do Something").
+			Attr("@click", web.POST().EventFunc("update2").Go()),
+	)
+	return
+}
+
+type Data1 struct {
+	Msg string
+}
+
+func update2(ctx *web.EventContext) (er web.EventResponse, err error) {
+	count++
+	ctx.Flash = &Data1{Msg: fmt.Sprintf("The page is reloaded: %d", count)}
+	er.Reload = true
+	return
+}
+
+var ReloadWithFlashPB = web.Page(ReloadWithFlash).EventFunc("update2", update2)
+
+const ReloadWithFlashPath = "/samples/reload_with_flash"
+
+// @snippet_end

+ 48 - 0
examples/e00_basics/shortcut.go

@@ -0,0 +1,48 @@
+package e00_basics
+
+// @snippet_begin(ShortCutSample)
+import (
+	. "github.com/qor5/ui/vuetify"
+	"github.com/qor5/web"
+	h "github.com/theplant/htmlgo"
+)
+
+func ShortCutSample(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	clickEvent := "locals.count += 1"
+	pr.Body = VContainer(
+		web.Scope(
+			VRow(
+				VCol(
+					VRow(
+						VBtn("count+1").Attr("@click", clickEvent).Class("mr-4"),
+						h.Text("Shortcut: enter"),
+					).Class("mb-10"),
+					VRow(
+						VBtn("toggle shortcut").Attr("@click", "locals.shortCutEnabled = !locals.shortCutEnabled"),
+					),
+				),
+				VCol(
+					VCard(
+						VCardTitle(h.Text("Shortcut Enabled")),
+						VCardText().Attr("v-text", "locals.shortCutEnabled"),
+					).Class("mb-10"),
+
+					VCard(
+						VCardTitle(h.Text("Count")),
+						VCardText().Attr("v-text", "locals.count"),
+					),
+				),
+			).Class("mt-10"),
+			// Add shortcut for this button. only available when drawer is opened
+			web.GlobalEvents().Attr(":filter", `(event, handler, eventName) => locals.shortCutEnabled == true`).Attr("@keydown.enter", clickEvent),
+		).Init(`{ shortCutEnabled: true, count: 0 }`).
+			VSlot("{ locals }"),
+	)
+	return
+}
+
+var ShortCutSamplePB = web.Page(ShortCutSample)
+
+const ShortCutSamplePath = "/samples/shortcut-sample"
+
+// @snippet_end

+ 47 - 0
examples/e00_basics/type-safe-builder-sample.go

@@ -0,0 +1,47 @@
+package e00_basics
+
+// @snippet_begin(TypeSafeBuilderSample)
+import (
+	"github.com/qor5/web"
+	. "github.com/theplant/htmlgo"
+)
+
+func result(args ...HTMLComponent) HTMLComponent {
+
+	var converted []HTMLComponent
+	for _, arg := range args {
+		converted = append(converted, Div(arg).Class("wrapped"))
+	}
+
+	return HTML(
+		Head(
+			Title("XML encoding with Go"),
+		),
+		Body(
+			H1("XML encoding with Go"),
+			P().Text("this format can be used as an alternative markup to XML"),
+			A().Href("http://golang.org").Text("Go"),
+			P(
+				Text("this is some"),
+				B("mixed"),
+				Text("text. For more see the"),
+				A().Href("http://golang.org").Text("Go"),
+				Text("project"),
+			),
+			P().Text("some text"),
+
+			P(converted...),
+		),
+	)
+}
+
+func TypeSafeBuilderSamplePF(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = result(H5("1"), B("2"), Strong("3"))
+	return
+}
+
+var TypeSafeBuilderSamplePFPB = web.Page(TypeSafeBuilderSamplePF)
+
+const TypeSafeBuilderSamplePath = "/samples/type_safe_builder_sample"
+
+// @snippet_end

+ 65 - 0
examples/e00_basics/use-tiptap-editor.go

@@ -0,0 +1,65 @@
+package e00_basics
+
+// @snippet_begin(HelloWorldTipTapSample)
+import (
+	"github.com/qor5/ui/tiptap"
+	"github.com/qor5/web"
+	. "github.com/theplant/htmlgo"
+	"github.com/yosssi/gohtml"
+)
+
+func HelloWorldTipTap(ctx *web.EventContext) (pr web.PageResponse, err error) {
+
+	defaultValue := ctx.R.FormValue("Content1")
+	if len(defaultValue) == 0 {
+		defaultValue = `
+			<h1>Hello</h1>
+			<p>
+				This is a nice editor
+			</p>
+			<ul>
+			  <li>
+				<p>
+				  123
+				</p>
+			  </li>
+			  <li>
+				<p>
+				  456
+				</p>
+			  </li>
+			  <li>
+				<p>
+				  789
+				</p>
+			  </li>
+			</ul>
+`
+	}
+
+	pr.Body = Div(
+		tiptap.TipTapEditor().
+			FieldName("Content1").
+			Value(defaultValue),
+		Hr(),
+		Pre(
+			gohtml.Format(ctx.R.FormValue("Content1")),
+		).Style("background-color: #f8f8f8; padding: 20px;"),
+		Button("Submit").Style("font-size: 24px").
+			Attr("@click", web.POST().EventFunc("refresh").Go()),
+	)
+
+	return
+}
+
+func refresh(ctx *web.EventContext) (er web.EventResponse, err error) {
+	er.Reload = true
+	return
+}
+
+var HelloWorldTipTapPB = web.Page(HelloWorldTipTap).
+	EventFunc("refresh", refresh)
+
+const HelloWorldTipTapPath = "/samples/hello_world_tiptap"
+
+// @snippet_end

+ 162 - 0
examples/e00_basics/web-scope.go

@@ -0,0 +1,162 @@
+package e00_basics
+
+import (
+	"github.com/qor5/docs/utils"
+	. "github.com/qor5/ui/vuetify"
+	"github.com/qor5/web"
+	. "github.com/theplant/htmlgo"
+)
+
+// @snippet_begin(WebScopeUseLocalsSample1)
+func UseLocals(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	pr.Body = VCard(
+		VBtn("Test Can Not Change Other Scope").Attr("@click", `locals.btnLabel = "YES"`),
+		web.Scope(
+			VCard(
+				VBtn("").
+					Attr("v-text", "locals.btnLabel").
+					Attr("@click", `
+if (locals.btnLabel == "Add") {
+	locals.items.push({text: "B", icon: "done"});
+	locals.btnLabel = "Remove";
+} else {
+	locals.items.pop();
+	locals.btnLabel = "Add";
+}`),
+
+				VList(
+					VSubheader(
+						Text("REPORTS"),
+					),
+					VListItemGroup(
+						VListItem(
+							VListItemIcon(
+								VIcon("").Attr("v-text", "item.icon"),
+							),
+							VListItemContent(
+								VListItemTitle().Attr("v-text", "item.text"),
+							),
+						).Attr("v-for", "(item, i) in locals.items").
+							Attr("x-bind:key", "i"),
+					).Attr("v-model", "locals.selectedItem").
+						Attr("color", "primary"),
+				).Attr("dense", ""),
+			).Class("mx-auto").
+				Attr("max-width", "300").
+				Attr("tile", ""),
+		).Init(`{ selectedItem: 1, btnLabel:"Add", items: [{text: "A", icon: "clock"}]}`).
+			VSlot("{ locals }"),
+	)
+	return
+}
+
+var UseLocalsPB = web.Page(UseLocals)
+
+// @snippet_end
+
+// @snippet_begin(WebScopeUsePlaidFormSample1)
+var materialID, materialName, rawMaterialID, rawMaterialName, countryID, countryName, productName string
+
+func UsePlaidForm(ctx *web.EventContext) (pr web.PageResponse, err error) {
+
+	pr.Body = Div(
+		H3("Form Content"),
+		utils.PrettyFormAsJSON(ctx),
+
+		Div(
+			Div(
+				Fieldset(
+					Legend("Product Form"),
+					Div(
+						Label("Product Name"),
+						Input("").Value(productName).Type("text").Attr(web.VFieldName("ProductName")...),
+					),
+					Div(
+						Label("Material ID"),
+						Input("").Value(materialID).Type("text").Disabled(true).Attr(web.VFieldName("MaterialID")...),
+					),
+
+					web.Scope(
+						Fieldset(
+							Legend("Material Form"),
+
+							Div(
+								Label("Material Name"),
+								Input("").Value(materialName).Type("text").Attr(web.VFieldName("MaterialName")...),
+							),
+							Div(
+								Label("Raw Material ID"),
+								Input("").Value(rawMaterialID).Type("text").Disabled(true).Attr(web.VFieldName("RawMaterialID")...),
+							),
+							web.Scope(
+								Fieldset(
+									Legend("Raw Material Form"),
+
+									Div(
+										Label("Raw Material Name"),
+										Input("").Value(rawMaterialName).Type("text").Attr(web.VFieldName("RawMaterialName")...),
+									),
+
+									Button("Send").Style(`background: orange;`).Attr("@click", web.POST().EventFunc("updateValue").Go()),
+								).Style(`background: orange;`),
+							).VSlot("{ plaidForm }"),
+
+							Button("Send").Style(`background: brown;`).Attr("@click", web.POST().EventFunc("updateValue").Go()),
+						).Style(`background: brown;`),
+					).VSlot("{ plaidForm }"),
+
+					Div(
+						Label("Country ID"),
+						Input("").Value(countryID).Type("text").Disabled(true).Attr(web.VFieldName("CountryID")...),
+					),
+
+					web.Scope(
+						Fieldset(
+							Legend("Country Of Origin Form"),
+
+							Div(
+								Label("Country Name"),
+								Input("").Value(countryName).Type("text").Attr(web.VFieldName("CountryName")...),
+							),
+
+							Button("Send").Style(`background: red;`).Attr("@click", web.POST().EventFunc("updateValue").Go()),
+						).Style(`background: red;`),
+					).VSlot("{ plaidForm }"),
+
+					Div(
+						Button("Send").Style(`background: grey;`).Attr("@click", web.POST().EventFunc("updateValue").Go())),
+				).Style(`background: grey;`)),
+		).Style(`width:600px;`),
+	)
+
+	return
+}
+
+func updateValue(ctx *web.EventContext) (er web.EventResponse, err error) {
+	ctx.R.ParseForm()
+	if v := ctx.R.Form.Get("ProductName"); v != "" {
+		productName = v
+	}
+	if v := ctx.R.Form.Get("MaterialName"); v != "" {
+		materialName = v
+		materialID = "66"
+	}
+	if v := ctx.R.Form.Get("RawMaterialName"); v != "" {
+		rawMaterialName = v
+		rawMaterialID = "88"
+	}
+	if v := ctx.R.Form.Get("CountryName"); v != "" {
+		countryName = v
+		countryID = "99"
+	}
+	er.Reload = true
+	return
+}
+
+var UsePlaidFormPB = web.Page(UsePlaidForm).
+	EventFunc("updateValue", updateValue)
+
+// @snippet_end
+
+const WebScopeUseLocalsPagePath = "/samples/web-scope-use-locals"
+const WebScopeUsePlaidFormPagePath = "/samples/web-scope-use-plaid-form"

+ 45 - 0
examples/e01_hello_button/page.go

@@ -0,0 +1,45 @@
+package e01_hello_button
+
+import (
+	"github.com/qor5/web"
+	. "github.com/theplant/htmlgo"
+)
+
+type mystate struct {
+	Message string
+}
+
+func HelloButton(ctx *web.EventContext) (pr web.PageResponse, err error) {
+
+	var s = &mystate{}
+	if ctx.Flash != nil {
+		s = ctx.Flash.(*mystate)
+	}
+
+	pr.Body = Div(
+		Button("Hello").Attr("@click", web.POST().EventFunc("reload").Go()),
+		Tag("input").
+			Attr("type", "text").
+			Attr("value", s.Message).
+			Attr("@input", web.POST().
+				EventFunc("reload").
+				FieldValue("Message", web.Var("$event.target.value")).
+				Go()),
+		Div().
+			Style("font-family: monospace;").
+			Text(s.Message),
+	)
+	return
+}
+
+func reload(ctx *web.EventContext) (r web.EventResponse, err error) {
+	var s = &mystate{}
+	ctx.MustUnmarshalForm(s)
+	ctx.Flash = s
+
+	r.Reload = true
+	return
+}
+
+var HelloButtonPB = web.Page(HelloButton).
+	EventFunc("reload", reload)

+ 140 - 0
examples/e05_hello_customized_component/page.go

@@ -0,0 +1,140 @@
+package e05_hello_customized_component
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/qor5/web"
+	. "github.com/theplant/htmlgo"
+)
+
+type TagsInputBuilder struct {
+	classNames   []string
+	selectedKeys []string
+	options      []*TagsInputOption
+}
+
+type TagsInputOption struct {
+	Key   string
+	Label HTMLComponent
+}
+
+func TagsInput() (r *TagsInputBuilder) {
+	r = &TagsInputBuilder{}
+	return
+}
+
+func (b *TagsInputBuilder) Class(names ...string) (r *TagsInputBuilder) {
+	b.classNames = names
+	return b
+}
+
+func (b *TagsInputBuilder) Selected(keys []string) (r *TagsInputBuilder) {
+	b.selectedKeys = keys
+	return b
+}
+
+func (b *TagsInputBuilder) Options(options ...*TagsInputOption) (r *TagsInputBuilder) {
+	b.options = options
+	return b
+}
+
+func contains(k string, in []string) bool {
+	for _, i := range in {
+		if k == i {
+			return true
+		}
+	}
+	return false
+}
+
+func (b *TagsInputBuilder) MarshalHTML(ctx context.Context) (r []byte, err error) {
+	// ui.Injector(ctx).PutScript(tagsInputScript)
+	// ui.Injector(ctx).PutStyle(tagsInputStyles)
+
+	selectedComps := []HTMLComponent{}
+	optionComps := []HTMLComponent{}
+	for _, op := range b.options {
+		optionComps = append(optionComps, op.Label)
+		if contains(op.Key, b.selectedKeys) {
+			selectedComps = append(selectedComps, op.Label)
+		}
+	}
+
+	root := Tag("tags-input").
+		Class("tagsInput").
+		Attr("v-slot", "{ parent }").
+		Children(
+			Div(
+				Div().Class("tagsInputSelected").Children(
+					selectedComps...,
+				),
+				Tag("button").Text("Toggle").Attr("v-on:click", "parent.toggle()"),
+			),
+			Div().
+				Class("tagsInputOptions").
+				Attr("v-bind:class", "{tagsInputOptionsOpen: parent.isOpen}").Children(
+				optionComps...,
+			),
+		)
+	return root.MarshalHTML(ctx)
+}
+
+// const tagsInputScript = `
+//	(window.__qor5VueComponentRegisters = (window.__qor5VueComponentRegisters || [])).push(function(Vue){
+//		Vue.component("tags-input", {
+//			data: function() {
+//				return {
+//					isOpen: false
+//				};
+//			},
+//			methods: {
+//				toggle: function() {
+//					this.isOpen = !this.isOpen;
+//				}
+//			},
+//			template: "<div><slot v-bind:parent='this'/></div>"
+//		});
+//	})
+// `
+//
+// const tagsInputStyles = `
+// .tagsInput .tagsInputOptions {
+//	display: none;
+// }
+//
+// .tagsInput .tagsInputOptions.tagsInputOptionsOpen {
+//	display: block;
+// }
+// `
+
+// Above is component code
+
+type mystate struct {
+	Message string
+}
+
+func HelloCustomziedComponent(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	// s := ctx.StateOrInit(&mystate{}).(*mystate)
+
+	opts := []*TagsInputOption{}
+	for i := 1; i < 11; i++ {
+		opts = append(opts, &TagsInputOption{
+			Key:   fmt.Sprint(i),
+			Label: Div().Text(fmt.Sprintf("label %d", i)),
+		})
+	}
+
+	pr.Body = Div(
+		TagsInput().Selected([]string{"1", "2", "3"}).Options(opts...),
+		Button("Refresh").Attr("@click",
+			web.POST().EventFunc("refresh").Go(),
+		),
+	)
+	return
+}
+
+func reload(ctx *web.EventContext) (r web.EventResponse, err error) {
+	r.Reload = true
+	return
+}

+ 201 - 0
examples/e10_vuetify_autocomplete/page.go

@@ -0,0 +1,201 @@
+package e10_vuetify_autocomplete
+
+// @snippet_begin(VuetifyAutoCompleteSample)
+
+import (
+	"fmt"
+
+	"github.com/qor5/admin/presets"
+	"github.com/qor5/admin/presets/gorm2op"
+	. "github.com/qor5/ui/vuetify"
+	"github.com/qor5/ui/vuetifyx"
+	"github.com/qor5/web"
+	h "github.com/theplant/htmlgo"
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+)
+
+type myFormValue struct {
+	Values1 []string
+	Values2 []string
+	Value3  string
+}
+
+type User struct {
+	Login string
+	Name  string
+}
+
+var selectedItems1 = []*User{
+	{Login: "sam", Name: "Sam"},
+	{Login: "charles", Name: "Charles"},
+}
+
+var options1 = []*User{
+	{Login: "sam", Name: "Sam"},
+	{Login: "john", Name: "John"},
+	{Login: "charles", Name: "Charles"},
+}
+
+var selectedItems2 = []*User{
+	{Login: "charles", Name: "Charles"},
+}
+
+var selectedItems3 = []*User{
+	{Login: "charles", Name: "Charles"},
+}
+
+var options2 = []*User{
+	{Login: "sam", Name: "Sam"},
+	{Login: "john", Name: "John"},
+	{Login: "charles", Name: "Charles"},
+}
+
+var globalState = &myFormValue{
+	Values1: []string{
+		"sam",
+		"charles",
+	},
+	Values2: []string{
+		"charles",
+	},
+	Value3: "charles",
+}
+
+type Product struct {
+	ID   uint `gorm:"primarykey"`
+	Name string
+}
+
+var loadMoreRes *vuetifyx.AutocompleteDataSource
+var pagingRes *vuetifyx.AutocompleteDataSource
+var ExamplePreset *presets.Builder
+
+func init() {
+	db, err := gorm.Open(sqlite.Open("/tmp/my.db"), &gorm.Config{})
+	if err != nil {
+		panic(err)
+	}
+
+	db.AutoMigrate(&Product{})
+	db.Where("1=1").Delete(&Product{})
+
+	for i := 1; i < 300; i++ {
+		db.Create(&Product{Name: fmt.Sprintf("Product %d", i)})
+	}
+
+	ExamplePreset = presets.New()
+	ExamplePreset.URIPrefix(VuetifyAutoCompletePresetPath).DataOperator(gorm2op.DataOperator(db))
+	listing := ExamplePreset.Model(&Product{}).Listing()
+	loadMoreRes = listing.ConfigureAutocompleteDataSource(
+		&presets.AutocompleteDataSourceConfig{
+			OptionValue: "ID",
+			OptionText:  "Name",
+			KeywordColumns: []string{
+				"Name",
+			},
+			PerPage: 50,
+		},
+		"loadMore",
+	)
+
+	pagingRes = listing.ConfigureAutocompleteDataSource(
+		&presets.AutocompleteDataSourceConfig{
+			OptionValue: "ID",
+			OptionText:  "Name",
+			KeywordColumns: []string{
+				"Name",
+			},
+			PerPage:  20,
+			IsPaging: true,
+			OrderBy:  "Name",
+		},
+		"paging",
+	)
+
+}
+
+func VuetifyAutocomplete(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	result := h.Ul()
+	for _, v := range globalState.Values1 {
+		result.AppendChildren(h.Li().Text(v))
+	}
+	pr.Body = VContainer(
+		h.H1("An auto complete that you can select multiple options with static data"),
+		VAutocomplete().
+			Items(options1).
+			FieldName("Values1").
+			ItemText("Name").
+			ItemValue("Login").
+			Label("Static Options").
+			Value(globalState.Values1),
+		result,
+
+		h.H1("An auto complete that you can select multiple options from data source by loading more"),
+		vuetifyx.VXAutocomplete().
+			FieldName("Values2").
+			Label("Load options from data source").
+			SetDataSource(loadMoreRes),
+
+		h.H1("An auto complete that you can select multiple options from data source resource by paging"),
+		vuetifyx.VXAutocomplete().
+			FieldName("Values2").
+			Label("Load options from data source").
+			SetDataSource(pagingRes),
+
+		h.H1("VSelect"),
+		VSelect().
+			Items(options1).    // Items is the data source
+			ItemText("Name").   // ItemText is the value that would be displayed to user. the argument is the corresponding field name in the Items. here is user.Name
+			ItemValue("Login"). // ItemValue is the value that will be passed with the form. same with ItemText, here is user.Login
+			FieldName("Value3").
+			Solo(true).
+			Value(globalState.Value3),
+		h.Pre(globalState.Value3),
+		VBtn("Update").
+			Color("success").
+			OnClick("update"),
+	)
+	return
+}
+
+func update(ctx *web.EventContext) (r web.EventResponse, err error) {
+	globalState = &myFormValue{}
+	ctx.MustUnmarshalForm(globalState)
+
+	selectedItems1 = []*User{}
+	for _, login := range globalState.Values1 {
+		for _, u := range options1 {
+			if u.Login == login {
+				selectedItems1 = append(selectedItems1, u)
+			}
+		}
+	}
+
+	selectedItems2 = []*User{}
+	for _, login := range globalState.Values2 {
+		for _, u := range options2 {
+			if u.Login == login {
+				selectedItems2 = append(selectedItems2, u)
+			}
+		}
+	}
+
+	selectedItems3 = []*User{}
+	for _, u := range options1 {
+		if u.Login == globalState.Value3 {
+			selectedItems3 = append(selectedItems3, u)
+		}
+	}
+	r.Reload = true
+
+	return
+}
+
+var VuetifyAutocompletePB = web.Page(VuetifyAutocomplete).
+	EventFunc("update", update)
+
+const VuetifyAutoCompletePath = "/samples/vuetify-auto-complete"
+const VuetifyAutoCompletePresetPath = "/samples/vuetify-auto-complete-preset"
+
+// @snippet_end

+ 132 - 0
examples/e11_vuetify_basic_inputs/page.go

@@ -0,0 +1,132 @@
+package e11_vuetify_basic_inputs
+
+// @snippet_begin(VuetifyBasicInputsSample)
+import (
+	"mime/multipart"
+
+	"github.com/qor5/docs/utils"
+	. "github.com/qor5/ui/vuetify"
+	"github.com/qor5/web"
+	h "github.com/theplant/htmlgo"
+)
+
+type myFormValue struct {
+	MyValue          string
+	TextareaValue    string
+	Gender           string
+	Agreed           bool
+	Feature1         bool
+	Slider1          int
+	PortalAddedValue string
+	Files1           []*multipart.FileHeader
+	Files2           []*multipart.FileHeader
+	Files3           []*multipart.FileHeader
+}
+
+var s = &myFormValue{
+	MyValue:       "123",
+	TextareaValue: "This is textarea value",
+	Gender:        "M",
+	Agreed:        false,
+	Feature1:      true,
+	Slider1:       60,
+}
+
+func VuetifyBasicInputs(ctx *web.EventContext) (pr web.PageResponse, err error) {
+
+	var verr web.ValidationErrors
+	if ve, ok := ctx.Flash.(web.ValidationErrors); ok {
+		verr = ve
+	}
+
+	pr.Body = VContainer(
+		utils.PrettyFormAsJSON(ctx),
+		VTextField().
+			Label("Form ValueIs").
+			Solo(true).
+			Clearable(true).
+			FieldName("MyValue").
+			ErrorMessages(verr.GetFieldErrors("MyValue")...).
+			Value(s.MyValue),
+		VTextarea().FieldName("TextareaValue").
+			ErrorMessages(verr.GetFieldErrors("TextareaValue")...).
+			Solo(true).Value(s.TextareaValue),
+		VRadioGroup(
+			VRadio().Value("F").Label("Female"),
+			VRadio().Value("M").Label("Male"),
+		).FieldName("Gender").Value(s.Gender),
+		VCheckbox().FieldName("Agreed").
+			ErrorMessages(verr.GetFieldErrors("Agreed")...).
+			Label("Agree").InputValue(s.Agreed),
+		VSwitch().FieldName("Feature1").InputValue(s.Feature1),
+
+		VSlider().FieldName("Slider1").
+			ErrorMessages(verr.GetFieldErrors("Slider1")...).
+			Value(s.Slider1),
+		web.Portal().Name("Portal1"),
+
+		VFileInput().FieldName("Files1"),
+
+		VFileInput().Label("Auto post to server after select file").Multiple(true).
+			Attr("@change", web.POST().
+				EventFunc("update").
+				FieldValue("Files2", web.Var("$event")).
+				Go()),
+
+		h.Div(
+			h.Input("Files3").Type("file").
+				Attr("@input", web.POST().
+					EventFunc("update").
+					FieldValue("Files3", web.Var("$event")).
+					Go()),
+		).Class("mb-4"),
+
+		VBtn("Update").OnClick("update").Color("primary"),
+		h.P().Text("The following button will update a portal with a hidden field, if you click this button, and then click the above update button, you will find additional value posted to server"),
+		VBtn("Add Portal Hidden Value").OnClick("addPortal"),
+	)
+
+	return
+}
+
+func addPortal(ctx *web.EventContext) (r web.EventResponse, err error) {
+	r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
+		Name: "Portal1",
+		Body: h.Input("").Type("hidden").Value("this is my portal added hidden value").Attr(web.VFieldName("PortalAddedValue")...),
+	})
+	return
+}
+
+func update(ctx *web.EventContext) (r web.EventResponse, err error) {
+	s = &myFormValue{}
+	ctx.MustUnmarshalForm(s)
+	verr := web.ValidationErrors{}
+	if len(s.MyValue) < 10 {
+		verr.FieldError("MyValue", "my value is too small")
+	}
+
+	if len(s.TextareaValue) > 5 {
+		verr.FieldError("TextareaValue", "textarea value is too large")
+	}
+
+	if !s.Agreed {
+		verr.FieldError("Agreed", "You must agree the terms")
+	}
+
+	if s.Slider1 > 50 {
+		verr.FieldError("Slider1", "You slide too much")
+	}
+
+	ctx.Flash = verr
+	r.Reload = true
+
+	return
+}
+
+var VuetifyBasicInputsPB = web.Page(VuetifyBasicInputs).
+	EventFunc("update", update).
+	EventFunc("addPortal", addPortal)
+
+// @snippet_end
+
+const VuetifyBasicInputsPath = "/samples/vuetify-basic-inputs"

+ 42 - 0
examples/e12_hello_vuetify_grid/page.go

@@ -0,0 +1,42 @@
+package e12_hello_vuetify_grid
+
+import (
+	"fmt"
+
+	. "github.com/qor5/ui/vuetify"
+	"github.com/qor5/web"
+	h "github.com/theplant/htmlgo"
+)
+
+func HelloVuetifyGrid(ctx *web.EventContext) (pr web.PageResponse, err error) {
+
+	row := func(col int, count int, color string) (r []h.HTMLComponent) {
+		for i := 0; i < count; i++ {
+			r = append(r, VFlex(
+				VCard(
+					VCardText(h.Text(fmt.Sprint(col))),
+				).Dark(true).Color(color),
+			).Col(Xs, col))
+		}
+		return
+	}
+
+	var lc []h.HTMLComponent
+	lc = append(lc, row(12, 1, "primary")...)
+	lc = append(lc, row(6, 2, "secondary")...)
+	lc = append(lc, row(4, 3, "primary")...)
+	lc = append(lc, row(3, 4, "secondary")...)
+	lc = append(lc, row(2, 6, "primary")...)
+	lc = append(lc, row(1, 12, "secondary")...)
+
+	pr.Body = VApp(
+		VMain(
+			VContainer(
+				VLayout(
+					lc...,
+				).Row(true).Wrap(true),
+			).GridList(Md).TextAlign(Xs, Center),
+		),
+	).Id("mainapp")
+	return
+}

+ 74 - 0
examples/e13_vuetify_list/page.go

@@ -0,0 +1,74 @@
+package e13_vuetify_list
+
+// @snippet_begin(VuetifyListSample)
+import (
+	. "github.com/qor5/ui/vuetify"
+	"github.com/qor5/web"
+	h "github.com/theplant/htmlgo"
+)
+
+func HelloVuetifyList(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	wrapper := func(children ...h.HTMLComponent) h.HTMLComponent {
+		return VContainer(
+			VLayout(
+				VFlex(
+					VCard(children...),
+				).Col(Xs, 6).Offset(Sm, 3),
+			).Row(true),
+		).GridList(Md).TextAlign(Xs, Center)
+	}
+
+	pr.Body = wrapper(
+		VToolbar(
+			// VToolbarSideIcon(),
+			VToolbarTitle("Inbox"),
+			VSpacer(),
+			VBtn("").Icon(true).Children(
+				VIcon("search"),
+			),
+		).Color("cyan").Dark(true),
+		VList(
+			VSubheader(h.Text("Today")),
+			VListItem(
+				VListItemAvatar(
+					h.Img("https://cdn.vuetifyxjs.com/images/lists/1.jpg"),
+				),
+				VListItemContent(
+					VListItemTitle(h.Text("Brunch this weekend?")),
+					VListItemSubtitle(
+						h.Span("Ali Connors").Class("text--primary"),
+						h.Text("&mdash; I'll be in your neighborhood doing errands this weekend. Do you want to hang out?"),
+					),
+				),
+			),
+			VDivider().Inset(true),
+			VListItem(
+				VListItemAvatar(
+					h.Img("https://cdn.vuetifyxjs.com/images/lists/2.jpg"),
+				),
+				VListItemContent(
+					VListItemTitle(h.RawHTML(`Summer BBQ <span class="grey--text text--lighten-1">4</span>`)),
+					VListItemSubtitle(h.RawHTML(`<span class='text--primary'>to Alex, Scott, Jennifer</span> &mdash; Wish I could come, but I'm out of town this weekend.`)),
+				),
+			),
+			VDivider().Inset(true),
+			VListItem(
+				VListItemAvatar(
+					h.Img("https://cdn.vuetifyxjs.com/images/lists/3.jpg"),
+				),
+				VListItemContent(
+					VListItemTitle(h.Text(`Oui oui`)),
+					VListItemSubtitle(h.RawHTML(`<span class='text--primary'>Sandra Adams</span> &mdash; Do you have Paris recommendations? Have you ever been?`)),
+				),
+			),
+		).TwoLine(true),
+	)
+
+	return
+}
+
+var HelloVuetifyListPB = web.Page(HelloVuetifyList)
+
+const HelloVuetifyListPath = "/samples/hello-vuetify-list"
+
+// @snippet_end

+ 126 - 0
examples/e14_vuetify_menu/page.go

@@ -0,0 +1,126 @@
+package e14_vuetify_menu
+
+// @snippet_begin(VuetifyMenuSample)
+
+import (
+	"github.com/qor5/docs/utils"
+	. "github.com/qor5/ui/vuetify"
+	"github.com/qor5/web"
+	h "github.com/theplant/htmlgo"
+)
+
+type formData struct {
+	EnableMessages bool
+	EnableHints    bool
+}
+
+var globalFavored bool
+
+const favoredIconPortalName = "favoredIcon"
+
+func HelloVuetifyMenu(ctx *web.EventContext) (pr web.PageResponse, err error) {
+
+	var fv formData
+	err = ctx.UnmarshalForm(&fv)
+	if err != nil {
+		return
+	}
+
+	pr.Body = VContainer(
+		utils.PrettyFormAsJSON(ctx),
+
+		VMenu(
+			web.Slot(
+				VBtn("Menu as Popover").
+					On("click", "vars.myMenuShow = true").
+					Dark(true).
+					Color("indigo"),
+			).Name("activator"),
+
+			VCard(
+				VList(
+					VListItem(
+						VListItemAvatar(
+							h.Img("https://cdn.vuetifyxjs.com/images/john.jpg").Alt("John"),
+						),
+						VListItemContent(
+							VListItemTitle(h.Text("John Leider")),
+							VListItemSubtitle(h.Text("Founder of Vuetify.js")),
+						),
+						VListItemAction(
+							web.Portal(
+								favoredIcon(),
+							).Name(favoredIconPortalName),
+						),
+					),
+				),
+				VDivider(),
+				VList(
+					VListItem(
+						VListItemAction(
+							VSwitch().Color("purple").
+								FieldName("EnableMessages").
+								InputValue(fv.EnableMessages),
+						),
+						VListItemTitle(h.Text("Enable messages")),
+					),
+					VListItem(
+						VListItemAction(
+							VSwitch().Color("purple").
+								FieldName("EnableHints").
+								InputValue(fv.EnableHints),
+						),
+						VListItemTitle(h.Text("Enable hints")),
+					),
+				),
+
+				VCardActions(
+					VSpacer(),
+					VBtn("Cancel").Text(true).
+						On("click", "vars.myMenuShow = false"),
+					VBtn("Save").Color("primary").
+						Text(true).OnClick("submit"),
+				),
+			),
+		).CloseOnContentClick(false).
+			NudgeWidth(200).
+			OffsetY(true).
+			Attr("v-model", "vars.myMenuShow"),
+	).Attr(web.InitContextVars, `{myMenuShow: false}`)
+
+	return
+}
+
+func favoredIcon() h.HTMLComponent {
+	color := ""
+	if globalFavored {
+		color = "red"
+	}
+
+	return VBtn("").Icon(true).Children(
+		VIcon("favorite").Color(color),
+	).OnClick("toggleFavored")
+}
+
+func toggleFavored(ctx *web.EventContext) (er web.EventResponse, err error) {
+	globalFavored = !globalFavored
+	er.UpdatePortals = append(er.UpdatePortals, &web.PortalUpdate{
+		Name: favoredIconPortalName,
+		Body: favoredIcon(),
+	})
+	return
+}
+
+func submit(ctx *web.EventContext) (er web.EventResponse, err error) {
+	er.Reload = true
+	er.VarsScript = "vars.myMenuShow = false"
+	return
+}
+
+var HelloVuetifyMenuPB = web.Page(HelloVuetifyMenu).
+	EventFunc("submit", submit).
+	EventFunc("toggleFavored", toggleFavored)
+
+const HelloVuetifyMenuPath = "/samples/hello-vuetify-menu"
+
+// @snippet_end

+ 98 - 0
examples/e15_vuetify_navigation_drawer/page.go

@@ -0,0 +1,98 @@
+package e15_vuetify_navigation_drawer
+
+// @snippet_begin(VuetifyNavigationDrawerSample)
+import (
+	"fmt"
+	"time"
+
+	. "github.com/qor5/ui/vuetify"
+	"github.com/qor5/web"
+	h "github.com/theplant/htmlgo"
+)
+
+func VuetifyNavigationDrawer(ctx *web.EventContext) (pr web.PageResponse, err error) {
+
+	pr.Body = VContainer(
+		h.H2("A drawer that has close button"),
+
+		VBtn("show").On("click", "vars.drawer1 = !vars.drawer1"),
+
+		VNavigationDrawer(
+			h.Text("Hi"),
+			VBtn("Close").On("click", "vars.drawer1 = false"),
+		).Temporary(true).
+			Attr("v-model", "vars.drawer1").
+			Right(true).
+			Bottom(true).
+			Absolute(true).
+			Width(600),
+
+		h.H2("Load a drawer from remote and show it").Class("pt-8"),
+
+		VBtn("Show Drawer 2").OnClick("showDrawer"),
+
+		web.Portal().Name("drawer2UpdateContent"),
+
+		web.Portal().Name("drawer2"),
+	).Attr(web.InitContextVars, `{drawer1: false, drawer2: false}`)
+
+	return
+}
+
+func showDrawer(ctx *web.EventContext) (er web.EventResponse, err error) {
+	er.UpdatePortals = append(er.UpdatePortals,
+		&web.PortalUpdate{
+			Name: "drawer2",
+			Body: VNavigationDrawer(
+				h.Text("Drawer 2"),
+				web.Portal(
+					textField(""),
+				).Name("InputPortal"),
+				VBtn("Update parent and close").
+					OnClick("updateParentAndClose"),
+			).Right(true).
+				Attr("v-model", "vars.drawer2").
+				Bottom(true).
+				Temporary(true).
+				Absolute(true).
+				Value(true).
+				Width(800),
+		},
+	)
+
+	er.VarsScript = `setTimeout(function(){ vars.drawer2 = true }, 100)`
+	return
+}
+
+func textField(value string, fieldErrors ...string) h.HTMLComponent {
+	return VTextField().
+		FieldName("Drawer2Input").
+		ErrorMessages(fieldErrors...).
+		Value(value)
+}
+
+func updateParentAndClose(ctx *web.EventContext) (er web.EventResponse, err error) {
+	if len(ctx.R.FormValue("Drawer2Input")) < 10 {
+		er.UpdatePortals = append(er.UpdatePortals, &web.PortalUpdate{
+			Name: "InputPortal",
+			Body: textField(ctx.R.FormValue("Drawer2Input"), "input more then 10 characters"),
+		})
+		return
+	}
+
+	er.UpdatePortals = append(er.UpdatePortals, &web.PortalUpdate{
+		Name: "drawer2UpdateContent",
+		Body: h.Text(fmt.Sprintf("Updated content at %s", time.Now())),
+	})
+
+	er.VarsScript = "vars.drawer2 = false"
+	return
+}
+
+var VuetifyNavigationDrawerPB = web.Page(VuetifyNavigationDrawer).
+	EventFunc("showDrawer", showDrawer).
+	EventFunc("updateParentAndClose", updateParentAndClose)
+
+const VuetifyNavigationDrawerPath = "/samples/vuetify-navigation-drawer"
+
+// @snippet_end

+ 60 - 0
examples/e16_hello_vuetify_simple_components/page.go

@@ -0,0 +1,60 @@
+package e16_hello_vuetify_simple_components
+
+import (
+	. "github.com/qor5/ui/vuetify"
+	"github.com/qor5/web"
+	h "github.com/theplant/htmlgo"
+)
+
+func HelloVuetifySimpleComponents(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	wrapper := func(children ...h.HTMLComponent) h.HTMLComponent {
+		return VApp(
+			VMain(
+				VContainer(
+					children...,
+				),
+			),
+		).Id("mainapp")
+	}
+
+	pr.Body = wrapper(
+		h.Div(
+			VAvatar(
+				h.Img("https://vuetifyjs.com/apple-touch-icon-180x180.png"),
+			).Color("grey lighten-3").Size(32),
+			VAvatar(
+				h.Img("https://vuetifyjs.com/apple-touch-icon-180x180.png"),
+			).Tile(true).Color("grey lighten-3").Size(32),
+		).Style("margin-bottom: 40px"),
+
+		h.Div(
+			VBadge(
+				web.Slot(
+					h.Span("6"),
+				).Name("badge"),
+				VIcon("shopping_cart").
+					Large(true).
+					Color("grey lighten-1"),
+			).Left(true),
+		).Style("margin-bottom: 40px"),
+
+		h.Div(
+			VChip(h.Text("Example Chip")),
+			VChip(h.Text("Example Chip")).Close(true),
+			VChip(
+				VAvatar(h.Img("https://randomuser.me/api/portraits/men/35.jpg")),
+				h.Text("Trevor Hansen"),
+			).Close(true),
+			VChip(
+				VAvatar(h.Text("A")).Class("teal"),
+				h.Text("ANZ Bank"),
+			),
+		).Style("margin-bottom: 40px"),
+
+		h.Div(
+			VAlert(h.Text("This is a success alert.")).Type("success").Value(true),
+		).Style("margin-bottom: 40px"),
+	)
+
+	return
+}

+ 160 - 0
examples/e17_hello_lazy_portals_and_reload/page.go

@@ -0,0 +1,160 @@
+package e17_hello_lazy_portals_and_reload
+
+// @snippet_begin(LazyPortalsAndReloadSample)
+
+import (
+	"fmt"
+	"time"
+
+	. "github.com/qor5/ui/vuetify"
+	"github.com/qor5/web"
+	h "github.com/theplant/htmlgo"
+)
+
+type mystate struct {
+	Company string
+	Error   string
+}
+
+var listItems = []string{"Apple", "Microsoft", "Google"}
+
+func LazyPortalsAndReload(ctx *web.EventContext) (pr web.PageResponse, err error) {
+
+	pr.Body = VApp(
+		VMain(
+			VContainer(
+				VDialog(
+					web.Slot(
+						VBtn("Select").Color("primary").Attr("v-on", "on"),
+					).Name("activator").Scope("{ on }"),
+					web.Portal().Loader(web.POST().EventFunc("menuItems")).Name("menuContent"),
+				),
+
+				h.Div(
+					h.H1("Portal A"),
+					web.Portal().Loader(web.POST().EventFunc("portal1")).Name("portalA"),
+				).Style("border: 2px solid blue;"),
+
+				h.Div(
+					h.H1("Portal B"),
+					web.Portal().Loader(web.POST().EventFunc("portal1")).Name("portalB"),
+				).Style("border: 2px solid red;"),
+
+				VBtn("Reload Portal A and B").OnClick("reloadAB").Color("orange").Dark(true),
+
+				h.Div(
+					h.H1("Portal C"),
+					web.Portal().Name("portalC"),
+				).Style("border: 2px solid blue;"),
+
+				h.Div(
+					h.H1("Portal D"),
+					web.Portal().Name("portalD"),
+				).Style("border: 2px solid red;"),
+
+				VBtn("Update Portal C and D").OnClick("updateCD").Color("primary").Dark(true),
+			),
+		),
+	)
+	return
+}
+
+func menuItems(ctx *web.EventContext) (r web.EventResponse, err error) {
+
+	var items []h.HTMLComponent
+	for _, item := range listItems {
+		items = append(items, VListItem(
+			VListItemTitle(h.Text(item)),
+		))
+	}
+
+	items = append(items, VDivider())
+
+	items = append(items,
+		VDialog(
+			web.Slot(
+				VListItemAction(
+					VBtn("Create New").Text(true).Attr("v-on", "on"),
+				),
+			).Name("activator").Scope("{ on }"),
+			web.Portal().Loader(web.POST().EventFunc("addItemForm")).Name("addItemForm").Visible("true"),
+		).Width("500"),
+	)
+
+	r.Body = VList(items...)
+	return
+}
+
+func addItemForm(ctx *web.EventContext) (r web.EventResponse, err error) {
+	var s = &mystate{}
+	ctx.MustUnmarshalForm(s)
+
+	textField := VTextField().FieldName("Company")
+
+	if len(s.Error) > 0 {
+		textField.Error(true).ErrorMessages(s.Error)
+	}
+
+	r.Body = VCard(
+		VCardText(
+			textField,
+		),
+		VCardActions(
+			VBtn("Create").Color("primary").OnClick("addItem"),
+		),
+	)
+	return
+}
+
+func addItem(ctx *web.EventContext) (r web.EventResponse, err error) {
+	var s = &mystate{}
+	ctx.MustUnmarshalForm(s)
+
+	if len(s.Company) < 5 {
+		s.Error = "too short"
+		r.ReloadPortals = []string{"addItemForm"}
+		return
+	}
+
+	listItems = append(listItems, s.Company)
+	s.Company = ""
+	s.Error = ""
+	r.ReloadPortals = []string{"menuContent"}
+	return
+}
+
+func portal1(ctx *web.EventContext) (r web.EventResponse, err error) {
+	r.Body = h.Text(fmt.Sprint(time.Now().UnixNano()))
+	return
+}
+
+func reloadAB(ctx *web.EventContext) (r web.EventResponse, err error) {
+	r.ReloadPortals = []string{"portalA", "portalB"}
+	return
+}
+
+func updateCD(ctx *web.EventContext) (r web.EventResponse, err error) {
+	r.UpdatePortals = append(r.UpdatePortals,
+		&web.PortalUpdate{
+			Name: "portalC",
+			Body: h.Text(fmt.Sprint(time.Now().UnixNano())),
+		},
+		&web.PortalUpdate{
+			Name: "portalD",
+			Body: h.Text(fmt.Sprint(time.Now().UnixNano())),
+		},
+	)
+	return
+}
+
+var LazyPortalsAndReloadPB = web.Page(LazyPortalsAndReload).
+	EventFunc("addItem", addItem).
+	EventFunc("menuItems", menuItems).
+	EventFunc("addItemForm", addItemForm).
+	EventFunc("portal1", portal1).
+	EventFunc("reloadAB", reloadAB).
+	EventFunc("updateCD", updateCD)
+
+const LazyPortalsAndReloadPath = "/samples/lazy-portals-and-reload"
+
+// @snippet_end

+ 51 - 0
examples/e18_filter_component/page.go

@@ -0,0 +1,51 @@
+package e18_filter_component
+
+import (
+	. "github.com/qor5/ui/vuetify"
+	"github.com/qor5/ui/vuetifyx"
+	"github.com/qor5/web"
+)
+
+func FilterComponent(ctx *web.EventContext) (pr web.PageResponse, err error) {
+
+	fd := vuetifyx.FilterData([]*vuetifyx.FilterItem{
+		{
+			Key:          "invoiceDate",
+			Label:        "Invoice Date",
+			ItemType:     vuetifyx.ItemTypeDatetimeRange,
+			SQLCondition: "InvoiceDate %s datetime(?, 'unixepoch')",
+			Selected:     true,
+		},
+		{
+			Key:          "country",
+			Label:        "Country",
+			ItemType:     vuetifyx.ItemTypeSelect,
+			SQLCondition: "upper(BillingCountry) %s upper(?)",
+			Options: []*vuetifyx.SelectItem{
+				{
+					Value: "US",
+					Text:  "United States",
+				},
+				{
+					Value: "CN",
+					Text:  "China",
+				},
+			},
+		},
+		{
+			Key:          "totalAmount",
+			Label:        "Total Amount",
+			ItemType:     vuetifyx.ItemTypeNumber,
+			SQLCondition: "Total %s ?",
+		},
+	})
+
+	fd.SetByQueryString(ctx.R.URL.RawQuery)
+
+	pr.Body = VApp(
+		VMain(
+			vuetifyx.VXFilter(fd),
+		),
+	)
+	return
+}

+ 121 - 0
examples/e19_stripeui_key_info/page.go

@@ -0,0 +1,121 @@
+package e19_stripeui_key_info
+
+import (
+	"fmt"
+	"time"
+
+	s "github.com/qor5/ui/stripeui"
+	. "github.com/qor5/ui/vuetify"
+	"github.com/qor5/web"
+	"github.com/sunfmin/reflectutils"
+	h "github.com/theplant/htmlgo"
+)
+
+type Event struct {
+	Title     string
+	CreatedAt time.Time
+}
+
+func KeyInfoDemo(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	data := []*Event{
+		{
+			"<span><strong>¥5,000</strong> was refunded from a <strong>¥236,170</strong> payment</span>",
+			time.Now(),
+		},
+		{
+			"<span><strong>¥207,626</strong> was refunded from a <strong>¥236,170</strong> payment</span>",
+			time.Now(),
+		},
+		{
+			"<span><strong>¥7,848</strong> was refunded from a <strong>¥236,170</strong> payment</span>",
+			time.Now(),
+		},
+		{
+			"<span><strong>¥5,000</strong> was refunded from a <strong>¥236,170</strong> payment</span>",
+			time.Now(),
+		},
+		{
+			"<span><strong>¥207,626</strong> was refunded from a <strong>¥236,170</strong> payment</span>",
+			time.Now(),
+		},
+		{
+			"<span><strong>¥7,848</strong> was refunded from a <strong>¥236,170</strong> payment</span>",
+			time.Now(),
+		},
+	}
+
+	dt := s.DataTable(data).WithoutHeader(true).LoadMoreAt(3, "Show More")
+
+	dt.Column("Title").CellComponentFunc(func(obj interface{}, fieldName string, ctx *web.EventContext) h.HTMLComponent {
+		return h.Td(h.RawHTML(fmt.Sprint(reflectutils.MustGet(obj, fieldName))))
+	})
+
+	dt.Column("CreatedAt").CellComponentFunc(func(obj interface{}, fieldName string, ctx *web.EventContext) h.HTMLComponent {
+		t := reflectutils.MustGet(obj, fieldName).(time.Time)
+		return h.Td(h.Text(t.Format("01/02/06, 15:04:05 PM"))).Class("text-right")
+	})
+
+	logsDt := s.DataTable(data).
+		WithoutHeader(true).
+		LoadMoreAt(3, "Show More").
+		LoadMoreURL("/e20_vuetify_expansion_panels").
+		RowExpandFunc(func(obj interface{}, ctx *web.EventContext) h.HTMLComponent {
+			return h.Div().Text(h.JSONString(obj)).Class("pa-5")
+
+		})
+
+	logsDt.Column("Title").CellComponentFunc(func(obj interface{}, fieldName string, ctx *web.EventContext) h.HTMLComponent {
+		return h.Td(h.RawHTML(fmt.Sprint(reflectutils.MustGet(obj, fieldName))))
+	})
+
+	logsDt.Column("CreatedAt").CellComponentFunc(func(obj interface{}, fieldName string, ctx *web.EventContext) h.HTMLComponent {
+		t := reflectutils.MustGet(obj, fieldName).(time.Time)
+		return h.Td(h.Text(t.Format("01/02/06, 15:04:05 PM"))).Class("text-right")
+	})
+
+	pr.Body = VApp(
+		VMain(
+			s.Card(
+				s.KeyInfo(
+					s.KeyField(h.Text(time.Now().Format("Jan _2, 15:04 PM"))).Label("Date"),
+					s.KeyField(h.A().Href("https://google.com").Text("customer0077N52")).Label("Customer"),
+					s.KeyField(h.Text("•••• 4242")).Label("Payment method").Icon(VIcon("credit_card")),
+					s.KeyField(h.Text("Normal")).Label("Risk evaluation").Icon(VChip(h.Text("43")).Small(true)),
+				),
+			).SystemBar(
+				VIcon("link"),
+				h.Text("Hello"),
+				VSpacer(),
+				h.Text("ch_1EJtQMAqkzzGorqLtIjCEPU5"),
+			).Header(
+				h.Text("$100.00USD"),
+				VChip(h.Text("Refunded"), VIcon("reply").Small(true)).Small(true),
+			).Actions(
+				VBtn("Edit").Depressed(true),
+			).Class("mb-4"),
+
+			s.Card(s.DetailInfo(
+				s.DetailColumn(
+					s.DetailField(s.OptionalText("cus_EnUK8WcwQkuKQP")).Label("ID"),
+					s.DetailField(s.OptionalText(time.Now().Format("2006/01/02 15:04"))).Label("Created"),
+					s.DetailField(s.OptionalText("hello@example.com")).Label("Email"),
+					s.DetailField(s.OptionalText("customer0077N52")).Label("Description"),
+					s.DetailField(s.OptionalText("B0E69DBD")).Label("Invoice prefix"),
+					s.DetailField(s.OptionalText("").ZeroLabel("No VAT number")).Label("VAT number"),
+					s.DetailField(s.OptionalText("Normal")).Label("Risk evaluation").Icon(VChip(h.Text("43")).Small(true)),
+				).Header("ACCOUNT INFORMATION"),
+				s.DetailColumn(
+					s.DetailField(s.OptionalText("").ZeroLabel("No address")).Label("Address"),
+					s.DetailField(s.OptionalText("").ZeroLabel("No phone number")).Label("Phone number"),
+				).Header("BILLING INFORMATION"),
+			)).HeaderTitle("Details").
+				Actions(VBtn("Update details").Depressed(true)).
+				Class("mb-4"),
+
+			s.Card(dt).HeaderTitle("Events").Class("mb-4"),
+
+			s.Card(logsDt).HeaderTitle("Logs").Class("mb-4"),
+		),
+	)
+	return
+}

+ 66 - 0
examples/e20_vuetify_expansion_panels/page.go

@@ -0,0 +1,66 @@
+package e20_vuetify_expansion_panels
+
+import (
+	"time"
+
+	s "github.com/qor5/ui/stripeui"
+	. "github.com/qor5/ui/vuetify"
+	"github.com/qor5/web"
+	h "github.com/theplant/htmlgo"
+)
+
+type Event struct {
+	Title     string
+	CreatedAt time.Time
+}
+
+func ExpansionPanelDemo(ctx *web.EventContext) (pr web.PageResponse, err error) {
+
+	pr.Body = VApp(
+		VMain(
+			VExpansionPanels(
+				VExpansionPanel(
+					VExpansionPanelHeader(
+						h.Text("VISA •••• 4242	11 / 2028"),
+						web.Slot(
+							VIcon("search"),
+						).Name("actions"),
+					).DisableIconRotate(true),
+					VExpansionPanelContent(
+						VDivider(),
+						s.DetailInfo(
+							s.DetailColumn(
+								s.DetailField(s.OptionalText("FENGMIN SUN").ZeroLabel("No Name")).Label("Name"),
+								s.DetailField(s.OptionalText("•••• 4242").ZeroLabel("No Number")).Label("Number"),
+								s.DetailField(s.OptionalText("QlfGjXhL3I1xfKVV").ZeroLabel("No Fingerprint")).Label("Fingerprint"),
+								s.DetailField(s.OptionalText("11 / 2028").ZeroLabel("No Expires")).Label("Expires"),
+								s.DetailField(s.OptionalText("Visa credit card").ZeroLabel("No Type")).Label("Type"),
+								s.DetailField(s.OptionalText("card_1EJtLGAqkzzGorqLeFb6h2YV").ZeroLabel("No Type")).Label("ID"),
+							),
+						).Class("pa-0"),
+					),
+				),
+
+				VExpansionPanel(
+					VExpansionPanelHeader(
+						h.Text("VISA •••• 2121	11 / 2028"),
+					),
+					VExpansionPanelContent(
+						VDivider(),
+						s.DetailInfo(
+							s.DetailColumn(
+								s.DetailField(s.OptionalText("FENGMIN SUN").ZeroLabel("No Name")).Label("Name"),
+								s.DetailField(s.OptionalText("•••• 4242").ZeroLabel("No Number")).Label("Number"),
+								s.DetailField(s.OptionalText("QlfGjXhL3I1xfKVV").ZeroLabel("No Fingerprint")).Label("Fingerprint"),
+								s.DetailField(s.OptionalText("11 / 2028").ZeroLabel("No Expires")).Label("Expires"),
+								s.DetailField(s.OptionalText("Visa credit card").ZeroLabel("No Type")).Label("Type"),
+								s.DetailField(s.OptionalText("card_1EJtLGAqkzzGorqLeFb6h2YV").ZeroLabel("No Type")).Label("ID"),
+							),
+						).Class("pa-0"),
+					),
+				),
+			),
+		),
+	)
+	return
+}

+ 295 - 0
examples/e21_presents/detailing.go

@@ -0,0 +1,295 @@
+package e21_presents
+
+import (
+	"fmt"
+	"net/url"
+	"time"
+
+	"github.com/qor5/admin/presets"
+	"github.com/qor5/admin/presets/actions"
+	"github.com/qor5/ui/stripeui"
+	. "github.com/qor5/ui/vuetify"
+	"github.com/qor5/web"
+	h "github.com/theplant/htmlgo"
+	"gorm.io/gorm"
+)
+
+// @snippet_begin(PresetsDetailPageTopNotesSample)
+
+type Note struct {
+	ID         int
+	SourceType string
+	SourceID   int
+	Content    string
+	CreatedAt  time.Time
+	UpdatedAt  time.Time
+}
+
+func PresetsDetailPageTopNotes(b *presets.Builder) (
+	cust *presets.ModelBuilder,
+	cl *presets.ListingBuilder,
+	ce *presets.EditingBuilder,
+	dp *presets.DetailingBuilder,
+	db *gorm.DB,
+) {
+	cust, cl, ce, db = PresetsEditingCustomizationValidation(b)
+	b.URIPrefix(PresetsDetailPageTopNotesPath)
+	err := db.AutoMigrate(&Note{})
+	if err != nil {
+		panic(err)
+	}
+
+	dp = cust.Detailing("TopNotes")
+
+	dp.Field("TopNotes").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
+		mi := field.ModelInfo
+		cu := obj.(*Customer)
+
+		title := cu.Name
+		if len(title) == 0 {
+			title = cu.Description
+		}
+
+		var notes []*Note
+		err := db.Where("source_type = 'Customer' AND source_id = ?", cu.ID).
+			Order("id DESC").
+			Find(&notes).Error
+		if err != nil {
+			panic(err)
+		}
+
+		dt := stripeui.DataTable(notes).WithoutHeader(true).LoadMoreAt(2, "Show More")
+
+		dt.Column("Content").CellComponentFunc(func(obj interface{}, fieldName string, ctx *web.EventContext) h.HTMLComponent {
+			n := obj.(*Note)
+			return h.Td(h.Div(
+				h.Div(
+					VIcon("comment").Color("blue").Small(true).Class("pr-2"),
+					h.Text(n.Content),
+				).Class("body-1"),
+				h.Div(
+					h.Text(n.CreatedAt.Format("Jan 02,15:04 PM")),
+					h.Text(" by Felix Sun"),
+				).Class("grey--text pl-7 body-2"),
+			).Class("my-3"))
+		})
+
+		cusID := fmt.Sprint(cu.ID)
+		dt.RowMenuItemFuncs(presets.EditDeleteRowMenuItemFuncs(mi, mi.PresetsPrefix()+"/notes", url.Values{"model": []string{"Customer"}, "model_id": []string{cusID}})...)
+
+		return stripeui.Card(
+			dt,
+		).HeaderTitle(title).
+			Actions(
+				VBtn("Add Note").
+					Depressed(true).
+					Attr("@click",
+						web.POST().EventFunc(actions.New).
+							Query("model", "Customer").
+							Query("model_id", cusID).
+							URL(mi.PresetsPrefix()+"/notes").
+							Go(),
+					),
+			).Class("mb-4")
+	})
+
+	b.Model(&Note{}).
+		InMenu(false).
+		Editing("Content").
+		SetterFunc(func(obj interface{}, ctx *web.EventContext) {
+			note := obj.(*Note)
+			note.SourceID = ctx.QueryAsInt("model_id")
+			note.SourceType = ctx.R.FormValue("model")
+		})
+	return
+}
+
+const PresetsDetailPageTopNotesPath = "/samples/presets-detail-page-top-notes"
+
+// @snippet_end
+
+// @snippet_begin(PresetsDetailPageDetailsSample)
+
+func PresetsDetailPageDetails(b *presets.Builder) (
+	cust *presets.ModelBuilder,
+	cl *presets.ListingBuilder,
+	ce *presets.EditingBuilder,
+	dp *presets.DetailingBuilder,
+	db *gorm.DB,
+) {
+	cust, cl, ce, dp, db = PresetsDetailPageTopNotes(b)
+	b.URIPrefix(PresetsDetailPageDetailsPath)
+	err := db.AutoMigrate(&CreditCard{})
+	if err != nil {
+		panic(err)
+	}
+	dp = cust.Detailing("TopNotes", "Details")
+	dp.Field("Details").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
+		mi := field.ModelInfo
+		cu := obj.(*Customer)
+		cusID := fmt.Sprint(cu.ID)
+
+		var termAgreed string
+		if cu.TermAgreedAt != nil {
+			termAgreed = cu.TermAgreedAt.Format("Jan 02,15:04 PM")
+		}
+
+		detail := stripeui.DetailInfo(
+			stripeui.DetailColumn(
+				stripeui.DetailField(stripeui.OptionalText(cu.Name).ZeroLabel("No Name")).Label("Name"),
+				stripeui.DetailField(stripeui.OptionalText(cu.Email).ZeroLabel("No Email")).Label("Email"),
+				stripeui.DetailField(stripeui.OptionalText(cusID).ZeroLabel("No ID")).Label("ID"),
+				stripeui.DetailField(stripeui.OptionalText(cu.CreatedAt.Format("Jan 02,15:04 PM")).ZeroLabel("")).Label("Created"),
+				stripeui.DetailField(stripeui.OptionalText(termAgreed).ZeroLabel("Not Agreed Yet")).Label("Terms Agreed"),
+			).Header("ACCOUNT INFORMATION"),
+			stripeui.DetailColumn(
+				stripeui.DetailField(h.RawHTML(cu.Description)).Label("Description"),
+			).Header("DETAILS"),
+		)
+
+		return stripeui.Card(detail).HeaderTitle("Details").
+			Actions(
+				VBtn("Agree Terms").
+					Depressed(true).Class("mr-2").
+					Attr("@click", web.POST().
+						EventFunc(actions.Action).
+						Query(presets.ParamAction, "AgreeTerms").
+						Query(presets.ParamID, cusID).
+						Go(),
+					),
+
+				VBtn("Update details").
+					Depressed(true).
+					Attr("@click", web.POST().
+						EventFunc(actions.Edit).
+						Query(presets.ParamOverlay, actions.Dialog).
+						Query(presets.ParamID, cusID).
+						URL(mi.PresetsPrefix()+"/customers").
+						Go(),
+					),
+			).Class("mb-4")
+	})
+
+	dp.Action("AgreeTerms").UpdateFunc(func(selectedIds []string, ctx *web.EventContext) (err error) {
+		if ctx.R.FormValue("Agree") != "true" {
+			ve := &web.ValidationErrors{}
+			ve.GlobalError("You must agree the terms")
+			err = ve
+			return
+		}
+
+		err = db.Model(&Customer{}).Where("id = ?", selectedIds[0]).
+			Updates(map[string]interface{}{"term_agreed_at": time.Now()}).Error
+
+		return
+	}).ComponentFunc(func(selectedIds []string, ctx *web.EventContext) h.HTMLComponent {
+		var alert h.HTMLComponent
+
+		if ve, ok := ctx.Flash.(*web.ValidationErrors); ok {
+			alert = VAlert(h.Text(ve.GetGlobalError())).Border("left").
+				Type("error").
+				Elevation(2).
+				ColoredBorder(true)
+		}
+
+		return h.Components(
+			alert,
+			VCheckbox().FieldName("Agree").Value(ctx.R.FormValue("Agree")).Label("Agree the terms"),
+		)
+	})
+	return
+}
+
+const PresetsDetailPageDetailsPath = "/samples/presets-detail-page-details"
+
+// @snippet_end
+
+// @snippet_begin(PresetsDetailPageCardsSample)
+
+type CreditCard struct {
+	ID              int
+	CustomerID      int
+	Number          string
+	ExpireYearMonth string
+	Name            string
+	Type            string
+	Phone           string
+	Email           string
+}
+
+func PresetsDetailPageCards(b *presets.Builder) (
+	cust *presets.ModelBuilder,
+	cl *presets.ListingBuilder,
+	ce *presets.EditingBuilder,
+	dp *presets.DetailingBuilder,
+	db *gorm.DB,
+) {
+	cust, cl, ce, dp, db = PresetsDetailPageDetails(b)
+	b.URIPrefix(PresetsDetailPageCardsPath)
+	err := db.AutoMigrate(&CreditCard{})
+	if err != nil {
+		panic(err)
+	}
+	dp = cust.Detailing("TopNotes", "Details", "Cards")
+
+	dp.Field("Cards").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
+		mi := field.ModelInfo
+		cu := obj.(*Customer)
+		cusID := fmt.Sprint(cu.ID)
+
+		var cards []*CreditCard
+		err := db.Where("customer_id = ?", cu.ID).Order("id ASC").Find(&cards).Error
+		if err != nil {
+			panic(err)
+		}
+
+		dt := stripeui.DataTable(cards).
+			WithoutHeader(true).
+			RowExpandFunc(func(obj interface{}, ctx *web.EventContext) h.HTMLComponent {
+				card := obj.(*CreditCard)
+				return stripeui.DetailInfo(
+					stripeui.DetailColumn(
+						stripeui.DetailField(stripeui.OptionalText(card.Name).ZeroLabel("No Name")).Label("Name"),
+						stripeui.DetailField(stripeui.OptionalText(card.Number).ZeroLabel("No Number")).Label("Number"),
+						stripeui.DetailField(stripeui.OptionalText(card.ExpireYearMonth).ZeroLabel("No Expires")).Label("Expires"),
+						stripeui.DetailField(stripeui.OptionalText(card.Type).ZeroLabel("No Type")).Label("Type"),
+						stripeui.DetailField(stripeui.OptionalText(card.Phone).ZeroLabel("No phone provided")).Label("Phone"),
+						stripeui.DetailField(stripeui.OptionalText(card.Email).ZeroLabel("No email provided")).Label("Email"),
+					),
+				)
+			}).RowMenuItemFuncs(presets.EditDeleteRowMenuItemFuncs(mi, mi.PresetsPrefix()+"/credit-cards", url.Values{"customerID": []string{cusID}})...)
+
+		dt.Column("Type")
+		dt.Column("Number")
+		dt.Column("ExpireYearMonth")
+
+		return stripeui.Card(dt).HeaderTitle("Cards").
+			Actions(
+				VBtn("Add Card").
+					Depressed(true).
+					Attr("@click",
+						web.POST().
+							EventFunc(actions.New).
+							Query("customerID", cusID).
+							URL(mi.PresetsPrefix()+"/credit-cards").
+							Go(),
+					).Class("mb-4"),
+			)
+	})
+
+	cc := b.Model(&CreditCard{}).
+		InMenu(false)
+
+	ccedit := cc.Editing("ExpireYearMonth", "Phone", "Email").
+		SetterFunc(func(obj interface{}, ctx *web.EventContext) {
+			card := obj.(*CreditCard)
+			card.CustomerID = ctx.QueryAsInt("customerID")
+		})
+
+	ccedit.Creating("Number")
+	return
+}
+
+const PresetsDetailPageCardsPath = "/samples/presets-detail-page-cards"
+
+// @snippet_end

+ 145 - 0
examples/e21_presents/editing.go

@@ -0,0 +1,145 @@
+package e21_presents
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+
+	"github.com/qor5/admin/presets"
+	"github.com/qor5/ui/tiptap"
+	"github.com/qor5/web"
+	"github.com/sunfmin/reflectutils"
+	h "github.com/theplant/htmlgo"
+	"gorm.io/gorm"
+)
+
+// @snippet_begin(PresetsEditingCustomizationDescriptionSample)
+
+func PresetsEditingCustomizationDescription(b *presets.Builder) (
+	cust *presets.ModelBuilder,
+	cl *presets.ListingBuilder,
+	ce *presets.EditingBuilder,
+	db *gorm.DB,
+) {
+	cust, cl, ce, db = PresetsListingCustomizationBulkActions(b)
+	b.URIPrefix(PresetsEditingCustomizationDescriptionPath)
+	b.ExtraAsset("/tiptap.js", "text/javascript", tiptap.JSComponentsPack())
+	b.ExtraAsset("/tiptap.css", "text/css", tiptap.CSSComponentsPack())
+
+	ce.Only("Name", "CompanyID", "Description")
+
+	ce.Field("Description").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
+		return tiptap.TipTapEditor().
+			FieldName(field.Name).
+			Value(field.Value(obj).(string))
+	})
+	return
+}
+
+const PresetsEditingCustomizationDescriptionPath = "/samples/presets-editing-customization-description"
+
+// @snippet_end
+
+// @snippet_begin(PresetsEditingCustomizationFileTypeSample)
+
+type MyFile string
+
+type Product struct {
+	ID        int
+	Title     string
+	MainImage MyFile
+}
+
+func PresetsEditingCustomizationFileType(b *presets.Builder) (
+	cust *presets.ModelBuilder,
+	cl *presets.ListingBuilder,
+	ce *presets.EditingBuilder,
+	db *gorm.DB,
+) {
+	cust, cl, ce, db = PresetsEditingCustomizationDescription(b)
+	err := db.AutoMigrate(&Product{})
+	if err != nil {
+		panic(err)
+	}
+
+	b.URIPrefix(PresetsEditingCustomizationFileTypePath)
+	b.FieldDefaults(presets.WRITE).
+		FieldType(MyFile("")).
+		ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
+			val := field.Value(obj).(MyFile)
+			var img h.HTMLComponent
+			if len(string(val)) > 0 {
+				img = h.Img(string(val))
+			}
+			var er h.HTMLComponent
+			if len(field.Errors) > 0 {
+				er = h.Div().Text(field.Errors[0]).Style("color:red")
+			}
+			return h.Div(
+				img,
+				er,
+				h.Input("").Type("file").Attr(web.VFieldName(fmt.Sprintf("%s_NewFile", field.Name))...),
+			)
+		}).
+		SetterFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) (err error) {
+			ff, _, _ := ctx.R.FormFile(fmt.Sprintf("%s_NewFile", field.Name))
+
+			if ff == nil {
+				return
+			}
+
+			req, err := http.NewRequest("PUT", "https://transfer.sh/myfile.png", ff)
+			if err != nil {
+				return
+			}
+			var res *http.Response
+			res, err = http.DefaultClient.Do(req)
+			if err != nil {
+				panic(err)
+			}
+			var b []byte
+			b, err = ioutil.ReadAll(res.Body)
+			if err != nil {
+				return
+			}
+			if res.StatusCode == 500 {
+				err = fmt.Errorf("%s", string(b))
+				return
+			}
+			err = reflectutils.Set(obj, field.Name, MyFile(b))
+			return
+		})
+
+	mb := b.Model(&Product{})
+	mb.Editing("Title", "MainImage")
+	return
+}
+
+const PresetsEditingCustomizationFileTypePath = "/samples/presets-editing-customization-file-type"
+
+// @snippet_end
+
+// @snippet_begin(PresetsEditingCustomizationValidationSample)
+
+func PresetsEditingCustomizationValidation(b *presets.Builder) (
+	cust *presets.ModelBuilder,
+	cl *presets.ListingBuilder,
+	ce *presets.EditingBuilder,
+	db *gorm.DB,
+) {
+	cust, cl, ce, db = PresetsEditingCustomizationDescription(b)
+	b.URIPrefix(PresetsEditingCustomizationValidationPath)
+
+	ce.ValidateFunc(func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) {
+		cus := obj.(*Customer)
+		if len(cus.Name) < 10 {
+			err.FieldError("Name", "name is too short")
+		}
+		return
+	})
+	return
+}
+
+const PresetsEditingCustomizationValidationPath = "/samples/presets-editing-customization-validation"
+
+// @snippet_end

+ 45 - 0
examples/e21_presents/filter.go

@@ -0,0 +1,45 @@
+package e21_presents
+
+// @snippet_begin(FilterSample)
+import (
+	"github.com/qor5/admin/presets"
+	"github.com/qor5/admin/presets/gorm2op"
+	"github.com/qor5/ui/vuetifyx"
+	"github.com/qor5/web"
+)
+
+func PresetsBasicFilter(b *presets.Builder) {
+	b.URIPrefix(PresetsBasicFilterPath).
+		DataOperator(gorm2op.DataOperator(DB))
+
+	// create a ModelBuilder
+	videoBuilder := b.Model(&Customer{})
+
+	// get its ListingBuilder
+	listing := videoBuilder.Listing()
+
+	// Call FilterDataFunc
+	listing.FilterDataFunc(func(ctx *web.EventContext) vuetifyx.FilterData {
+		// Prepare filter options, it is a two dimension array: [][]string{"text", "value"}
+		options := []*vuetifyx.SelectItem{{
+			Text:  "Draft",
+			Value: "draft",
+		}}
+
+		return []*vuetifyx.FilterItem{
+			{
+				Key:      "status",
+				Label:    "Status",
+				ItemType: vuetifyx.ItemTypeString,
+				// %s is the condition. e.g. >, >=, =, <, <=, like,
+				// ? is the value of of selected option
+				SQLCondition: `status %s ?`,
+				Options:      options,
+			},
+		}
+	})
+}
+
+// @snippet_end
+
+const PresetsBasicFilterPath = "/samples/basic_filter"

+ 88 - 0
examples/e21_presents/linkage_select_filter_item.go

@@ -0,0 +1,88 @@
+package e21_presents
+
+// @snippet_begin(LinkageSelectFilterItem)
+import (
+	"github.com/qor5/admin/presets"
+	"github.com/qor5/admin/presets/gorm2op"
+	vx "github.com/qor5/ui/vuetifyx"
+	"github.com/qor5/web"
+	h "github.com/theplant/htmlgo"
+)
+
+func PresetsLinkageSelectFilterItem(b *presets.Builder) {
+	b.URIPrefix(PresetsLinkageSelectFilterItemPath).
+		DataOperator(gorm2op.DataOperator(DB))
+
+	mb := b.Model(&Address{})
+
+	eb := mb.Editing("ProvinceCityDistrict")
+
+	eb.Field("ProvinceCityDistrict").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
+		m := obj.(*Address)
+
+		return vx.VXLinkageSelect().
+			FieldName(field.Name).
+			Items(getLinkageProvinceCityDistrictItems()...).
+			Labels(getLinkageProvinceCityDistrictLabels()...).
+			SelectedIDs(m.Province, m.City, m.District)
+	}).SetterFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) (err error) {
+		vs := ctx.R.Form["ProvinceCityDistrict"]
+		m := obj.(*Address)
+		m.Province = vs[0]
+		m.City = vs[1]
+		m.District = vs[2]
+		return nil
+	})
+
+	lb := mb.Listing()
+
+	lb.FilterDataFunc(func(ctx *web.EventContext) vx.FilterData {
+		return []*vx.FilterItem{
+			{
+				Key:      "province_city_district",
+				Label:    "Province&City&District",
+				ItemType: vx.ItemTypeLinkageSelect,
+				LinkageSelectData: vx.FilterLinkageSelectData{
+					Items:            getLinkageProvinceCityDistrictItems(),
+					Labels:           getLinkageProvinceCityDistrictLabels(),
+					SelectOutOfOrder: false,
+					SQLConditions:    []string{"province = ?", "city = ?", "district = ?"},
+				},
+			},
+		}
+	})
+}
+
+func getLinkageProvinceCityDistrictLabels() []string {
+	return []string{"Province", "City", "District"}
+}
+
+func getLinkageProvinceCityDistrictItems() [][]*vx.LinkageSelectItem {
+	return [][]*vx.LinkageSelectItem{
+		{
+			// use ID as Name if Name is empty
+			{ID: "浙江", ChildrenIDs: []string{"杭州", "宁波"}},
+			{ID: "江苏", ChildrenIDs: []string{"南京", "苏州"}},
+		},
+		{
+			{ID: "杭州", ChildrenIDs: []string{"拱墅区", "西湖区"}},
+			{ID: "宁波", ChildrenIDs: []string{"镇海区", "鄞州区"}},
+			{ID: "南京", ChildrenIDs: []string{"鼓楼区", "玄武区"}},
+			{ID: "苏州", ChildrenIDs: []string{"常熟区", "吴江区"}},
+		},
+		{
+			{ID: "拱墅区"},
+			{ID: "西湖区"},
+			{ID: "镇海区"},
+			{ID: "鄞州区"},
+			{ID: "鼓楼区"},
+			{ID: "玄武区"},
+			{ID: "常熟区"},
+			{ID: "吴江区"},
+		},
+	}
+}
+
+// @snippet_end
+
+const PresetsLinkageSelectFilterItemPath = "/samples/linkage_select_filter_item"

+ 334 - 0
examples/e21_presents/listing.go

@@ -0,0 +1,334 @@
+// @snippet_begin(PresetHelloWorldSample)
+package e21_presents
+
+import (
+	"fmt"
+	"net/url"
+	"time"
+
+	"github.com/qor5/admin/presets"
+	"github.com/qor5/admin/presets/actions"
+	"github.com/qor5/admin/presets/gorm2op"
+	v "github.com/qor5/ui/vuetify"
+	"github.com/qor5/ui/vuetifyx"
+	"github.com/qor5/web"
+	"github.com/qor5/x/i18n"
+	h "github.com/theplant/htmlgo"
+	"golang.org/x/text/language"
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
+)
+
+type Customer struct {
+	ID              int
+	Name            string
+	Email           string
+	Description     string
+	CompanyID       int
+	CreatedAt       time.Time
+	UpdatedAt       time.Time
+	ApprovedAt      *time.Time
+	TermAgreedAt    *time.Time
+	ApprovalComment string
+}
+
+type Address struct {
+	ID       int
+	Province string
+	City     string
+	District string
+}
+
+var DB *gorm.DB
+
+func init() {
+	DB = setupDB()
+}
+
+func setupDB() (db *gorm.DB) {
+	var err error
+	db, err = gorm.Open(sqlite.Open("/tmp/my.db"), &gorm.Config{})
+	if err != nil {
+		panic(err)
+	}
+	db.Logger.LogMode(logger.Info)
+	err = db.AutoMigrate(
+		&Customer{},
+		&Company{},
+		&Address{},
+	)
+	if err != nil {
+		panic(err)
+	}
+	return
+}
+
+func PresetsHelloWorld(b *presets.Builder) (m *presets.ModelBuilder, db *gorm.DB) {
+	db = DB
+
+	b.I18n().
+		SupportLanguages(language.English, language.SimplifiedChinese).
+		RegisterForModule(language.SimplifiedChinese, presets.ModelsI18nModuleKey, Messages_zh_CN)
+
+	b.URIPrefix(PresetsHelloWorldPath).
+		DataOperator(gorm2op.DataOperator(db))
+	m = b.Model(&Customer{})
+
+	return
+}
+
+const PresetsHelloWorldPath = "/samples/presets-hello-world"
+
+// @snippet_end
+
+// @snippet_begin(PresetsListingCustomizationFieldsSample)
+
+type Company struct {
+	ID   int
+	Name string
+}
+
+func PresetsListingCustomizationFields(b *presets.Builder) (
+	cust *presets.ModelBuilder,
+	cl *presets.ListingBuilder,
+	ce *presets.EditingBuilder,
+	db *gorm.DB,
+) {
+	cust, db = PresetsHelloWorld(b)
+	b.URIPrefix(PresetsListingCustomizationFieldsPath)
+
+	cl = cust.Listing("ID", "Name", "Company", "Email").
+		SearchColumns("name", "email").SelectableColumns(true)
+	cl.Field("Company").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
+		c := obj.(*Customer)
+		var comp Company
+		if c.CompanyID == 0 {
+			return h.Td()
+		}
+
+		db.First(&comp, "id = ?", c.CompanyID)
+		return h.Td(
+			h.A().Text(comp.Name).
+				Attr("@click",
+					web.POST().EventFunc(actions.Edit).
+						Query(presets.ParamID, fmt.Sprint(comp.ID)).
+						URL(PresetsListingCustomizationFieldsPath+"/companies").
+						Go()),
+			h.Text("-"),
+			h.A().Text("(Open in Dialog)").
+				Attr("@click",
+					web.POST().EventFunc(actions.Edit).
+						Query(presets.ParamID, fmt.Sprint(comp.ID)).
+						Query(presets.ParamOverlay, actions.Dialog).
+						URL(PresetsListingCustomizationFieldsPath+"/companies").
+						Go(),
+				),
+		)
+	})
+
+	ce = cust.Editing("Name", "CompanyID")
+
+	cust.RegisterEventFunc("updateCompanyList", func(ctx *web.EventContext) (r web.EventResponse, err error) {
+		companyID := ctx.QueryAsInt(presets.ParamOverlayUpdateID)
+		r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
+			Name: "companyListPortal",
+			Body: companyList(ctx, db, companyID),
+		})
+		return
+	})
+
+	ce.Field("CompanyID").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
+		c := obj.(*Customer)
+		return web.Portal(companyList(ctx, db, c.CompanyID)).Name("companyListPortal")
+	})
+
+	comp := b.Model(&Company{})
+	comp.Editing().ValidateFunc(func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) {
+		c := obj.(*Company)
+		if len(c.Name) < 5 {
+			err.GlobalError("name must longer than 5")
+		}
+		return
+	})
+
+	return
+}
+
+func companyList(ctx *web.EventContext, db *gorm.DB, companyID int) h.HTMLComponent {
+	msgr := i18n.MustGetModuleMessages(ctx.R, presets.ModelsI18nModuleKey, Messages_en_US).(*Messages)
+	var comps []Company
+	db.Find(&comps)
+	return h.Div(
+		v.VSelect().
+			Label(msgr.CustomersCompanyID).
+			Items(comps).
+			ItemText("Name").
+			ItemValue("ID").
+			Value(companyID).
+			FieldName("CompanyID"),
+
+		h.A().Text("Add Company").Attr("@click",
+			web.POST().
+				URL(PresetsListingCustomizationFieldsPath+"/companies").
+				EventFunc(actions.New).
+				Query(presets.ParamOverlay, actions.Dialog).
+				Query(presets.ParamOverlayAfterUpdateScript,
+					web.POST().EventFunc("updateCompanyList").Go()).
+				Go(),
+		),
+	)
+}
+
+const PresetsListingCustomizationFieldsPath = "/samples/presets-listing-customization-fields"
+
+// @snippet_end
+
+// @snippet_begin(PresetsListingCustomizationFiltersSample)
+
+func PresetsListingCustomizationFilters(b *presets.Builder) (
+	cust *presets.ModelBuilder,
+	cl *presets.ListingBuilder,
+	ce *presets.EditingBuilder,
+	db *gorm.DB,
+) {
+	cust, cl, ce, db = PresetsListingCustomizationFields(b)
+	b.URIPrefix(PresetsListingCustomizationFiltersPath)
+
+	cl.FilterDataFunc(func(ctx *web.EventContext) vuetifyx.FilterData {
+		msgr := i18n.MustGetModuleMessages(ctx.R, presets.ModelsI18nModuleKey, Messages_en_US).(*Messages)
+		var companyOptions []*vuetifyx.SelectItem
+		err := db.Model(&Company{}).Select("name as text, id as value").Scan(&companyOptions).Error
+		if err != nil {
+			panic(err)
+		}
+
+		return []*vuetifyx.FilterItem{
+			{
+				Key:          "created",
+				Label:        msgr.CustomersFilterCreated,
+				ItemType:     vuetifyx.ItemTypeDatetimeRange,
+				SQLCondition: `cast(strftime('%%s', created_at) as INTEGER) %s ?`,
+			},
+			{
+				Key:          "approved",
+				Label:        msgr.CustomersFilterApproved,
+				ItemType:     vuetifyx.ItemTypeDatetimeRange,
+				SQLCondition: `cast(strftime('%%s', approved_at) as INTEGER) %s ?`,
+			},
+			{
+				Key:          "name",
+				Label:        msgr.CustomersFilterName,
+				ItemType:     vuetifyx.ItemTypeString,
+				SQLCondition: `name %s ?`,
+			},
+			{
+				Key:          "company",
+				Label:        msgr.CustomersFilterCompany,
+				ItemType:     vuetifyx.ItemTypeSelect,
+				SQLCondition: `company_id %s ?`,
+				Options:      companyOptions,
+			},
+		}
+	})
+	return
+}
+
+const PresetsListingCustomizationFiltersPath = "/samples/presets-listing-customization-filters"
+
+// @snippet_end
+
+// @snippet_begin(PresetsListingCustomizationTabsSample)
+
+func PresetsListingCustomizationTabs(b *presets.Builder) (
+	cust *presets.ModelBuilder,
+	cl *presets.ListingBuilder,
+	ce *presets.EditingBuilder,
+	db *gorm.DB,
+) {
+	cust, cl, ce, db = PresetsListingCustomizationFilters(b)
+	b.URIPrefix(PresetsListingCustomizationTabsPath)
+
+	cl.FilterTabsFunc(func(ctx *web.EventContext) []*presets.FilterTab {
+		var c Company
+		db.First(&c)
+		return []*presets.FilterTab{
+			{
+				Label: "Felix",
+				Query: url.Values{"name.ilike": []string{"felix"}},
+			},
+			{
+				Label: "The Plant",
+				Query: url.Values{"company": []string{fmt.Sprint(c.ID)}},
+			},
+			{
+				Label: "Approved",
+				Query: url.Values{"approved.gt": []string{fmt.Sprint(1)}},
+			},
+			{
+				Label: "All",
+				Query: url.Values{"all": []string{"1"}},
+			},
+		}
+	})
+	return
+}
+
+const PresetsListingCustomizationTabsPath = "/samples/presets-listing-customization-tabs"
+
+// @snippet_end
+
+// @snippet_begin(PresetsListingCustomizationBulkActionsSample)
+
+func PresetsListingCustomizationBulkActions(b *presets.Builder) (
+	cust *presets.ModelBuilder,
+	cl *presets.ListingBuilder,
+	ce *presets.EditingBuilder,
+	db *gorm.DB,
+) {
+	cust, cl, ce, db = PresetsListingCustomizationTabs(b)
+	b.URIPrefix(PresetsListingCustomizationBulkActionsPath)
+
+	cl.BulkAction("Approve").Label("Approve").
+		UpdateFunc(func(selectedIds []string, ctx *web.EventContext) (err error) {
+			comment := ctx.R.FormValue("ApprovalComment")
+			if len(comment) < 10 {
+				ctx.Flash = "comment should larger than 10"
+				return
+			}
+			err = db.Model(&Customer{}).
+				Where("id IN (?)", selectedIds).
+				Updates(map[string]interface{}{"approved_at": time.Now(), "approval_comment": comment}).Error
+			if err != nil {
+				ctx.Flash = err.Error()
+			}
+			return
+		}).
+		ComponentFunc(func(selectedIds []string, ctx *web.EventContext) h.HTMLComponent {
+			comment := ctx.R.FormValue("ApprovalComment")
+			errorMessage := ""
+			if ctx.Flash != nil {
+				errorMessage = ctx.Flash.(string)
+			}
+			return v.VTextField().
+				FieldName("ApprovalComment").
+				Value(comment).
+				Label("Comment").
+				ErrorMessages(errorMessage)
+		})
+
+	cl.BulkAction("Delete").Label("Delete").
+		UpdateFunc(func(selectedIds []string, ctx *web.EventContext) (err error) {
+			err = db.Where("id IN (?)", selectedIds).Delete(&Customer{}).Error
+			return
+		}).
+		ComponentFunc(func(selectedIds []string, ctx *web.EventContext) h.HTMLComponent {
+			return h.Div().Text(fmt.Sprintf("Are you sure you want to delete %s ?", selectedIds)).Class("title deep-orange--text")
+		})
+
+	return
+}
+
+const PresetsListingCustomizationBulkActionsPath = "/samples/presets-listing-customization-bulk-actions"
+
+// @snippet_end

+ 55 - 0
examples/e21_presents/messages.go

@@ -0,0 +1,55 @@
+package e21_presents
+
+type Messages struct {
+	Admin                   string
+	Customers               string
+	Companies               string
+	CustomersName           string
+	CustomersCompanyID      string
+	CustomersDescription    string
+	CompaniesName           string
+	CustomersID             string
+	CustomersCompany        string
+	CustomersEmail          string
+	Customer                string
+	CustomersFilterCreated  string
+	CustomersFilterApproved string
+	CustomersFilterName     string
+	CustomersFilterCompany  string
+}
+
+var Messages_zh_CN = &Messages{
+	Admin:                   "管理系统",
+	Customers:               "客户",
+	Companies:               "公司",
+	CustomersName:           "姓名",
+	CustomersCompanyID:      "公司",
+	CustomersDescription:    "描述",
+	CompaniesName:           "公司名称",
+	CustomersID:             "ID",
+	CustomersCompany:        "公司",
+	CustomersEmail:          "电子邮件",
+	Customer:                "客户",
+	CustomersFilterCreated:  "创建日",
+	CustomersFilterApproved: "承认日",
+	CustomersFilterName:     "名称",
+	CustomersFilterCompany:  "属于公司",
+}
+
+var Messages_en_US = &Messages{
+	Admin:                   "Admin",
+	Customers:               "Customers",
+	Companies:               "Companies",
+	CustomersName:           "Name",
+	CustomersCompanyID:      "Company",
+	CustomersDescription:    "Description",
+	CompaniesName:           "Name",
+	CustomersID:             "ID",
+	CustomersCompany:        "Company",
+	CustomersEmail:          "Email",
+	Customer:                "Customer",
+	CustomersFilterCreated:  "Created",
+	CustomersFilterApproved: "Approved",
+	CustomersFilterName:     "Name",
+	CustomersFilterCompany:  "Company",
+}

+ 78 - 0
examples/e21_presents/model-builder-extensions.go

@@ -0,0 +1,78 @@
+package e21_presents
+
+import (
+	"fmt"
+
+	"github.com/qor5/admin/presets"
+	"github.com/qor5/admin/presets/actions"
+	. "github.com/qor5/ui/vuetify"
+	"github.com/qor5/web"
+	h "github.com/theplant/htmlgo"
+	"gorm.io/gorm"
+)
+
+// @snippet_begin(PresetsModelBuilderExtensionsSample)
+
+func PresetsModelBuilderExtensions(b *presets.Builder) (
+	mb *presets.ModelBuilder,
+	db *gorm.DB,
+) {
+	mb, db = PresetsHelloWorld(b)
+	b.URIPrefix(PresetsModelBuilderExtensionsPath)
+	mb.LayoutConfig(&presets.LayoutConfig{SearchBoxInvisible: true})
+
+	eb := mb.Editing("Actions", "Name").ActionsFunc(func(obj interface{}, ctx *web.EventContext) h.HTMLComponent {
+		return h.Components(
+			VSpacer(),
+			VBtn("Action 1"),
+			VBtn("Action 2"),
+			VBtn("Update").
+				Color("primary").
+				Attr("@click", web.POST().
+					EventFunc(actions.Update).
+					Queries(ctx.Queries()).
+					URL(mb.Info().ListingHref()).
+					Go()),
+		)
+	})
+
+	eb.Field("Actions").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
+		cust := obj.(*Customer)
+		return VBtn("Change Name").Attr("@click",
+			web.POST().
+				EventFunc("changeName").
+				Query(presets.ParamID, fmt.Sprint(cust.ID)).
+				Go(),
+		)
+	})
+
+	eb.ValidateFunc(func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) {
+		cust := obj.(*Customer)
+		if len(cust.Name) < 5 {
+			err.GlobalError("Name must be longer than 5")
+		}
+		return
+	})
+
+	mb.RegisterEventFunc("changeName", changeNameEventFunc(mb))
+
+	return
+}
+
+func changeNameEventFunc(mb *presets.ModelBuilder) web.EventFunc {
+	return func(ctx *web.EventContext) (r web.EventResponse, err error) {
+		eb := mb.Editing()
+		obj := mb.NewModel()
+		id := ctx.R.FormValue(presets.ParamID)
+		obj, err = eb.Fetcher(obj, id, ctx)
+		obj.(*Customer).Name = "Darwin"
+		err = eb.Saver(obj, id, ctx)
+		presets.ShowMessage(&r, "Nicely updated", "")
+		eb.UpdateOverlayContent(ctx, &r, obj, "Good work", err)
+		return
+	}
+}
+
+const PresetsModelBuilderExtensionsPath = "/samples/presets-model-builder-extensions"
+
+// @snippet_end

+ 46 - 0
examples/e21_presents/notification-center.go

@@ -0,0 +1,46 @@
+package e21_presents
+
+// @snippet_begin(NotificationCenterSample)
+import (
+	"github.com/qor5/admin/presets"
+	"github.com/qor5/admin/presets/gorm2op"
+	"github.com/qor5/docs/examples/utils"
+	v "github.com/qor5/ui/vuetify"
+	"github.com/qor5/web"
+	h "github.com/theplant/htmlgo"
+)
+
+func PresetsNotificationCenterSample(b *presets.Builder) {
+	db := utils.InitDB()
+	b.URIPrefix(NotificationCenterSamplePath).
+		DataOperator(gorm2op.DataOperator(db))
+
+	db.AutoMigrate(&utils.Page{})
+	b.Model(&utils.Page{})
+
+	b.NotificationFunc(NotifierComponent(), NotifierCount())
+
+	return
+}
+
+func NotifierComponent() func(ctx *web.EventContext) h.HTMLComponent {
+	return func(ctx *web.EventContext) h.HTMLComponent {
+		return v.VList(
+			v.VListItem(
+				v.VListItemContent(h.A(h.Label("New Notice:"),
+					h.Text("unread notes: 3")),
+				),
+			),
+		)
+	}
+}
+
+func NotifierCount() func(ctx *web.EventContext) int {
+	return func(ctx *web.EventContext) int {
+		// Use your own count calculation logic here
+		return 3
+	}
+}
+
+// @snippet_end
+const NotificationCenterSamplePath = "/samples/notification_center"

+ 85 - 0
examples/e21_presents/permissions.go

@@ -0,0 +1,85 @@
+package e21_presents
+
+import (
+	"net/http"
+
+	"github.com/qor5/admin/presets"
+	. "github.com/qor5/ui/vuetify"
+	"github.com/qor5/web"
+	"github.com/qor5/x/perm"
+	h "github.com/theplant/htmlgo"
+	"gorm.io/gorm"
+)
+
+// @snippet_begin(PresetsPermissionsSample)
+type User struct {
+	ID       uint
+	Username string
+}
+
+type Group struct {
+	ID   uint
+	Name string
+}
+
+func PresetsPermissions(b *presets.Builder) (
+	cust *presets.ModelBuilder,
+	cl *presets.ListingBuilder,
+	ce *presets.EditingBuilder,
+	dp *presets.DetailingBuilder,
+	db *gorm.DB,
+) {
+	cust, cl, ce, dp, db = PresetsDetailPageCards(b)
+	b.URIPrefix(PresetsPermissionsPath)
+
+	b.ProfileFunc(func(ctx *web.EventContext) h.HTMLComponent {
+		return VMenu(
+			web.Slot(
+				VBtn("").
+					Icon(true).
+					Attr("v-bind", "attrs", "v-on", "on").
+					Children(
+						VIcon("person"),
+					).Class("ml-2"),
+			).Name("activator").Scope("{ on, attrs }"),
+
+			VList(
+				VListItem(
+					VListItemTitle(h.Text("Logout")),
+				),
+			),
+		)
+	})
+
+	perm.Verbose = true
+	b.Permission(perm.New().
+		Policies(
+			perm.PolicyFor("editor").WhoAre(perm.Allowed).ToDo(perm.Anything).On(perm.Anything),
+			perm.PolicyFor("editor").WhoAre(perm.Denied).ToDo(presets.PermRead...).On("*user_management*"),
+			perm.PolicyFor("editor").WhoAre(perm.Denied).
+				ToDo(presets.PermCreate, presets.PermDelete).On("*customers*"),
+			perm.PolicyFor("editor").WhoAre(perm.Denied).
+				ToDo(presets.PermCreate, presets.PermUpdate).On("*companies*"),
+			perm.PolicyFor("editor").WhoAre(perm.Denied).
+				ToDo(presets.PermUpdate).On("*customers:*:company_id*"),
+			perm.PolicyFor("editor").WhoAre(perm.Denied).
+				ToDo("*bulk_actions:delete").On("*:customers*"),
+		).
+		SubjectsFunc(func(r *http.Request) []string {
+			return []string{"editor"}
+		}))
+
+	err := db.AutoMigrate(&User{}, &Group{})
+	if err != nil {
+		panic(err)
+	}
+
+	b.MenuGroup("User Management").SubItems("user", "group")
+	b.Model(&User{})
+	b.Model(&Group{})
+	return
+}
+
+const PresetsPermissionsPath = "/samples/presets-permissions"
+
+// @snippet_end

+ 117 - 0
examples/e22_vuetify_variant_sub_form/page.go

@@ -0,0 +1,117 @@
+package e22_vuetify_variant_sub_form
+
+// @snippet_begin(VuetifyVariantSubForm)
+
+import (
+	"github.com/qor5/docs/utils"
+	. "github.com/qor5/ui/vuetify"
+	"github.com/qor5/web"
+	h "github.com/theplant/htmlgo"
+)
+
+type myFormValue struct {
+	Type  string
+	Form1 struct {
+		Gender string
+	}
+	Form2 struct {
+		Feature1 bool
+		Slider1  int
+	}
+}
+
+func VuetifyVariantSubForm(ctx *web.EventContext) (pr web.PageResponse, err error) {
+
+	var fv myFormValue
+	ctx.MustUnmarshalForm(&fv)
+	if fv.Type == "" {
+		fv.Type = "Type1"
+	}
+	var verr web.ValidationErrors
+
+	pr.Body = VContainer(
+		utils.PrettyFormAsJSON(ctx),
+
+		VSelect().
+			Items([]string{
+				"Type1",
+				"Type2",
+			}).
+			Value(fv.Type).
+			Attr("@change", web.POST().
+				FieldValue("Type", web.Var("$event")).
+				EventFunc("switchForm").
+				Go()),
+
+		web.Portal(
+			h.If(fv.Type == "Type1",
+				form1(ctx, &fv),
+			).Else(
+				form2(ctx, &fv, &verr),
+			),
+		).Name("subform"),
+
+		VBtn("Submit").OnClick("submit"),
+	)
+	return
+}
+
+func form1(ctx *web.EventContext, fv *myFormValue) h.HTMLComponent {
+
+	return VContainer(
+		h.H1("Form1"),
+		VRadioGroup(
+			VRadio().Value("F").Label("Female"),
+			VRadio().Value("M").Label("Male"),
+		).FieldName("Form1.Gender").
+			Value(fv.Form1.Gender).
+			Label("Gender"),
+	)
+}
+func form2(ctx *web.EventContext, fv *myFormValue, verr *web.ValidationErrors) h.HTMLComponent {
+
+	return VContainer(
+		h.H1("Form2"),
+
+		VSwitch().
+			FieldName("Form2.Feature1").
+			InputValue(fv.Form2.Feature1).
+			Label("Feature1"),
+
+		VSlider().FieldName("Form2.Slider1").
+			ErrorMessages(verr.GetFieldErrors("Slider1")...).
+			Value(fv.Form2.Slider1).
+			Label("Slider1"),
+	)
+}
+
+func submit(ctx *web.EventContext) (r web.EventResponse, err error) {
+	r.Reload = true
+	return
+}
+
+func switchForm(ctx *web.EventContext) (r web.EventResponse, err error) {
+	var verr web.ValidationErrors
+
+	var fv myFormValue
+	ctx.MustUnmarshalForm(&fv)
+	form := form1(ctx, &fv)
+	if fv.Type == "Type2" {
+		form = form2(ctx, &fv, &verr)
+	}
+
+	r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
+		Name: "subform",
+		Body: form,
+	})
+
+	return
+}
+
+var VuetifyVariantSubFormPB = web.Page(VuetifyVariantSubForm).
+	EventFunc("switchForm", switchForm).
+	EventFunc("submit", submit)
+
+const VuetifyVariantSubFormPath = "/samples/vuetify-variant-sub-form"
+
+// @snippet_end

+ 132 - 0
examples/e23_vuetify_components_kitchen/page.go

@@ -0,0 +1,132 @@
+package e23_vuetify_components_kitchen
+
+// @snippet_begin(VuetifyComponentsKitchen)
+
+import (
+	"github.com/qor5/docs/utils"
+	. "github.com/qor5/ui/vuetify"
+	"github.com/qor5/web"
+	h "github.com/theplant/htmlgo"
+)
+
+var globalCities = []string{"Tokyo", "Hangzhou", "Shanghai"}
+
+type formVals struct {
+	Cities1 []string
+	Cities2 []string
+
+	MyItem string
+}
+
+var fv = formVals{
+	Cities1: []string{
+		"TK",
+		"LD",
+	},
+
+	Cities2: []string{
+		"Hangzhou",
+		"Shanghai",
+	},
+
+	MyItem: "VItem2",
+}
+
+func VuetifyComponentsKitchen(ctx *web.EventContext) (pr web.PageResponse, err error) {
+
+	var chips h.HTMLComponents
+	for _, city := range globalCities {
+		chips = append(chips,
+			VChip(h.Text(city)).
+				Close(true).
+				Attr("@click:close", web.POST().EventFunc("removeCity").Query("city", city).Go()),
+		)
+	}
+
+	pr.Body = VContainer(
+		utils.PrettyFormAsJSON(ctx),
+
+		h.H1("Chips delete"),
+		chips,
+
+		h.H1("Chips group"),
+		VChipGroup(
+			VChip(
+				h.Text("Hangzhou"),
+				VIcon("star").Right(true),
+			).Value("HZ"),
+			VChip(h.Text("Shanghai")).Value("SH").Filter(true).Label(true),
+			VChip(h.Text("Tokyo")).Value("TK").Filter(true),
+			VChip(h.Text("New York")).Value("NY"),
+			VChip(h.Text("London")).Value("LD"),
+		).ActiveClass("indigo darken-3 white--text").
+			// Mandatory(true).
+			FieldName("Cities1").
+			Value(fv.Cities1).
+			Multiple(true),
+		VAutocomplete().
+			Items(globalCities).
+			FieldName("Cities2").
+			Value(fv.Cities2),
+
+		h.H1("Items Group"),
+
+		VItemGroup(
+			VContainer(
+				VRow(
+					VCol(
+						VItem(
+							VCard(
+								VCardTitle(h.Text("Item1")),
+							).Attr(":color", "active ? \"primary\" : \"\"").
+								Attr("@click", "toggle"),
+						).Value("VItem1").Attr("v-slot", "{active, toggle}"),
+					),
+
+					VCol(
+						VItem(
+							VCard(
+								VCardTitle(h.Text("Item2")),
+							).Attr(":color", "active ? \"primary\" : \"\"").
+								Attr("@click", "toggle"),
+						).Value("VItem2").Attr("v-slot", "{active, toggle}"),
+					),
+				),
+			),
+		).FieldName("MyItem").
+			Value(fv.MyItem),
+
+		VBtn("Submit").
+			OnClick("submit"),
+	)
+	return
+}
+
+func submit(ctx *web.EventContext) (r web.EventResponse, err error) {
+	fv = formVals{}
+	ctx.MustUnmarshalForm(&fv)
+
+	r.Reload = true
+	return
+}
+
+func removeCity(ctx *web.EventContext) (r web.EventResponse, err error) {
+	city := ctx.R.FormValue("city")
+	var newCities []string
+	for _, c := range globalCities {
+		if c != city {
+			newCities = append(newCities, c)
+		}
+	}
+	globalCities = newCities
+	r.Reload = true
+	return
+}
+
+var VuetifyComponentsKitchenPB = web.Page(VuetifyComponentsKitchen).
+	EventFunc("removeCity", removeCity).
+	EventFunc("submit", submit)
+
+const VuetifyComponentsKitchenPath = "/samples/vuetify-components-kitchen"
+
+// @snippet_end

+ 59 - 0
examples/e24_vuetify_components_linkage_select/page.go

@@ -0,0 +1,59 @@
+package e24_vuetify_components_linkage_select
+
+// @snippet_begin(VuetifyComponentsLinkageSelect)
+
+import (
+	. "github.com/qor5/ui/vuetify"
+	vx "github.com/qor5/ui/vuetifyx"
+	"github.com/qor5/web"
+	"github.com/theplant/htmlgo"
+)
+
+func VuetifyComponentsLinkageSelect(ctx *web.EventContext) (pr web.PageResponse, err error) {
+	labels := []string{
+		"Province",
+		"City",
+		"District",
+	}
+	items := [][]*vx.LinkageSelectItem{
+		{
+			{ID: "1", Name: "浙江", ChildrenIDs: []string{"1", "2"}},
+			{ID: "2", Name: "江苏", ChildrenIDs: []string{"3", "4"}},
+		},
+		{
+			{ID: "1", Name: "杭州", ChildrenIDs: []string{"1", "2"}},
+			{ID: "2", Name: "宁波", ChildrenIDs: []string{"3", "4"}},
+			{ID: "3", Name: "南京", ChildrenIDs: []string{"5", "6"}},
+			{ID: "4", Name: "苏州", ChildrenIDs: []string{"7", "8"}},
+		},
+		{
+			{ID: "1", Name: "拱墅区"},
+			{ID: "2", Name: "西湖区"},
+			{ID: "3", Name: "镇海区"},
+			{ID: "4", Name: "鄞州区"},
+			{ID: "5", Name: "鼓楼区"},
+			{ID: "6", Name: "玄武区"},
+			{ID: "7", Name: "常熟区"},
+			{ID: "8", Name: "吴江区"},
+		},
+	}
+
+	pr.Body = VContainer(
+		htmlgo.H3("Basic"),
+		vx.VXLinkageSelect().Items(items...).Labels(labels...),
+		htmlgo.H3("SelectOutOfOrder"),
+		vx.VXLinkageSelect().Items(items...).Labels(labels...).SelectOutOfOrder(true),
+		htmlgo.H3("Chips"),
+		vx.VXLinkageSelect().Items(items...).Labels(labels...).Chips(true),
+		htmlgo.H3("Row"),
+		vx.VXLinkageSelect().Items(items...).Labels(labels...).Row(true),
+	)
+
+	return pr, nil
+}
+
+var VuetifyComponentsLinkageSelectPB = web.Page(VuetifyComponentsLinkageSelect)
+
+const VuetifyComponentsLinkageSelectPath = "/samples/vuetify-components-linkage-select"
+
+// @snippet_end

+ 111 - 0
examples/example_basics/listing.go

@@ -0,0 +1,111 @@
+package example_basics
+
+import (
+	"time"
+
+	"github.com/qor5/admin/presets"
+	"github.com/qor5/admin/presets/gorm2op"
+	"github.com/qor5/ui/vuetify"
+	"github.com/qor5/web"
+	h "github.com/theplant/htmlgo"
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
+)
+
+var DB *gorm.DB
+
+func init() {
+	DB = setupDB()
+}
+
+func setupDB() (db *gorm.DB) {
+	var err error
+	db, err = gorm.Open(sqlite.Open("/tmp/my.db"), &gorm.Config{})
+	if err != nil {
+		panic(err)
+	}
+	db.Logger.LogMode(logger.Info)
+	err = db.AutoMigrate(
+		&Post{},
+		&Category{},
+	)
+	if err != nil {
+		panic(err)
+	}
+	return
+}
+
+const ListingSamplePath = "/samples/listing"
+
+// @snippet_begin(PresetsListingSample)
+
+type Post struct {
+	ID        uint
+	Title     string
+	Body      string
+	UpdatedAt time.Time
+	CreatedAt time.Time
+
+	CategoryID uint
+}
+
+type Category struct {
+	ID   uint
+	Name string
+
+	UpdatedAt time.Time
+	CreatedAt time.Time
+}
+
+func ListingSample(b *presets.Builder) {
+	db := DB
+
+	// Setup the project name, ORM and Homepage
+	b.URIPrefix(ListingSamplePath).DataOperator(gorm2op.DataOperator(db))
+
+	// Register Post into the builder
+	// Use m to customize the model, Or config more models here.
+	postModelBuilder := b.Model(&Post{})
+	postModelBuilder.Listing("ID", "Title", "Body", "CategoryID", "VirtualField")
+
+	postModelBuilder.Listing().Searcher = func(model interface{}, params *presets.SearchParams, ctx *web.EventContext) (r interface{}, totalCount int, err error) {
+		qdb := db.Where("category_id != 0")
+		return gorm2op.DataOperator(qdb).Search(model, params, ctx)
+	}
+
+	rmn := postModelBuilder.Listing().RowMenu()
+	rmn.RowMenuItem("Show").ComponentFunc(func(obj interface{}, id string, ctx *web.EventContext) h.HTMLComponent {
+		return h.Text("Fake Show")
+	})
+
+	postModelBuilder.Listing().ActionsAsMenu(true)
+
+	postModelBuilder.Editing().Field("CategoryID").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
+		categories := []Category{}
+		if err := db.Find(&categories).Error; err != nil {
+			// ignore err for now
+		}
+
+		return vuetify.VAutocomplete().Chips(true).FieldName(field.Name).Label(field.Label).Value(field.Value(obj)).Items(categories).ItemText("Name").ItemValue("ID")
+	})
+
+	postModelBuilder.Listing().Field("CategoryID").Label("Category").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
+		c := Category{}
+		cid, _ := field.Value(obj).(uint)
+		if err := db.Where("id = ?", cid).Find(&c).Error; err != nil {
+			// ignore err in the example
+		}
+		return h.Td(h.Text(c.Name))
+	})
+
+	postModelBuilder.Listing().Field("VirtualField").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
+		return h.Td(h.Text("virtual field"))
+	})
+
+	b.Model(&Category{})
+	// Use m to customize the model, Or config more models here.
+	return
+}
+
+// @snippet_end

File diff suppressed because it is too large
+ 2 - 0
examples/examples-generated.go


+ 23 - 0
examples/utils/db.go

@@ -0,0 +1,23 @@
+package utils
+
+import (
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
+)
+
+func InitDB() (db *gorm.DB) {
+	var err error
+	db, err = gorm.Open(sqlite.Open("/tmp/my.db"), &gorm.Config{})
+	if err != nil {
+		panic(err)
+	}
+	db.Logger.LogMode(logger.Info)
+
+	return
+}
+
+type Page struct {
+	ID   int
+	Name string
+}

+ 3 - 0
go.mod

@@ -0,0 +1,3 @@
+module github.com/qor5/docs
+
+go 1.17

+ 23 - 0
main.go

@@ -0,0 +1,23 @@
+package docs
+
+import (
+	"fmt"
+	"net/http"
+	"os"
+)
+
+func main() {
+	mux := Mux("/")
+	port := os.Getenv("PORT")
+	if len(port) == 0 {
+		port = "9101"
+	}
+	// @snippet_begin(HelloWorldMainSample)
+	fmt.Println("Starting docs at :" + port)
+	http.Handle("/", mux)
+	err := http.ListenAndServe(":"+port, nil)
+	if err != nil {
+		panic(err)
+	}
+	// @snippet_end
+}

+ 82 - 0
menu.go

@@ -0,0 +1,82 @@
+package docs
+
+import (
+	"net/http"
+
+	"github.com/qor5/docs/content"
+	advanced_functions "github.com/qor5/docs/content/advanced-functions"
+	"github.com/qor5/docs/content/basics"
+	digging_deeper "github.com/qor5/docs/content/digging-deeper"
+	getting_started "github.com/qor5/docs/content/getting-started"
+	"github.com/qor5/docs/utils"
+	"github.com/theplant/docgo"
+)
+
+func DocMenu(prefix string) http.Handler {
+	return docgo.New().
+		SitePrefix(prefix).
+		Assets("/assets/", content.Assets).
+		MainPageTitle("QOR5 Document").
+		DocTree(
+			content.Home,
+			&docgo.DocsGroup{
+				Title: "Getting Started",
+				Docs: []*docgo.DocBuilder{
+					getting_started.OneMinuteQuickStart,
+				},
+			},
+			&docgo.DocsGroup{
+				Title: "Basics",
+				Docs: []*docgo.DocBuilder{
+					basics.Listing,
+					basics.Filter,
+					basics.EditingCustomizations,
+					basics.FormHandling,
+					basics.BasicInputs,
+					basics.AutoComplete,
+					basics.ShortCut,
+					basics.VariantSubForm,
+					basics.LinkageSelect,
+					basics.Permissions,
+					basics.NotificationCenter,
+				},
+			},
+
+			&docgo.DocsGroup{
+				Title: "Advanced Functions",
+				Docs: []*docgo.DocBuilder{
+					advanced_functions.PageFuncAndEventFunc,
+					advanced_functions.TheGoHTMLBuilder,
+					advanced_functions.ATasteOfUsingVuetifyInGo,
+					advanced_functions.ItsTheWholeHouse,
+					advanced_functions.NavigationDrawer,
+					advanced_functions.LazyPortalsAndReload,
+					advanced_functions.LayoutFunctionAndPageInjector,
+					advanced_functions.SwitchPagesWithPushState,
+					advanced_functions.ReloadPageWithAFlash,
+					advanced_functions.PartialRefreshWithPortal,
+					advanced_functions.ManipulatePageURLInEventFunc,
+					advanced_functions.SummaryOfEventResponse,
+					advanced_functions.WebScope,
+					advanced_functions.EventHandling,
+					advanced_functions.DetailPageForComplexObject,
+				},
+			},
+			&docgo.DocsGroup{
+				Title: "Digging Deeper",
+				Docs: []*docgo.DocBuilder{
+					digging_deeper.CompositeNewComponentWithGo,
+					digging_deeper.IntegrateAHeavyVueComponent,
+				},
+			},
+			&docgo.DocsGroup{
+				Title: "Appendix",
+				Docs: []*docgo.DocBuilder{
+					docgo.Doc(utils.ExamplesDoc()).
+						Title("All Demo Examples").
+						Slug("appendix/all-demo-examples"),
+				},
+			},
+		).
+		Build()
+}

+ 677 - 0
mux.go

@@ -0,0 +1,677 @@
+package docs
+
+import (
+	"fmt"
+	"net/http"
+	"os"
+	"strings"
+
+	"github.com/go-chi/chi/middleware"
+	"github.com/qor5/admin/presets"
+	"github.com/qor5/docs/examples/e00_basics"
+	"github.com/qor5/docs/examples/e10_vuetify_autocomplete"
+	"github.com/qor5/docs/examples/e11_vuetify_basic_inputs"
+	"github.com/qor5/docs/examples/e13_vuetify_list"
+	"github.com/qor5/docs/examples/e14_vuetify_menu"
+	"github.com/qor5/docs/examples/e15_vuetify_navigation_drawer"
+	"github.com/qor5/docs/examples/e17_hello_lazy_portals_and_reload"
+	"github.com/qor5/docs/examples/e21_presents"
+	"github.com/qor5/docs/examples/e22_vuetify_variant_sub_form"
+	"github.com/qor5/docs/examples/e23_vuetify_components_kitchen"
+	"github.com/qor5/docs/examples/e24_vuetify_components_linkage_select"
+	"github.com/qor5/docs/examples/example_basics"
+	"github.com/qor5/docs/utils"
+	"github.com/qor5/ui/tiptap"
+	v "github.com/qor5/ui/vuetify"
+	"github.com/qor5/ui/vuetifyx"
+	"github.com/qor5/web"
+	. "github.com/theplant/htmlgo"
+)
+
+type section struct {
+	title string
+	slug  string
+	items []*pageItem
+}
+
+type pageItem struct {
+	section string
+	slug    string
+	title   string
+	doc     HTMLComponent
+}
+
+func menuLinks(prefix string, secs []*section) (comp HTMLComponent) {
+	var nav = Nav().Class("side-tree-nav")
+	for _, sec := range secs {
+		secdiv := Div(
+			Div(
+				Div().Class("marker"),
+				Div().Class("text").Text(sec.title),
+			).Class("tree-item-title tree-branch-title js-item-title js-branch-title is_active"),
+		).Class("tree-item tree-branch js-item js-branch _opened")
+		for _, p := range sec.items {
+			secdiv.AppendChildren(
+				Div(
+					A(
+						Span("").Class("marker"),
+						Span(p.title).Class("text"),
+					).Class("tree-item-title tree-leaf-title js-item-title js-leaf-title").
+						Href(fmt.Sprintf("%s/%s/%s", prefix, sec.slug, p.slug)),
+				).Class("tree-item tree-leaf js-item js-leaf"),
+			)
+		}
+		nav.AppendChildren(secdiv)
+	}
+	comp = Aside(
+		Div(nav).Class("js-side-tree-nav"),
+	).Class("g-3")
+
+	return
+}
+
+func header() HTMLComponent {
+	return Header(
+		Div(
+			Div(
+				A().Href("/").Class("global-header-logo").Text("QOR5"),
+				Nav(
+					Div(
+						A().Href("https://github.com/qor5").Text("Github").Class("nav-item"),
+					).Class("nav-links"),
+				).Class("global-nav"),
+			).Class("g-layout"),
+		).Class("global-header-panel"),
+	).Class("global-header")
+}
+
+func footer() HTMLComponent {
+	return Footer(
+		Div(
+			Div(
+				Div(
+					Div(
+						Div().Class("terms-copyright").Text("Licensed under the MIT license"),
+					).Class("global-footer-row"),
+				).Class("global-footer-container"),
+			).Class("g-layout"),
+		).Class("global-footer-terms"),
+	).Role("contentinfo").Class("global-footer")
+}
+
+func addGA(ctx *web.EventContext) {
+	if strings.Index(ctx.R.Host, "localhost") >= 0 {
+		return
+	}
+	ctx.Injector.HeadHTML(`
+<!-- Global site tag (gtag.js) - Google Analytics -->
+<script async src="https://www.googletagmanager.com/gtag/js?id=UA-149605708-1"></script>
+<script>
+  window.dataLayer = window.dataLayer || [];
+  function gtag(){dataLayer.push(arguments);}
+  gtag('js', new Date());
+
+  gtag('config', 'UA-149605708-1');
+</script>
+`)
+}
+
+func layout(in web.PageFunc, secs []*section, prefix string, cp *pageItem) (out web.PageFunc) {
+	return func(ctx *web.EventContext) (pr web.PageResponse, err error) {
+		addGA(ctx)
+		pr.PageTitle = cp.title + " - " + "QOR5"
+
+		ctx.Injector.HeadHTML(`
+				<link rel="stylesheet" href="/assets/main.css">
+			`)
+
+		ctx.Injector.Title(cp.title)
+		ctx.Injector.HeadHTML(`
+			<script src='/assets/vue.js'></script>
+			<script src='/assets/codehighlight.js'></script>
+		`)
+
+		ctx.Injector.TailHTML(coreJSTags)
+
+		var innerPr web.PageResponse
+		innerPr, err = in(ctx)
+		if err != nil {
+			panic(err)
+		}
+
+		demo := innerPr.Body
+
+		ctx.Injector.HeadHTML(`
+		<style>
+			[v-cloak] {
+				display: none;
+			}
+		</style>
+		`)
+
+		pr.Body = Components(
+			Div(
+				header(),
+				Div(
+					Div(
+						menuLinks(prefix, secs),
+						Article(demo.(HTMLComponent)).Class("page-content g-9").Role("main"),
+					).Class("g-grid"),
+				).Class("g-layout global-content"),
+			).Class("global-layout"),
+			footer(),
+		)
+
+		return
+	}
+}
+
+// @snippet_begin(DemoLayoutSample)
+func demoLayout(in web.PageFunc) (out web.PageFunc) {
+	return func(ctx *web.EventContext) (pr web.PageResponse, err error) {
+		addGA(ctx)
+
+		ctx.Injector.HeadHTML(`
+			<script src='/assets/vue.js'></script>
+		`)
+
+		ctx.Injector.TailHTML(coreJSTags)
+		ctx.Injector.HeadHTML(`
+		<style>
+			[v-cloak] {
+				display: none;
+			}
+		</style>
+		`)
+
+		var innerPr web.PageResponse
+		innerPr, err = in(ctx)
+		if err != nil {
+			panic(err)
+		}
+
+		pr.Body = innerPr.Body
+
+		return
+	}
+}
+
+// @snippet_end
+
+// @snippet_begin(TipTapLayoutSample)
+func tiptapLayout(in web.PageFunc) (out web.PageFunc) {
+	return func(ctx *web.EventContext) (pr web.PageResponse, err error) {
+		addGA(ctx)
+
+		ctx.Injector.HeadHTML(`
+			<link rel="stylesheet" href="/assets/tiptap.css">
+			<script src='/assets/vue.js'></script>
+		`)
+
+		ctx.Injector.TailHTML(`
+<script src='/assets/tiptap.js'></script>
+<script src='/assets/main.js'></script>
+`)
+		ctx.Injector.HeadHTML(`
+		<style>
+			[v-cloak] {
+				display: none;
+			}
+		</style>
+		`)
+
+		var innerPr web.PageResponse
+		innerPr, err = in(ctx)
+		if err != nil {
+			panic(err)
+		}
+
+		pr.Body = innerPr.Body
+
+		return
+	}
+}
+
+// @snippet_end
+
+// @snippet_begin(DemoBootstrapLayoutSample)
+func demoBootstrapLayout(in web.PageFunc) (out web.PageFunc) {
+	return func(ctx *web.EventContext) (pr web.PageResponse, err error) {
+		addGA(ctx)
+
+		ctx.Injector.HeadHTML(`
+<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
+<script src='/assets/vue.js'></script>
+		`)
+
+		ctx.Injector.TailHTML(`
+<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
+<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
+<script src='/assets/main.js'></script>
+
+`)
+		ctx.Injector.HeadHTML(`
+		<style>
+			[v-cloak] {
+				display: none;
+			}
+		</style>
+		`)
+
+		var innerPr web.PageResponse
+		innerPr, err = in(ctx)
+		if err != nil {
+			panic(err)
+		}
+
+		pr.Body = innerPr.Body
+
+		return
+	}
+}
+
+// @snippet_end
+
+var coreJSTags = func() string {
+	if len(os.Getenv("DEV_CORE_JS")) > 0 {
+		return `
+<script src='http://localhost:3100/js/chunk-vendors.js'></script>
+<script src='http://localhost:3100/js/app.js'></script>
+`
+	}
+	return `<script src='/assets/main.js'></script>`
+}()
+
+var vuetifyJSTags = func() string {
+	if len(os.Getenv("DEV_VUETIFY_JS")) > 0 {
+		return `
+<script src='http://localhost:3080/js/chunk-vendors.js'></script>
+<script src='http://localhost:3080/js/app.js'></script>
+`
+	}
+	return `<script src='/assets/vuetify.js'></script>`
+}()
+
+// @snippet_begin(DemoVuetifyLayoutSample)
+func demoVuetifyLayout(in web.PageFunc) (out web.PageFunc) {
+	return func(ctx *web.EventContext) (pr web.PageResponse, err error) {
+		addGA(ctx)
+
+		ctx.Injector.HeadHTML(`
+			<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto+Mono" async>
+			<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" async>
+			<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" async>
+			<link rel="stylesheet" href="/assets/vuetify.css">
+			<script src='/assets/vue.js'></script>
+		`)
+
+		ctx.Injector.TailHTML(fmt.Sprintf("%s %s", vuetifyJSTags, coreJSTags))
+		ctx.Injector.HeadHTML(`
+		<style>
+			[v-cloak] {
+				display: none;
+			}
+		</style>
+		`)
+
+		var innerPr web.PageResponse
+		innerPr, err = in(ctx)
+		if err != nil {
+			panic(err)
+		}
+
+		pr.Body = v.VApp(
+			v.VMain(
+				innerPr.Body,
+			),
+		)
+
+		return
+	}
+}
+
+// @snippet_end
+
+func rf(comp HTMLComponent, p *pageItem) web.PageFunc {
+	return func(ctx *web.EventContext) (r web.PageResponse, err error) {
+		r.Body = Components(
+			utils.Anchor(H1(""), p.title),
+			comp,
+		)
+		return
+	}
+}
+
+func Mux(prefix string) http.Handler {
+
+	// @snippet_begin(HelloWorldMuxSample1)
+	mux := http.NewServeMux()
+	// @snippet_end
+
+	// @snippet_begin(ComponentsPackSample)
+	mux.Handle("/assets/main.js",
+		web.PacksHandler("text/javascript",
+			web.JSComponentsPack(),
+		),
+	)
+
+	mux.Handle("/assets/vue.js",
+		web.PacksHandler("text/javascript",
+			web.JSVueComponentsPack(),
+		),
+	)
+
+	// @snippet_end
+
+	// @snippet_begin(TipTapComponentsPackSample)
+	mux.Handle("/assets/tiptap.js",
+		web.PacksHandler("text/javascript",
+			tiptap.JSComponentsPack(),
+		),
+	)
+
+	mux.Handle("/assets/tiptap.css",
+		web.PacksHandler("text/css",
+			tiptap.CSSComponentsPack(),
+		),
+	)
+	// @snippet_end
+
+	// @snippet_begin(VuetifyComponentsPackSample)
+	mux.Handle("/assets/vuetify.js",
+		web.PacksHandler("text/javascript",
+			v.Vuetify(""),
+			v.JSComponentsPack(),
+			vuetifyx.JSComponentsPack(),
+		),
+	)
+
+	mux.Handle("/assets/vuetify.css",
+		web.PacksHandler("text/css",
+			v.CSSComponentsPack(),
+		),
+	)
+	// @snippet_end
+
+	mux.Handle("/favicon.ico", http.NotFoundHandler())
+
+	samplesMux := SamplesHandler(prefix)
+	mux.Handle("/samples/",
+		middleware.Logger(
+			middleware.RequestID(
+				samplesMux,
+			),
+		),
+	)
+
+	mux.Handle("/",
+		middleware.Logger(
+			middleware.RequestID(
+				DocMenu(prefix),
+			),
+		),
+	)
+	return mux
+}
+
+func SamplesHandler(prefix string) http.Handler {
+	mux := http.NewServeMux()
+	emptyUb := web.New().LayoutFunc(web.NoopLayoutFunc)
+
+	mux.Handle(e00_basics.TypeSafeBuilderSamplePath, e00_basics.TypeSafeBuilderSamplePFPB.Builder(emptyUb))
+
+	// @snippet_begin(HelloWorldMuxSample2)
+	mux.Handle(e00_basics.HelloWorldPath, e00_basics.HelloWorldPB)
+	// @snippet_end
+
+	// @snippet_begin(HelloWorldReloadMuxSample1)
+	mux.Handle(
+		e00_basics.HelloWorldReloadPath,
+		e00_basics.HelloWorldReloadPB.Wrap(demoLayout),
+	)
+	// @snippet_end
+
+	mux.Handle(
+		e00_basics.Page1Path,
+		e00_basics.Page1PB.Wrap(demoLayout),
+	)
+	mux.Handle(
+		e00_basics.Page2Path,
+		e00_basics.Page2PB.Wrap(demoLayout),
+	)
+
+	mux.Handle(
+		e00_basics.ReloadWithFlashPath,
+		e00_basics.ReloadWithFlashPB.Wrap(demoLayout),
+	)
+
+	mux.Handle(
+		e00_basics.PartialUpdatePagePath,
+		e00_basics.PartialUpdatePagePB.Wrap(demoLayout),
+	)
+
+	mux.Handle(
+		e00_basics.PartialReloadPagePath,
+		e00_basics.PartialReloadPagePB.Wrap(demoLayout),
+	)
+
+	mux.Handle(
+		e00_basics.MultiStatePagePath,
+		e00_basics.MultiStatePagePB.Wrap(demoLayout),
+	)
+
+	mux.Handle(
+		e00_basics.FormHandlingPagePath,
+		e00_basics.FormHandlingPagePB.Wrap(demoLayout),
+	)
+
+	mux.Handle(
+		e00_basics.CompositeComponentSample1PagePath,
+		e00_basics.CompositeComponentSample1PagePB.Wrap(demoBootstrapLayout),
+	)
+
+	mux.Handle(
+		e00_basics.HelloWorldTipTapPath,
+		e00_basics.HelloWorldTipTapPB.Wrap(tiptapLayout),
+	)
+
+	mux.Handle(
+		e13_vuetify_list.HelloVuetifyListPath,
+		e13_vuetify_list.HelloVuetifyListPB.Wrap(demoVuetifyLayout),
+	)
+
+	mux.Handle(
+		e14_vuetify_menu.HelloVuetifyMenuPath,
+		e14_vuetify_menu.HelloVuetifyMenuPB.Wrap(demoVuetifyLayout),
+	)
+
+	mux.Handle(
+		e00_basics.EventExamplePagePath,
+		e00_basics.ExamplePagePB.Wrap(demoVuetifyLayout),
+	)
+
+	mux.Handle(
+		e00_basics.EventHandlingPagePath,
+		e00_basics.EventHandlingPagePB.Wrap(demoVuetifyLayout),
+	)
+
+	mux.Handle(
+		e00_basics.WebScopeUseLocalsPagePath,
+		e00_basics.UseLocalsPB.Wrap(demoVuetifyLayout),
+	)
+
+	mux.Handle(
+		e00_basics.WebScopeUsePlaidFormPagePath,
+		e00_basics.UsePlaidFormPB.Wrap(demoVuetifyLayout),
+	)
+
+	mux.Handle(
+		e00_basics.ShortCutSamplePath,
+		e00_basics.ShortCutSamplePB.Wrap(demoVuetifyLayout),
+	)
+
+	mux.Handle(
+		e11_vuetify_basic_inputs.VuetifyBasicInputsPath,
+		e11_vuetify_basic_inputs.VuetifyBasicInputsPB.Wrap(demoVuetifyLayout),
+	)
+
+	mux.Handle(
+		e10_vuetify_autocomplete.VuetifyAutoCompletePath,
+		e10_vuetify_autocomplete.VuetifyAutocompletePB.Wrap(demoVuetifyLayout),
+	)
+
+	mux.Handle(
+		e10_vuetify_autocomplete.VuetifyAutoCompletePresetPath+"/",
+		e10_vuetify_autocomplete.ExamplePreset,
+	)
+
+	mux.Handle(
+		e22_vuetify_variant_sub_form.VuetifyVariantSubFormPath,
+		e22_vuetify_variant_sub_form.VuetifyVariantSubFormPB.Wrap(demoVuetifyLayout),
+	)
+
+	mux.Handle(
+		e23_vuetify_components_kitchen.VuetifyComponentsKitchenPath,
+		e23_vuetify_components_kitchen.VuetifyComponentsKitchenPB.Wrap(demoVuetifyLayout),
+	)
+
+	mux.Handle(
+		e15_vuetify_navigation_drawer.VuetifyNavigationDrawerPath,
+		e15_vuetify_navigation_drawer.VuetifyNavigationDrawerPB.Wrap(demoVuetifyLayout),
+	)
+
+	mux.Handle(
+		e17_hello_lazy_portals_and_reload.LazyPortalsAndReloadPath,
+		e17_hello_lazy_portals_and_reload.LazyPortalsAndReloadPB.Wrap(demoVuetifyLayout),
+	)
+
+	mux.Handle(
+		e24_vuetify_components_linkage_select.VuetifyComponentsLinkageSelectPath,
+		e24_vuetify_components_linkage_select.VuetifyComponentsLinkageSelectPB.Wrap(demoVuetifyLayout),
+	)
+
+	// @snippet_begin(MountPresetHelloWorldSample)
+	c00 := presets.New().AssetFunc(addGA)
+	e21_presents.PresetsHelloWorld(c00)
+	mux.Handle(
+		e21_presents.PresetsHelloWorldPath+"/",
+		c00,
+	)
+	// @snippet_end
+
+	c01 := presets.New().AssetFunc(addGA)
+	e21_presents.PresetsListingCustomizationFields(c01)
+	mux.Handle(
+		e21_presents.PresetsListingCustomizationFieldsPath+"/",
+		c01,
+	)
+
+	c02 := presets.New().AssetFunc(addGA)
+	e21_presents.PresetsListingCustomizationFilters(c02)
+	mux.Handle(
+		e21_presents.PresetsListingCustomizationFiltersPath+"/",
+		c02,
+	)
+
+	c03 := presets.New().AssetFunc(addGA)
+	e21_presents.PresetsListingCustomizationTabs(c03)
+	mux.Handle(
+		e21_presents.PresetsListingCustomizationTabsPath+"/",
+		c03,
+	)
+
+	c04 := presets.New().AssetFunc(addGA)
+	e21_presents.PresetsListingCustomizationBulkActions(c04)
+	mux.Handle(
+		e21_presents.PresetsListingCustomizationBulkActionsPath+"/",
+		c04,
+	)
+
+	c05 := presets.New().AssetFunc(addGA)
+	e21_presents.PresetsEditingCustomizationDescription(c05)
+	mux.Handle(
+		e21_presents.PresetsEditingCustomizationDescriptionPath+"/",
+		c05,
+	)
+
+	c06 := presets.New().AssetFunc(addGA)
+	e21_presents.PresetsEditingCustomizationFileType(c06)
+	mux.Handle(
+		e21_presents.PresetsEditingCustomizationFileTypePath+"/",
+		c06,
+	)
+
+	c07 := presets.New().AssetFunc(addGA)
+	e21_presents.PresetsEditingCustomizationValidation(c07)
+	mux.Handle(
+		e21_presents.PresetsEditingCustomizationValidationPath+"/",
+		c07,
+	)
+
+	c08 := presets.New().AssetFunc(addGA)
+	e21_presents.PresetsDetailPageTopNotes(c08)
+	mux.Handle(
+		e21_presents.PresetsDetailPageTopNotesPath+"/",
+		c08,
+	)
+
+	c09 := presets.New().AssetFunc(addGA)
+	e21_presents.PresetsDetailPageDetails(c09)
+	mux.Handle(
+		e21_presents.PresetsDetailPageDetailsPath+"/",
+		c09,
+	)
+
+	c10 := presets.New().AssetFunc(addGA)
+	e21_presents.PresetsDetailPageCards(c10)
+	mux.Handle(
+		e21_presents.PresetsDetailPageCardsPath+"/",
+		c10,
+	)
+
+	c11 := presets.New().AssetFunc(addGA)
+	e21_presents.PresetsPermissions(c11)
+	mux.Handle(
+		e21_presents.PresetsPermissionsPath+"/",
+		c11,
+	)
+
+	c12 := presets.New().AssetFunc(addGA)
+	e21_presents.PresetsModelBuilderExtensions(c12)
+	mux.Handle(
+		e21_presents.PresetsModelBuilderExtensionsPath+"/",
+		c12,
+	)
+
+	c13 := presets.New().AssetFunc(addGA)
+	e21_presents.PresetsBasicFilter(c13)
+	mux.Handle(
+		e21_presents.PresetsBasicFilterPath+"/",
+		c13,
+	)
+
+	c14 := presets.New().AssetFunc(addGA)
+	e21_presents.PresetsNotificationCenterSample(c14)
+	mux.Handle(
+		e21_presents.NotificationCenterSamplePath+"/",
+		c14,
+	)
+
+	c15 := presets.New().AssetFunc(addGA)
+	e21_presents.PresetsLinkageSelectFilterItem(c15)
+	mux.Handle(
+		e21_presents.PresetsLinkageSelectFilterItemPath+"/",
+		c15,
+	)
+
+	c16 := presets.New().AssetFunc(addGA)
+	example_basics.ListingSample(c16)
+
+	mux.Handle(
+		example_basics.ListingSamplePath+"/",
+		c16,
+	)
+
+	return mux
+}

+ 8 - 0
plantbuild/build.jsonnet

@@ -0,0 +1,8 @@
+local c = import 'dc.jsonnet';
+local dc = c {
+  dockerRegistry: 'gcr.io',
+};
+
+dc.build_apps_image('sunfmin/sunfmin', [
+  {name: 'qor5-docs', dockerfile: './docs/Dockerfile', context: '../'},
+])

+ 75 - 0
utils/utils.go

@@ -0,0 +1,75 @@
+package utils
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/qor5/web"
+	"github.com/shurcooL/sanitized_anchor_name"
+	. "github.com/theplant/htmlgo"
+)
+
+func Anchor(h *HTMLTagBuilder, text string) HTMLComponent {
+	anchorName := sanitized_anchor_name.Create(text)
+	return h.Children(
+		Text(text),
+		A().Class("anchor").Href(fmt.Sprintf("#%s", anchorName)),
+	).Id(anchorName)
+}
+
+type Example struct {
+	Title      string
+	DemoPath   string
+	SourcePath string
+}
+
+var LiveExamples []*Example
+
+func Demo(title string, demoPath string, sourcePath string) HTMLComponent {
+	ex := &Example{
+		Title:      title,
+		DemoPath:   demoPath,
+		SourcePath: fmt.Sprintf("https://github.com/qor5/docs/tree/master/examples/%s", sourcePath),
+	}
+
+	LiveExamples = append(LiveExamples, ex)
+
+	return Div(
+		Div(
+			A().Text("Check the demo").Href(ex.DemoPath).Target("_blank"),
+			Text(" | "),
+			A().Text("Source on GitHub").
+				Href(ex.SourcePath).
+				Target("_blank"),
+		).Class("demo"),
+	)
+}
+
+func ExamplesDoc() HTMLComponent {
+	u := Ul()
+	for _, le := range LiveExamples {
+		u.AppendChildren(
+			Li(
+				A().Href(le.DemoPath).Text(le.Title).Target("_blank"),
+				Text(" | "),
+				A().Href(le.SourcePath).Text("Source").Target("_blank"),
+			),
+		)
+	}
+	return u
+}
+
+func PrettyFormAsJSON(ctx *web.EventContext) HTMLComponent {
+	if ctx.R.MultipartForm == nil {
+		return nil
+	}
+
+	formData, err := json.MarshalIndent(ctx.R.MultipartForm, "", "\t")
+	if err != nil {
+		panic(err)
+	}
+
+	return Pre(
+		string(formData),
+	)
+}

Some files were not shown because too many files changed in this diff